@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.
- package/LICENSE +674 -0
- package/dist/chunk-NASKU36A.js +551 -0
- package/dist/encode/webp-jsquash.d.ts +37 -0
- package/dist/encode/webp-jsquash.js +86 -0
- package/dist/index.d.ts +654 -0
- package/dist/index.js +2788 -0
- package/dist/testing/index.d.ts +22 -0
- package/dist/testing/index.js +76 -0
- package/dist/webp-D106DLhy.d.ts +84 -0
- package/dist/widget-DABrpucj.d.ts +174 -0
- package/package.json +41 -0
|
@@ -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
|
+
};
|