@swf2es/player-hosts 0.1.4 → 0.1.6

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.
Files changed (43) hide show
  1. package/dist/thread/format/bitmap.js +13 -14
  2. package/dist/thread/format/index.js +1 -1
  3. package/dist/thread/format/swf.js +11 -1
  4. package/dist/thread/player/avm1/globals/BitmapData.js +2 -1
  5. package/dist/thread/player/bitmap/composite.js +767 -0
  6. package/dist/thread/player/bitmap/draw.js +3 -3
  7. package/dist/thread/player/bitmap/filters.js +32 -32
  8. package/dist/thread/player/bitmap/ops.js +2 -766
  9. package/dist/thread/player/bitmap/pixel-bender.js +4 -1
  10. package/dist/thread/player/display/bounds.js +105 -0
  11. package/dist/thread/player/display/displayobject.js +1 -1
  12. package/dist/thread/player/display/drawing.js +25 -10
  13. package/dist/thread/player/display/instantiate.js +28 -29
  14. package/dist/thread/player/display/shapes.js +72 -54
  15. package/dist/thread/player/display/textobjects.js +34 -1
  16. package/dist/thread/player/player.js +4 -3
  17. package/dist/thread/player/playerglobal/flash/desktop/NativeApplication.js +1 -1
  18. package/dist/thread/player/playerglobal/flash/display/BitmapData.js +3 -2
  19. package/dist/thread/player/playerglobal/flash/display/DisplayObject.js +2 -2
  20. package/dist/thread/player/playerglobal/flash/display/SimpleButton.js +12 -11
  21. package/dist/thread/player/playerglobal/flash/display/Stage.js +2 -8
  22. package/dist/thread/player/playerglobal/flash/display/shaders.js +1 -1
  23. package/dist/thread/player/playerglobal/flash/filesystem/paths.js +1 -1
  24. package/dist/thread/player/playerglobal/flash/filesystem/writes.js +1 -1
  25. package/dist/thread/player/playerglobal/flash/globalization/locale.js +1 -1
  26. package/dist/thread/player/scripting/events.js +2 -2
  27. package/dist/thread/player/scripting/lifecycle.js +39 -19
  28. package/dist/thread/player/scripting/symbols.js +24 -30
  29. package/dist/thread/player/scripting.js +5 -9
  30. package/dist/thread/runtime/avm1/errors.js +1 -1
  31. package/dist/thread/runtime/avm1/numbers.js +7 -8
  32. package/dist/thread/runtime/avm2/amf.js +2 -1
  33. package/dist/thread/runtime/avm2/names.js +0 -4
  34. package/dist/thread/runtime/avm2/natives/bytearray.js +6 -166
  35. package/dist/thread/runtime/avm2/natives/regexp.js +68 -64
  36. package/dist/thread/runtime/avm2/natives/shell-workers.js +1 -1
  37. package/dist/thread/runtime/avm2/natives/shell.js +2 -1
  38. package/dist/thread/runtime/avm2/natives/xml/parser.js +2 -2
  39. package/dist/thread/runtime/avm2/numbers.js +1 -1
  40. package/dist/thread/runtime/avm2/runtime.js +10 -8
  41. package/dist/thread/runtime/avm2/traits.js +1 -1
  42. package/dist/thread/runtime/avm2/utf8.js +167 -0
  43. package/package.json +2 -2
