spine-html 0.5.2 → 0.6.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 CHANGED
@@ -86,8 +86,29 @@ with a self-contained test suite (backend visual parity + invariants) and CI.
86
86
  - ✅ Dirty-skip: a mesh whose canvas-space vertices didn't change reuses last frame's
87
87
  raster — the CSS translate still tracks it, so parts that hold a pose (or move by
88
88
  whole pixels) pay zero raster. 10 frozen spineboys: WebKit ~141 → ~0.5 ms/frame
89
- - Clipping deliberately unsupported (counted and skipped); layered transparent
90
- parts + `overflow: hidden` cover the practical cases
89
+ - Clipping attachments as element-level CSS `clip-path`: spine-core's semantics
90
+ (one active clip at a time, applied from the clip's slot through its end slot
91
+ inclusive), with the polygon expressed in each element's own local frame — the
92
+ inverse of the `<img>` matrix for rigid slots, the canvas translate for mesh
93
+ slots. Covers **part masks** (a clip with a real end slot, like this repository's
94
+ own `portal` animation) and **whole-skeleton clips** (an end slot that never
95
+ arrives, which takes a fast path: one `clip-path` on the root instead of one per
96
+ element). **Inverse clips** are supported too: the region they keep has a hole
97
+ in it, so it needs two rings, and `polygon()` carries only one — a box and a
98
+ polygon listed inside one `polygon()` become a single self-intersecting ring
99
+ whose even-odd fill leaves a wedge along the seam between them (measured, on a
100
+ corner of spineboy's boot). So an inverse clip is written as a two-subpath
101
+ `path(evenodd, …)` whose outer ring is the element's own box. The whole feature
102
+ is element-level, so both mesh backends see the same thing and neither raster
103
+ path knows clipping exists; `renderer.clipping = false`
104
+ restores the old counted-and-skipped behaviour. Concave polygons go to CSS as
105
+ authored — the convex decomposition spine's CPU clipper performs exists to feed a
106
+ triangle rasterizer and has no job here. Two things this does not do: Spine's
107
+ clipper convexifies an *inverse* polygon (convex hull) where CSS clips it as
108
+ authored, so the two agree exactly when that polygon is convex — which is the
109
+ shape `inverse` is meant for; and the cost of many simultaneous `clip-path`s on
110
+ real-device Safari is **not measured** (the headless numbers that exist are not
111
+ Safari evidence — see Measured above)
91
112
  - ✅ Safari mesh cost root-caused by on-device triangulation (two corrections deep):
92
113
  the early "~15× slower per-triangle path" was a **headless-WebKit artifact**
93
114
  (software rasterization), and the follow-up "on par with Chromium" held only for
@@ -278,6 +299,46 @@ what the atlas declares, so a page declared at a size its artwork was not
278
299
  packed at reads every region from the wrong place in both tiers — correct the
279
300
  `size:` line, which is what every other Spine runtime needs too.
280
301
 
302
+ ### Premultiplied pages (`pma: true`)
303
+
304
+ The Spine texture packer premultiplies alpha by default, and says so with a page
305
+ line:
306
+
307
+ ```
308
+ hero.png
309
+ size: 2048, 2048
310
+ pma: true
311
+ ```
312
+
313
+ Such a page's RGB is already multiplied by its alpha, and nothing is asked of
314
+ you: the flag is read off the atlas and each tier is handed the page in the
315
+ convention it actually consumes. The `webgl` mesh backend uploads the texels
316
+ unconverted (its blend already expects premultiplied source — lossless). The DOM
317
+ and canvas2d tiers cannot: an `<img>` and `drawImage` composite straight alpha by
318
+ definition, so they read a straight-alpha derivation of the page —
319
+ `rgb = round(rgb * 255 / a)`, computed once and shared by the region cuts and
320
+ the mesh raster. Without it every semi-transparent texel is multiplied by its
321
+ alpha a second time and draws darker than it was authored: soft edges, soft
322
+ shadows, glows.
323
+
324
+ What it costs: **one page-sized canvas per premultiplied page image**
325
+ (width × height × 4 bytes — 1 MiB for a 1024×256 page, 16 MiB for a 2048×2048
326
+ one), alive as long as you hold the page image and released with it; the cache
327
+ is weak, so there is nothing to free by hand. A page with no `pma:` line derives
328
+ nothing and takes exactly the path it always did. The division is 8-bit and
329
+ starts from a canvas read, which has itself quantized a premultiplied texel, so
330
+ a very transparent texel can land a few levels off — exact at alpha 0 and 255,
331
+ within one level of 255 above alpha 128, and a little more below that, by an
332
+ amount that belongs to the browser's canvas rather than to this package (some
333
+ rasterizers round premultiplied storage several times more coarsely than
334
+ others). It is bounded by what that canvas already costs, and it is invisible
335
+ wherever the texel is: a texel at alpha 11 is ~4% opaque. The `webgl` backend,
336
+ having no such step, is exact.
337
+
338
+ One consequence worth knowing if your atlas is one part per page: a whole-page
339
+ region on a premultiplied page is **cut** rather than handed through, because
340
+ the page's own URL holds premultiplied pixels (see below).
341
+
281
342
  ### What unloading frees
