kei-lisp-plugin-graphics 1.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -12
- package/dist/index.cjs +698 -270
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -34
- package/dist/index.d.ts +50 -34
- package/dist/index.js +698 -270
- package/dist/index.js.map +1 -1
- package/package.json +23 -11
package/dist/index.cjs
CHANGED
|
@@ -1,26 +1,128 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let kei_lisp = require("kei-lisp");
|
|
3
3
|
//#region src/GraphicsPlugin/index.ts
|
|
4
|
+
const LINE_CAPS = /* @__PURE__ */ new Set([
|
|
5
|
+
"butt",
|
|
6
|
+
"round",
|
|
7
|
+
"square"
|
|
8
|
+
]);
|
|
9
|
+
const LINE_JOINS = /* @__PURE__ */ new Set([
|
|
10
|
+
"miter",
|
|
11
|
+
"round",
|
|
12
|
+
"bevel"
|
|
13
|
+
]);
|
|
14
|
+
const TEXT_ALIGNS = /* @__PURE__ */ new Set([
|
|
15
|
+
"left",
|
|
16
|
+
"right",
|
|
17
|
+
"center",
|
|
18
|
+
"start",
|
|
19
|
+
"end"
|
|
20
|
+
]);
|
|
21
|
+
const TEXT_BASELINES = /* @__PURE__ */ new Set([
|
|
22
|
+
"top",
|
|
23
|
+
"hanging",
|
|
24
|
+
"middle",
|
|
25
|
+
"alphabetic",
|
|
26
|
+
"ideographic",
|
|
27
|
+
"bottom"
|
|
28
|
+
]);
|
|
29
|
+
const TEXT_DIRECTIONS = /* @__PURE__ */ new Set([
|
|
30
|
+
"ltr",
|
|
31
|
+
"rtl",
|
|
32
|
+
"inherit"
|
|
33
|
+
]);
|
|
34
|
+
const FONT_KERNINGS = /* @__PURE__ */ new Set([
|
|
35
|
+
"auto",
|
|
36
|
+
"normal",
|
|
37
|
+
"none"
|
|
38
|
+
]);
|
|
39
|
+
const FONT_STRETCHES = /* @__PURE__ */ new Set([
|
|
40
|
+
"ultra-condensed",
|
|
41
|
+
"extra-condensed",
|
|
42
|
+
"condensed",
|
|
43
|
+
"semi-condensed",
|
|
44
|
+
"normal",
|
|
45
|
+
"semi-expanded",
|
|
46
|
+
"expanded",
|
|
47
|
+
"extra-expanded",
|
|
48
|
+
"ultra-expanded"
|
|
49
|
+
]);
|
|
50
|
+
const FONT_VARIANTS = /* @__PURE__ */ new Set([
|
|
51
|
+
"normal",
|
|
52
|
+
"small-caps",
|
|
53
|
+
"all-small-caps",
|
|
54
|
+
"petite-caps",
|
|
55
|
+
"all-petite-caps",
|
|
56
|
+
"unicase",
|
|
57
|
+
"titling-caps"
|
|
58
|
+
]);
|
|
59
|
+
const TEXT_RENDERINGS = /* @__PURE__ */ new Set([
|
|
60
|
+
"auto",
|
|
61
|
+
"optimizeSpeed",
|
|
62
|
+
"optimizeLegibility",
|
|
63
|
+
"geometricPrecision"
|
|
64
|
+
]);
|
|
65
|
+
const PATTERN_REPETITIONS = /* @__PURE__ */ new Set([
|
|
66
|
+
"repeat",
|
|
67
|
+
"repeat-x",
|
|
68
|
+
"repeat-y",
|
|
69
|
+
"no-repeat"
|
|
70
|
+
]);
|
|
71
|
+
const COMPOSITE_OPERATIONS = /* @__PURE__ */ new Set([
|
|
72
|
+
"source-over",
|
|
73
|
+
"source-in",
|
|
74
|
+
"source-out",
|
|
75
|
+
"source-atop",
|
|
76
|
+
"destination-over",
|
|
77
|
+
"destination-in",
|
|
78
|
+
"destination-out",
|
|
79
|
+
"destination-atop",
|
|
80
|
+
"lighter",
|
|
81
|
+
"copy",
|
|
82
|
+
"xor",
|
|
83
|
+
"multiply",
|
|
84
|
+
"screen",
|
|
85
|
+
"overlay",
|
|
86
|
+
"darken",
|
|
87
|
+
"lighten",
|
|
88
|
+
"color-dodge",
|
|
89
|
+
"color-burn",
|
|
90
|
+
"hard-light",
|
|
91
|
+
"soft-light",
|
|
92
|
+
"difference",
|
|
93
|
+
"exclusion",
|
|
94
|
+
"hue",
|
|
95
|
+
"saturation",
|
|
96
|
+
"color",
|
|
97
|
+
"luminosity"
|
|
98
|
+
]);
|
|
4
99
|
/**
|
|
5
100
|
* @class
|
|
6
101
|
* @classdesc Canvas2D drawing plugin for the kei-lisp interpreter. Implements
|
|
7
102
|
* the `KeiLispPlugin` contract (`name` / `has` / `apply`) and
|
|
8
|
-
* exposes
|
|
103
|
+
* exposes 75 `g…` Lisp functions that proxy to a 2D rendering
|
|
9
104
|
* context.
|
|
10
105
|
* @author Keisuke Ikeda
|
|
11
|
-
* @this {GraphicsPlugin}
|
|
12
106
|
*/
|
|
13
|
-
var GraphicsPlugin = class GraphicsPlugin
|
|
107
|
+
var GraphicsPlugin = class GraphicsPlugin {
|
|
14
108
|
/**
|
|
15
109
|
* Dispatch map from a Lisp function name (InterpretedSymbol) to the name of
|
|
16
|
-
* the GraphicsPlugin method that implements it.
|
|
110
|
+
* the GraphicsPlugin method that implements it. Private so hosts cannot
|
|
111
|
+
* mutate the registered function set.
|
|
17
112
|
*/
|
|
18
|
-
static
|
|
113
|
+
static #builtInFunctions = GraphicsPlugin.#setup();
|
|
114
|
+
/**
|
|
115
|
+
* Lists every Lisp function name this plugin registers.
|
|
116
|
+
* @return the sorted `g…` function names
|
|
117
|
+
*/
|
|
118
|
+
static functionNames() {
|
|
119
|
+
return [...GraphicsPlugin.#builtInFunctions.keys()].map(String).sort((a, b) => a.localeCompare(b));
|
|
120
|
+
}
|
|
19
121
|
/**
|
|
20
122
|
* Builds the dispatch table.
|
|
21
123
|
* @return the dispatch table
|
|
22
124
|
*/
|
|
23
|
-
static setup() {
|
|
125
|
+
static #setup() {
|
|
24
126
|
const aTable = /* @__PURE__ */ new Map();
|
|
25
127
|
aTable.set(kei_lisp.InterpretedSymbol.of("galpha"), "gAlpha");
|
|
26
128
|
aTable.set(kei_lisp.InterpretedSymbol.of("garc"), "gArc");
|
|
@@ -59,17 +161,55 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
59
161
|
aTable.set(kei_lisp.InterpretedSymbol.of("gstroke-text"), "gStrokeText");
|
|
60
162
|
aTable.set(kei_lisp.InterpretedSymbol.of("gstroke-tri"), "gStrokeTri");
|
|
61
163
|
aTable.set(kei_lisp.InterpretedSymbol.of("gtext-align"), "gTextAlign");
|
|
62
|
-
aTable.set(kei_lisp.InterpretedSymbol.of("gtext-
|
|
164
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gtext-direction"), "gTextDirection");
|
|
63
165
|
aTable.set(kei_lisp.InterpretedSymbol.of("gtext-font"), "gTextFont");
|
|
166
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gtext-baseline"), "gTextBaseline");
|
|
167
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gtext-dire"), "gTextDirection");
|
|
64
168
|
aTable.set(kei_lisp.InterpretedSymbol.of("gtext-line"), "gTextBaseline");
|
|
65
169
|
aTable.set(kei_lisp.InterpretedSymbol.of("gtranslate"), "gTranslate");
|
|
66
170
|
aTable.set(kei_lisp.InterpretedSymbol.of("grect"), "gRect");
|
|
67
171
|
aTable.set(kei_lisp.InterpretedSymbol.of("grotate"), "gRotate");
|
|
68
172
|
aTable.set(kei_lisp.InterpretedSymbol.of("gsave"), "gSave");
|
|
69
173
|
aTable.set(kei_lisp.InterpretedSymbol.of("grestore"), "gRestore");
|
|
174
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gellipse"), "gEllipse");
|
|
175
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("ground-rect"), "gRoundRect");
|
|
176
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gline-dash"), "gLineDash");
|
|
177
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gline-dash-offset"), "gLineDashOffset");
|
|
178
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gmiter-limit"), "gMiterLimit");
|
|
179
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gclip"), "gClip");
|
|
180
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gis-point-in-path"), "gIsPointInPath");
|
|
181
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gis-point-in-stroke"), "gIsPointInStroke");
|
|
182
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gtransform"), "gTransform");
|
|
183
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gset-transform"), "gSetTransform");
|
|
184
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("greset-transform"), "gResetTransform");
|
|
185
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gcomposite"), "gComposite");
|
|
186
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gfilter"), "gFilter");
|
|
187
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gimage-smoothing"), "gImageSmoothing");
|
|
188
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gmeasure-text"), "gMeasureText");
|
|
189
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gletter-spacing"), "gLetterSpacing");
|
|
190
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gword-spacing"), "gWordSpacing");
|
|
191
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gfont-kerning"), "gFontKerning");
|
|
192
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gfont-stretch"), "gFontStretch");
|
|
193
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gfont-variant"), "gFontVariant");
|
|
194
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gtext-rendering"), "gTextRendering");
|
|
195
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gclear-rect"), "gClearRect");
|
|
196
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("greset"), "gReset");
|
|
197
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gwidth"), "gWidth");
|
|
198
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gheight"), "gHeight");
|
|
199
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gpixel"), "gPixel");
|
|
200
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gset-pixel"), "gSetPixel");
|
|
201
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("glinear-gradient"), "gLinearGradient");
|
|
202
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gradial-gradient"), "gRadialGradient");
|
|
203
|
+
aTable.set(kei_lisp.InterpretedSymbol.of("gconic-gradient"), "gConicGradient");
|
|
70
204
|
return aTable;
|
|
71
205
|
}
|
|
72
206
|
/**
|
|
207
|
+
* Loaded-image cache shared by `gimage` and `gpattern`. Repeated draws of
|
|
208
|
+
* the same `src` reuse the loaded element and run synchronously, so they
|
|
209
|
+
* keep their place in the drawing order instead of racing the load.
|
|
210
|
+
*/
|
|
211
|
+
#imageCache = /* @__PURE__ */ new Map();
|
|
212
|
+
/**
|
|
73
213
|
* Plugin identifier, used for diagnostics.
|
|
74
214
|
*/
|
|
75
215
|
name = "graphics";
|
|
@@ -82,22 +222,221 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
82
222
|
* @param canvas - the canvas to draw to
|
|
83
223
|
*/
|
|
84
224
|
constructor(canvas) {
|
|
85
|
-
super();
|
|
86
225
|
this.canvas = canvas;
|
|
87
226
|
this.ctx = canvas.getContext("2d");
|
|
88
227
|
this.isOpen = false;
|
|
89
228
|
}
|
|
90
229
|
/**
|
|
91
|
-
* Writes a diagnostic line
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
230
|
+
* Writes a diagnostic line to `process.stderr`, matching the convention
|
|
231
|
+
* used by kei-lisp itself (`Applier.format` writes to `process.stdout`).
|
|
232
|
+
* In a Node runtime this hits the real stderr; in a browser kei-lisp host
|
|
233
|
+
* (e.g. kei-lisp-web) the host typically swaps `process.stderr.write` for
|
|
234
|
+
* a sink that routes to the REPL output panel. In a plain browser with no
|
|
235
|
+
* `process` shim at all, the line falls back to `console.error` instead of
|
|
236
|
+
* throwing.
|
|
97
237
|
* @param line - the line to write
|
|
98
238
|
*/
|
|
99
239
|
#print(line) {
|
|
100
|
-
|
|
240
|
+
const stderr = globalThis.process?.stderr;
|
|
241
|
+
if (typeof stderr?.write === "function") {
|
|
242
|
+
stderr.write(line + "\n");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
console.error(line);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Resolves an image for the given source and runs `draw` with it —
|
|
249
|
+
* synchronously when the image is already loaded, on its `load` event
|
|
250
|
+
* otherwise. A load failure prints a diagnostic once.
|
|
251
|
+
* @param source - the image URL / data URI
|
|
252
|
+
* @param draw - the drawing action to run once the image is available
|
|
253
|
+
*/
|
|
254
|
+
#withImage(source, draw) {
|
|
255
|
+
const cached = this.#imageCache.get(source);
|
|
256
|
+
const image = cached ?? new Image();
|
|
257
|
+
if (cached === void 0) {
|
|
258
|
+
image.src = source;
|
|
259
|
+
this.#imageCache.set(source, image);
|
|
260
|
+
image.addEventListener("error", () => {
|
|
261
|
+
this.#print(`Can not load image: ${source}`);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (image.complete) {
|
|
265
|
+
draw(image);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
image.addEventListener("load", () => {
|
|
269
|
+
draw(image);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Shared guard-and-dispatch skeleton for the newer `g…` methods: checks the
|
|
274
|
+
* context and the open flag, runs `body`, and converts both a `null` return
|
|
275
|
+
* and a thrown exception into `failureMessage` + `Cons.nil`.
|
|
276
|
+
* @param failureMessage - the diagnostic printed when `body` fails
|
|
277
|
+
* @param body - the drawing action; returns the Lisp result, or `null` on
|
|
278
|
+
* bad arguments
|
|
279
|
+
* @return the body's result, or `Cons.nil` on failure
|
|
280
|
+
*/
|
|
281
|
+
#execute(failureMessage, body) {
|
|
282
|
+
if (!this.checkSupport()) return kei_lisp.Cons.nil;
|
|
283
|
+
if (!this.isOpen) {
|
|
284
|
+
this.#print("The canvas is closed and cannot be executed.");
|
|
285
|
+
return kei_lisp.Cons.nil;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const result = body(this.ctx);
|
|
289
|
+
if (result !== null) return result;
|
|
290
|
+
} catch {}
|
|
291
|
+
this.#print(failureMessage);
|
|
292
|
+
return kei_lisp.Cons.nil;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Reads a single string argument and validates it against an allowlist.
|
|
296
|
+
* @param arguments_ - the evaluated argument list
|
|
297
|
+
* @param allowed - the accepted values
|
|
298
|
+
* @return the validated string, or `null` on wrong arity, a non-string
|
|
299
|
+
* argument, or a value outside the allowlist
|
|
300
|
+
*/
|
|
301
|
+
#enumString(arguments_, allowed) {
|
|
302
|
+
if (arguments_.length() !== 1 || !kei_lisp.Cons.isString(arguments_.car)) return null;
|
|
303
|
+
return allowed.has(arguments_.car) ? arguments_.car : null;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Reads exactly `count` numbers from the argument list.
|
|
307
|
+
* @param arguments_ - the evaluated argument list
|
|
308
|
+
* @param count - the expected argument count
|
|
309
|
+
* @return the numbers, or `null` on wrong arity or a non-number argument
|
|
310
|
+
*/
|
|
311
|
+
#numbers(arguments_, count) {
|
|
312
|
+
if (arguments_.length() !== count) return null;
|
|
313
|
+
const values = [];
|
|
314
|
+
let rest = arguments_;
|
|
315
|
+
for (let index = 0; index < count; index++) {
|
|
316
|
+
if (!kei_lisp.Cons.isNumber(rest.car)) return null;
|
|
317
|
+
values.push(rest.car);
|
|
318
|
+
rest = rest.cdr;
|
|
319
|
+
}
|
|
320
|
+
return values;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Flattens the argument list into a JS array.
|
|
324
|
+
* @param arguments_ - the evaluated argument list
|
|
325
|
+
* @return the argument values in order
|
|
326
|
+
*/
|
|
327
|
+
#listValues(arguments_) {
|
|
328
|
+
const values = [];
|
|
329
|
+
let rest = arguments_;
|
|
330
|
+
for (let index = arguments_.length(); index > 0; index--) {
|
|
331
|
+
values.push(rest.car);
|
|
332
|
+
rest = rest.cdr;
|
|
333
|
+
}
|
|
334
|
+
return values;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Builds a Lisp list (Cons chain) from JS values.
|
|
338
|
+
* @param values - the values, at least one
|
|
339
|
+
* @return the list head
|
|
340
|
+
*/
|
|
341
|
+
#toList(values) {
|
|
342
|
+
const head = new kei_lisp.Cons(values[0]);
|
|
343
|
+
let tail = head;
|
|
344
|
+
for (let index = 1; index < values.length; index++) {
|
|
345
|
+
tail.cdr = new kei_lisp.Cons(values[index]);
|
|
346
|
+
tail = tail.cdr;
|
|
347
|
+
}
|
|
348
|
+
return head;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Applies `(offset color)` pairs to a gradient.
|
|
352
|
+
* @param gradient - the gradient to add stops to
|
|
353
|
+
* @param stops - alternating numeric offsets (0–1) and CSS color strings
|
|
354
|
+
* @return true when every pair was valid and at least one was applied
|
|
355
|
+
*/
|
|
356
|
+
#applyGradientStops(gradient, stops) {
|
|
357
|
+
if (stops.length === 0 || stops.length % 2 !== 0) return false;
|
|
358
|
+
for (let index = 0; index < stops.length; index += 2) {
|
|
359
|
+
const offset = stops[index];
|
|
360
|
+
const color = stops[index + 1];
|
|
361
|
+
if (!kei_lisp.Cons.isNumber(offset) || !kei_lisp.Cons.isString(color)) return false;
|
|
362
|
+
gradient.addColorStop(offset, color);
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Sets both `fillStyle` and `strokeStyle` to the given gradient after
|
|
368
|
+
* applying its stops, mirroring how `gcolor` sets both styles at once.
|
|
369
|
+
* @param context - the 2D context
|
|
370
|
+
* @param gradient - the gradient to install
|
|
371
|
+
* @param stops - alternating offsets and colors (see #applyGradientStops)
|
|
372
|
+
* @return `t` on success, `null` on invalid stops
|
|
373
|
+
*/
|
|
374
|
+
#installGradient(context, gradient, stops) {
|
|
375
|
+
if (!this.#applyGradientStops(gradient, stops)) return null;
|
|
376
|
+
context.fillStyle = gradient;
|
|
377
|
+
context.strokeStyle = gradient;
|
|
378
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Encodes the canvas via `toDataURL` and triggers a browser download through
|
|
382
|
+
* a temporary `<a download>` element. Requires a DOM (`document`) and an
|
|
383
|
+
* `HTMLCanvasElement`; `OffscreenCanvas` has no `toDataURL`, and Node.js has
|
|
384
|
+
* no `document` — those callers must pass a file path instead.
|
|
385
|
+
* @param mimeType - the image MIME type to encode
|
|
386
|
+
* @param label - the format name used in diagnostics ("jpeg" / "png")
|
|
387
|
+
* @return `t` on success, `Cons.nil` otherwise
|
|
388
|
+
*/
|
|
389
|
+
#downloadCanvas(mimeType, label) {
|
|
390
|
+
if (typeof document === "undefined" || !("toDataURL" in this.canvas)) {
|
|
391
|
+
this.#print(`Can not save ${label}. Browser download needs a DOM and an HTMLCanvasElement; pass a file path to save on Node.js.`);
|
|
392
|
+
return kei_lisp.Cons.nil;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const link = document.createElement("a");
|
|
396
|
+
link.href = this.canvas.toDataURL(mimeType);
|
|
397
|
+
link.download = "canvas";
|
|
398
|
+
document.body.append(link);
|
|
399
|
+
link.click();
|
|
400
|
+
link.remove();
|
|
401
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
402
|
+
} catch {
|
|
403
|
+
this.#print(`Can not save ${label}. If you are using an image in the canvas, you can't save ${label}.`);
|
|
404
|
+
return kei_lisp.Cons.nil;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Writes the encoded canvas image to a file path on Node.js, using
|
|
409
|
+
* `process.getBuiltinModule('node:fs')` so browser bundles never see a
|
|
410
|
+
* `node:fs` import. `HTMLCanvasElement` (and node-canvas) encode
|
|
411
|
+
* synchronously via `toDataURL`; `OffscreenCanvas` only offers the async
|
|
412
|
+
* `convertToBlob`, so its file is written after this returns — the same
|
|
413
|
+
* fire-and-forget contract as `gImage`.
|
|
414
|
+
* @param path - the destination file path
|
|
415
|
+
* @param mimeType - the image MIME type to encode
|
|
416
|
+
* @param label - the format name used in diagnostics ("jpeg" / "png")
|
|
417
|
+
* @return `t` on success, `Cons.nil` otherwise
|
|
418
|
+
*/
|
|
419
|
+
#writeCanvasToFile(path, mimeType, label) {
|
|
420
|
+
const fs = typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("node:fs") : void 0;
|
|
421
|
+
if (fs === void 0) {
|
|
422
|
+
this.#print(`Can not save ${label}. Saving to a file path requires Node.js.`);
|
|
423
|
+
return kei_lisp.Cons.nil;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
if ("toDataURL" in this.canvas) {
|
|
427
|
+
const dataUrl = this.canvas.toDataURL(mimeType);
|
|
428
|
+
const base64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
|
|
429
|
+
fs.writeFileSync(path, Buffer.from(base64, "base64"));
|
|
430
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
431
|
+
}
|
|
432
|
+
this.canvas.convertToBlob({ type: mimeType }).then(async (blob) => {
|
|
433
|
+
fs.writeFileSync(path, new Uint8Array(await blob.arrayBuffer()));
|
|
434
|
+
});
|
|
435
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
436
|
+
} catch {
|
|
437
|
+
this.#print(`Can not save ${label}.`);
|
|
438
|
+
return kei_lisp.Cons.nil;
|
|
439
|
+
}
|
|
101
440
|
}
|
|
102
441
|
/**
|
|
103
442
|
* Returns true if this plugin handles the given symbol.
|
|
@@ -105,7 +444,7 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
105
444
|
* @return true if `apply` should be called
|
|
106
445
|
*/
|
|
107
446
|
has(aSymbol) {
|
|
108
|
-
return GraphicsPlugin.
|
|
447
|
+
return GraphicsPlugin.#builtInFunctions.has(aSymbol);
|
|
109
448
|
}
|
|
110
449
|
/**
|
|
111
450
|
* Dispatches the given symbol to the matching `g…` method.
|
|
@@ -115,39 +454,17 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
115
454
|
* @return the method's result, or `Cons.nil` if dispatch fails
|
|
116
455
|
*/
|
|
117
456
|
apply(aSymbol, arguments_, _context) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* @return the method's result, or `Cons.nil` if not registered
|
|
125
|
-
*/
|
|
126
|
-
selectProcedure(procedure, arguments_) {
|
|
127
|
-
if (GraphicsPlugin.buildInFunctions.has(procedure)) return this.buildInFunction(procedure, arguments_);
|
|
128
|
-
this.#print(`I could find no procedure description for ${String(procedure)}`);
|
|
129
|
-
return kei_lisp.Cons.nil;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Looks up and invokes the JS method that implements the given Lisp symbol.
|
|
133
|
-
* Throws `TypeError` when the dispatch table points to a method that does
|
|
134
|
-
* not exist on the instance — this matches the legacy behavior, where the
|
|
135
|
-
* Ramda `R.invoker(1, methodName)(args, this)` call would surface the same
|
|
136
|
-
* error by attempting to call `undefined`.
|
|
137
|
-
* @param procedure - the Lisp symbol
|
|
138
|
-
* @param arguments_ - the evaluated argument list
|
|
139
|
-
* @return the method's result
|
|
140
|
-
*/
|
|
141
|
-
buildInFunction(procedure, arguments_) {
|
|
142
|
-
const methodName = GraphicsPlugin.buildInFunctions.get(procedure);
|
|
143
|
-
const method = this[methodName];
|
|
144
|
-
if (typeof method !== "function") throw new TypeError(`${this.constructor.name} does not have a method named "${methodName}"`);
|
|
145
|
-
return method.call(this, arguments_);
|
|
457
|
+
const methodName = GraphicsPlugin.#builtInFunctions.get(aSymbol);
|
|
458
|
+
if (methodName === void 0) {
|
|
459
|
+
this.#print(`I could find no procedure description for ${String(aSymbol)}`);
|
|
460
|
+
return kei_lisp.Cons.nil;
|
|
461
|
+
}
|
|
462
|
+
return this[methodName].call(this, arguments_);
|
|
146
463
|
}
|
|
147
464
|
/**
|
|
148
465
|
* Checks whether the canvas exposes a usable 2D context, and narrows
|
|
149
466
|
* `this.ctx` to non-null for the caller when it returns true.
|
|
150
|
-
* @return type predicate — true when
|
|
467
|
+
* @return type predicate — true when context is non-null
|
|
151
468
|
*/
|
|
152
469
|
checkSupport() {
|
|
153
470
|
if (this.ctx === null) {
|
|
@@ -256,19 +573,15 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
256
573
|
this.#print("The canvas is closed and cannot be executed.");
|
|
257
574
|
return kei_lisp.Cons.nil;
|
|
258
575
|
}
|
|
259
|
-
gClear() {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
this.
|
|
576
|
+
gClear(arguments_) {
|
|
577
|
+
return this.#execute("Can not clear.", (context) => {
|
|
578
|
+
const aColor = arguments_.length() === 0 ? "#ffffff" : this.selectColor(arguments_);
|
|
579
|
+
const previous = context.fillStyle;
|
|
580
|
+
context.fillStyle = aColor;
|
|
581
|
+
context.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
582
|
+
context.fillStyle = previous;
|
|
265
583
|
return kei_lisp.InterpretedSymbol.of("t");
|
|
266
|
-
}
|
|
267
|
-
this.#print("Can not clear.");
|
|
268
|
-
return kei_lisp.Cons.nil;
|
|
269
|
-
}
|
|
270
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
271
|
-
return kei_lisp.Cons.nil;
|
|
584
|
+
});
|
|
272
585
|
}
|
|
273
586
|
gClose() {
|
|
274
587
|
if (!this.checkSupport()) return kei_lisp.Cons.nil;
|
|
@@ -355,26 +668,17 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
355
668
|
return kei_lisp.Cons.nil;
|
|
356
669
|
}
|
|
357
670
|
gFillText(arguments_) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
this.#print("Can not draw fill text.");
|
|
371
|
-
return kei_lisp.Cons.nil;
|
|
372
|
-
} catch {
|
|
373
|
-
this.#print("Can not draw fill text.");
|
|
374
|
-
return kei_lisp.Cons.nil;
|
|
375
|
-
}
|
|
376
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
377
|
-
return kei_lisp.Cons.nil;
|
|
671
|
+
return this.#execute("Can not draw fill text.", (context) => {
|
|
672
|
+
const length_ = arguments_.length();
|
|
673
|
+
if (length_ !== 3 && length_ !== 4) return null;
|
|
674
|
+
const [a0, a1, a2, a3] = this.#listValues(arguments_);
|
|
675
|
+
if (!kei_lisp.Cons.isString(a0) || !kei_lisp.Cons.isNumber(a1) || !kei_lisp.Cons.isNumber(a2)) return null;
|
|
676
|
+
if (length_ === 4) {
|
|
677
|
+
if (!kei_lisp.Cons.isNumber(a3)) return null;
|
|
678
|
+
context.fillText(a0, a1, a2, a3);
|
|
679
|
+
} else context.fillText(a0, a1, a2);
|
|
680
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
681
|
+
});
|
|
378
682
|
}
|
|
379
683
|
gFillTri(arguments_) {
|
|
380
684
|
if (!this.checkSupport()) return kei_lisp.Cons.nil;
|
|
@@ -421,50 +725,21 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
421
725
|
return kei_lisp.Cons.nil;
|
|
422
726
|
}
|
|
423
727
|
gImage(arguments_) {
|
|
424
|
-
|
|
425
|
-
if (this.isOpen) try {
|
|
728
|
+
return this.#execute("Can not draw Image.", (context) => {
|
|
426
729
|
const length_ = arguments_.length();
|
|
427
|
-
if (length_
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
} else if (length_ === 5) {
|
|
442
|
-
const a0 = arguments_.car;
|
|
443
|
-
const cdr1 = arguments_.cdr;
|
|
444
|
-
const a1 = cdr1.car;
|
|
445
|
-
const cdr2 = cdr1.cdr;
|
|
446
|
-
const a2 = cdr2.car;
|
|
447
|
-
const cdr3 = cdr2.cdr;
|
|
448
|
-
const a3 = cdr3.car;
|
|
449
|
-
const a4 = cdr3.cdr.car;
|
|
450
|
-
if (kei_lisp.Cons.isString(a0) && kei_lisp.Cons.isNumber(a1) && kei_lisp.Cons.isNumber(a2) && kei_lisp.Cons.isNumber(a3) && kei_lisp.Cons.isNumber(a4)) {
|
|
451
|
-
const context = this.ctx;
|
|
452
|
-
const anImage = new Image();
|
|
453
|
-
anImage.src = a0;
|
|
454
|
-
anImage.onload = () => {
|
|
455
|
-
context.drawImage(anImage, a1, a2, a3, a4);
|
|
456
|
-
};
|
|
457
|
-
return kei_lisp.InterpretedSymbol.of("t");
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
this.#print("Can not draw Image.");
|
|
461
|
-
return kei_lisp.Cons.nil;
|
|
462
|
-
} catch {
|
|
463
|
-
this.#print("Can not draw Image.");
|
|
464
|
-
return kei_lisp.Cons.nil;
|
|
465
|
-
}
|
|
466
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
467
|
-
return kei_lisp.Cons.nil;
|
|
730
|
+
if (length_ !== 3 && length_ !== 5) return null;
|
|
731
|
+
const [a0, a1, a2, a3, a4] = this.#listValues(arguments_);
|
|
732
|
+
if (!kei_lisp.Cons.isString(a0) || !kei_lisp.Cons.isNumber(a1) || !kei_lisp.Cons.isNumber(a2)) return null;
|
|
733
|
+
if (length_ === 5) {
|
|
734
|
+
if (!kei_lisp.Cons.isNumber(a3) || !kei_lisp.Cons.isNumber(a4)) return null;
|
|
735
|
+
this.#withImage(a0, (image) => {
|
|
736
|
+
context.drawImage(image, a1, a2, a3, a4);
|
|
737
|
+
});
|
|
738
|
+
} else this.#withImage(a0, (image) => {
|
|
739
|
+
context.drawImage(image, a1, a2);
|
|
740
|
+
});
|
|
741
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
742
|
+
});
|
|
468
743
|
}
|
|
469
744
|
gLineTo(arguments_) {
|
|
470
745
|
if (!this.checkSupport()) return kei_lisp.Cons.nil;
|
|
@@ -487,38 +762,20 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
487
762
|
return kei_lisp.Cons.nil;
|
|
488
763
|
}
|
|
489
764
|
gLineCap(arguments_) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
this.#print("Can not set line cap.");
|
|
498
|
-
return kei_lisp.Cons.nil;
|
|
499
|
-
} catch {
|
|
500
|
-
this.#print("Can not set line cap.");
|
|
501
|
-
return kei_lisp.Cons.nil;
|
|
502
|
-
}
|
|
503
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
504
|
-
return kei_lisp.Cons.nil;
|
|
765
|
+
return this.#execute("Can not set line cap. Expected \"butt\" / \"round\" / \"square\".", (context) => {
|
|
766
|
+
const value = this.#enumString(arguments_, LINE_CAPS);
|
|
767
|
+
if (value === null) return null;
|
|
768
|
+
context.lineCap = value;
|
|
769
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
770
|
+
});
|
|
505
771
|
}
|
|
506
772
|
gLineJoin(arguments_) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
this.#print("Can not set line join.");
|
|
515
|
-
return kei_lisp.Cons.nil;
|
|
516
|
-
} catch {
|
|
517
|
-
this.#print("Can not set line join.");
|
|
518
|
-
return kei_lisp.Cons.nil;
|
|
519
|
-
}
|
|
520
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
521
|
-
return kei_lisp.Cons.nil;
|
|
773
|
+
return this.#execute("Can not set line join. Expected \"miter\" / \"round\" / \"bevel\".", (context) => {
|
|
774
|
+
const value = this.#enumString(arguments_, LINE_JOINS);
|
|
775
|
+
if (value === null) return null;
|
|
776
|
+
context.lineJoin = value;
|
|
777
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
778
|
+
});
|
|
522
779
|
}
|
|
523
780
|
gLineWidth(arguments_) {
|
|
524
781
|
if (!this.checkSupport()) return kei_lisp.Cons.nil;
|
|
@@ -574,30 +831,20 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
574
831
|
return kei_lisp.Cons.nil;
|
|
575
832
|
}
|
|
576
833
|
gPattern(arguments_) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
anImage.src = a0;
|
|
587
|
-
anImage.onload = () => {
|
|
588
|
-
context.fillStyle = context.createPattern(anImage, aString);
|
|
589
|
-
};
|
|
590
|
-
return kei_lisp.InterpretedSymbol.of("t");
|
|
834
|
+
return this.#execute("Can not set pattern. Expected an image source and a repetition (\"repeat\" / \"repeat-x\" / \"repeat-y\" / \"no-repeat\").", (context) => {
|
|
835
|
+
if (arguments_.length() !== 2) return null;
|
|
836
|
+
const [a0, a1] = this.#listValues(arguments_);
|
|
837
|
+
if (!kei_lisp.Cons.isString(a0) || !kei_lisp.Cons.isString(a1) || !PATTERN_REPETITIONS.has(a1)) return null;
|
|
838
|
+
this.#withImage(a0, (image) => {
|
|
839
|
+
const pattern = context.createPattern(image, a1);
|
|
840
|
+
if (pattern === null) {
|
|
841
|
+
this.#print("Can not set pattern. The image could not be used as a pattern.");
|
|
842
|
+
return;
|
|
591
843
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
return kei_lisp.
|
|
595
|
-
}
|
|
596
|
-
this.#print("Can not set pattern.");
|
|
597
|
-
return kei_lisp.Cons.nil;
|
|
598
|
-
}
|
|
599
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
600
|
-
return kei_lisp.Cons.nil;
|
|
844
|
+
context.fillStyle = pattern;
|
|
845
|
+
});
|
|
846
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
847
|
+
});
|
|
601
848
|
}
|
|
602
849
|
gQuadCurveTo(arguments_) {
|
|
603
850
|
if (!this.checkSupport()) return kei_lisp.Cons.nil;
|
|
@@ -623,41 +870,28 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
623
870
|
this.#print("The canvas is closed and cannot be executed.");
|
|
624
871
|
return kei_lisp.Cons.nil;
|
|
625
872
|
}
|
|
626
|
-
gSaveJpeg() {
|
|
627
|
-
|
|
628
|
-
if (this.isOpen) try {
|
|
629
|
-
const anImage = new Image();
|
|
630
|
-
anImage.crossOrigin = "Anonymous";
|
|
631
|
-
anImage.src = this.canvas.toDataURL("image/jpeg");
|
|
632
|
-
const link = document.createElement("a");
|
|
633
|
-
link.href = anImage.src;
|
|
634
|
-
link.download = "canvas";
|
|
635
|
-
document.body.append(link);
|
|
636
|
-
link.click();
|
|
637
|
-
link.remove();
|
|
638
|
-
return kei_lisp.InterpretedSymbol.of("t");
|
|
639
|
-
} catch {
|
|
640
|
-
this.#print("Can not save jpeg. If you are using an image in the canvas, you can't save jpeg.");
|
|
641
|
-
return kei_lisp.Cons.nil;
|
|
642
|
-
}
|
|
643
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
644
|
-
return kei_lisp.Cons.nil;
|
|
873
|
+
gSaveJpeg(arguments_) {
|
|
874
|
+
return this.saveCanvas(arguments_, "image/jpeg", "jpeg");
|
|
645
875
|
}
|
|
646
|
-
gSavePng() {
|
|
876
|
+
gSavePng(arguments_) {
|
|
877
|
+
return this.saveCanvas(arguments_, "image/png", "png");
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Shared implementation of `gsave-jpeg` / `gsave-png`. With no argument it
|
|
881
|
+
* triggers a browser download; with a single string argument it writes the
|
|
882
|
+
* encoded image to that file path on Node.js.
|
|
883
|
+
* @param arguments_ - the evaluated argument list (empty, or one path string)
|
|
884
|
+
* @param mimeType - the image MIME type to encode
|
|
885
|
+
* @param label - the format name used in diagnostics ("jpeg" / "png")
|
|
886
|
+
* @return `t` on success, `Cons.nil` otherwise
|
|
887
|
+
*/
|
|
888
|
+
saveCanvas(arguments_, mimeType, label) {
|
|
647
889
|
if (!this.checkSupport()) return kei_lisp.Cons.nil;
|
|
648
|
-
if (this.isOpen)
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
link.href = anImage.src;
|
|
654
|
-
link.download = "canvas";
|
|
655
|
-
document.body.append(link);
|
|
656
|
-
link.click();
|
|
657
|
-
link.remove();
|
|
658
|
-
return kei_lisp.InterpretedSymbol.of("t");
|
|
659
|
-
} catch {
|
|
660
|
-
this.#print("Can not save png. If you are using an image in the canvas, you can't save png.");
|
|
890
|
+
if (this.isOpen) {
|
|
891
|
+
const length_ = arguments_.length();
|
|
892
|
+
if (length_ === 0) return this.#downloadCanvas(mimeType, label);
|
|
893
|
+
if (length_ === 1 && kei_lisp.Cons.isString(arguments_.car)) return this.#writeCanvasToFile(arguments_.car, mimeType, label);
|
|
894
|
+
this.#print(`Can not save ${label}.`);
|
|
661
895
|
return kei_lisp.Cons.nil;
|
|
662
896
|
}
|
|
663
897
|
this.#print("The canvas is closed and cannot be executed.");
|
|
@@ -822,26 +1056,17 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
822
1056
|
return kei_lisp.Cons.nil;
|
|
823
1057
|
}
|
|
824
1058
|
gStrokeText(arguments_) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if (
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
this.#print("Can not draw stroke text.");
|
|
838
|
-
return kei_lisp.Cons.nil;
|
|
839
|
-
} catch {
|
|
840
|
-
this.#print("Can not draw stroke text.");
|
|
841
|
-
return kei_lisp.Cons.nil;
|
|
842
|
-
}
|
|
843
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
844
|
-
return kei_lisp.Cons.nil;
|
|
1059
|
+
return this.#execute("Can not draw stroke text.", (context) => {
|
|
1060
|
+
const length_ = arguments_.length();
|
|
1061
|
+
if (length_ !== 3 && length_ !== 4) return null;
|
|
1062
|
+
const [a0, a1, a2, a3] = this.#listValues(arguments_);
|
|
1063
|
+
if (!kei_lisp.Cons.isString(a0) || !kei_lisp.Cons.isNumber(a1) || !kei_lisp.Cons.isNumber(a2)) return null;
|
|
1064
|
+
if (length_ === 4) {
|
|
1065
|
+
if (!kei_lisp.Cons.isNumber(a3)) return null;
|
|
1066
|
+
context.strokeText(a0, a1, a2, a3);
|
|
1067
|
+
} else context.strokeText(a0, a1, a2);
|
|
1068
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1069
|
+
});
|
|
845
1070
|
}
|
|
846
1071
|
gStrokeTri(arguments_) {
|
|
847
1072
|
if (!this.checkSupport()) return kei_lisp.Cons.nil;
|
|
@@ -877,53 +1102,28 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
877
1102
|
return kei_lisp.Cons.nil;
|
|
878
1103
|
}
|
|
879
1104
|
gTextAlign(arguments_) {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
this.#print("Can not set text align.");
|
|
887
|
-
return kei_lisp.Cons.nil;
|
|
888
|
-
} catch {
|
|
889
|
-
this.#print("Can not set text align.");
|
|
890
|
-
return kei_lisp.Cons.nil;
|
|
891
|
-
}
|
|
892
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
893
|
-
return kei_lisp.Cons.nil;
|
|
1105
|
+
return this.#execute("Can not set text align. Expected \"left\" / \"right\" / \"center\" / \"start\" / \"end\".", (context) => {
|
|
1106
|
+
const value = this.#enumString(arguments_, TEXT_ALIGNS);
|
|
1107
|
+
if (value === null) return null;
|
|
1108
|
+
context.textAlign = value;
|
|
1109
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1110
|
+
});
|
|
894
1111
|
}
|
|
895
1112
|
gTextBaseline(arguments_) {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
if (
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
this.#print("Can not set text baseline.");
|
|
903
|
-
return kei_lisp.Cons.nil;
|
|
904
|
-
} catch {
|
|
905
|
-
this.#print("Can not set text baseline.");
|
|
906
|
-
return kei_lisp.Cons.nil;
|
|
907
|
-
}
|
|
908
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
909
|
-
return kei_lisp.Cons.nil;
|
|
1113
|
+
return this.#execute("Can not set text baseline. Expected \"top\" / \"hanging\" / \"middle\" / \"alphabetic\" / \"ideographic\" / \"bottom\".", (context) => {
|
|
1114
|
+
const value = this.#enumString(arguments_, TEXT_BASELINES);
|
|
1115
|
+
if (value === null) return null;
|
|
1116
|
+
context.textBaseline = value;
|
|
1117
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1118
|
+
});
|
|
910
1119
|
}
|
|
911
1120
|
gTextDirection(arguments_) {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
}
|
|
919
|
-
this.#print("Can not set text direction.");
|
|
920
|
-
return kei_lisp.Cons.nil;
|
|
921
|
-
} catch {
|
|
922
|
-
this.#print("Can not set text direction.");
|
|
923
|
-
return kei_lisp.Cons.nil;
|
|
924
|
-
}
|
|
925
|
-
this.#print("The canvas is closed and cannot be executed.");
|
|
926
|
-
return kei_lisp.Cons.nil;
|
|
1121
|
+
return this.#execute("Can not set text direction. Expected \"ltr\" / \"rtl\" / \"inherit\".", (context) => {
|
|
1122
|
+
const value = this.#enumString(arguments_, TEXT_DIRECTIONS);
|
|
1123
|
+
if (value === null) return null;
|
|
1124
|
+
context.direction = value;
|
|
1125
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1126
|
+
});
|
|
927
1127
|
}
|
|
928
1128
|
gTextFont(arguments_) {
|
|
929
1129
|
if (!this.checkSupport()) return kei_lisp.Cons.nil;
|
|
@@ -1004,7 +1204,7 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
1004
1204
|
/**
|
|
1005
1205
|
* Pushes the current drawing state (styles, transform, clip) onto the
|
|
1006
1206
|
* context's state stack. Pairs with `gRestore` to let Lisp callers manage
|
|
1007
|
-
* state explicitly. Replaces the legacy per-method `
|
|
1207
|
+
* state explicitly. Replaces the legacy per-method `context.save()` calls, which
|
|
1008
1208
|
* pushed state on every draw with no matching `restore()` and grew the stack
|
|
1009
1209
|
* unbounded.
|
|
1010
1210
|
* @return `t` on success, `Cons.nil` otherwise
|
|
@@ -1038,6 +1238,234 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
|
|
|
1038
1238
|
this.#print("The canvas is closed and cannot be executed.");
|
|
1039
1239
|
return kei_lisp.Cons.nil;
|
|
1040
1240
|
}
|
|
1241
|
+
gEllipse(arguments_) {
|
|
1242
|
+
return this.#execute("Can not draw ellipse.", (context) => {
|
|
1243
|
+
const a = this.#numbers(arguments_, 8);
|
|
1244
|
+
if (a === null) return null;
|
|
1245
|
+
context.ellipse(a[0], a[1], a[2], a[3], Math.PI / 180 * a[4], Math.PI / 180 * a[5], Math.PI / 180 * a[6], a[7] >= 0);
|
|
1246
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
gRoundRect(arguments_) {
|
|
1250
|
+
return this.#execute("Can not draw round rectangle.", (context) => {
|
|
1251
|
+
const a = this.#numbers(arguments_, 5);
|
|
1252
|
+
if (a === null) return null;
|
|
1253
|
+
context.roundRect(a[0], a[1], a[2], a[3], a[4]);
|
|
1254
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
gLineDash(arguments_) {
|
|
1258
|
+
return this.#execute("Can not set line dash.", (context) => {
|
|
1259
|
+
const segments = this.#numbers(arguments_, arguments_.length());
|
|
1260
|
+
if (segments === null) return null;
|
|
1261
|
+
context.setLineDash(segments);
|
|
1262
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
gLineDashOffset(arguments_) {
|
|
1266
|
+
return this.#execute("Can not set line dash offset.", (context) => {
|
|
1267
|
+
const a = this.#numbers(arguments_, 1);
|
|
1268
|
+
if (a === null) return null;
|
|
1269
|
+
context.lineDashOffset = a[0];
|
|
1270
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
gMiterLimit(arguments_) {
|
|
1274
|
+
return this.#execute("Can not set miter limit.", (context) => {
|
|
1275
|
+
const a = this.#numbers(arguments_, 1);
|
|
1276
|
+
if (a === null) return null;
|
|
1277
|
+
context.miterLimit = a[0];
|
|
1278
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
gClip() {
|
|
1282
|
+
return this.#execute("Can not clip.", (context) => {
|
|
1283
|
+
context.clip();
|
|
1284
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
gIsPointInPath(arguments_) {
|
|
1288
|
+
return this.#execute("Can not test point in path.", (context) => {
|
|
1289
|
+
const a = this.#numbers(arguments_, 2);
|
|
1290
|
+
if (a === null) return null;
|
|
1291
|
+
return context.isPointInPath(a[0], a[1]) ? kei_lisp.InterpretedSymbol.of("t") : kei_lisp.Cons.nil;
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
gIsPointInStroke(arguments_) {
|
|
1295
|
+
return this.#execute("Can not test point in stroke.", (context) => {
|
|
1296
|
+
const a = this.#numbers(arguments_, 2);
|
|
1297
|
+
if (a === null) return null;
|
|
1298
|
+
return context.isPointInStroke(a[0], a[1]) ? kei_lisp.InterpretedSymbol.of("t") : kei_lisp.Cons.nil;
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
gTransform(arguments_) {
|
|
1302
|
+
return this.#execute("Can not transform.", (context) => {
|
|
1303
|
+
const a = this.#numbers(arguments_, 6);
|
|
1304
|
+
if (a === null) return null;
|
|
1305
|
+
context.transform(a[0], a[1], a[2], a[3], a[4], a[5]);
|
|
1306
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
gSetTransform(arguments_) {
|
|
1310
|
+
return this.#execute("Can not set transform.", (context) => {
|
|
1311
|
+
const a = this.#numbers(arguments_, 6);
|
|
1312
|
+
if (a === null) return null;
|
|
1313
|
+
context.setTransform(a[0], a[1], a[2], a[3], a[4], a[5]);
|
|
1314
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
gResetTransform() {
|
|
1318
|
+
return this.#execute("Can not reset transform.", (context) => {
|
|
1319
|
+
context.resetTransform();
|
|
1320
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
gComposite(arguments_) {
|
|
1324
|
+
return this.#execute("Can not set composite operation. Expected a globalCompositeOperation keyword such as \"source-over\" / \"multiply\" / \"screen\".", (context) => {
|
|
1325
|
+
const value = this.#enumString(arguments_, COMPOSITE_OPERATIONS);
|
|
1326
|
+
if (value === null) return null;
|
|
1327
|
+
context.globalCompositeOperation = value;
|
|
1328
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
gFilter(arguments_) {
|
|
1332
|
+
return this.#execute("Can not set filter.", (context) => {
|
|
1333
|
+
if (arguments_.length() !== 1 || !kei_lisp.Cons.isString(arguments_.car)) return null;
|
|
1334
|
+
context.filter = arguments_.car;
|
|
1335
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
gImageSmoothing(arguments_) {
|
|
1339
|
+
return this.#execute("Can not set image smoothing.", (context) => {
|
|
1340
|
+
const quality = arguments_.length() === 1 ? arguments_.car : null;
|
|
1341
|
+
if (quality !== "off" && quality !== "low" && quality !== "medium" && quality !== "high") return null;
|
|
1342
|
+
if (quality === "off") context.imageSmoothingEnabled = false;
|
|
1343
|
+
else {
|
|
1344
|
+
context.imageSmoothingEnabled = true;
|
|
1345
|
+
context.imageSmoothingQuality = quality;
|
|
1346
|
+
}
|
|
1347
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
gMeasureText(arguments_) {
|
|
1351
|
+
return this.#execute("Can not measure text.", (context) => {
|
|
1352
|
+
if (arguments_.length() !== 1 || !kei_lisp.Cons.isString(arguments_.car)) return null;
|
|
1353
|
+
return context.measureText(arguments_.car).width;
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
gLetterSpacing(arguments_) {
|
|
1357
|
+
return this.#execute("Can not set letter spacing.", (context) => {
|
|
1358
|
+
if (arguments_.length() !== 1 || !kei_lisp.Cons.isString(arguments_.car)) return null;
|
|
1359
|
+
context.letterSpacing = arguments_.car;
|
|
1360
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
gWordSpacing(arguments_) {
|
|
1364
|
+
return this.#execute("Can not set word spacing.", (context) => {
|
|
1365
|
+
if (arguments_.length() !== 1 || !kei_lisp.Cons.isString(arguments_.car)) return null;
|
|
1366
|
+
context.wordSpacing = arguments_.car;
|
|
1367
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
gFontKerning(arguments_) {
|
|
1371
|
+
return this.#execute("Can not set font kerning. Expected \"auto\" / \"normal\" / \"none\".", (context) => {
|
|
1372
|
+
const value = this.#enumString(arguments_, FONT_KERNINGS);
|
|
1373
|
+
if (value === null) return null;
|
|
1374
|
+
context.fontKerning = value;
|
|
1375
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
gFontStretch(arguments_) {
|
|
1379
|
+
return this.#execute("Can not set font stretch. Expected a font-stretch keyword such as \"condensed\" / \"normal\" / \"expanded\".", (context) => {
|
|
1380
|
+
const value = this.#enumString(arguments_, FONT_STRETCHES);
|
|
1381
|
+
if (value === null) return null;
|
|
1382
|
+
context.fontStretch = value;
|
|
1383
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
gFontVariant(arguments_) {
|
|
1387
|
+
return this.#execute("Can not set font variant. Expected a font-variant-caps keyword such as \"normal\" / \"small-caps\".", (context) => {
|
|
1388
|
+
const value = this.#enumString(arguments_, FONT_VARIANTS);
|
|
1389
|
+
if (value === null) return null;
|
|
1390
|
+
context.fontVariantCaps = value;
|
|
1391
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
gTextRendering(arguments_) {
|
|
1395
|
+
return this.#execute("Can not set text rendering. Expected \"auto\" / \"optimizeSpeed\" / \"optimizeLegibility\" / \"geometricPrecision\".", (context) => {
|
|
1396
|
+
const value = this.#enumString(arguments_, TEXT_RENDERINGS);
|
|
1397
|
+
if (value === null) return null;
|
|
1398
|
+
context.textRendering = value;
|
|
1399
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
gClearRect(arguments_) {
|
|
1403
|
+
return this.#execute("Can not clear rectangle.", (context) => {
|
|
1404
|
+
const a = this.#numbers(arguments_, 4);
|
|
1405
|
+
if (a === null) return null;
|
|
1406
|
+
context.clearRect(a[0], a[1], a[2], a[3]);
|
|
1407
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
gReset() {
|
|
1411
|
+
return this.#execute("Can not reset.", (context) => {
|
|
1412
|
+
context.reset();
|
|
1413
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
gWidth() {
|
|
1417
|
+
return this.#execute("Can not get width.", () => this.canvas.width);
|
|
1418
|
+
}
|
|
1419
|
+
gHeight() {
|
|
1420
|
+
return this.#execute("Can not get height.", () => this.canvas.height);
|
|
1421
|
+
}
|
|
1422
|
+
gPixel(arguments_) {
|
|
1423
|
+
return this.#execute("Can not read pixel.", (context) => {
|
|
1424
|
+
const a = this.#numbers(arguments_, 2);
|
|
1425
|
+
if (a === null) return null;
|
|
1426
|
+
const data = context.getImageData(a[0], a[1], 1, 1).data;
|
|
1427
|
+
return this.#toList([
|
|
1428
|
+
data[0],
|
|
1429
|
+
data[1],
|
|
1430
|
+
data[2],
|
|
1431
|
+
data[3]
|
|
1432
|
+
]);
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
gSetPixel(arguments_) {
|
|
1436
|
+
return this.#execute("Can not write pixel.", (context) => {
|
|
1437
|
+
const a = this.#numbers(arguments_, 6);
|
|
1438
|
+
if (a === null) return null;
|
|
1439
|
+
const imageData = context.createImageData(1, 1);
|
|
1440
|
+
imageData.data[0] = a[2];
|
|
1441
|
+
imageData.data[1] = a[3];
|
|
1442
|
+
imageData.data[2] = a[4];
|
|
1443
|
+
imageData.data[3] = a[5];
|
|
1444
|
+
context.putImageData(imageData, a[0], a[1]);
|
|
1445
|
+
return kei_lisp.InterpretedSymbol.of("t");
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
gLinearGradient(arguments_) {
|
|
1449
|
+
return this.#execute("Can not set linear gradient.", (context) => {
|
|
1450
|
+
const [x0, y0, x1, y1, ...stops] = this.#listValues(arguments_);
|
|
1451
|
+
if (stops.length < 2 || !kei_lisp.Cons.isNumber(x0) || !kei_lisp.Cons.isNumber(y0) || !kei_lisp.Cons.isNumber(x1) || !kei_lisp.Cons.isNumber(y1)) return null;
|
|
1452
|
+
return this.#installGradient(context, context.createLinearGradient(x0, y0, x1, y1), stops);
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
gRadialGradient(arguments_) {
|
|
1456
|
+
return this.#execute("Can not set radial gradient.", (context) => {
|
|
1457
|
+
const [x0, y0, r0, x1, y1, r1, ...stops] = this.#listValues(arguments_);
|
|
1458
|
+
if (stops.length < 2 || !kei_lisp.Cons.isNumber(x0) || !kei_lisp.Cons.isNumber(y0) || !kei_lisp.Cons.isNumber(r0) || !kei_lisp.Cons.isNumber(x1) || !kei_lisp.Cons.isNumber(y1) || !kei_lisp.Cons.isNumber(r1)) return null;
|
|
1459
|
+
return this.#installGradient(context, context.createRadialGradient(x0, y0, r0, x1, y1, r1), stops);
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
gConicGradient(arguments_) {
|
|
1463
|
+
return this.#execute("Can not set conic gradient.", (context) => {
|
|
1464
|
+
const [angle, x, y, ...stops] = this.#listValues(arguments_);
|
|
1465
|
+
if (stops.length < 2 || !kei_lisp.Cons.isNumber(angle) || !kei_lisp.Cons.isNumber(x) || !kei_lisp.Cons.isNumber(y)) return null;
|
|
1466
|
+
return this.#installGradient(context, context.createConicGradient(Math.PI / 180 * angle, x, y), stops);
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1041
1469
|
/**
|
|
1042
1470
|
* Parses a color spec from the head of the argument Cons. Accepts
|
|
1043
1471
|
* (1 string), (3 numbers — rgb), or (4 numbers — rgba); falls back to
|