@koiosdigital/matrx-render 0.1.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.
@@ -0,0 +1,551 @@
1
+ // src/canvas.ts
2
+ var Canvas = class {
3
+ width;
4
+ height;
5
+ /** Premultiplied RGBA, length = width * height * 4. */
6
+ pix;
7
+ constructor(width, height) {
8
+ this.width = width;
9
+ this.height = height;
10
+ this.pix = new Uint8Array(width * height * 4);
11
+ }
12
+ /**
13
+ * Fill every pixel with `c`, ignoring any clip — mirrors gg.Clear()
14
+ * (draw.Src over the whole image). Used by Root for solid backgrounds.
15
+ */
16
+ clear(c) {
17
+ const a = c.a;
18
+ const r = Math.floor(c.r * 257 * a / 255) >> 8;
19
+ const g = Math.floor(c.g * 257 * a / 255) >> 8;
20
+ const b = Math.floor(c.b * 257 * a / 255) >> 8;
21
+ const { pix } = this;
22
+ for (let i = 0; i < pix.length; i += 4) {
23
+ pix[i] = r;
24
+ pix[i + 1] = g;
25
+ pix[i + 2] = b;
26
+ pix[i + 3] = a;
27
+ }
28
+ }
29
+ /** Premultiplied RGBA of pixel (x, y). Out of bounds returns zeros. */
30
+ at(x, y) {
31
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
32
+ return [0, 0, 0, 0];
33
+ }
34
+ const i = (y * this.width + x) * 4;
35
+ const p = this.pix;
36
+ return [p[i], p[i + 1], p[i + 2], p[i + 3]];
37
+ }
38
+ };
39
+
40
+ // src/color.ts
41
+ var WHITE = { r: 255, g: 255, b: 255, a: 255 };
42
+ var BLACK = { r: 0, g: 0, b: 0, a: 255 };
43
+ var TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 };
44
+ var HEX = /^[0-9a-fA-F]+$/;
45
+ function parseColor(scol) {
46
+ const s = scol.startsWith("#") ? scol.slice(1) : scol;
47
+ if (!HEX.test(s)) {
48
+ throw new Error(`color: ${s} is not a hex-color`);
49
+ }
50
+ let r, g, b, a;
51
+ switch (s.length) {
52
+ case 3: {
53
+ [r, g, b] = [0, 1, 2].map((i) => parseInt(s[i], 16));
54
+ a = 15;
55
+ break;
56
+ }
57
+ case 4: {
58
+ [r, g, b, a] = [0, 1, 2, 3].map((i) => parseInt(s[i], 16));
59
+ break;
60
+ }
61
+ case 6: {
62
+ [r, g, b] = [0, 2, 4].map((i) => parseInt(s.slice(i, i + 2), 16));
63
+ a = 255;
64
+ return { r, g, b, a };
65
+ }
66
+ case 8: {
67
+ [r, g, b, a] = [0, 2, 4, 6].map((i) => parseInt(s.slice(i, i + 2), 16));
68
+ return { r, g, b, a };
69
+ }
70
+ default:
71
+ throw new Error(`color: ${s} is not a hex-color`);
72
+ }
73
+ return { r: r | r << 4, g: g | g << 4, b: b | b << 4, a: a | a << 4 };
74
+ }
75
+ function color16(c) {
76
+ return {
77
+ r: Math.floor(c.r * 257 * c.a / 255),
78
+ g: Math.floor(c.g * 257 * c.a / 255),
79
+ b: Math.floor(c.b * 257 * c.a / 255),
80
+ a: c.a * 257
81
+ };
82
+ }
83
+
84
+ // src/blend.ts
85
+ var M = 65535;
86
+ function fillRectSolid(dst, x0, y0, x1, y1, c, mask) {
87
+ if (x0 < 0) x0 = 0;
88
+ if (y0 < 0) y0 = 0;
89
+ if (x1 > dst.width) x1 = dst.width;
90
+ if (y1 > dst.height) y1 = dst.height;
91
+ if (x0 >= x1 || y0 >= y1) return;
92
+ const { pix, width } = dst;
93
+ for (let y = y0; y < y1; y++) {
94
+ let i = (y * width + x0) * 4;
95
+ for (let x = x0; x < x1; x++, i += 4) {
96
+ let ma = M;
97
+ if (mask !== null) {
98
+ ma = Math.floor(ma * mask[y * width + x] / 255);
99
+ if (ma === 0) continue;
100
+ }
101
+ const a = (M - Math.floor(c.a * ma / M)) * 257;
102
+ pix[i] = Math.floor((pix[i] * a + c.r * ma) / M) >> 8;
103
+ pix[i + 1] = Math.floor((pix[i + 1] * a + c.g * ma) / M) >> 8;
104
+ pix[i + 2] = Math.floor((pix[i + 2] * a + c.b * ma) / M) >> 8;
105
+ pix[i + 3] = Math.floor((pix[i + 3] * a + c.a * ma) / M) >> 8;
106
+ }
107
+ }
108
+ }
109
+ function drawCanvasOver(dst, src, tx, ty, mask) {
110
+ const x0 = Math.max(0, tx);
111
+ const y0 = Math.max(0, ty);
112
+ const x1 = Math.min(dst.width, tx + src.width);
113
+ const y1 = Math.min(dst.height, ty + src.height);
114
+ if (x0 >= x1 || y0 >= y1) return;
115
+ const dp = dst.pix;
116
+ const sp = src.pix;
117
+ for (let y = y0; y < y1; y++) {
118
+ let di = (y * dst.width + x0) * 4;
119
+ let si = ((y - ty) * src.width + (x0 - tx)) * 4;
120
+ for (let x = x0; x < x1; x++, di += 4, si += 4) {
121
+ let ma = M;
122
+ if (mask !== null) {
123
+ ma = mask[y * dst.width + x] * 257;
124
+ if (ma === 0) continue;
125
+ }
126
+ const sr = Math.floor(sp[si] * 257 * ma / M);
127
+ const sg = Math.floor(sp[si + 1] * 257 * ma / M);
128
+ const sb = Math.floor(sp[si + 2] * 257 * ma / M);
129
+ const sa = Math.floor(sp[si + 3] * 257 * ma / M);
130
+ if (sa === 0 && sr === 0 && sg === 0 && sb === 0) continue;
131
+ const q = M - sa;
132
+ dp[di] = sr + Math.floor(dp[di] * 257 * q / M) >> 8;
133
+ dp[di + 1] = sg + Math.floor(dp[di + 1] * 257 * q / M) >> 8;
134
+ dp[di + 2] = sb + Math.floor(dp[di + 2] * 257 * q / M) >> 8;
135
+ dp[di + 3] = sa + Math.floor(dp[di + 3] * 257 * q / M) >> 8;
136
+ }
137
+ }
138
+ }
139
+ function paintPixel(dst, x, y, c, ma) {
140
+ if (x < 0 || y < 0 || x >= dst.width || y >= dst.height || ma === 0) return;
141
+ const i = (y * dst.width + x) * 4;
142
+ const pix = dst.pix;
143
+ const a = (M - Math.floor(c.a * ma / M)) * 257;
144
+ pix[i] = Math.floor((pix[i] * a + c.r * ma) / M) >> 8;
145
+ pix[i + 1] = Math.floor((pix[i + 1] * a + c.g * ma) / M) >> 8;
146
+ pix[i + 2] = Math.floor((pix[i + 2] * a + c.b * ma) / M) >> 8;
147
+ pix[i + 3] = Math.floor((pix[i + 3] * a + c.a * ma) / M) >> 8;
148
+ }
149
+ function blendSampledPixel(dst, x, y, sr, sg, sb, sa, ma) {
150
+ if (x < 0 || y < 0 || x >= dst.width || y >= dst.height || ma === 0) return;
151
+ const i = (y * dst.width + x) * 4;
152
+ const p = dst.pix;
153
+ const er = Math.floor(sr * ma / M);
154
+ const eg = Math.floor(sg * ma / M);
155
+ const eb = Math.floor(sb * ma / M);
156
+ const ea = Math.floor(sa * ma / M);
157
+ if (ea === 0 && er === 0 && eg === 0 && eb === 0) return;
158
+ const q = M - ea;
159
+ p[i] = er + Math.floor(p[i] * 257 * q / M) >> 8;
160
+ p[i + 1] = eg + Math.floor(p[i + 1] * 257 * q / M) >> 8;
161
+ p[i + 2] = eb + Math.floor(p[i + 2] * 257 * q / M) >> 8;
162
+ p[i + 3] = ea + Math.floor(p[i + 3] * 257 * q / M) >> 8;
163
+ }
164
+ function blendPixel(dst, x, y, c, ma) {
165
+ if (x < 0 || y < 0 || x >= dst.width || y >= dst.height || ma === 0) return;
166
+ const i = (y * dst.width + x) * 4;
167
+ const p = dst.pix;
168
+ const sr = Math.floor(c.r * ma / M);
169
+ const sg = Math.floor(c.g * ma / M);
170
+ const sb = Math.floor(c.b * ma / M);
171
+ const sa = Math.floor(c.a * ma / M);
172
+ const q = M - sa;
173
+ p[i] = sr + Math.floor(p[i] * 257 * q / M) >> 8;
174
+ p[i + 1] = sg + Math.floor(p[i + 1] * 257 * q / M) >> 8;
175
+ p[i + 2] = sb + Math.floor(p[i + 2] * 257 * q / M) >> 8;
176
+ p[i + 3] = sa + Math.floor(p[i + 3] * 257 * q / M) >> 8;
177
+ }
178
+
179
+ // src/raster.ts
180
+ function clipHalfPlane(poly, side, intersect) {
181
+ const out = [];
182
+ for (let i = 0; i < poly.length; i++) {
183
+ const cur = poly[i];
184
+ const prev = poly[(i + poly.length - 1) % poly.length];
185
+ const curIn = side(cur) >= 0;
186
+ const prevIn = side(prev) >= 0;
187
+ if (curIn) {
188
+ if (!prevIn) out.push(intersect(prev, cur));
189
+ out.push(cur);
190
+ } else if (prevIn) {
191
+ out.push(intersect(prev, cur));
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+ function polyArea(poly) {
197
+ let area = 0;
198
+ for (let i = 0; i < poly.length; i++) {
199
+ const a = poly[i];
200
+ const b = poly[(i + 1) % poly.length];
201
+ area += a.x * b.y - b.x * a.y;
202
+ }
203
+ return Math.abs(area) / 2;
204
+ }
205
+ function pixelCoverage(poly, px, py) {
206
+ let p = poly;
207
+ const x0 = px, x1 = px + 1, y0 = py, y1 = py + 1;
208
+ const ix = (a, b, x) => ({
209
+ x,
210
+ y: a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x)
211
+ });
212
+ const iy = (a, b, y) => ({
213
+ x: a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y),
214
+ y
215
+ });
216
+ p = clipHalfPlane(p, (q) => q.x - x0, (a, b) => ix(a, b, x0));
217
+ if (p.length === 0) return 0;
218
+ p = clipHalfPlane(p, (q) => x1 - q.x, (a, b) => ix(a, b, x1));
219
+ if (p.length === 0) return 0;
220
+ p = clipHalfPlane(p, (q) => q.y - y0, (a, b) => iy(a, b, y0));
221
+ if (p.length === 0) return 0;
222
+ p = clipHalfPlane(p, (q) => y1 - q.y, (a, b) => iy(a, b, y1));
223
+ if (p.length === 0) return 0;
224
+ return polyArea(p);
225
+ }
226
+ function rasterizePolygon(poly, w, h, emit) {
227
+ if (poly.length < 3) return;
228
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
229
+ for (const p of poly) {
230
+ if (p.x < minX) minX = p.x;
231
+ if (p.y < minY) minY = p.y;
232
+ if (p.x > maxX) maxX = p.x;
233
+ if (p.y > maxY) maxY = p.y;
234
+ }
235
+ const px0 = Math.max(0, Math.floor(minX));
236
+ const py0 = Math.max(0, Math.floor(minY));
237
+ const px1 = Math.min(w, Math.ceil(maxX));
238
+ const py1 = Math.min(h, Math.ceil(maxY));
239
+ for (let y = py0; y < py1; y++) {
240
+ for (let x = px0; x < px1; x++) {
241
+ const cov = pixelCoverage(poly, x, y);
242
+ if (cov <= 0) continue;
243
+ const ma = Math.min(65535, Math.round(cov * 65535));
244
+ if (ma > 0) emit(x, y, ma);
245
+ }
246
+ }
247
+ }
248
+
249
+ // src/context.ts
250
+ var identity = () => ({ xx: 1, yx: 0, xy: 0, yy: 1, x0: 0, y0: 0 });
251
+ function multiply(b, a) {
252
+ return {
253
+ xx: b.xx * a.xx + b.yx * a.xy,
254
+ yx: b.xx * a.yx + b.yx * a.yy,
255
+ xy: b.xy * a.xx + b.yy * a.xy,
256
+ yy: b.xy * a.yx + b.yy * a.yy,
257
+ x0: b.x0 * a.xx + b.y0 * a.xy + a.x0,
258
+ y0: b.x0 * a.yx + b.y0 * a.yy + a.y0
259
+ };
260
+ }
261
+ function transformPoint(m, x, y) {
262
+ return { x: m.xx * x + m.xy * y + m.x0, y: m.yx * x + m.yy * y + m.y0 };
263
+ }
264
+ function isIntegerTranslation(m) {
265
+ return m.xx === 1 && m.yx === 0 && m.xy === 0 && m.yy === 1 && Number.isInteger(m.x0) && Number.isInteger(m.y0);
266
+ }
267
+ function invert(m) {
268
+ const det = m.xx * m.yy - m.xy * m.yx;
269
+ if (det === 0 || !Number.isFinite(det)) return null;
270
+ const inv = 1 / det;
271
+ return {
272
+ xx: m.yy * inv,
273
+ xy: -m.xy * inv,
274
+ yx: -m.yx * inv,
275
+ yy: m.xx * inv,
276
+ x0: (m.xy * m.y0 - m.yy * m.x0) * inv,
277
+ y0: (m.yx * m.x0 - m.xx * m.y0) * inv
278
+ };
279
+ }
280
+ var DrawContext = class {
281
+ canvas;
282
+ st;
283
+ stack = [];
284
+ constructor(canvas) {
285
+ this.canvas = canvas;
286
+ this.st = { matrix: identity(), color: color16(TRANSPARENT), mask: null };
287
+ }
288
+ push() {
289
+ this.stack.push({ ...this.st });
290
+ }
291
+ pop() {
292
+ const st = this.stack.pop();
293
+ if (!st) {
294
+ throw new Error("DrawContext.pop() without matching push()");
295
+ }
296
+ this.st = st;
297
+ }
298
+ // --- Transform ops (compose in local space, like gg) ---------------------
299
+ translate(dx, dy) {
300
+ this.st.matrix = multiply(
301
+ { xx: 1, yx: 0, xy: 0, yy: 1, x0: dx, y0: dy },
302
+ this.st.matrix
303
+ );
304
+ }
305
+ scale(sx, sy) {
306
+ this.st.matrix = multiply(
307
+ { xx: sx, yx: 0, xy: 0, yy: sy, x0: 0, y0: 0 },
308
+ this.st.matrix
309
+ );
310
+ }
311
+ /** Anticlockwise rotation about the origin, radians. */
312
+ rotate(angle) {
313
+ const c = Math.cos(angle);
314
+ const s = Math.sin(angle);
315
+ this.st.matrix = multiply(
316
+ { xx: c, yx: s, xy: -s, yy: c, x0: 0, y0: 0 },
317
+ this.st.matrix
318
+ );
319
+ }
320
+ scaleAbout(sx, sy, x, y) {
321
+ this.translate(x, y);
322
+ this.scale(sx, sy);
323
+ this.translate(-x, -y);
324
+ }
325
+ rotateAbout(angle, x, y) {
326
+ this.translate(x, y);
327
+ this.rotate(angle);
328
+ this.translate(-x, -y);
329
+ }
330
+ setColor(c) {
331
+ this.st.color = color16(c);
332
+ }
333
+ // --- Drawing --------------------------------------------------------------
334
+ /** Transformed corners of rect (x,y,w,h), in path order. */
335
+ rectQuad(x, y, w, h) {
336
+ const m = this.st.matrix;
337
+ return [
338
+ transformPoint(m, x, y),
339
+ transformPoint(m, x + w, y),
340
+ transformPoint(m, x + w, y + h),
341
+ transformPoint(m, x, y + h)
342
+ ];
343
+ }
344
+ /**
345
+ * gg DrawRectangle(x,y,w,h) + Fill().
346
+ * A negative width/height rectangle is a reversed-winding path covering
347
+ * [x+w, x] × [y+h, y]; the non-zero winding rule still fills it.
348
+ */
349
+ fillRect(x, y, w, h) {
350
+ if (w < 0) {
351
+ x += w;
352
+ w = -w;
353
+ }
354
+ if (h < 0) {
355
+ y += h;
356
+ h = -h;
357
+ }
358
+ if (isIntegerTranslation(this.st.matrix)) {
359
+ const dx0 = this.st.matrix.x0 + x;
360
+ const dy0 = this.st.matrix.y0 + y;
361
+ fillRectSolid(
362
+ this.canvas,
363
+ dx0,
364
+ dy0,
365
+ dx0 + w,
366
+ dy0 + h,
367
+ this.st.color,
368
+ this.st.mask
369
+ );
370
+ return;
371
+ }
372
+ const quad = this.rectQuad(x, y, w, h);
373
+ const { canvas } = this;
374
+ const mask = this.st.mask;
375
+ const color = this.st.color;
376
+ rasterizePolygon(quad, canvas.width, canvas.height, (px, py, cov) => {
377
+ let ma = cov;
378
+ if (mask !== null) {
379
+ ma = Math.floor(ma * mask[py * canvas.width + px] / 255);
380
+ if (ma === 0) return;
381
+ }
382
+ paintPixel(canvas, px, py, color, ma);
383
+ });
384
+ }
385
+ /**
386
+ * gg DrawRectangle(x,y,w,h) + Clip(): intersect the clip region with the
387
+ * given rectangle (in current-transform space).
388
+ */
389
+ clipRect(x, y, w, h) {
390
+ if (w < 0) {
391
+ x += w;
392
+ w = -w;
393
+ }
394
+ if (h < 0) {
395
+ y += h;
396
+ h = -h;
397
+ }
398
+ const { width, height } = this.canvas;
399
+ const next = new Uint8Array(width * height);
400
+ const prev = this.st.mask;
401
+ if (isIntegerTranslation(this.st.matrix)) {
402
+ const x0 = Math.max(0, this.st.matrix.x0 + x);
403
+ const y0 = Math.max(0, this.st.matrix.y0 + y);
404
+ const x1 = Math.min(width, this.st.matrix.x0 + x + w);
405
+ const y1 = Math.min(height, this.st.matrix.y0 + y + h);
406
+ for (let yy = y0; yy < y1; yy++) {
407
+ const row = yy * width;
408
+ for (let xx = x0; xx < x1; xx++) {
409
+ next[row + xx] = prev === null ? 255 : prev[row + xx];
410
+ }
411
+ }
412
+ } else {
413
+ rasterizePolygon(this.rectQuad(x, y, w, h), width, height, (px, py, cov) => {
414
+ let a = cov >> 8;
415
+ if (prev !== null) {
416
+ a = Math.floor(a * 257 * (prev[py * width + px] * 257) / 65535) >> 8;
417
+ }
418
+ next[py * width + px] = a;
419
+ });
420
+ }
421
+ this.st.mask = next;
422
+ }
423
+ /**
424
+ * gg SetPixel: sets the pixel to the current color, converting like Go's
425
+ * image.RGBA.Set (NRGBA → premultiplied). Note: like gg, this *ignores*
426
+ * the clip mask and compositing — it is a raw store. Plot depends on it.
427
+ */
428
+ setPixel(x, y) {
429
+ const { canvas } = this;
430
+ if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) return;
431
+ const i = (y * canvas.width + x) * 4;
432
+ const c = this.st.color;
433
+ canvas.pix[i] = c.r >> 8;
434
+ canvas.pix[i + 1] = c.g >> 8;
435
+ canvas.pix[i + 2] = c.b >> 8;
436
+ canvas.pix[i + 3] = c.a >> 8;
437
+ }
438
+ /** Apply the current matrix to a point (gg TransformPoint). */
439
+ transformPoint(x, y) {
440
+ return transformPoint(this.st.matrix, x, y);
441
+ }
442
+ /**
443
+ * Fill an arbitrary polygon given in local (pre-transform) coordinates
444
+ * with the current color — the gg path-Fill equivalent for non-rect
445
+ * shapes (Circle, PieChart). Coverage-based AA, honors the clip mask.
446
+ */
447
+ fillPolygon(localPts) {
448
+ const m = this.st.matrix;
449
+ const device = localPts.map((p) => transformPoint(m, p.x, p.y));
450
+ const { canvas } = this;
451
+ const mask = this.st.mask;
452
+ const color = this.st.color;
453
+ rasterizePolygon(device, canvas.width, canvas.height, (px, py, cov) => {
454
+ let ma = cov;
455
+ if (mask !== null) {
456
+ ma = Math.floor(ma * mask[py * canvas.width + px] / 255);
457
+ if (ma === 0) return;
458
+ }
459
+ paintPixel(canvas, px, py, color, ma);
460
+ });
461
+ }
462
+ /** gg DrawImage(src, x, y) with the current transform and clip. */
463
+ drawCanvas(src, x, y) {
464
+ const s2d = multiply(
465
+ { xx: 1, yx: 0, xy: 0, yy: 1, x0: x, y0: y },
466
+ this.st.matrix
467
+ );
468
+ if (isIntegerTranslation(s2d)) {
469
+ drawCanvasOver(this.canvas, src, s2d.x0, s2d.y0, this.st.mask);
470
+ return;
471
+ }
472
+ const d2s = invert(s2d);
473
+ if (!d2s) return;
474
+ const corners = [
475
+ transformPoint(s2d, 0, 0),
476
+ transformPoint(s2d, src.width, 0),
477
+ transformPoint(s2d, src.width, src.height),
478
+ transformPoint(s2d, 0, src.height)
479
+ ];
480
+ const px0 = Math.max(0, Math.floor(Math.min(...corners.map((p) => p.x))));
481
+ const py0 = Math.max(0, Math.floor(Math.min(...corners.map((p) => p.y))));
482
+ const px1 = Math.min(this.canvas.width, Math.ceil(Math.max(...corners.map((p) => p.x))));
483
+ const py1 = Math.min(this.canvas.height, Math.ceil(Math.max(...corners.map((p) => p.y))));
484
+ const mask = this.st.mask;
485
+ const sp = src.pix;
486
+ const sw = src.width;
487
+ const sh = src.height;
488
+ const tap = (tx, ty) => {
489
+ if (tx < 0 || ty < 0 || tx >= sw || ty >= sh) return [0, 0, 0, 0];
490
+ const i = (ty * sw + tx) * 4;
491
+ return [sp[i] * 257, sp[i + 1] * 257, sp[i + 2] * 257, sp[i + 3] * 257];
492
+ };
493
+ for (let dy = py0; dy < py1; dy++) {
494
+ for (let dx = px0; dx < px1; dx++) {
495
+ let ma = 65535;
496
+ if (mask !== null) {
497
+ ma = mask[dy * this.canvas.width + dx] * 257;
498
+ if (ma === 0) continue;
499
+ }
500
+ const p = transformPoint(d2s, dx + 0.5, dy + 0.5);
501
+ const sx = p.x - 0.5;
502
+ const sy = p.y - 0.5;
503
+ const sx0 = Math.floor(sx);
504
+ const sy0 = Math.floor(sy);
505
+ const fx = sx - sx0;
506
+ const fy = sy - sy0;
507
+ if (sx0 < -1 || sy0 < -1 || sx0 >= sw || sy0 >= sh) continue;
508
+ const t00 = tap(sx0, sy0);
509
+ const t10 = tap(sx0 + 1, sy0);
510
+ const t01 = tap(sx0, sy0 + 1);
511
+ const t11 = tap(sx0 + 1, sy0 + 1);
512
+ const w00 = (1 - fx) * (1 - fy);
513
+ const w10 = fx * (1 - fy);
514
+ const w01 = (1 - fx) * fy;
515
+ const w11 = fx * fy;
516
+ const sr = t00[0] * w00 + t10[0] * w10 + t01[0] * w01 + t11[0] * w11;
517
+ const sg = t00[1] * w00 + t10[1] * w10 + t01[1] * w01 + t11[1] * w11;
518
+ const sb = t00[2] * w00 + t10[2] * w10 + t01[2] * w01 + t11[2] * w11;
519
+ const sa = t00[3] * w00 + t10[3] * w10 + t01[3] * w01 + t11[3] * w11;
520
+ if (sa === 0 && sr === 0 && sg === 0 && sb === 0) continue;
521
+ blendSampledPixel(
522
+ this.canvas,
523
+ dx,
524
+ dy,
525
+ Math.round(sr),
526
+ Math.round(sg),
527
+ Math.round(sb),
528
+ Math.round(sa),
529
+ ma
530
+ );
531
+ }
532
+ }
533
+ }
534
+ };
535
+
536
+ // src/geometry.ts
537
+ function bounds(w, h) {
538
+ return { w: Math.abs(w), h: Math.abs(h) };
539
+ }
540
+
541
+ export {
542
+ Canvas,
543
+ blendPixel,
544
+ WHITE,
545
+ BLACK,
546
+ TRANSPARENT,
547
+ parseColor,
548
+ color16,
549
+ DrawContext,
550
+ bounds
551
+ };
@@ -0,0 +1,37 @@
1
+ import { F as FrameEncoder, W as WebpDecoder } from '../webp-D106DLhy.js';
2
+ import '../widget-DABrpucj.js';
3
+
4
+ /**
5
+ * FrameEncoder adapter for @jsquash/webp (WASM libwebp; runs in Workers,
6
+ * browsers, and Node). Kept out of the package root so consumers that don't
7
+ * encode WebP never load the emscripten glue.
8
+ *
9
+ * Determinism (§8): we bypass @jsquash's `init()`, which auto-selects a
10
+ * SIMD or non-SIMD build at runtime — two different codec builds that can
11
+ * emit different (equally valid) bitstreams, which would fracture the
12
+ * sha256/ETag across environments. Instead the NON-SIMD factory is wired
13
+ * explicitly and the caller must supply that exact wasm module
14
+ * (`codec/enc/webp_enc.wasm`): a Workers wasm-module binding in prod, a
15
+ * compiled module from disk in Node/tests.
16
+ *
17
+ * Frames are encoded LOSSLESS. Note (documented divergence): the Go
18
+ * renderer's libwebp path encodes lossy-by-default; for 64×32 pixel art
19
+ * lossless is exact and comparable in size, and pixel parity is perceptual
20
+ * (§14). Pin @jsquash/webp exactly and surface its version in the
21
+ * render-plane cache key.
22
+ */
23
+
24
+ /**
25
+ * Create a FrameEncoder backed by the non-SIMD @jsquash/webp codec.
26
+ * `wasm` must be the compiled `@jsquash/webp/codec/enc/webp_enc.wasm`
27
+ * module (Workers binding import or WebAssembly.compile of the file).
28
+ */
29
+ declare function jsquashFrameEncoder(wasm: WebAssembly.Module): FrameEncoder;
30
+ /**
31
+ * Create a WebpDecoder (for the Image widget — `setWebpDecoder()`) backed
32
+ * by the non-SIMD @jsquash/webp codec. `wasm` must be the compiled
33
+ * `@jsquash/webp/codec/dec/webp_dec.wasm` module.
34
+ */
35
+ declare function jsquashWebpDecoder(wasm: WebAssembly.Module): WebpDecoder;
36
+
37
+ export { jsquashFrameEncoder, jsquashWebpDecoder };
@@ -0,0 +1,86 @@
1
+ // src/encode/webp-jsquash.ts
2
+ import webpEncFactory from "@jsquash/webp/codec/enc/webp_enc.js";
3
+ import webpDecFactory from "@jsquash/webp/codec/dec/webp_dec.js";
4
+ var ENCODE_OPTIONS = {
5
+ quality: 75,
6
+ target_size: 0,
7
+ target_PSNR: 0,
8
+ method: 4,
9
+ sns_strength: 50,
10
+ filter_strength: 60,
11
+ filter_sharpness: 0,
12
+ filter_type: 1,
13
+ partitions: 0,
14
+ segments: 4,
15
+ pass: 1,
16
+ show_compressed: 0,
17
+ preprocessing: 0,
18
+ autofilter: 0,
19
+ partition_limit: 0,
20
+ alpha_compression: 1,
21
+ alpha_filtering: 1,
22
+ alpha_quality: 100,
23
+ lossless: 1,
24
+ exact: 0,
25
+ image_hint: 0,
26
+ emulate_jpeg_size: 0,
27
+ thread_level: 0,
28
+ low_memory: 0,
29
+ near_lossless: 100,
30
+ use_delta_palette: 0,
31
+ use_sharp_yuv: 0
32
+ };
33
+ var modulePromise = null;
34
+ function jsquashFrameEncoder(wasm) {
35
+ return async (rgba, width, height) => {
36
+ if (!modulePromise) {
37
+ modulePromise = webpEncFactory({
38
+ noInitialRun: true,
39
+ instantiateWasm: (imports, callback) => {
40
+ const instance = new WebAssembly.Instance(wasm, imports);
41
+ callback(instance);
42
+ return instance.exports;
43
+ }
44
+ });
45
+ }
46
+ const mod = await modulePromise;
47
+ const result = mod.encode(rgba, width, height, ENCODE_OPTIONS);
48
+ if (!result) {
49
+ throw new Error("WebP encoding error");
50
+ }
51
+ return new Uint8Array(result.buffer, result.byteOffset, result.byteLength).slice();
52
+ };
53
+ }
54
+ var decModulePromise = null;
55
+ function jsquashWebpDecoder(wasm) {
56
+ return async (data) => {
57
+ if (!decModulePromise) {
58
+ decModulePromise = webpDecFactory({
59
+ noInitialRun: true,
60
+ instantiateWasm: (imports, callback) => {
61
+ const instance = new WebAssembly.Instance(wasm, imports);
62
+ callback(instance);
63
+ return instance.exports;
64
+ }
65
+ });
66
+ }
67
+ const mod = await decModulePromise;
68
+ const result = mod.decode(data);
69
+ if (!result) {
70
+ throw new Error("WebP decoding error");
71
+ }
72
+ return {
73
+ width: result.width,
74
+ height: result.height,
75
+ rgba: new Uint8Array(
76
+ result.data.buffer,
77
+ result.data.byteOffset,
78
+ result.data.byteLength
79
+ ).slice()
80
+ };
81
+ };
82
+ }
83
+ export {
84
+ jsquashFrameEncoder,
85
+ jsquashWebpDecoder
86
+ };