kei-lisp-plugin-graphics 2.0.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,160 @@ 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");
100
378
  }
101
379
  /**
102
380
  * Encodes the canvas via `toDataURL` and triggers a browser download through
@@ -165,7 +443,7 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
165
443
  * @return true if `apply` should be called
166
444
  */
167
445
  has(aSymbol) {
168
- return GraphicsPlugin.buildInFunctions.has(aSymbol);
446
+ return GraphicsPlugin.#builtInFunctions.has(aSymbol);
169
447
  }
170
448
  /**
171
449
  * Dispatches the given symbol to the matching `g…` method.
@@ -175,39 +453,17 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
175
453
  * @return the method's result, or `Cons.nil` if dispatch fails
176
454
  */
177
455
  apply(aSymbol, arguments_, _context) {
178
- return this.selectProcedure(aSymbol, arguments_);
179
- }
180
- /**
181
- * Resolves the procedure name and invokes the matching method.
182
- * @param procedure - the Lisp symbol
183
- * @param arguments_ - the evaluated argument list
184
- * @return the method's result, or `Cons.nil` if not registered
185
- */
186
- selectProcedure(procedure, arguments_) {
187
- if (GraphicsPlugin.buildInFunctions.has(procedure)) return this.buildInFunction(procedure, arguments_);
188
- this.#print(`I could find no procedure description for ${String(procedure)}`);
189
- return Cons.nil;
190
- }
191
- /**
192
- * Looks up and invokes the JS method that implements the given Lisp symbol.
193
- * Throws `TypeError` when the dispatch table points to a method that does
194
- * not exist on the instance — this matches the legacy behavior, where the
195
- * Ramda `R.invoker(1, methodName)(args, this)` call would surface the same
196
- * error by attempting to call `undefined`.
197
- * @param procedure - the Lisp symbol
198
- * @param arguments_ - the evaluated argument list
199
- * @return the method's result
200
- */
201
- buildInFunction(procedure, arguments_) {
202
- const methodName = GraphicsPlugin.buildInFunctions.get(procedure);
203
- const method = this[methodName];
204
- if (typeof method !== "function") throw new TypeError(`${this.constructor.name} does not have a method named "${methodName}"`);
205
- 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_);
206
462
  }
207
463
  /**
208
464
  * Checks whether the canvas exposes a usable 2D context, and narrows
209
465
  * `this.ctx` to non-null for the caller when it returns true.
210
- * @return type predicate — true when ctx is non-null
466
+ * @return type predicate — true when context is non-null
211
467
  */
212
468
  checkSupport() {
213
469
  if (this.ctx === null) {
@@ -316,19 +572,15 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
316
572
  this.#print("The canvas is closed and cannot be executed.");
317
573
  return Cons.nil;
318
574
  }
319
- gClear() {
320
- if (!this.checkSupport()) return Cons.nil;
321
- if (this.isOpen) try {
322
- this.ctx.fillStyle = "#ffffff";
323
- this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
324
- 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;
325
582
  return InterpretedSymbol.of("t");
326
- } catch {
327
- this.#print("Can not clear.");
328
- return Cons.nil;
329
- }
330
- this.#print("The canvas is closed and cannot be executed.");
331
- return Cons.nil;
583
+ });
332
584
  }
333
585
  gClose() {
334
586
  if (!this.checkSupport()) return Cons.nil;
@@ -415,26 +667,17 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
415
667
  return Cons.nil;
416
668
  }
417
669
  gFillText(arguments_) {
418
- if (!this.checkSupport()) return Cons.nil;
419
- if (this.isOpen) try {
420
- if (arguments_.length() === 3) {
421
- const a0 = arguments_.car;
422
- const cdr1 = arguments_.cdr;
423
- const a1 = cdr1.car;
424
- const a2 = cdr1.cdr.car;
425
- if (Cons.isString(a0) && Cons.isNumber(a1) && Cons.isNumber(a2)) {
426
- this.ctx.fillText(a0, a1, a2);
427
- return InterpretedSymbol.of("t");
428
- }
429
- }
430
- this.#print("Can not draw fill text.");
431
- return Cons.nil;
432
- } catch {
433
- this.#print("Can not draw fill text.");
434
- return Cons.nil;
435
- }
436
- this.#print("The canvas is closed and cannot be executed.");
437
- 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
+ });
438
681
  }
439
682
  gFillTri(arguments_) {
440
683
  if (!this.checkSupport()) return Cons.nil;
@@ -481,50 +724,21 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
481
724
  return Cons.nil;
482
725
  }