282
343
 
283
344
  `revokeRegions()` — and `assets.dispose()`, which just calls it — frees the blob
@@ -337,7 +398,11 @@ flag. Two things to know:
337
398
  - Regions like these cover their whole page, so `unpackRegions` hands the page
338
399
  image straight through instead of cutting and re-encoding it — at any image
339
400
  resolution, since covering the page is a statement about the declared size.
340
- Load cost for this atlas shape is just the image loads.
401
+ Load cost for this atlas shape is just the image loads. The exception is a
402
+ page marked `pma: true`: its URL holds premultiplied pixels, which an `<img>`
403
+ would composite as straight alpha, so those regions are cut from the
404
+ straight-alpha derivation like any other (and the blob is revoked by
405
+ `revokeRegions` like any other).
341
406
 
342
407
  For a **rigid-only** skeleton you can skip atlas unpacking altogether and hand
343
408
  the renderer a map you build yourself — meshes cannot, because the deform tier
@@ -385,6 +450,36 @@ const renderer = new SpineHtmlRenderer(rootElement, regionImages);
385
450
  mesh once (no reallocation), so it is fine to expose as a user setting.
386
451
  - `renderer.triangleExpand` — clip overdraw in px that closes antialiased mesh
387
452
  seams (default 0.5). Also re-rasters every mesh once when changed.
453
+ - `renderer.clipping` — apply clipping attachments (default `true`). What it
454
+ writes is one CSS `clip-path` per element the active clip covers, in that
455
+ element's own local frame; nothing reaches the raster backends, and a clip is
456
+ not part of the mesh dirty signature, so a mesh that held still keeps reusing
457
+ its raster under a moving clip. **Writes happen on change only**: each
458
+ clip-path is cached exactly as `transform` is, coordinates are quantized to
459
+ 1/1000 of a local unit so float jitter cannot defeat that cache, and a static
460
+ polygon over a static pose therefore costs **zero style writes per frame after
461
+ the first** — `renderer.clipWriteCount` is the check, alongside
462
+ `clipCount` (applied) and `clipSkipCount` (not applied: switched off, a second
463
+ clip met while one was active, an inactive bone, a degenerate polygon).
464
+ **The whole-skeleton fast path**: when the clip starts before anything has been
465
+ drawn and never ends, one `clip-path` goes on the root instead of one per
466
+ element. The root is *your* element, so its inline `clip-path` is **borrowed,
467
+ not taken** — saved on the first write and put back verbatim when the clip
468
+ stops covering the frame, when `clipping` goes `false`, and by `dispose()`.
469
+ Per-element clip-paths and the root clip-path are never both in force for the
470
+ same clip. Two things to know about that path: the polygon is written in the
471
+ root's **border box** frame, which is where absolutely-positioned slot elements
472
+ start too *unless the root has a CSS border* (a border would offset the
473
+ whole-skeleton clip by its width — keep borders off the render root, which is
474
+ the normal shape for a 0×0 origin element); and an inverse clip never takes it,
475
+ because its CSS form needs an outer ring around a box and the root has none.
476
+ One further consequence of that path: a `clip-path` other than `none` makes an
477
+ element a stacking context (CSS Masking), so a whole-skeleton clip isolates
478
+ `mix-blend-mode` slots from backdrops *outside* the root — which a root
479
+ carrying a `transform` (the usual pan/zoom stage) already does. Per-element
480
+ clips do not change blending, since a blended slot is its own stacking context
481
+ either way. Setting `clipping = false` removes every clip-path this renderer
482
+ wrote.
388
483
 
389
484
  **A zoomable stage.** The mesh tier rasters at `world × pixelRatio` in the
390
485
  root's own coordinates, and it cannot see a CSS transform above the root — so