@@ -0,0 +1,767 @@
1
+ // BitmapData.draw of a bitmap on the CPU: a source composited onto a
2
+ // store through a matrix, a colour transform, a clip and a blend mode,
3
+ // as Flash's pixels under adl have it, a row at a time; the render
4
+ // module's DrawKernel draws what it can natively. Display objects reach
5
+ // it as a bitmap of what the renderer drew (draw.ts).
6
+ import { DRAW_OPAQUE, DRAW_SKIP_ZERO_ALPHA, DRAW_SMOOTH, DRAW_TRANSFORM, DrawKernel, } from "../../native/render/index.js";
7
+ import { BitmapStore, over } from "./bitmap.js";
8
+ import { drawKernel } from "./kernel.js";
9
+ // The blend modes draw composites with, as the numbers the pixel loops take,
10
+ // worked out once a draw.
11
+ const NORMAL = 0;
12
+ const MULTIPLY = 1;
13
+ const SCREEN = 2;
14
+ const LIGHTEN = 3;
15
+ const DARKEN = 4;
16
+ const DIFFERENCE = 5;
17
+ const ADD = 6;
18
+ const SUBTRACT = 7;
19
+ const INVERT = 8;
20
+ const OVERLAY = 9;
21
+ const HARDLIGHT = 10;
22
+ const ERASE = 11;
23
+ const ALPHA = 12;
24
+ /**
25
+ * Each blend mode draw takes, by its name; any other draws nothing. Alpha
26
+ * and erase need a layer: a display object drawn has one, and they then
27
+ * scale the destination's alpha where it covers; a BitmapData drawn has
28
+ * not, and they do nothing.
29
+ */
30
+ const MODES = new Map([
31
+ ["normal", NORMAL],
32
+ ["layer", NORMAL],
33
+ ["multiply", MULTIPLY],
34
+ ["screen", SCREEN],
35
+ ["lighten", LIGHTEN],
36
+ ["darken", DARKEN],
37
+ ["difference", DIFFERENCE],
38
+ ["add", ADD],
39
+ ["subtract", SUBTRACT],
40
+ ["invert", INVERT],
41
+ ["overlay", OVERLAY],
42
+ ["hardlight", HARDLIGHT],
43
+ ["erase", ERASE],
44
+ ["alpha", ALPHA],
45
+ ]);
46
+ /**
47
+ * draw of a bitmap source: each destination pixel in the clip takes the
48
+ * source pixel under its centre, through the matrix's inverse, clamped to
49
+ * the source's edges (bilinear when smoothing), scaled by how much of the
50
+ * pixel the transformed source covers (a 4 x 4 sample grid), passed
51
+ * through the colour transform as colorTransform does, and composited by
52
+ * the blend mode. Flash's own pixels, under adl: nearest at the centre,
53
+ * normal as the store's source-over, alpha and erase as nothing unless the
54
+ * source is a display object's `layer`: a Bitmap's, every pixel of which it
55
+ * covers, or a drawing's snapshot, which covers only where it drew.
56
+ */
57
+ export function drawBitmap(store, source, m, ct, mode, clip, smooth, rows = false, layer = null) {
58
+ const code = MODES.get(mode);
59
+ const whole = code === ERASE || code === ALPHA;
60
+ if (code === undefined || (whole && layer === null)) {
61
+ return;
62
+ }
63
+ // A drawing's alpha blend leaves the destination where it drew nothing, or drew clear (adl).
64
+ const keepClear = code === ALPHA && layer === "drawing";
65
+ // Flash rasterises the translation snapped down to quarter pixels, which
66
+ // its 4 x 4 coverage then reads exactly (a move of 0.49 covers 3/4 and
67
+ // 1/4 of the edge pixels under adl).
68
+ m = { ...m, tx: Math.floor(m.tx * 4) / 4, ty: Math.floor(m.ty * 4) / 4 };
69
+ const det = m.a * m.d - m.b * m.c;
70
+ if (det === 0) {
71
+ return;
72
+ }
73
+ // The destination pixels the source can reach: its corners through the matrix.
74
+ const corners = [
75
+ [0, 0],
76
+ [source.width, 0],
77
+ [0, source.height],
78
+ [source.width, source.height],
79
+ ].map(([x, y]) => [m.a * x + m.c * y + m.tx, m.b * x + m.d * y + m.ty]);
80
+ const xs = corners.map((p) => p[0]);
81
+ const ys = corners.map((p) => p[1]);
82
+ let area = store.clip({
83
+ x: Math.floor(Math.min(...xs)),
84
+ y: Math.floor(Math.min(...ys)),
85
+ width: Math.ceil(Math.max(...xs)) - Math.floor(Math.min(...xs)),
86
+ height: Math.ceil(Math.max(...ys)) - Math.floor(Math.min(...ys)),
87
+ });
88
+ if (area && clip) {
89
+ area = store.clip({
90
+ x: Math.max(area.x, clip.x),
91
+ y: Math.max(area.y, clip.y),
92
+ width: Math.min(area.x + area.width, clip.x + clip.width) - Math.max(area.x, clip.x),
93
+ height: Math.min(area.y + area.height, clip.y + clip.height) - Math.max(area.y, clip.y),
94
+ });
95
+ }
96
+ if (!area) {
97
+ return;
98
+ }
99
+ // A layer's blend reads the source as it was: of a Bitmap of the bitmap itself, the part
100
+ // that can reach the area is copied first.
101
+ if (whole && source === store) {
102
+ [source, m] = reachable(store, m, area);
103
+ }
104
+ const transform = ct ? prepareColorTransform(ct) : null;
105
+ // Unscaled at a whole pixel, as every snapshot of a display object is
106
+ // composited: each covered pixel is covered whole and is the source's
107
+ // own, so it is copied straight, as the general path below would.
108
+ const moved = m.a === 1 &&
109
+ m.b === 0 &&
110
+ m.c === 0 &&
111
+ m.d === 1 &&
112
+ Number.isInteger(m.tx) &&
113
+ Number.isInteger(m.ty) &&
114
+ source !== store;
115
+ // The inverse: from a destination point to the source's.
116
+ const ia = m.d / det;
117
+ const ib = -m.b / det;
118
+ const ic = -m.c / det;
119
+ const id = m.a / det;
120
+ const itx = -(ia * m.tx + ic * m.ty);
121
+ const ity = -(ib * m.tx + id * m.ty);
122
+ if (code === NORMAL && source !== store) {
123
+ const inverse = job.inverse;
124
+ inverse[0] = ia;
125
+ inverse[1] = ib;
126
+ inverse[2] = ic;
127
+ inverse[3] = id;
128
+ inverse[4] = itx;
129
+ inverse[5] = ity;
130
+ if (drawnNatively(store, source, m, moved, area, transform, smooth)) {
131
+ return;
132
+ }
133
+ }
134
+ if (moved) {
135
+ if (composite(store, source, area, m.tx, m.ty, transform, code, keepClear)) {
136
+ store.changed();
137
+ }
138
+ return;
139
+ }
140
+ // How many of the pixel's 4 x 4 samples fall inside the source.
141
+ const coverage = (x, y) => {
142
+ let hits = 0;
143
+ for (let k = 0; k < 16; k++) {
144
+ const px = x + ((k & 3) + 0.5) / 4;
145
+ const py = y + ((k >> 2) + 0.5) / 4;
146
+ const sx = ia * px + ic * py + itx;
147
+ const sy = ib * px + id * py + ity;
148
+ if (sx >= 0 && sy >= 0 && sx < source.width && sy < source.height) {
149
+ hits++;
150
+ }
151
+ }
152
+ return hits;
153
+ };
154
+ // The arrays taken once. What the source reads as: its own pixels drawn into itself, else as
155
+ // a draw reads them. The store's, which bring back what the GPU drew last or copy a library
156
+ // bitmap's, are taken at the first pixel put, but first when the source is the store, which
157
+ // reads them as written.
158
+ let into = source === store ? store.pixels : null;
159
+ const pixels = source === store ? source.readable : source.drawable;
160
+ // Whether a pixel was put, for the store to count a change: a draw that puts none is none.
161
+ let wrote = false;
162
+ // Through a Bitmap of itself, a BitmapData is drawn in plain scan order,
163
+ // each pixel reading what the ones before it wrote, which smears both
164
+ // ways (adl): pixel by pixel. A layer's blend has a copy of it by now.
165
+ if (source === store && !rows) {
166
+ for (let y = area.y; y < area.y + area.height; y++) {
167
+ for (let x = area.x; x < area.x + area.width; x++) {
168
+ const hits = coverage(x, y);
169
+ if (hits === 0) {
170
+ continue;
171
+ }
172
+ const cx = x + 0.5;
173
+ const cy = y + 0.5;
174
+ const sx = ia * cx + ic * cy + itx;
175
+ const sy = ib * cx + id * cy + ity;
176
+ let p = smooth
177
+ ? bilinear(pixels, source.width, source.height, sx - 0.5, sy - 0.5)
178
+ : nearest(pixels, source.width, source.height, sx, sy);
179
+ if (transform) {
180
+ p = transformed(p, transform);
181
+ }
182
+ if (hits < 16) {
183
+ p = scaleBy(p, hits / 16);
184
+ }
185
+ into ??= store.pixels;
186
+ put(store, into, y * store.width + x, code, p);
187
+ wrote = true;
188
+ }
189
+ }
190
+ if (wrote) {
191
+ store.changed();
192
+ }
193
+ return;
194
+ }
195
+ // Otherwise a row at a time, each pass over it in a loop of its own, so
196
+ // that no pixel crosses a call: a pixel past 2^30 a call returns is a
197
+ // number V8 allocates. Every pixel of a row is read before one is
198
+ // written, which is how Flash draws a BitmapData into itself: a move
199
+ // right keeps the pixels, a move down smears rows.
200
+ const n = area.width;
201
+ const buffers = takeRows(n);
202
+ try {
203
+ const { pixels: row, hits, us, vs } = buffers;
204
+ const w = source.width;
205
+ const h = source.height;
206
+ for (let y = area.y; y < area.y + area.height; y++) {
207
+ // Where all 16 samples of a pixel surely fall inside the source, and
208
+ // where none can: only the pixels between are tested sample by sample.
209
+ spanFrom = Number.NEGATIVE_INFINITY;
210
+ spanTo = Number.POSITIVE_INFINITY;
211
+ const spans = !whole && coveredSpan(ia, ic, itx, w, y) && coveredSpan(ib, id, ity, h, y);
212
+ const fullFrom = spans ? Math.max(area.x, spanFrom) : 0;
213
+ const fullTo = spans ? Math.min(area.x + n, spanTo) : 0;
214
+ let any = 0;
215
+ for (let x = area.x, k = 0; k < n; x++, k++) {
216
+ const cx = x + 0.5;
217
+ const cy = y + 0.5;
218
+ const sx = ia * cx + ic * cy + itx;
219
+ const sy = ib * cx + id * cy + ity;
220
+ let hit = 16;
221
+ if (whole) {
222
+ // Erase and alpha take a pixel whole where the source is at its centre, as adl
223
+ // draws a shape's edge for them, an edge through the centre counted on its right
224
+ // and below.
225
+ hit = sx > 0 && sy > 0 && sx <= w && sy <= h ? 16 : 0;
226
+ }
227
+ else if (x < fullFrom || x >= fullTo) {
228
+ hit = coverage(x, y);
229
+ }
230
+ hits[k] = hit;
231
+ if (hit === 0) {
232
+ continue;
233
+ }
234
+ any++;
235
+ us[k] = sx;
236
+ vs[k] = sy;
237
+ }
238
+ if (any === 0) {
239
+ continue;
240
+ }
241
+ sampleRow(pixels, w, h, us, vs, hits, row, n, smooth);
242
+ if (transform) {
243
+ transformRow(row, hits, n, transform);
244
+ }
245
+ coverRow(row, hits, n);
246
+ into ??= store.pixels;
247
+ blendRow(store, into, y * store.width + area.x, row, hits, n, code, keepClear);
248
+ wrote = true;
249
+ }
250
+ }
251
+ finally {
252
+ giveRowsBack(buffers);
253
+ }
254
+ if (wrote) {
255
+ store.changed();
256
+ }
257
+ }
258
+ /** The draw kernel's job, filled for each draw: drawBitmap allocates nothing for it. */
259
+ const job = DrawKernel.job();
260
+ /** How far a source point may lie for the draw kernel, whose numbers are then exact as drawBitmap's. */
261
+ const REACH = 2 ** 30;
262
+ /**
263
+ * A draw in the normal blend mode of another store, by the draw kernel
264
+ * (draw.zig), whose pixels are those of the paths below, to the bit, the
265
+ * matrix's inverse in `job.inverse` already: whether it drew it, which it
266
+ * does not for numbers it cannot take as they are, nor where the realm has
267
+ * no kernel.
268
+ */
269
+ function drawnNatively(store, source, m, moved, area, ct, smooth) {
270
+ // A store the GPU drew last is read back only by a draw that puts a pixel, as below.
271
+ if (store.newerOnGpu) {
272
+ return false;
273
+ }
274
+ // The source rows the draw reads: the area's, moved; else those its corners reach, and one
275
+ // more each way for smoothing and rounding. A point far out, or not finite, is drawn below.
276
+ const inverse = job.inverse;
277
+ let top;
278
+ let bottom;
279
+ if (moved) {
280
+ if (Math.abs(m.tx) >= 2 ** 31 || Math.abs(m.ty) >= 2 ** 31) {
281
+ return false;
282
+ }
283
+ top = area.y - m.ty;
284
+ bottom = top + area.height;
285
+ }
286
+ else {
287
+ const x0 = area.x;
288
+ const x1 = area.x + area.width;
289
+ const y0 = area.y;
290
+ const y1 = area.y + area.height;
291
+ const ia = inverse[0];
292
+ const ib = inverse[1];
293
+ const ic = inverse[2];
294
+ const id = inverse[3];
295
+ const itx = inverse[4];
296
+ const ity = inverse[5];
297
+ const ya = ib * x0 + id * y0 + ity;
298
+ const yb = ib * x1 + id * y0 + ity;
299
+ const yc = ib * x0 + id * y1 + ity;
300
+ const yd = ib * x1 + id * y1 + ity;
301
+ const xa = ia * x0 + ic * y0 + itx;
302
+ const xb = ia * x1 + ic * y0 + itx;
303
+ const xc = ia * x0 + ic * y1 + itx;
304
+ const xd = ia * x1 + ic * y1 + itx;
305
+ const low = Math.min(ya, yb, yc, yd);
306
+ const high = Math.max(ya, yb, yc, yd);
307
+ const far = Math.max(Math.abs(xa), Math.abs(xb), Math.abs(xc), Math.abs(xd));
308
+ if (!(Math.abs(low) < REACH && Math.abs(high) < REACH && far < REACH)) {
309
+ return false;
310
+ }
311
+ top = Math.max(0, Math.min(source.height - 1, Math.floor(low) - 1));
312
+ bottom = Math.max(top + 1, Math.min(source.height, Math.ceil(high) + 1));
313
+ }
314
+ if (ct) {
315
+ const t = job.transform;
316
+ t[0] = ct.rMul;
317
+ t[1] = ct.gMul;
318
+ t[2] = ct.bMul;
319
+ t[3] = ct.aMul;
320
+ t[4] = ct.rAdd;
321
+ t[5] = ct.gAdd;
322
+ t[6] = ct.bAdd;
323
+ t[7] = ct.aAdd;
324
+ if (!t.every(Number.isFinite)) {
325
+ return false;
326
+ }
327
+ }
328
+ // Copying wide source rows for a small clip can cost more than the draw.
329
+ // Whole-pixel moves do less work per destination pixel, so their limit is lower.
330
+ const sourcePixels = source.width * (bottom - top);
331
+ const copiedPerPixel = moved ? 32 : 128;
332
+ if (sourcePixels > Math.max(1024, area.width * area.height * copiedPerPixel)) {
333
+ return false;
334
+ }
335
+ const kernel = drawKernel();
336
+ if (!kernel) {
337
+ return false;
338
+ }
339
+ job.source = source.drawable;
340
+ job.sourceWidth = source.width;
341
+ job.sourceHeight = source.height;
342
+ job.sourceTop = top;
343
+ job.sourceRows = bottom - top;
344
+ job.into = store.readable;
345
+ job.intoWidth = store.width;
346
+ job.x = area.x;
347
+ job.y = area.y;
348
+ job.width = area.width;
349
+ job.height = area.height;
350
+ job.moved = moved;
351
+ job.tx = moved ? m.tx : 0;
352
+ job.ty = moved ? m.ty : 0;
353
+ job.flags =
354
+ (smooth ? DRAW_SMOOTH : 0) |
355
+ (ct ? DRAW_TRANSFORM : 0) |
356
+ (store.transparent ? 0 : DRAW_OPAQUE) |
357
+ (ct?.skipZeroAlpha ? DRAW_SKIP_ZERO_ALPHA : 0);
358
+ const result = kernel.draw(job);
359
+ // The job holds no store's pixels past the draw.
360
+ job.source = EMPTY;
361
+ job.into = EMPTY;
362
+ if (result === "refused") {
363
+ return false;
364
+ }
365
+ if (result === "written") {
366
+ kernel.writeBack(job, store.pixels);
367
+ store.changed();
368
+ }
369
+ return true;
370
+ }
371
+ const EMPTY = new Uint32Array(0);
372
+ /**
373
+ * The row buffers kept between draws, which drawBitmap and composite take
374
+ * for a draw and give back after it. A draw runs to its end without
375
+ * calling out, so no other draw can take them in the middle of one:
376
+ * `scratchTaken` holds that, and a draw that finds them taken throws
377
+ * rather than overwrite a row another is working on.
378
+ */
379
+ let scratch = {
380
+ pixels: new Uint32Array(0),
381
+ hits: new Uint8Array(0),
382
+ us: new Float64Array(0),
383
+ vs: new Float64Array(0),
384
+ };
385
+ let scratchTaken = false;
386
+ /** The widest row whose buffers are kept for the next draw: a wider one's go with its draw. */
387
+ const ROW_KEPT = 8192;
388
+ /** Buffers for rows of `n` pixels: the kept ones where they are wide enough, until given back. */
389
+ function takeRows(n) {
390
+ if (scratch.pixels.length >= n) {
391
+ if (scratchTaken) {
392
+ throw new Error("drawBitmap's row buffers are taken by a draw already under way");
393
+ }
394
+ scratchTaken = true;
395
+ return scratch;
396
+ }
397
+ const buffers = {
398
+ pixels: new Uint32Array(n),
399
+ hits: new Uint8Array(n),
400
+ us: new Float64Array(n),
401
+ vs: new Float64Array(n),
402
+ };
403
+ if (n <= ROW_KEPT && !scratchTaken) {
404
+ scratch = buffers;
405
+ scratchTaken = true;
406
+ }
407
+ return buffers;
408
+ }
409
+ function giveRowsBack(buffers) {
410
+ if (buffers === scratch) {
411
+ scratchTaken = false;
412
+ }
413
+ }
414
+ /** coveredSpan's answer: the first pixel of a row covered whole and the one past the last. */
415
+ let spanFrom = 0;
416
+ let spanTo = 0;
417
+ /**
418
+ * Narrows spanFrom to spanTo, the destination pixels in row y whose 16 samples
419
+ * all have u = a x + c y' + t within [0, size), y' each of the row's four
420
+ * sample rows; false where u barely moves along x without being still,
421
+ * which leaves every pixel to be tested. The span keeps a margin of a
422
+ * thousandth of a pixel, and more where the numbers are large, from where
423
+ * the samples' own arithmetic decides, so a pixel in it is one the tests
424
+ * find whole.
425
+ */
426
+ function coveredSpan(a, c, t, size, y) {
427
+ if (a === 0) {
428
+ // u is the same all along a sample row, worked out as the tests do.
429
+ for (let k = 0; k < 4; k++) {
430
+ const u = c * (y + (k + 0.5) / 4) + t;
431
+ if (!(u >= 0 && u < size)) {
432
+ spanFrom = Number.POSITIVE_INFINITY;
433
+ }
434
+ }
435
+ return true;
436
+ }
437
+ if (Math.abs(a) < 1e-6) {
438
+ return false;
439
+ }
440
+ for (let k = 0; k < 4; k++) {
441
+ const offset = c * (y + (k + 0.5) / 4) + t;
442
+ // The x where u is 0 and where it is size, in either order.
443
+ const at0 = -offset / a;
444
+ const atSize = (size - offset) / a;
445
+ const margin = 0.001 +
446
+ (1e-12 * (Math.abs(offset) + size + Math.abs(a) * (Math.abs(at0) + Math.abs(atSize)))) /
447
+ Math.abs(a);
448
+ const low = Math.min(at0, atSize) + margin;
449
+ const high = Math.max(at0, atSize) - margin;
450
+ if (!Number.isFinite(low) || !Number.isFinite(high)) {
451
+ return false;
452
+ }
453
+ // Samples lie at x + 1/8 to x + 7/8.
454
+ spanFrom = Math.max(spanFrom, Math.ceil(low - 0.125));
455
+ spanTo = Math.min(spanTo, Math.floor(high - 0.875) + 1);
456
+ }
457
+ return true;
458
+ }
459
+ /** The source's pixel under each covered pixel's point of the row, nearest or bilinear. */
460
+ function sampleRow(from, width, height, us, vs, hits, row, n, smooth) {
461
+ for (let k = 0; k < n; k++) {
462
+ if (hits[k] === 0) {
463
+ continue;
464
+ }
465
+ row[k] = smooth
466
+ ? bilinear(from, width, height, us[k] - 0.5, vs[k] - 0.5)
467
+ : nearest(from, width, height, us[k], vs[k]);
468
+ }
469
+ }
470
+ /** The row's covered pixels through the colour transform, a run of one pixel transformed once. */
471
+ function transformRow(row, hits, n, ct) {
472
+ let last = -1;
473
+ let out = 0;
474
+ for (let k = 0; k < n; k++) {
475
+ if (hits[k] === 0) {
476
+ continue;
477
+ }
478
+ const p = row[k];
479
+ if (p !== last) {
480
+ last = p;
481
+ out = transformed(p, ct);
482
+ }
483
+ row[k] = out;
484
+ }
485
+ }
486
+ /** The row's partly covered pixels scaled by their coverage. */
487
+ function coverRow(row, hits, n) {
488
+ for (let k = 0; k < n; k++) {
489
+ const hit = hits[k];
490
+ if (hit !== 0 && hit < 16) {
491
+ row[k] = scaleBy(row[k], hit / 16);
492
+ }
493
+ }
494
+ }
495
+ /**
496
+ * The row's covered pixels composited into the store's pixels from `at` by
497
+ * the blend mode; with `keepClear`, a clear one leaves the destination be.
498
+ */
499
+ function blendRow(store, into, at, row, hits, n, mode, keepClear) {
500
+ if (mode === NORMAL) {
501
+ const opaque = store.transparent ? 0 : 0xff000000;
502
+ for (let k = 0; k < n; k++) {
503
+ if (hits[k] === 0) {
504
+ continue;
505
+ }
506
+ const p = row[k];
507
+ const sa = p >>> 24;
508
+ into[at + k] = sa === 255 ? p : (over(p, into[at + k]) | opaque) >>> 0;
509
+ }
510
+ return;
511
+ }
512
+ for (let k = 0; k < n; k++) {
513
+ if (hits[k] !== 0 && !(keepClear && row[k] >>> 24 === 0)) {
514
+ put(store, into, at + k, mode, row[k]);
515
+ }
516
+ }
517
+ }
518
+ /** `source` at (tx, ty) composited into `area` of the store, a pixel each; whether it put one. */
519
+ function composite(store, source, area, tx, ty, ct, mode, keepClear) {
520
+ const from = source.drawable;
521
+ const opaque = store.transparent ? 0 : 0xff000000;
522
+ // The store's pixels, taken as drawBitmap takes them: at the first pixel put.
523
+ let into = null;
524
+ if (!ct && mode === NORMAL) {
525
+ for (let y = area.y; y < area.y + area.height; y++) {
526
+ let i = y * store.width + area.x;
527
+ let j = (y - ty) * source.width + (area.x - tx);
528
+ for (let x = 0; x < area.width; x++, i++, j++) {
529
+ // Source over, inline: what blend("normal") computes.
530
+ const p = from[j];
531
+ const sa = p >>> 24;
532
+ if (sa === 0) {
533
+ continue;
534
+ }
535
+ into ??= store.pixels;
536
+ into[i] = sa === 255 ? p : (over(p, into[i]) | opaque) >>> 0;
537
+ }
538
+ }
539
+ return into !== null;
540
+ }
541
+ // A row at a time through the passes drawBitmap's rows take, every pixel covered.
542
+ const n = area.width;
543
+ const buffers = takeRows(n);
544
+ try {
545
+ const { pixels: row, hits } = buffers;
546
+ hits.fill(16, 0, n);
547
+ into = store.pixels;
548
+ for (let y = area.y; y < area.y + area.height; y++) {
549
+ const j = (y - ty) * source.width + (area.x - tx);
550
+ // A view and a copy: a loop copying it was slower, for all a view allocates.
551
+ row.set(from.subarray(j, j + n));
552
+ if (ct) {
553
+ transformRow(row, hits, n, ct);
554
+ }
555
+ blendRow(store, into, y * store.width + area.x, row, hits, n, mode, keepClear);
556
+ }
557
+ }
558
+ finally {
559
+ giveRowsBack(buffers);
560
+ }
561
+ return true;
562
+ }
563
+ /**
564
+ * A drawn pixel composited into the store by the blend mode, opaque where
565
+ * the store is, but for erase and alpha: an opaque bitmap holds the alpha
566
+ * they leave, as Flash's memory does, and reads it back (getPixel32).
567
+ */
568
+ function put(store, into, i, mode, p) {
569
+ const out = blend(mode, p, into[i]);
570
+ if (store.transparent) {
571
+ into[i] = out;
572
+ }
573
+ else if (mode === ERASE || mode === ALPHA) {
574
+ into[i] = out;
575
+ store.holdsAlpha ||= out >>> 24 !== 255;
576
+ }
577
+ else {
578
+ into[i] = (out | 0xff000000) >>> 0;
579
+ }
580
+ }
581
+ /**
582
+ * The part of `store` a draw of itself through `m` into `area` can read,
583
+ * copied, as a layer reads it before the draw writes, and the matrix that
584
+ * draws the copy where the store would be drawn: the area's corners taken
585
+ * back through the matrix, a pixel wider for smoothing, within the store.
586
+ */
587
+ function reachable(store, m, area) {
588
+ const det = m.a * m.d - m.b * m.c;
589
+ const back = [
590
+ [area.x, area.y],
591
+ [area.x + area.width, area.y],
592
+ [area.x, area.y + area.height],
593
+ [area.x + area.width, area.y + area.height],
594
+ ].map(([x, y]) => [
595
+ (m.d * (x - m.tx) - m.c * (y - m.ty)) / det,
596
+ (m.a * (y - m.ty) - m.b * (x - m.tx)) / det,
597
+ ]);
598
+ const x0 = Math.max(0, Math.floor(Math.min(...back.map((p) => p[0]))) - 1);
599
+ const y0 = Math.max(0, Math.floor(Math.min(...back.map((p) => p[1]))) - 1);
600
+ const x1 = Math.min(store.width, Math.ceil(Math.max(...back.map((p) => p[0]))) + 1);
601
+ const y1 = Math.min(store.height, Math.ceil(Math.max(...back.map((p) => p[1]))) + 1);
602
+ const width = Math.max(0, x1 - x0);
603
+ const height = Math.max(0, y1 - y0);
604
+ const copy = new BitmapStore(width, height, store.transparent, 0);
605
+ const from = store.readable;
606
+ const into = copy.pixels;
607
+ for (let y = 0; y < height; y++) {
608
+ into.set(from.subarray((y0 + y) * store.width + x0, (y0 + y) * store.width + x0 + width), y * width);
609
+ }
610
+ copy.holdsAlpha = store.holdsAlpha;
611
+ return [copy, { ...m, tx: m.tx + m.a * x0 + m.c * y0, ty: m.ty + m.b * x0 + m.d * y0 }];
612
+ }
613
+ function nearest(from, width, height, x, y) {
614
+ const px = Math.min(width - 1, Math.max(0, Math.floor(x)));
615
+ const py = Math.min(height - 1, Math.max(0, Math.floor(y)));
616
+ return from[py * width + px];
617
+ }
618
+ /** Bilinear between the four premultiplied pixels round (x, y), clamped to the edges. */
619
+ function bilinear(from, width, height, x, y) {
620
+ const x0 = Math.floor(x);
621
+ const y0 = Math.floor(y);
622
+ const fx = x - x0;
623
+ const fy = y - y0;
624
+ const left = Math.min(width - 1, Math.max(0, x0));
625
+ const right = Math.min(width - 1, Math.max(0, x0 + 1));
626
+ const top = Math.min(height - 1, Math.max(0, y0)) * width;
627
+ const bottom = Math.min(height - 1, Math.max(0, y0 + 1)) * width;
628
+ const p00 = from[top + left];
629
+ const p10 = from[top + right];
630
+ const p01 = from[bottom + left];
631
+ const p11 = from[bottom + right];
632
+ const gx = 1 - fx;
633
+ const gy = 1 - fy;
634
+ return (((lerp4(p00, p10, p01, p11, 24, fx, fy, gx, gy) << 24) |
635
+ (lerp4(p00, p10, p01, p11, 16, fx, fy, gx, gy) << 16) |
636
+ (lerp4(p00, p10, p01, p11, 8, fx, fy, gx, gy) << 8) |
637
+ lerp4(p00, p10, p01, p11, 0, fx, fy, gx, gy)) >>>
638
+ 0);
639
+ }
640
+ /** One channel of bilinear's four pixels, weighed by fx and fy and by gx and gy, their complements. */
641
+ function lerp4(p00, p10, p01, p11, shift, fx, fy, gx, gy) {
642
+ const top = ((p00 >>> shift) & 0xff) * gx + ((p10 >>> shift) & 0xff) * fx;
643
+ const bottom = ((p01 >>> shift) & 0xff) * gx + ((p11 >>> shift) & 0xff) * fx;
644
+ return Math.round(top * gy + bottom * fy);
645
+ }
646
+ /** A premultiplied pixel through a colour transform, as colorTransform computes it. */
647
+ function transformed(p, ct) {
648
+ const a0 = p >>> 24;
649
+ if (a0 === 0 && ct.skipZeroAlpha) {
650
+ return 0;
651
+ }
652
+ const a = colorChannel(a0, ct.aMul, ct.aAdd);
653
+ return (((a << 24) |
654
+ (drawColorChannel(p, a0, 16, ct.rMul, ct.rAdd, a) << 16) |
655
+ (drawColorChannel(p, a0, 8, ct.gMul, ct.gAdd, a) << 8) |
656
+ drawColorChannel(p, a0, 0, ct.bMul, ct.bAdd, a)) >>>
657
+ 0);
658
+ }
659
+ function prepareColorTransform(ct) {
660
+ return {
661
+ rMul: Math.trunc(ct.rMul * 256),
662
+ gMul: Math.trunc(ct.gMul * 256),
663
+ bMul: Math.trunc(ct.bMul * 256),
664
+ aMul: Math.trunc(ct.aMul * 256),
665
+ rAdd: Math.trunc(ct.rAdd),
666
+ gAdd: Math.trunc(ct.gAdd),
667
+ bAdd: Math.trunc(ct.bAdd),
668
+ aAdd: Math.trunc(ct.aAdd),
669
+ // The alpha-zero guard tests the original offset before truncation.
670
+ skipZeroAlpha: ct.aAdd <= 0,
671
+ };
672
+ }
673
+ // These stay outside the pixel loops so a transform creates no per-pixel closures.
674
+ function colorChannel(v, mul, add) {
675
+ return Math.max(0, Math.min(255, Math.floor((v * mul) / 256) + add));
676
+ }
677
+ function straightChannel(p, alpha, shift) {
678
+ return alpha === 0 ? 0 : Math.min(255, Math.floor((((p >>> shift) & 0xff) * 255) / alpha));
679
+ }
680
+ function premultipliedChannel(v, alpha) {
681
+ return alpha === 255 ? v : (v * (alpha + 1)) >> 8;
682
+ }
683
+ function drawColorChannel(p, alpha, shift, mul, add, outAlpha) {
684
+ const v = colorChannel(straightChannel(p, alpha, shift), mul, add);
685
+ return premultipliedChannel(v, outAlpha);
686
+ }
687
+ /** A premultiplied pixel with all four channels scaled by a coverage. */
688
+ function scaleBy(p, f) {
689
+ return (((Math.round((p >>> 24) * f) << 24) |
690
+ (Math.round(((p >>> 16) & 0xff) * f) << 16) |
691
+ (Math.round(((p >>> 8) & 0xff) * f) << 8) |
692
+ Math.round((p & 0xff) * f)) >>>
693
+ 0);
694
+ }
695
+ /**
696
+ * The destination alone, as erase and alpha leave it: each channel scaled
697
+ * by k / 255, what the source's alpha leaves or is, floored, as adl's are.
698
+ */
699
+ function scaleFloored(d, k) {
700
+ const a = Math.floor(((d >>> 24) * k) / 255);
701
+ const r = Math.floor((((d >>> 16) & 0xff) * k) / 255);
702
+ const g = Math.floor((((d >>> 8) & 0xff) * k) / 255);
703
+ const b = Math.floor(((d & 0xff) * k) / 255);
704
+ return ((a << 24) | (r << 16) | (g << 8) | b) >>> 0;
705
+ }
706
+ /** x * y / 255, rounded as the blend modes' arithmetic has it. */
707
+ function mul(x, y) {
708
+ return (x * y + 127) / 255;
709
+ }
710
+ /**
711
+ * Premultiplied s over d by a blend mode, the W3C's compositing with each
712
+ * mode's mix. Every helper takes its operands as arguments, so a pixel
713
+ * allocates nothing: a closure over them would take a context each call.
714
+ */
715
+ function blend(mode, s, d) {
716
+ if (mode === NORMAL) {
717
+ return over(s, d);
718
+ }
719
+ const sa = s >>> 24;
720
+ if (mode === ERASE || mode === ALPHA) {
721
+ return scaleFloored(d, mode === ERASE ? 255 - sa : sa);
722
+ }
723
+ const da = d >>> 24;
724
+ const a = mode === ADD || mode === SUBTRACT
725
+ ? Math.min(255, sa + da)
726
+ : Math.min(255, Math.round(sa + da - mul(sa, da)));
727
+ return (((a << 24) |
728
+ (blendChannel(mode, (s >>> 16) & 0xff, (d >>> 16) & 0xff, sa, da) << 16) |
729
+ (blendChannel(mode, (s >>> 8) & 0xff, (d >>> 8) & 0xff, sa, da) << 8) |
730
+ blendChannel(mode, s & 0xff, d & 0xff, sa, da)) >>>
731
+ 0);
732
+ }
733
+ /** A colour channel of blend: source cs and destination cd under alphas sa and da. */
734
+ function blendChannel(mode, cs, cd, sa, da) {
735
+ let b;
736
+ switch (mode) {
737
+ case MULTIPLY:
738
+ b = mul(cs, cd);
739
+ break;
740
+ case SCREEN:
741
+ b = (cs * da) / 255 + (cd * sa) / 255 - mul(cs, cd);
742
+ break;
743
+ case LIGHTEN:
744
+ b = Math.max(mul(cs, da), mul(cd, sa));
745
+ break;
746
+ case DARKEN:
747
+ b = Math.min(mul(cs, da), mul(cd, sa));
748
+ break;
749
+ case DIFFERENCE:
750
+ b = Math.abs(mul(cs, da) - mul(cd, sa));
751
+ break;
752
+ case ADD:
753
+ return Math.min(255, cs + cd);
754
+ case SUBTRACT:
755
+ return Math.max(0, cd - cs);
756
+ case INVERT:
757
+ // The destination inverted, the source's colour unused.
758
+ return Math.max(0, Math.min(255, Math.floor(mul(255 - cd, sa) + mul(cd, 255 - sa))));
759
+ case OVERLAY:
760
+ b = 2 * cd <= da ? 2 * mul(cs, cd) : mul(sa, da) - 2 * mul(da - cd, sa - cs);
761
+ break;
762
+ default:
763
+ b = 2 * cs <= sa ? 2 * mul(cs, cd) : mul(sa, da) - 2 * mul(da - cd, sa - cs);
764
+ break;
765
+ }
766
+ return Math.max(0, Math.min(255, Math.floor(b + mul(cs, 255 - da) + mul(cd, 255 - sa))));
767
+ }