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 +117 -6
- package/dist/DomTexture.d.ts +44 -0
- package/dist/DomTexture.d.ts.map +1 -1
- package/dist/DomTexture.js +166 -2
- package/dist/DomTexture.js.map +1 -1
- package/dist/MeshGlBlitter.d.ts +20 -0
- package/dist/MeshGlBlitter.d.ts.map +1 -1
- package/dist/MeshGlBlitter.js +31 -8
- package/dist/MeshGlBlitter.js.map +1 -1
- package/dist/SpineHtmlRenderer.d.ts +100 -7
- package/dist/SpineHtmlRenderer.d.ts.map +1 -1
- package/dist/SpineHtmlRenderer.js +332 -13
- package/dist/SpineHtmlRenderer.js.map +1 -1
- package/package.json +1 -1
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
|
-
-
|
|
90
|
-
|
|
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),
|
|
422
|
-
|
|
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-
|
|
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
|
package/dist/DomTexture.d.ts
CHANGED
|
@@ -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
|
package/dist/DomTexture.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/DomTexture.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/DomTexture.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DomTexture.js","sourceRoot":"","sources":["../src/DomTexture.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,GAKR,MAAM,8BAA8B,CAAC;AAEtC;;;;GAIG;AACH,MAAM,OAAO,UAAW,SAAQ,OAAO;IACrC,UAAU,CAAC,UAAyB,EAAE,UAAyB,IAAS,CAAC;IACzE,QAAQ,CAAC,MAAmB,EAAE,MAAmB,IAAS,CAAC;IAC3D,OAAO,KAAU,CAAC;CACnB;AAwBD;;;;;;;GAOG;AACH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;AAEpC,yEAAyE;AACzE,SAAS,WAAW,CAAC,GAAW;IAC9B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC;QAAE,OAAO;IACnC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,qBAAqB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAgB9C
|
|
1
|
+
{"version":3,"file":"DomTexture.js","sourceRoot":"","sources":["../src/DomTexture.ts"],"names":[],"mappings":"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"}
|
package/dist/MeshGlBlitter.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/MeshGlBlitter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|