@@ -418,8 +513,9 @@ Debug knobs (query string): `?skel=pro|ess` `?anim=walk` `?count=10` pick the sc
418
513
  ratio, `?timescale=0` freezes the pose (every mesh should report "reused"),
419
514
  `?expand=0` disables the crack-closing clip overdraw, `?backend=webgl` rasterizes
420
515
  meshes through the shared WebGL blitter (also a live header select; the stats line
421
- names the active backend), and `?time=1.2` seeks every instance to the same pose
422
- for deterministic captures.
516
+ names the active backend), `?clipping=0` turns clipping attachments back off (they
517
+ are then counted and skipped, as before v0.6 — try it on `?anim=portal`), and
518
+ `?time=1.2` seeks every instance to the same pose for deterministic captures.
423
519
 
424
520
  ## Tests
425
521
 
@@ -436,9 +532,24 @@ are diffed directly with a shift-tolerant comparison, so missing parts,
436
532
  wrong colors, and tint/blend divergence fail regardless of platform.
437
533
  Hairline seams are guarded by a deterministic canary (`?expand=0` must
438
534
  change the canvas2d raster), and counter tests pin the dirty-skip /
439
- grow-only-backing / clip-skip invariants plus spine-core's region corner
535
+ grow-only-backing / clip-counter invariants plus spine-core's region corner
440
536
  order (BL, UL, UR, BR).
441
537
 
538
+ Clipping gets its own oracle (`tests/clipping.spec.ts`), because a clip-path
539
+ that went through the wrong transform still parses and still counts as applied.
540
+ A clipped capture is compared against the **unclipped** capture of the same
541
+ slots masked in **screen space** by the world polygon — built once through the
542
+ stage transform, never through an element's local frame, so the check cannot
543
+ agree with the renderer by sharing its mistake. What it asserts is occupancy,
544
+ not pixel values: outside a 3 px band around the polygon's outline (where the
545
+ browser's clip-path antialiasing and the canvas `ctx.clip()` the oracle uses
546
+ legitimately differ), **no artwork may survive where the polygon excludes it
547
+ and none may go missing where it does not** — both counts absolute, no budget.
548
+ Pixel values are logged but not asserted, because a clipped element is drawn
549
+ through a mask and its silhouettes come out a shade different all over the
550
+ picture; a control capture, clipped by a polygon that removes nothing, pins
551
+ that down by coming back byte-identical to the unclipped one.
552
+
442
553
  The loading path is not observable in a rendered frame, so it gets its own
443
554
  page (`tests/harness.html`, a second build entry) that exposes the library to
444
555
  the specs directly. Its oracle is the browser: a revoked object URL stops
@@ -30,6 +30,50 @@ export interface RegionImage {
30
30
  /** Unpacked height in atlas units — see `width`. */
31
31
  height: number;
32
32
  }
