spine-html 0.4.1 → 0.5.1
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/NOTICE.md +4 -3
- package/README.md +141 -3
- package/dist/DomTexture.d.ts +26 -3
- package/dist/DomTexture.d.ts.map +1 -1
- package/dist/DomTexture.js +164 -61
- package/dist/DomTexture.js.map +1 -1
- package/dist/MeshGlBlitter.d.ts +43 -1
- package/dist/MeshGlBlitter.d.ts.map +1 -1
- package/dist/MeshGlBlitter.js +85 -8
- package/dist/MeshGlBlitter.js.map +1 -1
- package/dist/SpineHtmlRenderer.d.ts +121 -7
- package/dist/SpineHtmlRenderer.d.ts.map +1 -1
- package/dist/SpineHtmlRenderer.js +265 -30
- package/dist/SpineHtmlRenderer.js.map +1 -1
- package/dist/binary.d.ts +48 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +29 -0
- package/dist/binary.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/loadAtlasAssets.d.ts +73 -0
- package/dist/loadAtlasAssets.d.ts.map +1 -0
- package/dist/loadAtlasAssets.js +75 -0
- package/dist/loadAtlasAssets.js.map +1 -0
- package/dist/loadSkeletonAssets.d.ts +41 -22
- package/dist/loadSkeletonAssets.d.ts.map +1 -1
- package/dist/loadSkeletonAssets.js +48 -49
- package/dist/loadSkeletonAssets.js.map +1 -1
- package/package.json +5 -1
- package/dist/main.d.ts +0 -2
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js +0 -238
- package/dist/main.js.map +0 -1
package/NOTICE.md
CHANGED
|
@@ -14,6 +14,7 @@ redistribution includes the Spine Runtimes license and copyright notice.
|
|
|
14
14
|
|
|
15
15
|
## Example assets
|
|
16
16
|
|
|
17
|
-
The spineboy skeleton, atlas, and images used by the demo
|
|
18
|
-
Software. They are **not redistributed** in this
|
|
19
|
-
downloads them from the official spine-runtimes
|
|
17
|
+
The spineboy skeleton exports (JSON and binary), atlas, and images used by the demo
|
|
18
|
+
and the tests are owned by Esoteric Software. They are **not redistributed** in this
|
|
19
|
+
repository; `scripts/fetch-assets.sh` downloads them from the official spine-runtimes
|
|
20
|
+
repository for local evaluation.
|
package/README.md
CHANGED
|
@@ -148,12 +148,78 @@ assets.dispose();
|
|
|
148
148
|
`spine-html` is a third-party renderer and is not affiliated with or endorsed by
|
|
149
149
|
Esoteric Software.
|
|
150
150
|
|
|
151
|
+
### One atlas, several skeletons
|
|
152
|
+
|
|
153
|
+
Skeletons that share an atlas — a character's `-ess` and `-pro` exports, a whole
|
|
154
|
+
cast packed onto one page — load in two steps instead. Calling
|
|
155
|
+
`loadSkeletonAssets` once per skeleton would fetch and unpack the same pages
|
|
156
|
+
again and mint a second set of region blob URLs:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { loadAtlasAssets, loadSkeletonJson, SpineHtmlRenderer } from 'spine-html';
|
|
160
|
+
|
|
161
|
+
const shared = await loadAtlasAssets({ atlasUrl: '/spineboy/spineboy.atlas' });
|
|
162
|
+
const [essData, proData] = await Promise.all([
|
|
163
|
+
loadSkeletonJson(shared, '/spineboy/spineboy-ess.json'),
|
|
164
|
+
loadSkeletonJson(shared, '/spineboy/spineboy-pro.json'),
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const ess = new SpineHtmlRenderer(essRoot, shared.regionImages);
|
|
168
|
+
const pro = new SpineHtmlRenderer(proRoot, shared.regionImages);
|
|
169
|
+
|
|
170
|
+
// Unloading: every renderer first, then the one dispose() that owns the bitmaps.
|
|
171
|
+
ess.dispose();
|
|
172
|
+
pro.dispose();
|
|
173
|
+
shared.dispose();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`loadAtlasAssets` takes the atlas half of the options above (`resolvePage`,
|
|
177
|
+
`crossOrigin`, `fetch`) and `loadSkeletonJson` the skeleton half (`scale`,
|
|
178
|
+
`fetch`). Ownership is the thing to keep straight: the atlas assets belong to
|
|
179
|
+
the caller, reading a skeleton against them never frees them, and one
|
|
180
|
+
`regionImages` map is meant to be handed to several renderers — so there is
|
|
181
|
+
exactly one `dispose()` for however many skeletons were read. `loadSkeletonJson`
|
|
182
|
+
asks only for `{ atlas }`, so a caller that built its atlas by hand (below) can
|
|
183
|
+
still use it for the read. `loadSkeletonAssets` is these two calls with the
|
|
184
|
+
atlas half kept private: use it for one atlas and one skeleton, this for the
|
|
185
|
+
rest.
|
|
186
|
+
|
|
187
|
+
### Binary exports (`.skel`)
|
|
188
|
+
|
|
189
|
+
Binary exports are read by a second parser, `SkeletonBinary`, which is as big as
|
|
190
|
+
the JSON one. It lives behind its own entry point so that consumers who only
|
|
191
|
+
ever read `.json` never carry it:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { loadAtlasAssets, SpineHtmlRenderer } from 'spine-html';
|
|
195
|
+
import { loadSkeletonBinary } from 'spine-html/binary';
|
|
196
|
+
|
|
197
|
+
const shared = await loadAtlasAssets({ atlasUrl: '/spineboy/spineboy.atlas' });
|
|
198
|
+
const data = await loadSkeletonBinary(shared, '/spineboy/spineboy-pro.skel');
|
|
199
|
+
|
|
200
|
+
const renderer = new SpineHtmlRenderer(rootElement, shared.regionImages);
|
|
201
|
+
// Unloading, as above: renderer.dispose(); shared.dispose();
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`loadSkeletonBinary` is `loadSkeletonJson` with the parser swapped: same
|
|
205
|
+
arguments, the same `{ scale, fetch }` options, the same ownership rule — it
|
|
206
|
+
never frees `assets`, whichever way it ends. Those two calls are the binary path
|
|
207
|
+
in full. There is no one-call `loadSkeletonAssetsBinary`, because the atlas half
|
|
208
|
+
is precisely what a caller reading binary usually wants kept in hand, and hiding
|
|
209
|
+
it again would only earn back a line.
|
|
210
|
+
|
|
211
|
+
The subpath is the whole mechanism, so import it as written: pulling
|
|
212
|
+
`loadSkeletonBinary` out of `'spine-html'` is not possible, by design. Worth
|
|
213
|
+
knowing about the format itself: binary exports are version-locked to the
|
|
214
|
+
runtime that reads them, so a `.skel` written by an editor newer than your
|
|
215
|
+
installed `@esotericsoftware/spine-core` fails in the read, not in the fetch.
|
|
216
|
+
|
|
151
217
|
### Loading it yourself
|
|
152
218
|
|
|
153
219
|
`loadSkeletonAssets` is optional sugar over five `spine-core` calls, and the
|
|
154
220
|
package works without it. Drop to the low-level path whenever you need
|
|
155
|
-
something
|
|
156
|
-
|
|
221
|
+
something no loader does — images that are already in memory, an atlas that is
|
|
222
|
+
not fetched from a URL at all:
|
|
157
223
|
|
|
158
224
|
```ts
|
|
159
225
|
import { TextureAtlas, AtlasAttachmentLoader, SkeletonJson }
|
|
@@ -181,6 +247,35 @@ it — and calling it twice is a no-op. Load once for the page's lifetime and yo
|
|
|
181
247
|
can ignore it; load and unload repeatedly without it and you leak an atlas per
|
|
182
248
|
cycle.
|
|
183
249
|
|
|
250
|
+
### What unloading frees
|
|
251
|
+
|
|
252
|
+
`revokeRegions()` — and `assets.dispose()`, which just calls it — frees the blob
|
|
253
|
+
URLs `unpackRegions` minted, and nothing else. The rest is pinned by objects you
|
|
254
|
+
own: an atlas **page image** is held by the `DomTexture` you attached to its
|
|
255
|
+
page, and `SkeletonData` reaches that same image through every attachment
|
|
256
|
+
(`region.texture.getImage()`), so a page is released only once the atlas, the
|
|
257
|
+
skeleton data and every renderer drawing from them are gone. `atlas.dispose()`
|
|
258
|
+
does not do it — `DomTexture.dispose()` is a no-op, because the image is yours.
|
|
259
|
+
With the `webgl` mesh backend the shared blitter also uploads each page image on
|
|
260
|
+
first use, but that GL texture is not the module's forever: every renderer
|
|
261
|
+
drawing the page holds a reference to it, `renderer.dispose()` hands those back,
|
|
262
|
+
and the texture is deleted once the last renderer using that page is disposed —
|
|
263
|
+
a later frame that needs the page uploads it again. The cache is keyed weakly by
|
|
264
|
+
the page image, so a renderer dropped without `dispose()` costs GPU memory until
|
|
265
|
+
the context is lost, but never keeps the image itself alive.
|
|
266
|
+
|
|
267
|
+
A meshed skeleton samples the page bitmap every frame, so its decoded form stays
|
|
268
|
+
in use: a floor on the order of `page width × page height × 4` bytes per page —
|
|
269
|
+
1 MiB for spineboy's single 1024×256 page. A rigid-only skeleton draws nothing
|
|
270
|
+
from the page after `unpackRegions`, and what a merely reachable, undrawn image
|
|
271
|
+
costs is up to the browser, not measurable from script.
|
|
272
|
+
|
|
273
|
+
Unload in this order:
|
|
274
|
+
|
|
275
|
+
1. `renderer.dispose()`, for every renderer using the images.
|
|
276
|
+
2. `revokeRegions(regionImages)` (or `assets.dispose()`).
|
|
277
|
+
3. Drop your references to the atlas and the skeleton data.
|
|
278
|
+
|
|
184
279
|
### One part per page (loose part PNGs)
|
|
185
280
|
|
|
186
281
|
Not every pipeline runs the Spine editor's texture packer. If your parts are
|
|
@@ -227,10 +322,28 @@ const renderer = new SpineHtmlRenderer(rootElement, regionImages);
|
|
|
227
322
|
raster matches the screen: `devicePixelRatio * rootScale`). **Writing it
|
|
228
323
|
reallocates every mesh canvas backing store on the next frame**, and each
|
|
229
324
|
reallocation recreates a GPU surface — the cost that took real Safari to ~3 fps
|
|
230
|
-
when it happened per frame.
|
|
325
|
+
when it happened per frame. Each canvas is then sized from what the new ratio
|
|
326
|
+
needs, in both directions: lowering the ratio gives the backing pixels back
|
|
327
|
+
(the backing is grow-only *within* a ratio, not across a change of one). Set
|
|
328
|
+
it when a layout settles, never per frame:
|
|
231
329
|
debounce resize drags and quantize the value instead of tracking it
|
|
232
330
|
continuously. `renderer.canvasReallocCount` is the check — it must fall back to
|
|
233
331
|
zero within a second or two.
|
|
332
|
+
- `renderer.syncPixelRatio()` — measures the root's effective on-screen scale
|
|
333
|
+
and sets `pixelRatio` to `devicePixelRatio × scale`, returning the ratio now
|
|
334
|
+
in effect. It appends a hidden 100 px box to the root, reads its box once and
|
|
335
|
+
removes it (the root itself is usually 0×0), so it is **one forced layout per
|
|
336
|
+
call** — which is exactly why the renderer never calls it for you: there is no
|
|
337
|
+
per-frame layout read anywhere in this library. Call it when a zoom or a
|
|
338
|
+
layout *settles* (gesture end, debounced resize), not during the drag. A
|
|
339
|
+
change under 0.1% is ignored, so layout jitter cannot churn GPU surfaces, and
|
|
340
|
+
a root that is not laid out (a `display: none` ancestor) leaves the ratio
|
|
341
|
+
alone. Under an ancestor rotation the measured box is inflated and the ratio
|
|
342
|
+
errs high — oversampling costs pixels, undersampling costs picture.
|
|
343
|
+
- `renderer.meshBackingPixels` — allocated mesh-canvas backing pixels
|
|
344
|
+
(Σ width × height), computed on demand. This is what `pixelRatio` moves
|
|
345
|
+
quadratically, and the cheapest way to see an oversampling stage; the demo
|
|
346
|
+
prints it in the stats line as `backing N Mpx`.
|
|
234
347
|
- `renderer.meshBackend` — `'canvas2d'` (default) or `'webgl'`; same output, but
|
|
235
348
|
heavy deforming scenes on Safari want `'webgl'` (see Measured above). Falls back
|
|
236
349
|
to canvas2d automatically when WebGL is unavailable. Switching re-rasters every
|
|
@@ -238,6 +351,22 @@ const renderer = new SpineHtmlRenderer(rootElement, regionImages);
|
|
|
238
351
|
- `renderer.triangleExpand` — clip overdraw in px that closes antialiased mesh
|
|
239
352
|
seams (default 0.5). Also re-rasters every mesh once when changed.
|
|
240
353
|
|
|
354
|
+
**A zoomable stage.** The mesh tier rasters at `world × pixelRatio` in the
|
|
355
|
+
root's own coordinates, and it cannot see a CSS transform above the root — so
|
|
356
|
+
the most natural pan/zoom stage, `transform: scale(zoom)` on an ancestor, makes
|
|
357
|
+
it oversample by `1/zoom²` in backing pixels until the zoom is folded in. The
|
|
358
|
+
picture stays correct throughout, which is what makes this easy to ship: only
|
|
359
|
+
the allocation and the frame rate move. Call `syncPixelRatio()` when the zoom
|
|
360
|
+
settles, or set `pixelRatio = devicePixelRatio * zoom` yourself. Measured
|
|
361
|
+
on-device (Chromium, dpr 2, stage under `scale(0.25)`, ~2.2 M CSS px on screen):
|
|
362
|
+
|
|
363
|
+
| scene | `pixelRatio` | `meshBackingPixels` | fps |
|
|
364
|
+
| --- | --- | --- | --- |
|
|
365
|
+
| 57 meshes | `devicePixelRatio` (2) | 139.1 Mpx | 14–21 |
|
|
366
|
+
| 57 meshes | `dpr × zoom` (0.5) | 9.1 Mpx | 61 |
|
|
367
|
+
| 92 meshes | `devicePixelRatio` (2) | 141.1 Mpx | 36–39 |
|
|
368
|
+
| 92 meshes | `dpr × zoom` (0.5) | 9.1 Mpx | 60–61 |
|
|
369
|
+
|
|
241
370
|
## Demo (this repository)
|
|
242
371
|
|
|
243
372
|
```bash
|
|
@@ -282,6 +411,15 @@ resolving, so blob ownership — every unpacked URL freed, nothing the caller
|
|
|
282
411
|
owns touched, nothing stranded by a failed load — is asserted rather than
|
|
283
412
|
assumed.
|
|
284
413
|
|
|
414
|
+
The server is part of the test: a run tests whatever is being served on its
|
|
415
|
+
port. Two checkouts of this repository on one machine (a worktree, a second
|
|
416
|
+
clone) default to the same port 4321 and reuse an existing server, so the
|
|
417
|
+
second run quietly tests the first one's build — green, and about the wrong
|
|
418
|
+
tree. Give each concurrent checkout its own port with `TEST_PORT`
|
|
419
|
+
(`TEST_PORT=4333 bun run test`); setting it also disables server reuse, so a
|
|
420
|
+
busy port fails the run instead of being borrowed. Unset, nothing changes:
|
|
421
|
+
port 4321, reuse unless `CI`.
|
|
422
|
+
|
|
285
423
|
Standing rule: **headless numbers are never Safari performance evidence** —
|
|
286
424
|
nothing in the suite asserts timing, and headless-WebKit fps/ms readings do
|
|
287
425
|
not transfer (software rasterizer, measured up to 28× off real Safari). The
|
package/dist/DomTexture.d.ts
CHANGED
|
@@ -30,14 +30,37 @@ export interface RegionImage {
|
|
|
30
30
|
* rendering is pure DOM (one <img> per slot, one CSS matrix write per frame).
|
|
31
31
|
*
|
|
32
32
|
* A region that covers its whole page unrotated skips the cut and reuses the
|
|
33
|
-
* page image URL — see the pass-through
|
|
33
|
+
* page image URL — see the pass-through in cutRegion.
|
|
34
|
+
*
|
|
35
|
+
* **The cuts run concurrently.** PNG encoding is asynchronous and the browser
|
|
36
|
+
* does it off the main thread, so the encodes are started together and awaited
|
|
37
|
+
* together instead of one `toBlob` per iteration: awaited in series a load
|
|
38
|
+
* costs the *sum* of every encode, which a throttled document turns into one
|
|
39
|
+
* full throttle period per region (measured: a single 8×8 `toBlob` took 7.5 s
|
|
40
|
+
* to call back in a hidden desktop Chromium pane). How many run at once is
|
|
41
|
+
* bounded by CUT_BACKING_BUDGET_PX above, not by an option.
|
|
42
|
+
*
|
|
43
|
+
* Concurrency changes nothing a caller can observe:
|
|
44
|
+
*
|
|
45
|
+
* - **Order.** The returned map is keyed in `atlas.regions` order whatever
|
|
46
|
+
* order the blobs arrive in — the map is built after the cuts settle, from
|
|
47
|
+
* the region list, not from the arrivals.
|
|
48
|
+
* - **Duplicate names.** The last region of a given name in atlas order wins,
|
|
49
|
+
* and the shadowed URL is revoked (and dropped from the ledger) rather than
|
|
50
|
+
* stranded.
|
|
51
|
+
* - **Failure.** Every URL this call minted is revoked before the error
|
|
52
|
+
* propagates, *including cuts that were still in flight when the first
|
|
53
|
+
* failure happened* — their blobs arrive later, and a load that walked away
|
|
54
|
+
* from them would mint owned URLs nobody could ever revoke. So a failure
|
|
55
|
+
* stops new cuts from starting but still waits for the started ones, and the
|
|
56
|
+
* error it raises is the one from the earliest region in atlas order (what
|
|
57
|
+
* the serial loop used to throw). Nothing here rejects unobserved.
|
|
34
58
|
*
|
|
35
59
|
* The blob URLs stay alive until revokeRegions() frees them — a document-wide
|
|
36
60
|
* allocation the GC cannot reclaim on its own. Callers that load and unload
|
|
37
61
|
* skeletons repeatedly (cutscenes, level transitions) must pair every
|
|
38
62
|
* unpackRegions() with a revokeRegions(); a caller that loads once for the
|
|
39
|
-
* page lifetime can ignore it.
|
|
40
|
-
* everything it already minted is revoked before the error propagates.
|
|
63
|
+
* page lifetime can ignore it.
|
|
41
64
|
*/
|
|
42
65
|
export declare function unpackRegions(atlas: TextureAtlas, pageImages: Map<string, HTMLImageElement>): Promise<Map<string, RegionImage>>;
|
|
43
66
|
/**
|
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,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,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,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;CAChB;AAsGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;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,81 @@ function revokeOwned(url) {
|
|
|
24
24
|
return;
|
|
25
25
|
URL.revokeObjectURL(url);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Backing pixels the cuts still waiting for their PNG encode may hold at once.
|
|
29
|
+
*
|
|
30
|
+
* A started cut keeps its canvas alive until `toBlob` calls back, so starting
|
|
31
|
+
* every region at once peaks at Σ(region area) of canvas backing — on top of
|
|
32
|
+
* the decoded pages, which stay resident throughout. Measured by counting the
|
|
33
|
+
* simultaneously-alive cut canvases and their backing at `toBlob` (chromium):
|
|
34
|
+
* the demo's 40-region 1024×256 atlas peaks at all 40 cuts and 194,742 px
|
|
35
|
+
* (0.19 Mpx ≈ 0.74 MiB), which is nothing; a synthetic 256-region atlas on
|
|
36
|
+
* four 2048×2048 pages peaks at 16.78 Mpx ≈ 64 MiB, which doubles that
|
|
37
|
+
* atlas's resident cost for the length of the load.
|
|
38
|
+
*
|
|
39
|
+
* So the cuts are started against a budget rather than all at once. 4 Mpx is
|
|
40
|
+
* one 2048×2048 page's worth (≈ 16 MiB at 4 bytes/px): small atlases still run
|
|
41
|
+
* every region concurrently (spineboy: all 40, budget never engaged), and the
|
|
42
|
+
* synthetic one runs 64 cuts at a time — 4 waves instead of 256 serial
|
|
43
|
+
* encodes, for a quarter of the peak. A single region bigger than the budget
|
|
44
|
+
* is started anyway when nothing else is in flight, so the budget can never
|
|
45
|
+
* starve a cut.
|
|
46
|
+
*/
|
|
47
|
+
const CUT_BACKING_BUDGET_PX = 4 * 1024 * 1024;
|
|
48
|
+
/**
|
|
49
|
+
* One region's bitmap: the whole-page pass-through, or a cut canvas encoded to
|
|
50
|
+
* a blob URL this module then owns.
|
|
51
|
+
*
|
|
52
|
+
* Everything before the `toBlob` await runs synchronously, so calling this is
|
|
53
|
+
* what "starts" a cut: the canvas is painted and the encode is handed to the
|
|
54
|
+
* browser before control comes back.
|
|
55
|
+
*/
|
|
56
|
+
async function cutRegion(region, pageImages) {
|
|
57
|
+
const image = pageImages.get(region.page.name);
|
|
58
|
+
if (!image)
|
|
59
|
+
throw new Error(`Missing page image: ${region.page.name}`);
|
|
60
|
+
const w = region.width;
|
|
61
|
+
const h = region.height;
|
|
62
|
+
// Whole-page pass-through: an unrotated region covering its entire page
|
|
63
|
+
// would be cut into a pixel-for-pixel copy of the page image, so reuse
|
|
64
|
+
// the page URL instead — no canvas, no PNG re-encode, no second decoded
|
|
65
|
+
// copy in memory, and nothing to revoke afterwards. Atlases written as
|
|
66
|
+
// one part per page (the loose-part-PNG workflow, where every part is
|
|
67
|
+
// declared its own page) hit this for every single region.
|
|
68
|
+
//
|
|
69
|
+
// The test is against the image, not the atlas `size:` line: a page
|
|
70
|
+
// declared at the wrong size still goes through the cut, since the cut
|
|
71
|
+
// is what the region's coordinates actually describe.
|
|
72
|
+
if (region.degrees === 0 &&
|
|
73
|
+
region.x === 0 &&
|
|
74
|
+
region.y === 0 &&
|
|
75
|
+
w === image.naturalWidth &&
|
|
76
|
+
h === image.naturalHeight) {
|
|
77
|
+
return { url: image.src, width: w, height: h };
|
|
78
|
+
}
|
|
79
|
+
const canvas = document.createElement('canvas');
|
|
80
|
+
canvas.width = w;
|
|
81
|
+
canvas.height = h;
|
|
82
|
+
const ctx = canvas.getContext('2d');
|
|
83
|
+
if (!ctx)
|
|
84
|
+
throw new Error('2d context unavailable');
|
|
85
|
+
if (region.degrees === 90) {
|
|
86
|
+
// The region is packed rotated: it occupies an h×w rect in the page.
|
|
87
|
+
// Rotate it back so the bitmap is in artwork orientation.
|
|
88
|
+
ctx.translate(0, h);
|
|
89
|
+
ctx.rotate(-Math.PI / 2);
|
|
90
|
+
ctx.drawImage(image, region.x, region.y, h, w, 0, 0, h, w);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
ctx.drawImage(image, region.x, region.y, w, h, 0, 0, w, h);
|
|
94
|
+
}
|
|
95
|
+
const blob = await new Promise((resolve, reject) => {
|
|
96
|
+
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
|
|
97
|
+
});
|
|
98
|
+
const url = URL.createObjectURL(blob);
|
|
99
|
+
ownedUrls.add(url);
|
|
100
|
+
return { url, width: w, height: h };
|
|
101
|
+
}
|
|
27
102
|
/**
|
|
28
103
|
* Cuts every atlas region out of the page image into its own bitmap once at
|
|
29
104
|
* load time, restoring 90° packing rotation, so the per-frame path never
|
|
@@ -33,78 +108,106 @@ function revokeOwned(url) {
|
|
|
33
108
|
* rendering is pure DOM (one <img> per slot, one CSS matrix write per frame).
|
|
34
109
|
*
|
|
35
110
|
* A region that covers its whole page unrotated skips the cut and reuses the
|
|
36
|
-
* page image URL — see the pass-through
|
|
111
|
+
* page image URL — see the pass-through in cutRegion.
|
|
112
|
+
*
|
|
113
|
+
* **The cuts run concurrently.** PNG encoding is asynchronous and the browser
|
|
114
|
+
* does it off the main thread, so the encodes are started together and awaited
|
|
115
|
+
* together instead of one `toBlob` per iteration: awaited in series a load
|
|
116
|
+
* costs the *sum* of every encode, which a throttled document turns into one
|
|
117
|
+
* full throttle period per region (measured: a single 8×8 `toBlob` took 7.5 s
|
|
118
|
+
* to call back in a hidden desktop Chromium pane). How many run at once is
|
|
119
|
+
* bounded by CUT_BACKING_BUDGET_PX above, not by an option.
|
|
120
|
+
*
|
|
121
|
+
* Concurrency changes nothing a caller can observe:
|
|
122
|
+
*
|
|
123
|
+
* - **Order.** The returned map is keyed in `atlas.regions` order whatever
|
|
124
|
+
* order the blobs arrive in — the map is built after the cuts settle, from
|
|
125
|
+
* the region list, not from the arrivals.
|
|
126
|
+
* - **Duplicate names.** The last region of a given name in atlas order wins,
|
|
127
|
+
* and the shadowed URL is revoked (and dropped from the ledger) rather than
|
|
128
|
+
* stranded.
|
|
129
|
+
* - **Failure.** Every URL this call minted is revoked before the error
|
|
130
|
+
* propagates, *including cuts that were still in flight when the first
|
|
131
|
+
* failure happened* — their blobs arrive later, and a load that walked away
|
|
132
|
+
* from them would mint owned URLs nobody could ever revoke. So a failure
|
|
133
|
+
* stops new cuts from starting but still waits for the started ones, and the
|
|
134
|
+
* error it raises is the one from the earliest region in atlas order (what
|
|
135
|
+
* the serial loop used to throw). Nothing here rejects unobserved.
|
|
37
136
|
*
|
|
38
137
|
* The blob URLs stay alive until revokeRegions() frees them — a document-wide
|
|
39
138
|
* allocation the GC cannot reclaim on its own. Callers that load and unload
|
|
40
139
|
* skeletons repeatedly (cutscenes, level transitions) must pair every
|
|
41
140
|
* unpackRegions() with a revokeRegions(); a caller that loads once for the
|
|
42
|
-
* page lifetime can ignore it.
|
|
43
|
-
* everything it already minted is revoked before the error propagates.
|
|
141
|
+
* page lifetime can ignore it.
|
|
44
142
|
*/
|
|
45
143
|
export async function unpackRegions(atlas, pageImages) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
h === image.naturalHeight) {
|
|
78
|
-
put(atlasRegion.name, { url: image.src, width: w, height: h });
|
|
79
|
-
continue;
|
|
144
|
+
const regions = atlas.regions;
|
|
145
|
+
const cuts = new Array(regions.length);
|
|
146
|
+
// The earliest-in-atlas-order failure is the one that propagates, so that a
|
|
147
|
+
// broken atlas reports the same region whichever encode happens to land
|
|
148
|
+
// first.
|
|
149
|
+
let failedIndex = -1;
|
|
150
|
+
let failure;
|
|
151
|
+
let inFlight = [];
|
|
152
|
+
let inFlightPixels = 0;
|
|
153
|
+
for (let index = 0; index < regions.length && failedIndex === -1; index++) {
|
|
154
|
+
const region = regions[index];
|
|
155
|
+
if (!region)
|
|
156
|
+
continue;
|
|
157
|
+
// A pass-through allocates no canvas, but it also resolves without waiting
|
|
158
|
+
// for anything, so charging it the region's area costs at most one drain.
|
|
159
|
+
const pixels = region.width * region.height;
|
|
160
|
+
while (inFlight.length > 0 && inFlightPixels + pixels > CUT_BACKING_BUDGET_PX) {
|
|
161
|
+
await Promise.race(inFlight.map((job) => job.promise));
|
|
162
|
+
inFlight = inFlight.filter((job) => !job.done);
|
|
163
|
+
if (failedIndex !== -1)
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (failedIndex !== -1)
|
|
167
|
+
break;
|
|
168
|
+
inFlightPixels += pixels;
|
|
169
|
+
const job = { done: false, promise: Promise.resolve() };
|
|
170
|
+
// Jobs settle, they never reject: the failure is recorded here so that
|
|
171
|
+
// nothing is left for an unhandled-rejection handler to find.
|
|
172
|
+
job.promise = (async () => {
|
|
173
|
+
try {
|
|
174
|
+
cuts[index] = await cutRegion(region, pageImages);
|
|
80
175
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
throw new Error('2d context unavailable');
|
|
87
|
-
if (region.degrees === 90) {
|
|
88
|
-
// The region is packed rotated: it occupies an h×w rect in the page.
|
|
89
|
-
// Rotate it back so the bitmap is in artwork orientation.
|
|
90
|
-
ctx.translate(0, h);
|
|
91
|
-
ctx.rotate(-Math.PI / 2);
|
|
92
|
-
ctx.drawImage(image, atlasRegion.x, atlasRegion.y, h, w, 0, 0, h, w);
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (failedIndex === -1 || index < failedIndex) {
|
|
178
|
+
failedIndex = index;
|
|
179
|
+
failure = error;
|
|
180
|
+
}
|
|
93
181
|
}
|
|
94
|
-
|
|
95
|
-
|
|
182
|
+
finally {
|
|
183
|
+
inFlightPixels -= pixels;
|
|
184
|
+
job.done = true;
|
|
96
185
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
186
|
+
})();
|
|
187
|
+
inFlight.push(job);
|
|
188
|
+
}
|
|
189
|
+
// Every cut this call started is settled after this, so the cleanup below
|
|
190
|
+
// reaches the blobs that arrived after the first failure too.
|
|
191
|
+
await Promise.all(inFlight.map((job) => job.promise));
|
|
192
|
+
if (failedIndex !== -1) {
|
|
193
|
+
for (const cut of cuts)
|
|
194
|
+
if (cut)
|
|
195
|
+
revokeOwned(cut.url);
|
|
196
|
+
throw failure;
|
|
104
197
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
198
|
+
const result = new Map();
|
|
199
|
+
for (let index = 0; index < regions.length; index++) {
|
|
200
|
+
const region = regions[index];
|
|
201
|
+
const cut = cuts[index];
|
|
202
|
+
if (!region || !cut)
|
|
203
|
+
continue;
|
|
204
|
+
// Duplicate region names would otherwise strand the shadowed URL in the
|
|
205
|
+
// ledger with nothing left pointing at it. Map.set keeps the key where it
|
|
206
|
+
// first appeared, so atlas order survives the overwrite.
|
|
207
|
+
const shadowed = result.get(region.name);
|
|
208
|
+
if (shadowed)
|
|
209
|
+
revokeOwned(shadowed.url);
|
|
210
|
+
result.set(region.name, cut);
|
|
108
211
|
}
|
|
109
212
|
return result;
|
|
110
213
|
}
|
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;AAeD;;;;;;;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
|
|
1
|
+
{"version":3,"file":"DomTexture.js","sourceRoot":"","sources":["../src/DomTexture.ts"],"names":[],"mappings":"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"}
|
package/dist/MeshGlBlitter.d.ts
CHANGED
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
* preserveDrawingBuffer nor cross-frame content is relied on. The buffer
|
|
21
21
|
* grows quantized and never shrinks, mirroring the per-part backing policy
|
|
22
22
|
* (reallocating GPU surfaces per frame is a Safari killer).
|
|
23
|
+
*
|
|
24
|
+
* Page textures are the one thing here with a lifetime, and since this object
|
|
25
|
+
* outlives every renderer they are reference-counted: a renderer retains each
|
|
26
|
+
* page it queues a job for and releases them in dispose(), and the last
|
|
27
|
+
* release deletes the GL texture (a later job re-uploads it). The cache is
|
|
28
|
+
* keyed weakly by the page image, so a consumer that never disposes still
|
|
29
|
+
* loses nothing but GPU memory — this object never pins the caller's image.
|
|
23
30
|
*/
|
|
24
31
|
export interface MeshBlitJob {
|
|
25
32
|
/** Destination per-part canvas; the blit clears its full backing first. */
|
|
@@ -40,10 +47,22 @@ export interface MeshBlitJob {
|
|
|
40
47
|
}
|
|
41
48
|
declare class MeshGlBlitter {
|
|
42
49
|
lost: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Page textures created minus deleted, i.e. how many uploads are live right
|
|
52
|
+
* now. Deterministic (no GC in the path), which is what the lifetime tests
|
|
53
|
+
* assert against — see tests/gl-textures.spec.ts.
|
|
54
|
+
*/
|
|
55
|
+
liveTextureCount: number;
|
|
43
56
|
private readonly canvas;
|
|
44
57
|
private readonly gl;
|
|
45
58
|
private readonly uResolution;
|
|
46
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Weak by the page image on purpose: a consumer that forgets dispose()
|
|
61
|
+
* leaks its texture until context loss, but dropping the image still lets
|
|
62
|
+
* the image (and then the WebGLTexture this entry holds) be collected.
|
|
63
|
+
* Reassigned wholesale on context loss, never iterated.
|
|
64
|
+
*/
|
|
65
|
+
private textures;
|
|
47
66
|
private readonly maxSize;
|
|
48
67
|
private vertexData;
|
|
49
68
|
/** Per-job packed rect origins, filled by flush(). */
|
|
@@ -56,6 +75,29 @@ declare class MeshGlBlitter {
|
|
|
56
75
|
* cannot fit) so the caller can rasterize the batch on the canvas2d path.
|
|
57
76
|
*/
|
|
58
77
|
flush(jobs: MeshBlitJob[]): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Declares one more user of `page`. The upload itself stays lazy (the first
|
|
80
|
+
* flush that binds the page does it) — this only counts, so that the last
|
|
81
|
+
* release() can free the texture instead of the module holding it forever.
|
|
82
|
+
* Callers retain once per page and hand the same page back exactly once;
|
|
83
|
+
* SpineHtmlRenderer keeps that ledger and settles it in dispose().
|
|
84
|
+
*/
|
|
85
|
+
retain(page: HTMLImageElement): void;
|
|
86
|
+
/**
|
|
87
|
+
* Gives back one retain(). The GL texture goes when the last user of the
|
|
88
|
+
* page is gone; a later job for the same page simply uploads it again. A
|
|
89
|
+
* page the cache does not know (a release after a context loss dropped
|
|
90
|
+
* everything, or a double dispose) is a no-op, so this is safe to call
|
|
91
|
+
* unconditionally.
|
|
92
|
+
*/
|
|
93
|
+
release(page: HTMLImageElement): void;
|
|
94
|
+
/**
|
|
95
|
+
* Context loss invalidates every texture handle at once. Drop the cache and
|
|
96
|
+
* the count instead of carrying dead handles (deleting them is pointless —
|
|
97
|
+
* the GPU side is already gone) and let the next frames re-upload if the
|
|
98
|
+
* caller keeps drawing; the renderer meanwhile falls back to canvas2d.
|
|
99
|
+
*/
|
|
100
|
+
private markLost;
|
|
59
101
|
private compile;
|
|
60
102
|
private textureFor;
|
|
61
103
|
}
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|