kei-lisp-plugin-graphics 2.0.0 → 3.0.1

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