33
+ /**
34
+ * Page pixels in the alpha convention a DOM/canvas2d consumer needs: the page
35
+ * image itself, or the derived canvas below when the page is premultiplied.
36
+ */
37
+ export type PageSource = HTMLImageElement | HTMLCanvasElement;
38
+ /**
39
+ * How many straight-alpha page derivations have been built in this document.
40
+ *
41
+ * Deterministic (no GC in the path) and monotonic, so a test reads it before
42
+ * and after and asserts the *difference*. Exported for the tests only — it is
43
+ * not re-exported from index.ts and the `exports` map denies deep imports, so
44
+ * it is not package API. (The same arrangement as `liveTextureCount` on the GL
45
+ * blitter, and for the same reason: a cache that works is invisible.)
46
+ */
47
+ export declare function straightAlphaDerivations(): number;
48
+ /**
49
+ * The page as a straight-alpha source: the image itself when `pma` is false,
50
+ * otherwise its (cached) un-premultiplied derivation.
51
+ *
52
+ * `rgb = round(rgb * 255 / a)`, clamped, with `a === 0` left at (0,0,0,0) —
53
+ * there is no colour to recover from a fully transparent texel.
54
+ *
55
+ * **Precision.** The division is done in 8 bits and cannot be done anywhere
56
+ * else: a 2D canvas stores premultiplied colour, so `getImageData` hands back
57
+ * `round(round(u * a / 255) * 255 / a)` for a file value `u`, already quantized
58
+ * by the read (measured on both engines: put→get moves a value by up to 127 at
59
+ * a = 1). The colour a low-alpha texel composites to is therefore off by at
60
+ * most ~min(a, 127.5/a + 0.5) of 255 — nothing at a = 255 or a = 0, ≤ 1 above
61
+ * a = 128, worst ~12 around a = 11, and bounded by `a` below that. Writing the
62
+ * values back is exact, not a second premultiply: the canvas quantizer is
63
+ * idempotent, so putting a value that came out of it stores it unchanged
64
+ * (measured: a second round trip moves nothing on either engine). The remaining
65
+ * error is one-way rounding on near-invisible texels, against a defect that
66
+ * darkened every semi-transparent texel by up to 59 luma.
67
+ *
68
+ * A page image that cannot be read — no 2D context, or a cross-origin image
69
+ * without CORS, where `getImageData` throws — falls back to the page itself and
70
+ * caches *that*: the colour is then as wrong as it is today, but it happens
71
+ * once per image instead of once per frame, and no frame throws. (The cut path
72
+ * already fails on such an image at `toBlob`, so only the mesh tier can get
73
+ * here.) An image that has not decoded yet is left uncached, so the derivation
74
+ * happens once it has.
75
+ */
76
+ export declare function straightAlphaSource(page: HTMLImageElement, pma: boolean): PageSource;
33
77
  /**
34
78
  * Cuts every atlas region out of the page image into its own bitmap once at
35
79
  * load time, restoring 90° packing rotation, so the per-frame path never
@@ -1 +1 @@
1
- {"version":3,"file":"DomTexture.d.ts","sourceRoot":"","sources":["../src/DomTexture.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,YAAY,EAEZ,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AAEtC;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,OAAO;IACrC,UAAU,CAAC,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,aAAa,GAAG,IAAI;IACtE,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IACxD,OAAO,IAAI,IAAI;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;;OASG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;CAChB;AAsMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,YAAY,EACnB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,GACxC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CA0EnC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI,CAEpE"}
1
+ {"version":3,"file":"DomTexture.d.ts","sourceRoot":"","sources":["../src/DomTexture.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,YAAY,EAEZ,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AAEtC;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,OAAO;IACrC,UAAU,CAAC,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,aAAa,GAAG,IAAI;IACtE,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IACxD,OAAO,IAAI,IAAI;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;;OASG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;CAChB;AAkBD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAqC9D;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,EAAE,OAAO,GAAG,UAAU,CAcpF;AAgQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,YAAY,EACnB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,GACxC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CA0EnC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI,CAEpE"}
@@ -24,6 +24,137 @@ function revokeOwned(url) {
24
24
  return;
25
25
  URL.revokeObjectURL(url);
26
26
  }
27
+ /**
28
+ * Straight-alpha derivations of `pma: true` page images, one per image.
29
+ *
30
+ * Spine's texture packer premultiplies by default, and an atlas says so with a
31
+ * `pma: true` page line: the file's RGB is already multiplied by its alpha. The
32
+ * DOM and canvas 2D have no such mode — an <img> and `drawImage` composite
33
+ * straight alpha by definition, so they multiply by alpha again and every
34
+ * semi-transparent texel comes out darker than it was authored (#37). Both
35
+ * consumers of that convention here — the region cuts below and the canvas2d
36
+ * mesh raster in SpineHtmlRenderer — therefore read this derivation instead of
37
+ * the page, and it is *shared*: deriving per consumer would pay for the same
38
+ * page once per renderer and once again for the load.
39
+ *
40
+ * (The GL mesh backend needs no derivation. It consumes premultiplied texels by
41
+ * construction, so it just skips `UNPACK_PREMULTIPLY_ALPHA_WEBGL` at upload —
42
+ * lossless, and the reason the two backends differ at all on such a page.)
43
+ *
44
+ * Weak by the page image, exactly like the GL texture cache: this package never
45
+ * pins the caller's image, and a page the caller drops takes its derivation
46
+ * with it. So nothing has to be freed by hand and `dispose()` stays out of it —
47
+ * the ownership boundary is the same one `revokeRegions` respects.
48
+ *
49
+ * **What it costs while alive:** one page-sized canvas per `pma` page image
50
+ * (4 bytes/px — 1 MiB for a 1024×256 page, 16 MiB for a 2048×2048 one), held
51
+ * for as long as the caller holds the page. It is deliberately *not* charged to
52
+ * the cut-backing budget below: that budget bounds the canvases in flight
53
+ * during a load, a peak that appears and drains, while this sits alongside the
54
+ * decoded page itself for the page's whole life — the same category as the
55
+ * decoded pages the budget already documents as resident throughout.
56
+ */
57
+ const straightAlphaPages = new WeakMap();
58
+ /** Derivations performed since load — see straightAlphaDerivations(). */
59
+ let derivationCount = 0;
60
+ /**
61
+ * How many straight-alpha page derivations have been built in this document.
62
+ *
63
+ * Deterministic (no GC in the path) and monotonic, so a test reads it before
64
+ * and after and asserts the *difference*. Exported for the tests only — it is
65
+ * not re-exported from index.ts and the `exports` map denies deep imports, so
66
+ * it is not package API. (The same arrangement as `liveTextureCount` on the GL
67
+ * blitter, and for the same reason: a cache that works is invisible.)
68
+ */
69
+ export function straightAlphaDerivations() {
70
+ return derivationCount;
71
+ }
72
+ /**
73
+ * The page as a straight-alpha source: the image itself when `pma` is false,
74
+ * otherwise its (cached) un-premultiplied derivation.
75
+ *
76
+ * `rgb = round(rgb * 255 / a)`, clamped, with `a === 0` left at (0,0,0,0) —
77
+ * there is no colour to recover from a fully transparent texel.
78
+ *
79
+ * **Precision.** The division is done in 8 bits and cannot be done anywhere
80
+ * else: a 2D canvas stores premultiplied colour, so `getImageData` hands back
81
+ * `round(round(u * a / 255) * 255 / a)` for a file value `u`, already quantized
82
+ * by the read (measured on both engines: put→get moves a value by up to 127 at
83
+ * a = 1). The colour a low-alpha texel composites to is therefore off by at
84
+ * most ~min(a, 127.5/a + 0.5) of 255 — nothing at a = 255 or a = 0, ≤ 1 above
85
+ * a = 128, worst ~12 around a = 11, and bounded by `a` below that. Writing the
86
+ * values back is exact, not a second premultiply: the canvas quantizer is
87
+ * idempotent, so putting a value that came out of it stores it unchanged
88
+ * (measured: a second round trip moves nothing on either engine). The remaining
89
+ * error is one-way rounding on near-invisible texels, against a defect that
90
+ * darkened every semi-transparent texel by up to 59 luma.
91
+ *
92
+ * A page image that cannot be read — no 2D context, or a cross-origin image
93
+ * without CORS, where `getImageData` throws — falls back to the page itself and
94
+ * caches *that*: the colour is then as wrong as it is today, but it happens
95
+ * once per image instead of once per frame, and no frame throws. (The cut path
96
+ * already fails on such an image at `toBlob`, so only the mesh tier can get
97
+ * here.) An image that has not decoded yet is left uncached, so the derivation
98
+ * happens once it has.
99
+ */
100
+ export function straightAlphaSource(page, pma) {
101
+ if (!pma)
102
+ return page;
103
+ const cached = straightAlphaPages.get(page);
104
+ if (cached)
105
+ return cached;
106
+ const width = page.naturalWidth;
107
+ const height = page.naturalHeight;
108
+ // Nothing to read yet: decide later rather than caching an empty canvas.
109
+ if (!(width > 0) || !(height > 0))
110
+ return page;
111
+ const derived = unpremultiply(page, width, height);
112
+ straightAlphaPages.set(page, derived);
113
+ if (derived !== page)
114
+ derivationCount++;
115
+ return derived;
116
+ }
117
+ /** The pixel pass behind straightAlphaSource; `page` back on any read failure. */
118
+ function unpremultiply(page, width, height) {
119
+ const canvas = document.createElement('canvas');
120
+ canvas.width = width;
121
+ canvas.height = height;
122
+ // No willReadFrequently: this reads once and is then a drawImage *source*
123
+ // for the rest of its life, which wants the accelerated surface.
124
+ const ctx = canvas.getContext('2d');
125
+ if (!ctx)
126
+ return page;
127
+ ctx.drawImage(page, 0, 0);
128
+ let pixels;
129
+ try {
130
+ pixels = ctx.getImageData(0, 0, width, height);
131
+ }
132
+ catch {
133
+ // Tainted canvas (cross-origin page image, no CORS).
134
+ return page;
135
+ }
136
+ const data = pixels.data;
137
+ for (let i = 0; i < data.length; i += 4) {
138
+ const a = data[i + 3];
139
+ if (a === 255)
140
+ continue;
141
+ if (a === 0) {
142
+ // Nothing to divide by, and nothing there to see.
143
+ data[i] = 0;
144
+ data[i + 1] = 0;
145
+ data[i + 2] = 0;
146
+ continue;
147
+ }
148
+ const scale = 255 / a;
149
+ // Rounded explicitly: assigning a fraction to a Uint8ClampedArray rounds
150
+ // half-to-even, which is not the rule stated above.
151
+ data[i] = Math.min(255, Math.round(data[i] * scale));
152
+ data[i + 1] = Math.min(255, Math.round(data[i + 1] * scale));
153
+ data[i + 2] = Math.min(255, Math.round(data[i + 2] * scale));
154
+ }
155
+ ctx.putImageData(pixels, 0, 0);
156
+ return canvas;
157
+ }
27
158
  /**
28
159
  * Backing pixels the cuts still waiting for their PNG encode may hold at once.
29
160
  *
@@ -80,6 +211,22 @@ const CUT_BACKING_BUDGET_PX = 4 * 1024 * 1024;
80
211
  * neighbouring pixel into every cut on every side, which is bleed the caller
81
212
  * can see. A rect that scales down below one pixel still gets a 1×1 bitmap,
82
213
  * since a zero-sized canvas is not one.
214
+ *
215
+ * The alternative was measured, not argued away (#35): sampling the
216
+ * *fractional* rect with interpolation — what a GPU does with normalized UVs —
217
+ * buys exact placement with a resample of the whole bitmap. Both rules drew
218
+ * the same frozen rigid-only pose from a rescaled page, diffed against that
219
+ * pose drawn from the 1:1 page. The result is split, so the lossless copy
220
+ * stays: sampling lowers the raw difference and the peak channel delta, while
221
+ * rounding lowers the count of pixels with no in-tolerance match anywhere in
222
+ * the reference's 3×3 — rounding displaces the picture, which a shift-tolerant
223
+ * compare forgives, and sampling blurs it, which it does not. On that
224
+ * shift-tolerant count at 0.5×, the common case, rounding is ahead in every
225
+ * cell measured, by 1.3× to 2.2× (chromium 638 vs 1238, webkit 1443 vs 1888) —
226
+ * an ordering rather than a count, since the counts themselves move with the
227
+ * raster. At an integer page scale the two rules come out bit-identical, there
228
+ * being no fractional part to disagree about, so an @2x page never had a
229
+ * question to answer. `tests/cut-rule.spec.ts` holds the 0.5× half of this.
83
230
  */