483
726
  gImage(arguments_) {
484
- if (!this.checkSupport()) return Cons.nil;
485
- if (this.isOpen) try {
727
+ return this.#execute("Can not draw Image.", (context) => {
486
728
  const length_ = arguments_.length();
487
- if (length_ === 3) {
488
- const a0 = arguments_.car;
489
- const cdr1 = arguments_.cdr;
490
- const a1 = cdr1.car;
491
- const a2 = cdr1.cdr.car;
492
- if (Cons.isString(a0) && Cons.isNumber(a1) && Cons.isNumber(a2)) {
493
- const context = this.ctx;
494
- const anImage = new Image();
495
- anImage.src = a0;
496
- anImage.onload = () => {
497
- context.drawImage(anImage, a1, a2);
498
- };
499
- return InterpretedSymbol.of("t");
500
- }
501
- } else if (length_ === 5) {
502
- const a0 = arguments_.car;
503
- const cdr1 = arguments_.cdr;
504
- const a1 = cdr1.car;
505
- const cdr2 = cdr1.cdr;
506
- const a2 = cdr2.car;
507
- const cdr3 = cdr2.cdr;
508
- const a3 = cdr3.car;
509
- const a4 = cdr3.cdr.car;
510
- if (Cons.isString(a0) && Cons.isNumber(a1) && Cons.isNumber(a2) && Cons.isNumber(a3) && Cons.isNumber(a4)) {
511
- const context = this.ctx;
512
- const anImage = new Image();
513
- anImage.src = a0;
514
- anImage.onload = () => {
515
- context.drawImage(anImage, a1, a2, a3, a4);
516
- };
517
- return InterpretedSymbol.of("t");
518
- }
519
- }
520
- this.#print("Can not draw Image.");
521
- return Cons.nil;
522
- } catch {
523
- this.#print("Can not draw Image.");
524
- return Cons.nil;
525
- }
526
- this.#print("The canvas is closed and cannot be executed.");
527
- 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
+ });
528
742
  }
529
743
  gLineTo(arguments_) {
530
744
  if (!this.checkSupport()) return Cons.nil;
@@ -547,36 +761,20 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
547
761
  return Cons.nil;
548
762
  }
549
763
  gLineCap(arguments_) {
550
- if (!this.checkSupport()) return Cons.nil;
551
- if (this.isOpen) try {
552
- if (arguments_.length() === 1 && Cons.isString(arguments_.car)) {
553
- this.ctx.lineCap = arguments_.car;
554
- return InterpretedSymbol.of("t");
555
- }
556
- this.#print("Can not set line cap.");
557
- return Cons.nil;
558
- } catch {
559
- this.#print("Can not set line cap.");
560
- return Cons.nil;
561
- }
562
- this.#print("The canvas is closed and cannot be executed.");
563
- 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
+ });
564
770
  }
565
771
  gLineJoin(arguments_) {
566
- if (!this.checkSupport()) return Cons.nil;
567
- if (this.isOpen) try {
568
- if (arguments_.length() === 1 && Cons.isString(arguments_.car)) {
569
- this.ctx.lineJoin = arguments_.car;
570
- return InterpretedSymbol.of("t");
571
- }
572
- this.#print("Can not set line join.");
573
- return Cons.nil;
574
- } catch {
575
- this.#print("Can not set line join.");
576
- return Cons.nil;
577
- }
578
- this.#print("The canvas is closed and cannot be executed.");
579
- 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
+ });
580
778
  }
581
779
  gLineWidth(arguments_) {
582
780
  if (!this.checkSupport()) return Cons.nil;
@@ -632,30 +830,20 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
632
830
  return Cons.nil;
633
831
  }
634
832
  gPattern(arguments_) {
635
- if (!this.checkSupport()) return Cons.nil;
636
- if (this.isOpen) try {
637
- if (arguments_.length() === 2) {
638
- const a0 = arguments_.car;
639
- const a1 = arguments_.cdr.car;
640
- if (Cons.isString(a0) && Cons.isNumber(a1)) {
641
- const aString = a1 === 0 ? "repeat" : a1 > 0 ? "repeat-x" : "repeat-y";
642
- const context = this.ctx;
643
- const anImage = new Image();
644
- anImage.src = a0;
645
- anImage.onload = () => {
646
- context.fillStyle = context.createPattern(anImage, aString);
647
- };
648
- 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;
649
842
  }
650
- }
651
- this.#print("Can not set pattern.");
652
- return Cons.nil;
653
- } catch {
654
- this.#print("Can not set pattern.");
655
- return Cons.nil;
656
- }
657
- this.#print("The canvas is closed and cannot be executed.");
658
- return Cons.nil;
843
+ context.fillStyle = pattern;
844
+ });
845
+ return InterpretedSymbol.of("t");
846
+ });
659
847
  }
