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