84
231
  function planCut(region, image) {
85
232
  const iw = image.naturalWidth;
@@ -103,7 +250,19 @@ function planCut(region, image) {
103
250
  // whole page is a whole page whether it ships at 1×, half or double, and the
104
251
  // cut would copy the image either way. (It used to be tested against the
105
252
  // image, which was the same rule as long as the two always agreed.)
106
- if (!rotated && region.x === 0 && region.y === 0 && packedW === pageW && packedH === pageH) {
253
+ //
254
+ // Except on a premultiplied page, where the page URL is exactly what must not
255
+ // be handed through: those pixels are premultiplied and an <img> composites
256
+ // straight alpha, so passing the URL along would ship the defect to the one
257
+ // atlas shape that never even allocates a canvas (one part per page). Such a
258
+ // region is cut like any other — from the straight-alpha derivation — and the
259
+ // blob it mints is owned and revoked like any other.
260
+ if (!region.page.pma &&
261
+ !rotated &&
262
+ region.x === 0 &&
263
+ region.y === 0 &&
264
+ packedW === pageW &&
265
+ packedH === pageH) {
107
266
  return { passThrough: true, sx: 0, sy: 0, sw: iw, sh: ih, cw: iw, ch: ih };
108
267
  }
109
268
  const scaleX = iw / pageW;
@@ -168,7 +327,12 @@ async function cutRegion(region, pageImages) {
168
327
  ctx.translate(0, plan.ch);
169
328
  ctx.rotate(-Math.PI / 2);
170
329
  }
171
- ctx.drawImage(image, plan.sx, plan.sy, plan.sw, plan.sh, 0, 0, plan.sw, plan.sh);
330
+ // Straight alpha on a premultiplied page, since the cut is encoded to a PNG
331
+ // and shown in an <img>. The rect was planned against the image, and the
332
+ // derivation is that image's own size, so the coordinates carry over
333
+ // unchanged — including on a page that ships at another resolution.
334
+ const source = straightAlphaSource(image, region.page.pma);
335
+ ctx.drawImage(source, plan.sx, plan.sy, plan.sw, plan.sh, 0, 0, plan.sw, plan.sh);
172
336
  const blob = await new Promise((resolve, reject) => {
173
337
  canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
174
338
  });
@@ -1 +1 @@
1
- {"version":3,"file":"DomTexture.js","sourceRoot":"","sources":["../src/DomTexture.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DomTexture.js","sourceRoot":"","sources":["../src/DomTexture.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,8 @@
7
7
  * shelf-packed into a rect of the offscreen canvas, its triangles are drawn
8
8
  * textured (atlas page texture, premultiplied alpha), and each rect is blitted
9
9
  * onto the mesh's per-part 2d canvas with an unclipped drawImage rect copy.
10
+ * The texture is premultiplied either by the upload or by the exporter — a
11
+ * `pma: true` page arrives that way, so it is uploaded unconverted (textureFor).
10
12
  *
11
13
  * Why this exists: Safari antialiases canvas2d clip paths, so the standard
12
14
  * per-triangle clip+transform+drawImage mapping pays a per-triangle AA-mask
@@ -33,6 +35,13 @@ export interface MeshBlitJob {
33
35
  canvas: HTMLCanvasElement;
34
36
  /** Atlas page image the mesh samples (cached as a GL texture on first use). */
35
37
  page: HTMLImageElement;
38
+ /**
39
+ * `page.pma` — whether the page's texels are already premultiplied, which
40
+ * decides whether the upload premultiplies them again (see textureFor). It
41
+ * travels with the page because it describes the page's *pixels*, and the
42
+ * blend below wants them premultiplied exactly once.
43
+ */
44
+ pma: boolean;
36
45
  /** Bbox-relative vertices in CSS px, x/y interleaved (indexed via `triangles`). */
37
46
  vertices: Float64Array;
38
47
  /** Normalized page UVs aligned with `vertices`. */
@@ -99,6 +108,17 @@ declare class MeshGlBlitter {
99
108
  */
100
109
  private markLost;
101
110
  private compile;
111
+ /**
112
+ * The page's GL texture, uploaded on demand.
113
+ *
114
+ * `pma` is a property of the page's *pixels*, so one image cannot honestly be
115
+ * both: two atlases naming the same image with different `pma:` lines are not
116
+ * a mixed page, one of them is simply wrong about the file. The cache is keyed
117
+ * by the image, so rather than trusting that, a flag that disagrees with the
118
+ * live upload re-uploads under the new one — the pathological case stays
119
+ * correct (at the price of re-uploading while it lasts) instead of silently
120
+ * drawing one of the two atlases wrong.
121
+ */
102
122
  private textureFor;
103
123
  }
104
124
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"MeshGlBlitter.d.ts","sourceRoot":"","sources":["../src/MeshGlBlitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,MAAM,EAAE,iBAAiB,CAAC;IAC1B,+EAA+E;IAC/E,IAAI,EAAE,gBAAgB,CAAC;IACvB,mFAAmF;IACnF,QAAQ,EAAE,YAAY,CAAC;IACvB,mDAAmD;IACnD,GAAG,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACvB,gDAAgD;IAChD,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAC7B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAoCD,cAAM,aAAa;IACjB,IAAI,UAAS;IACb;;;;OAIG;IACH,gBAAgB,SAAK;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;IAC3D,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAwB;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuB;IACnD;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAgD;IAChE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,UAAU,CAA0B;IAC5C,sDAAsD;IACtD,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,KAAK,CAAgB;;IAoD7B;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,OAAO;IAwGnC;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAMpC;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAarC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,UAAU;CA6BnB;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CASvD"}
1
+ {"version":3,"file":"MeshGlBlitter.d.ts","sourceRoot":"","sources":["../src/MeshGlBlitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,MAAM,EAAE,iBAAiB,CAAC;IAC1B,+EAA+E;IAC/E,IAAI,EAAE,gBAAgB,CAAC;IACvB;;;;;OAKG;IACH,GAAG,EAAE,OAAO,CAAC;IACb,mFAAmF;IACnF,QAAQ,EAAE,YAAY,CAAC;IACvB,mDAAmD;IACnD,GAAG,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACvB,gDAAgD;IAChD,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAC7B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAsCD,cAAM,aAAa;IACjB,IAAI,UAAS;IACb;;;;OAIG;IACH,gBAAgB,SAAK;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;IAC3D,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAwB;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuB;IACnD;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAgD;IAChE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,UAAU,CAA0B;IAC5C,sDAAsD;IACtD,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,KAAK,CAAgB;;IAoD7B;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,OAAO;IAwGnC;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAMpC;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAarC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,OAAO;IAYf;;;;;;;;;;OAUG;IACH,OAAO,CAAC,UAAU;CAuCnB;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CASvD"}
@@ -7,6 +7,8 @@
7
7
  * shelf-packed into a rect of the offscreen canvas, its triangles are drawn
8
8
  * textured (atlas page texture, premultiplied alpha), and each rect is blitted
9
9
  * onto the mesh's per-part 2d canvas with an unclipped drawImage rect copy.
10
+ * The texture is premultiplied either by the upload or by the exporter — a
11
+ * `pma: true` page arrives that way, so it is uploaded unconverted (textureFor).
10
12
  *
11
13
  * Why this exists: Safari antialiases canvas2d clip paths, so the standard
12
14
  * per-triangle clip+transform+drawImage mapping pays a per-triangle AA-mask
@@ -201,7 +203,7 @@ class MeshGlBlitter {
201
203
  for (let i = 0; i < jobs.length; i++) {
202
204
  const job = jobs[i];
203
205
  if (job.page !== boundPage) {
204
- gl.bindTexture(gl.TEXTURE_2D, this.textureFor(job.page));
206
+ gl.bindTexture(gl.TEXTURE_2D, this.textureFor(job.page, job.pma));
205
207
  boundPage = job.page;
206
208
  }
207
209
  // Scissor is bottom-left origin; pack coords are top-left origin.
@@ -242,7 +244,7 @@ class MeshGlBlitter {
242
244
  if (entry)
243
245
  entry.users++;
244
246
  else
245
- this.textures.set(page, { texture: null, users: 1 });
247
+ this.textures.set(page, { texture: null, users: 1, pma: false });
246
248
  }
247
249
  /**
248
250
  * Gives back one retain(). The GL texture goes when the last user of the
@@ -290,17 +292,37 @@ class MeshGlBlitter {
290
292
  }
291
293
  return shader;
292
294
  }
293
- textureFor(page) {
295
+ /**
296
+ * The page's GL texture, uploaded on demand.
297
+ *
298
+ * `pma` is a property of the page's *pixels*, so one image cannot honestly be
299
+ * both: two atlases naming the same image with different `pma:` lines are not
300
+ * a mixed page, one of them is simply wrong about the file. The cache is keyed
301
+ * by the image, so rather than trusting that, a flag that disagrees with the
302
+ * live upload re-uploads under the new one — the pathological case stays
303
+ * correct (at the price of re-uploading while it lasts) instead of silently
304
+ * drawing one of the two atlases wrong.
305
+ */
306
+ textureFor(page, pma) {
294
307
  let entry = this.textures.get(page);
295
- if (entry?.texture)
296
- return entry.texture;
308
+ if (entry?.texture) {
309
+ if (entry.pma === pma)
310
+ return entry.texture;
311
+ this.gl.deleteTexture(entry.texture);
312
+ entry.texture = null;
313
+ this.liveTextureCount--;
314
+ }
297
315
  const gl = this.gl;
298
316
  const texture = gl.createTexture();
299
317
  if (!texture)
300
318
  throw new Error('createTexture failed');
301
319
  gl.bindTexture(gl.TEXTURE_2D, texture);
302
- // Premultiply at upload so blending and the premultiplied canvas agree.
303
- gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
320
+ // Premultiply at upload so blending and the premultiplied canvas agree
321
+ // unless the page is already premultiplied (`pma: true`), where doing it
322
+ // again is the second multiply that darkens every semi-transparent texel
323
+ // (#37). The blend below is premultiplied source-over either way, so a pma
324
+ // page is simply uploaded as it is: no conversion, nothing lost.
325
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, pma ? 0 : 1);
304
326
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
305
327
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, page);
306
328
  // Linear, no mips, clamped — NPOT-safe in WebGL1.
@@ -314,9 +336,10 @@ class MeshGlBlitter {
314
336
  // does not re-upload per flush; with no users only a context loss (or
315
337
  // the image being collected, which takes the weak entry with it) clears
316
338
  // it, which is exactly the old behavior for a caller that opts out.
317
- entry = { texture: null, users: 0 };
339
+ entry = { texture: null, users: 0, pma };
318
340
  this.textures.set(page, entry);
319
341
  }
342
+ entry.pma = pma;
320
343
  entry.texture = texture;
321
344
  this.liveTextureCount++;
322
345
  return texture;