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