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.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 45 `g…` Lisp functions that proxy to a 2D rendering
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 extends Object {
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 buildInFunctions = GraphicsPlugin.setup();
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-dire"), "gTextDirection");
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 directly to `process.stderr`, matching the
92
- * convention used by kei-lisp itself (`Applier.format` writes to
93
- * `process.stdout`). In a Node runtime this hits the real stderr; in a
94
- * browser kei-lisp host (e.g. kei-lisp-web) the host typically swaps
95
- * `process.stderr.write` for a sink that routes to the REPL output panel,
96
- * so the same call reaches the user via the host's normal output channel.
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
- process.stderr.write(line + "\n");
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.buildInFunctions.has(aSymbol);
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
- return this.selectProcedure(aSymbol, arguments_);
119
- }
120
- /**
121
- * Resolves the procedure name and invokes the matching method.
122
- * @param procedure - the Lisp symbol
123
- * @param arguments_ - the evaluated argument list
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 ctx is non-null
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
261
- if (this.isOpen) try {
262
- this.ctx.fillStyle = "#ffffff";
263
- this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
264
- this.ctx.fillStyle = "#000000";
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
- } catch {
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
359
- if (this.isOpen) try {
360
- if (arguments_.length() === 3) {
361
- const a0 = arguments_.car;
362
- const cdr1 = arguments_.cdr;
363
- const a1 = cdr1.car;
364
- const a2 = cdr1.cdr.car;
365
- if (kei_lisp.Cons.isString(a0) && kei_lisp.Cons.isNumber(a1) && kei_lisp.Cons.isNumber(a2)) {
366
- this.ctx.fillText(a0, a1, a2);
367
- return kei_lisp.InterpretedSymbol.of("t");
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
425
- if (this.isOpen) try {
728
+ return this.#execute("Can not draw Image.", (context) => {
426
729
  const length_ = arguments_.length();
427
- if (length_ === 3) {
428
- const a0 = arguments_.car;
429
- const cdr1 = arguments_.cdr;
430
- const a1 = cdr1.car;
431
- const a2 = cdr1.cdr.car;
432
- if (kei_lisp.Cons.isString(a0) && kei_lisp.Cons.isNumber(a1) && kei_lisp.Cons.isNumber(a2)) {
433
- const context = this.ctx;
434
- const anImage = new Image();
435
- anImage.src = a0;
436
- anImage.onload = () => {
437
- context.drawImage(anImage, a1, a2);
438
- };
439
- return kei_lisp.InterpretedSymbol.of("t");
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
491
- if (this.isOpen) try {
492
- if (arguments_.length() === 1 && kei_lisp.Cons.isNumber(arguments_.car)) {
493
- const aString = arguments_.car === 0 ? "butt" : arguments_.car > 0 ? "round" : "square";
494
- this.ctx.lineCap = aString;
495
- return kei_lisp.InterpretedSymbol.of("t");
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
508
- if (this.isOpen) try {
509
- if (arguments_.length() === 1 && kei_lisp.Cons.isNumber(arguments_.car)) {
510
- const aString = arguments_.car === 0 ? "miter" : arguments_.car > 0 ? "round" : "bevel";
511
- this.ctx.lineJoin = aString;
512
- return kei_lisp.InterpretedSymbol.of("t");
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
578
- if (this.isOpen) try {
579
- if (arguments_.length() === 2) {
580
- const a0 = arguments_.car;
581
- const a1 = arguments_.cdr.car;
582
- if (kei_lisp.Cons.isString(a0) && kei_lisp.Cons.isNumber(a1)) {
583
- const aString = a1 === 0 ? "repeat" : a1 > 0 ? "repeat-x" : "repeat-y";
584
- const context = this.ctx;
585
- const anImage = new Image();
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
- this.#print("Can not set pattern.");
594
- return kei_lisp.Cons.nil;
595
- } catch {
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
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) try {
649
- const anImage = new Image();
650
- anImage.crossOrigin = "Anonymous";
651
- anImage.src = this.canvas.toDataURL("image/png");
652
- const link = document.createElement("a");
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
826
- if (this.isOpen) try {
827
- if (arguments_.length() === 3) {
828
- const a0 = arguments_.car;
829
- const cdr1 = arguments_.cdr;
830
- const a1 = cdr1.car;
831
- const a2 = cdr1.cdr.car;
832
- if (kei_lisp.Cons.isString(a0) && kei_lisp.Cons.isNumber(a1) && kei_lisp.Cons.isNumber(a2)) {
833
- this.ctx.strokeText(a0, a1, a2);
834
- return kei_lisp.InterpretedSymbol.of("t");
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
881
- if (this.isOpen) try {
882
- if (arguments_.length() === 1 && kei_lisp.Cons.isString(arguments_.car)) {
883
- this.ctx.textAlign = arguments_.car;
884
- return kei_lisp.InterpretedSymbol.of("t");
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
897
- if (this.isOpen) try {
898
- if (arguments_.length() === 1 && kei_lisp.Cons.isString(arguments_.car)) {
899
- this.ctx.textBaseline = arguments_.car;
900
- return kei_lisp.InterpretedSymbol.of("t");
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
- if (!this.checkSupport()) return kei_lisp.Cons.nil;
913
- if (this.isOpen) try {
914
- if (arguments_.length() === 1 && kei_lisp.Cons.isNumber(arguments_.car)) {
915
- const aString = arguments_.car === 0 ? "inherit" : arguments_.car > 0 ? "rtl" : "ltr";
916
- this.ctx.direction = aString;
917
- return kei_lisp.InterpretedSymbol.of("t");
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 `ctx.save()` calls, which
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