660
848
  gQuadCurveTo(arguments_) {
661
849
  if (!this.checkSupport()) return Cons.nil;
@@ -867,26 +1055,17 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
867
1055
  return Cons.nil;
868
1056
  }
869
1057
  gStrokeText(arguments_) {
870
- if (!this.checkSupport()) return Cons.nil;
871
- if (this.isOpen) try {
872
- if (arguments_.length() === 3) {
873
- const a0 = arguments_.car;
874
- const cdr1 = arguments_.cdr;
875
- const a1 = cdr1.car;
876
- const a2 = cdr1.cdr.car;
877
- if (Cons.isString(a0) && Cons.isNumber(a1) && Cons.isNumber(a2)) {
878
- this.ctx.strokeText(a0, a1, a2);
879
- return InterpretedSymbol.of("t");
880
- }
881
- }
882
- this.#print("Can not draw stroke text.");
883
- return Cons.nil;
884
- } catch {
885
- this.#print("Can not draw stroke text.");
886
- return Cons.nil;
887
- }
888
- this.#print("The canvas is closed and cannot be executed.");
889
- 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
+ });
890
1069
  }
891
1070
  gStrokeTri(arguments_) {
892
1071
  if (!this.checkSupport()) return Cons.nil;
@@ -922,52 +1101,28 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
922
1101
  return Cons.nil;
923
1102
  }
924
1103
  gTextAlign(arguments_) {
925
- if (!this.checkSupport()) return Cons.nil;
926
- if (this.isOpen) try {
927
- if (arguments_.length() === 1 && Cons.isString(arguments_.car)) {
928
- this.ctx.textAlign = arguments_.car;
929
- return InterpretedSymbol.of("t");
930
- }
931
- this.#print("Can not set text align.");
932
- return Cons.nil;
933
- } catch {
934
- this.#print("Can not set text align.");
935
- return Cons.nil;
936
- }
937
- this.#print("The canvas is closed and cannot be executed.");
938
- 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
+ });
939
1110
  }
940
1111
  gTextBaseline(arguments_) {
941
- if (!this.checkSupport()) return Cons.nil;
942
- if (this.isOpen) try {
943
- if (arguments_.length() === 1 && Cons.isString(arguments_.car)) {
944
- this.ctx.textBaseline = arguments_.car;
945
- return InterpretedSymbol.of("t");
946
- }
947
- this.#print("Can not set text baseline.");
948
- return Cons.nil;
949
- } catch {
950
- this.#print("Can not set text baseline.");
951
- return Cons.nil;
952
- }
953
- this.#print("The canvas is closed and cannot be executed.");
954
- 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
+ });
955
1118
  }
956
1119
  gTextDirection(arguments_) {
957
- if (!this.checkSupport()) return Cons.nil;
958
- if (this.isOpen) try {
959
- if (arguments_.length() === 1 && Cons.isString(arguments_.car)) {
960
- this.ctx.direction = arguments_.car;
961
- return InterpretedSymbol.of("t");
962
- }
963
- this.#print("Can not set text direction.");
964
- return Cons.nil;
965
- } catch {
966
- this.#print("Can not set text direction.");
967
- return Cons.nil;
968
- }
969
- this.#print("The canvas is closed and cannot be executed.");
970
- 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
+ });
971
1126
  }
972
1127
  gTextFont(arguments_) {
973
1128
  if (!this.checkSupport()) return Cons.nil;
@@ -1048,7 +1203,7 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
1048
1203
  /**
1049
1204
  * Pushes the current drawing state (styles, transform, clip) onto the
1050
1205
  * context's state stack. Pairs with `gRestore` to let Lisp callers manage
1051
- * state explicitly. Replaces the legacy per-method `ctx.save()` calls, which
1206
+ * state explicitly. Replaces the legacy per-method `context.save()` calls, which
1052
1207
  * pushed state on every draw with no matching `restore()` and grew the stack
1053
1208
  * unbounded.
1054
1209
  * @return `t` on success, `Cons.nil` otherwise
@@ -1082,6 +1237,234 @@ var GraphicsPlugin = class GraphicsPlugin extends Object {
1082
1237
  this.#print("The canvas is closed and cannot be executed.");
1083
1238
  return Cons.nil;
1084
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
+ }
1085
1468
  /**
1086
1469
  * Parses a color spec from the head of the argument Cons. Accepts
1087
1470
  * (1 string), (3 numbers — rgb), or (4 numbers — rgba); falls back to