spine-html 0.2.0 → 0.4.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 +197 -39
- package/dist/DomTexture.d.ts +31 -2
- package/dist/DomTexture.d.ts.map +1 -1
- package/dist/DomTexture.js +104 -27
- package/dist/DomTexture.js.map +1 -1
- package/dist/MeshGlBlitter.d.ts +69 -0
- package/dist/MeshGlBlitter.d.ts.map +1 -0
- package/dist/MeshGlBlitter.js +265 -0
- package/dist/MeshGlBlitter.js.map +1 -0
- package/dist/SpineHtmlRenderer.d.ts +41 -3
- package/dist/SpineHtmlRenderer.d.ts.map +1 -1
- package/dist/SpineHtmlRenderer.js +120 -25
- package/dist/SpineHtmlRenderer.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/loadSkeletonAssets.d.ts +69 -0
- package/dist/loadSkeletonAssets.d.ts.map +1 -0
- package/dist/loadSkeletonAssets.js +80 -0
- package/dist/loadSkeletonAssets.js.map +1 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -25,25 +25,49 @@ The idea has been floated on the official forum three times since 2014 and was n
|
|
|
25
25
|
built — the blockers named were meshes, clipping, and DOM update overhead. This project
|
|
26
26
|
is the experiment: how far does the DOM actually go, and how cheap is it?
|
|
27
27
|
|
|
28
|
-
Measured so far (PoC,
|
|
28
|
+
Measured so far (PoC, Apple silicon — headless and on-device numbers are labeled,
|
|
29
|
+
they do **not** substitute for each other):
|
|
29
30
|
|
|
30
|
-
- Rigid only (spineboy-ess): 10 skeletons / 180 slot images at
|
|
31
|
-
+ ~0.22 ms DOM writes per frame** — about 1.5% of a
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
- Rigid only (spineboy-ess), headless Chromium: 10 skeletons / 180 slot images at
|
|
32
|
+
**~0.03 ms skeleton math + ~0.22 ms DOM writes per frame** — about 1.5% of a
|
|
33
|
+
60 fps frame budget. On-device Safari: 10 rigid skeletons hold **60 fps**.
|
|
34
|
+
- With meshes (spineboy-pro), headless Chromium: 1 skeleton = **~0.05 ms + ~0.5 ms
|
|
35
|
+
render** (8 mesh canvases, 323 triangles); 10 running skeletons = **~3.5 ms/frame**
|
|
36
|
+
total, smooth.
|
|
37
|
+
- Dirty-skip: 10 *static* skeletons (pose held) = **~0.4 ms/frame** headless and
|
|
38
|
+
**~0.6 ms** on-device Safari — unchanged meshes reuse their raster, so idle or
|
|
39
|
+
held parts cost nothing anywhere.
|
|
40
|
+
- Real Safari (on-device), canvas2d mesh backend: in-callback JS stays **~4–5
|
|
41
|
+
ms/frame** for 10 running meshed skeletons — but the rAF rate tells the real
|
|
42
|
+
story: **meshed ×1 = 37 fps, meshed ×10 = 3–4 fps** (rigid-only ×10 = 60 fps).
|
|
43
|
+
The cost lives outside the frame callback, in the compositor/GPU process:
|
|
44
|
+
Safari antialiases canvas2d **clip paths**, so every triangle pays for an AA
|
|
45
|
+
mask. Chromium doesn't antialias clips and stays smooth. This is what the
|
|
46
|
+
optional WebGL mesh backend (below) removes.
|
|
47
|
+
- Real Safari (on-device), **webgl mesh backend**: meshed ×1 holds **60 fps**
|
|
48
|
+
(vs 37 on canvas2d), and the meshed ×10 stress scene jumps **3–4 → 45 fps** —
|
|
49
|
+
80 mesh canvases / 3230 triangles redrawn every frame (0 reused, the worst
|
|
50
|
+
case), in-callback JS ~5.8 ms at dpr=1. The per-triangle clip-AA tax is gone;
|
|
51
|
+
the remaining gap to 60 is the blit/compositing cost of a 10-skeleton stress
|
|
52
|
+
scene, not a per-triangle cost.
|
|
53
|
+
- Headless-WebKit numbers are a **software rasterizer** and measured up to 28×
|
|
54
|
+
off real Safari in both directions — useful for visual regression only, never
|
|
55
|
+
as Safari perf evidence.
|
|
38
56
|
|
|
39
57
|
## Status
|
|
40
58
|
|
|
41
|
-
**
|
|
59
|
+
**Production.** The questions this started as a PoC to answer — how far does the
|
|
60
|
+
DOM actually go, and how cheap is it — are answered with on-device numbers: the
|
|
61
|
+
practical scenario (a character or two, mostly holding pose, a few parts
|
|
62
|
+
deforming) holds **60 fps on every engine measured**, and the 10-skeleton
|
|
63
|
+
stress scene holds 45 fps on the weakest one (Safari, webgl backend). Ships
|
|
64
|
+
with a self-contained test suite (backend visual parity + invariants) and CI.
|
|
42
65
|
|
|
43
66
|
- ✅ Region attachments (rigid parts): exact affine mapping, draw-order via `z-index`,
|
|
44
67
|
attachment swaps, alpha
|
|
45
68
|
- ✅ Atlas unpacking at load time (90°-packed regions restored), so the rigid per-frame
|
|
46
|
-
path never touches a canvas
|
|
69
|
+
path never touches a canvas — a region that covers its whole page is passed through
|
|
70
|
+
uncut, and `revokeRegions()` frees the rest when a skeleton is unloaded
|
|
47
71
|
- ✅ Mesh attachments (deform tier): small per-part canvases sized to the mesh's world
|
|
48
72
|
bounds, interleaved with the rigid `<img>` slots in one stacking context — the DOM
|
|
49
73
|
handles the bones, a rasterizer handles the warps
|
|
@@ -64,11 +88,25 @@ Measured so far (PoC, Chromium on Apple silicon):
|
|
|
64
88
|
whole pixels) pay zero raster. 10 frozen spineboys: WebKit ~141 → ~0.5 ms/frame
|
|
65
89
|
- ⬜ Clipping — deliberately unsupported (counted and skipped); layered transparent
|
|
66
90
|
parts + `overflow: hidden` cover the practical cases
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
- ✅ Safari mesh cost root-caused by on-device triangulation (two corrections deep):
|
|
92
|
+
the early "~15× slower per-triangle path" was a **headless-WebKit artifact**
|
|
93
|
+
(software rasterization), and the follow-up "on par with Chromium" held only for
|
|
94
|
+
in-callback JS time. Real-Safari rAF rates — rigid ×10 = 60 fps, meshed ×1 =
|
|
95
|
+
37 fps, meshed ×10 = 3–4 fps with JS flat at ~4–5 ms — put the real cost in the
|
|
96
|
+
GPU process: Safari antialiases canvas2d **clip paths**, so the per-triangle clip
|
|
97
|
+
mapping pays a per-triangle AA-mask tax (the same AA that caused the seam cracks)
|
|
98
|
+
- ✅ Optional WebGL blit backend for the mesh tier: `renderer.meshBackend =
|
|
99
|
+
'webgl'` (default stays `'canvas2d'`). All dirty meshes are shelf-packed into
|
|
100
|
+
**one shared offscreen WebGL context** (module-level — browsers cap contexts at
|
|
101
|
+
~16), drawn as textured triangles with premultiplied alpha, then rect-blitted
|
|
102
|
+
onto the same per-part canvases with an unclipped `drawImage` — cheap on Safari.
|
|
103
|
+
Everything element-level is unchanged: z-index interleave, tint filter,
|
|
104
|
+
`mix-blend-mode`, dirty-skip, grow-only backing. GL rasterizes shared triangle
|
|
105
|
+
edges seamlessly, so this path needs no crack overdraw. Falls back to canvas2d
|
|
106
|
+
when WebGL is unavailable or the context is lost. Backend parity verified by
|
|
107
|
+
headless Chromium+WebKit screenshot diffs (glow / clipping / tint scenes;
|
|
108
|
+
sub-pixel edge differences only). On-device Safari, meshed ×10 with every mesh
|
|
109
|
+
redrawn per frame: **3–4 fps (canvas2d) → 45 fps (webgl)**
|
|
72
110
|
|
|
73
111
|
## Install
|
|
74
112
|
|
|
@@ -77,30 +115,21 @@ npm i spine-html @esotericsoftware/spine-core
|
|
|
77
115
|
```
|
|
78
116
|
|
|
79
117
|
```ts
|
|
80
|
-
import {
|
|
81
|
-
|
|
82
|
-
import {
|
|
118
|
+
import { Skeleton, AnimationState, AnimationStateData, Physics }
|
|
119
|
+
from '@esotericsoftware/spine-core';
|
|
120
|
+
import { loadSkeletonAssets, SpineHtmlRenderer } from 'spine-html';
|
|
83
121
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const image = await loadImage(page.name); // your loader
|
|
89
|
-
page.setTexture(new DomTexture(image));
|
|
90
|
-
pageImages.set(page.name, image);
|
|
91
|
-
}
|
|
92
|
-
const regionImages = await unpackRegions(atlas, pageImages);
|
|
93
|
-
const data = new SkeletonJson(new AtlasAttachmentLoader(atlas)).readSkeletonData(jsonText);
|
|
122
|
+
const assets = await loadSkeletonAssets({
|
|
123
|
+
atlasUrl: '/spineboy/spineboy.atlas',
|
|
124
|
+
skeletonUrl: '/spineboy/spineboy-pro.json',
|
|
125
|
+
});
|
|
94
126
|
|
|
95
127
|
// A positioned element becomes the skeleton origin (Spine is Y-up: the
|
|
96
|
-
// skeleton grows upward from it).
|
|
97
|
-
const skeleton = new Skeleton(data);
|
|
98
|
-
const state = new AnimationState(new AnimationStateData(data));
|
|
128
|
+
// skeleton grows upward from it). Layout and scaling are the caller's.
|
|
129
|
+
const skeleton = new Skeleton(assets.data);
|
|
130
|
+
const state = new AnimationState(new AnimationStateData(assets.data));
|
|
99
131
|
state.setAnimation(0, 'walk', true);
|
|
100
|
-
const renderer = new SpineHtmlRenderer(rootElement, regionImages);
|
|
101
|
-
// Mesh canvases raster at devicePixelRatio by default; if you scale the
|
|
102
|
-
// root element, fold that scale in so the raster matches the screen:
|
|
103
|
-
// renderer.pixelRatio = devicePixelRatio * rootScale;
|
|
132
|
+
const renderer = new SpineHtmlRenderer(rootElement, assets.regionImages);
|
|
104
133
|
|
|
105
134
|
function frame(delta: number) {
|
|
106
135
|
state.update(delta);
|
|
@@ -109,11 +138,106 @@ function frame(delta: number) {
|
|
|
109
138
|
skeleton.updateWorldTransform(Physics.update);
|
|
110
139
|
renderer.render(skeleton);
|
|
111
140
|
}
|
|
141
|
+
|
|
142
|
+
// Unloading (a cutscene ends, a level swaps): elements first, bitmaps second.
|
|
143
|
+
// The unpacked regions are blob URLs — nothing else frees them.
|
|
144
|
+
renderer.dispose();
|
|
145
|
+
assets.dispose();
|
|
112
146
|
```
|
|
113
147
|
|
|
114
148
|
`spine-html` is a third-party renderer and is not affiliated with or endorsed by
|
|
115
149
|
Esoteric Software.
|
|
116
150
|
|
|
151
|
+
### Loading it yourself
|
|
152
|
+
|
|
153
|
+
`loadSkeletonAssets` is optional sugar over five `spine-core` calls, and the
|
|
154
|
+
package works without it. Drop to the low-level path whenever you need
|
|
155
|
+
something it does not do — one atlas shared by several skeletons, a binary
|
|
156
|
+
export, images that are already in memory:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { TextureAtlas, AtlasAttachmentLoader, SkeletonJson }
|
|
160
|
+
from '@esotericsoftware/spine-core';
|
|
161
|
+
import { DomTexture, unpackRegions, revokeRegions } from 'spine-html';
|
|
162
|
+
|
|
163
|
+
const atlas = new TextureAtlas(atlasText);
|
|
164
|
+
const pageImages = new Map<string, HTMLImageElement>();
|
|
165
|
+
for (const page of atlas.pages) {
|
|
166
|
+
const image = await loadImage(page.name); // your loader
|
|
167
|
+
page.setTexture(new DomTexture(image));
|
|
168
|
+
pageImages.set(page.name, image);
|
|
169
|
+
}
|
|
170
|
+
const regionImages = await unpackRegions(atlas, pageImages);
|
|
171
|
+
const data = new SkeletonJson(new AtlasAttachmentLoader(atlas)).readSkeletonData(jsonText);
|
|
172
|
+
|
|
173
|
+
// …and on unload, after every renderer using them is disposed:
|
|
174
|
+
revokeRegions(regionImages);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`unpackRegions` mints one blob URL per region; `revokeRegions` is its
|
|
178
|
+
counterpart. It only frees URLs `unpackRegions` created, so a map you built
|
|
179
|
+
yourself (below) and page images reused by the whole-page pass-through survive
|
|
180
|
+
it — and calling it twice is a no-op. Load once for the page's lifetime and you
|
|
181
|
+
can ignore it; load and unload repeatedly without it and you leak an atlas per
|
|
182
|
+
cycle.
|
|
183
|
+
|
|
184
|
+
### One part per page (loose part PNGs)
|
|
185
|
+
|
|
186
|
+
Not every pipeline runs the Spine editor's texture packer. If your parts are
|
|
187
|
+
loose PNGs, declare each one as its own atlas page — a blank line closes a page
|
|
188
|
+
block, the next line opens the next:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
head.png
|
|
192
|
+
size: 512, 512
|
|
193
|
+
head
|
|
194
|
+
bounds: 0, 0, 512, 512
|
|
195
|
+
|
|
196
|
+
torso.png
|
|
197
|
+
size: 640, 480
|
|
198
|
+
torso
|
|
199
|
+
bounds: 0, 0, 640, 480
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`spine-core` parses this as a normal multi-page atlas and nothing here needs a
|
|
203
|
+
flag. Two things to know:
|
|
204
|
+
|
|
205
|
+
- **`size:` must be the PNG's real pixel size.** UVs are derived from it
|
|
206
|
+
(`region.x / page.width`), and a wrong value skews the mesh tier while the
|
|
207
|
+
rigid tier still looks fine — a confusing failure to chase.
|
|
208
|
+
- Regions like these cover their whole page, so `unpackRegions` hands the page
|
|
209
|
+
image straight through instead of cutting and re-encoding it. Load cost for
|
|
210
|
+
this atlas shape is just the image loads.
|
|
211
|
+
|
|
212
|
+
For a **rigid-only** skeleton you can skip atlas unpacking altogether and hand
|
|
213
|
+
the renderer a map you build yourself — meshes cannot, because the deform tier
|
|
214
|
+
samples the page bitmap through the atlas region:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const regionImages = new Map([
|
|
218
|
+
['head', { url: '/parts/head.png', width: 512, height: 512 }],
|
|
219
|
+
]);
|
|
220
|
+
const renderer = new SpineHtmlRenderer(rootElement, regionImages);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Runtime knobs and what they cost
|
|
224
|
+
|
|
225
|
+
- `renderer.pixelRatio` — mesh-canvas backing pixels per CSS pixel (defaults to
|
|
226
|
+
`devicePixelRatio`; if you scale the root element, fold that scale in so the
|
|
227
|
+
raster matches the screen: `devicePixelRatio * rootScale`). **Writing it
|
|
228
|
+
reallocates every mesh canvas backing store on the next frame**, and each
|
|
229
|
+
reallocation recreates a GPU surface — the cost that took real Safari to ~3 fps
|
|
230
|
+
when it happened per frame. Set it when a layout settles, never per frame:
|
|
231
|
+
debounce resize drags and quantize the value instead of tracking it
|
|
232
|
+
continuously. `renderer.canvasReallocCount` is the check — it must fall back to
|
|
233
|
+
zero within a second or two.
|
|
234
|
+
- `renderer.meshBackend` — `'canvas2d'` (default) or `'webgl'`; same output, but
|
|
235
|
+
heavy deforming scenes on Safari want `'webgl'` (see Measured above). Falls back
|
|
236
|
+
to canvas2d automatically when WebGL is unavailable. Switching re-rasters every
|
|
237
|
+
mesh once (no reallocation), so it is fine to expose as a user setting.
|
|
238
|
+
- `renderer.triangleExpand` — clip overdraw in px that closes antialiased mesh
|
|
239
|
+
seams (default 0.5). Also re-rasters every mesh once when changed.
|
|
240
|
+
|
|
117
241
|
## Demo (this repository)
|
|
118
242
|
|
|
119
243
|
```bash
|
|
@@ -127,15 +251,49 @@ The official spineboy example assets are downloaded automatically on first `dev`
|
|
|
127
251
|
|
|
128
252
|
Debug knobs (query string): `?skel=pro|ess` `?anim=walk` `?count=10` pick the scene,
|
|
129
253
|
`?tint=ff8080` tints the whole skeleton, `?dpr=2` overrides the mesh-canvas backing
|
|
130
|
-
ratio, `?timescale=0` freezes the pose (every mesh should report "reused"),
|
|
131
|
-
`?expand=0` disables the crack-closing clip overdraw
|
|
254
|
+
ratio, `?timescale=0` freezes the pose (every mesh should report "reused"),
|
|
255
|
+
`?expand=0` disables the crack-closing clip overdraw, `?backend=webgl` rasterizes
|
|
256
|
+
meshes through the shared WebGL blitter (also a live header select; the stats line
|
|
257
|
+
names the active backend), and `?time=1.2` seeks every instance to the same pose
|
|
258
|
+
for deterministic captures.
|
|
259
|
+
|
|
260
|
+
## Tests
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
bun run test # builds + serves the demo, then runs chromium + webkit
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Playwright drives the demo (`tests/`, config in `playwright.config.ts`; CI
|
|
267
|
+
runs the same suite on ubuntu). The visual check is **A/B within one run**:
|
|
268
|
+
no golden snapshots are committed (they rot across platforms/GPUs) — instead
|
|
269
|
+
the same deterministic pose (`?time` + `?timescale=0`) is screenshotted with
|
|
270
|
+
`?backend=canvas2d` and `?backend=webgl` in the same engine and the buffers
|
|
271
|
+
are diffed directly with a shift-tolerant comparison, so missing parts,
|
|
272
|
+
wrong colors, and tint/blend divergence fail regardless of platform.
|
|
273
|
+
Hairline seams are guarded by a deterministic canary (`?expand=0` must
|
|
274
|
+
change the canvas2d raster), and counter tests pin the dirty-skip /
|
|
275
|
+
grow-only-backing / clip-skip invariants plus spine-core's region corner
|
|
276
|
+
order (BL, UL, UR, BR).
|
|
277
|
+
|
|
278
|
+
The loading path is not observable in a rendered frame, so it gets its own
|
|
279
|
+
page (`tests/harness.html`, a second build entry) that exposes the library to
|
|
280
|
+
the specs directly. Its oracle is the browser: a revoked object URL stops
|
|
281
|
+
resolving, so blob ownership — every unpacked URL freed, nothing the caller
|
|
282
|
+
owns touched, nothing stranded by a failed load — is asserted rather than
|
|
283
|
+
assumed.
|
|
284
|
+
|
|
285
|
+
Standing rule: **headless numbers are never Safari performance evidence** —
|
|
286
|
+
nothing in the suite asserts timing, and headless-WebKit fps/ms readings do
|
|
287
|
+
not transfer (software rasterizer, measured up to 28× off real Safari). The
|
|
288
|
+
perf oracle is the demo's stats line on a real device.
|
|
132
289
|
|
|
133
290
|
## How it works
|
|
134
291
|
|
|
135
292
|
1. `@esotericsoftware/spine-core` loads the skeleton and drives `AnimationState`,
|
|
136
293
|
constraints, and physics — all CPU-side, renderer-agnostic.
|
|
137
294
|
2. At load time each atlas region is cut into its own bitmap (rotation restored), one
|
|
138
|
-
blob URL per region
|
|
295
|
+
blob URL per region — except regions that already cover their whole page, which are
|
|
296
|
+
used as they are.
|
|
139
297
|
3. Per frame, for each slot in draw order: `computeWorldVertices` yields the region's
|
|
140
298
|
four corners in world space (order **BL, UL, UR, BR** — note the comments inside
|
|
141
299
|
`computeWorldVertices` are stale). Flip Y (Spine is Y-up), derive the affine from
|
package/dist/DomTexture.d.ts
CHANGED
|
@@ -10,7 +10,11 @@ export declare class DomTexture extends Texture {
|
|
|
10
10
|
dispose(): void;
|
|
11
11
|
}
|
|
12
12
|
export interface RegionImage {
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* URL of the unpacked (rotation-restored) region pixels: a blob URL minted
|
|
15
|
+
* by unpackRegions, the page image URL itself when the region covers its
|
|
16
|
+
* whole page, or anything the caller put there in a hand-built map.
|
|
17
|
+
*/
|
|
14
18
|
url: string;
|
|
15
19
|
/** Unpacked width in atlas pixels. */
|
|
16
20
|
width: number;
|
|
@@ -20,10 +24,35 @@ export interface RegionImage {
|
|
|
20
24
|
/**
|
|
21
25
|
* Cuts every atlas region out of the page image into its own bitmap once at
|
|
22
26
|
* load time, restoring 90° packing rotation, so the per-frame path never
|
|
23
|
-
* touches a canvas. Returns
|
|
27
|
+
* touches a canvas. Returns image URLs keyed by region name.
|
|
24
28
|
*
|
|
25
29
|
* This is a loading-pipeline step, not a rendering step: after this runs,
|
|
26
30
|
* rendering is pure DOM (one <img> per slot, one CSS matrix write per frame).
|
|
31
|
+
*
|
|
32
|
+
* A region that covers its whole page unrotated skips the cut and reuses the
|
|
33
|
+
* page image URL — see the pass-through below.
|
|
34
|
+
*
|
|
35
|
+
* The blob URLs stay alive until revokeRegions() frees them — a document-wide
|
|
36
|
+
* allocation the GC cannot reclaim on its own. Callers that load and unload
|
|
37
|
+
* skeletons repeatedly (cutscenes, level transitions) must pair every
|
|
38
|
+
* unpackRegions() with a revokeRegions(); a caller that loads once for the
|
|
39
|
+
* page lifetime can ignore it. If this function throws part-way through,
|
|
40
|
+
* everything it already minted is revoked before the error propagates.
|
|
27
41
|
*/
|
|
28
42
|
export declare function unpackRegions(atlas: TextureAtlas, pageImages: Map<string, HTMLImageElement>): Promise<Map<string, RegionImage>>;
|
|
43
|
+
/**
|
|
44
|
+
* Frees the blob URLs unpackRegions minted for `images` — the counterpart of
|
|
45
|
+
* unpackRegions, to call once the skeletons using them are gone (after
|
|
46
|
+
* `renderer.dispose()`, since a live <img> would keep pointing at a dead URL).
|
|
47
|
+
*
|
|
48
|
+
* Only URLs this module created are revoked: entries the caller supplied
|
|
49
|
+
* itself (a hand-built map of loose part PNGs) and page images reused
|
|
50
|
+
* verbatim by the whole-page pass-through are left alone. Idempotent —
|
|
51
|
+
* revoking twice is a no-op, so it is safe on a map that is shared or
|
|
52
|
+
* partially reused.
|
|
53
|
+
*
|
|
54
|
+
* The map keeps its entries; drop the reference (or reload) afterwards, since
|
|
55
|
+
* the URLs no longer resolve.
|
|
56
|
+
*/
|
|
57
|
+
export declare function revokeRegions(images: Map<string, RegionImage>): void;
|
|
29
58
|
//# sourceMappingURL=DomTexture.d.ts.map
|
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
|
|
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;AAkBD;;;;;;;;;;;;;;;;;GAiBG;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,CAqEnC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI,CAEpE"}
|
package/dist/DomTexture.js
CHANGED
|
@@ -9,44 +9,121 @@ export class DomTexture extends Texture {
|
|
|
9
9
|
setWraps(_uWrap, _vWrap) { }
|
|
10
10
|
dispose() { }
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Blob URLs minted by unpackRegions, i.e. the ones this module owns.
|
|
14
|
+
*
|
|
15
|
+
* Ownership has to be tracked because a RegionImage map is not necessarily
|
|
16
|
+
* ours: callers may hand the renderer a map built from their own URLs (loose
|
|
17
|
+
* part PNGs need no unpacking at all). revokeRegions() therefore frees only
|
|
18
|
+
* what is listed here and leaves caller-owned URLs untouched.
|
|
19
|
+
*/
|
|
20
|
+
const ownedUrls = new Set();
|
|
21
|
+
/** Revokes `url` if unpackRegions minted it. Idempotent, caller-safe. */
|
|
22
|
+
function revokeOwned(url) {
|
|
23
|
+
if (!ownedUrls.delete(url))
|
|
24
|
+
return;
|
|
25
|
+
URL.revokeObjectURL(url);
|
|
26
|
+
}
|
|
12
27
|
/**
|
|
13
28
|
* Cuts every atlas region out of the page image into its own bitmap once at
|
|
14
29
|
* load time, restoring 90° packing rotation, so the per-frame path never
|
|
15
|
-
* touches a canvas. Returns
|
|
30
|
+
* touches a canvas. Returns image URLs keyed by region name.
|
|
16
31
|
*
|
|
17
32
|
* This is a loading-pipeline step, not a rendering step: after this runs,
|
|
18
33
|
* rendering is pure DOM (one <img> per slot, one CSS matrix write per frame).
|
|
34
|
+
*
|
|
35
|
+
* A region that covers its whole page unrotated skips the cut and reuses the
|
|
36
|
+
* page image URL — see the pass-through below.
|
|
37
|
+
*
|
|
38
|
+
* The blob URLs stay alive until revokeRegions() frees them — a document-wide
|
|
39
|
+
* allocation the GC cannot reclaim on its own. Callers that load and unload
|
|
40
|
+
* skeletons repeatedly (cutscenes, level transitions) must pair every
|
|
41
|
+
* unpackRegions() with a revokeRegions(); a caller that loads once for the
|
|
42
|
+
* page lifetime can ignore it. If this function throws part-way through,
|
|
43
|
+
* everything it already minted is revoked before the error propagates.
|
|
19
44
|
*/
|
|
20
45
|
export async function unpackRegions(atlas, pageImages) {
|
|
21
46
|
const result = new Map();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
// Duplicate region names would otherwise strand the shadowed URL in the
|
|
48
|
+
// ledger with nothing left pointing at it.
|
|
49
|
+
const put = (name, entry) => {
|
|
50
|
+
const shadowed = result.get(name);
|
|
51
|
+
if (shadowed)
|
|
52
|
+
revokeOwned(shadowed.url);
|
|
53
|
+
result.set(name, entry);
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
for (const region of atlas.regions) {
|
|
57
|
+
const atlasRegion = region;
|
|
58
|
+
const image = pageImages.get(atlasRegion.page.name);
|
|
59
|
+
if (!image)
|
|
60
|
+
throw new Error(`Missing page image: ${atlasRegion.page.name}`);
|
|
61
|
+
const w = region.width;
|
|
62
|
+
const h = region.height;
|
|
63
|
+
// Whole-page pass-through: an unrotated region covering its entire page
|
|
64
|
+
// would be cut into a pixel-for-pixel copy of the page image, so reuse
|
|
65
|
+
// the page URL instead — no canvas, no PNG re-encode, no second decoded
|
|
66
|
+
// copy in memory, and nothing to revoke afterwards. Atlases written as
|
|
67
|
+
// one part per page (the loose-part-PNG workflow, where every part is
|
|
68
|
+
// declared its own page) hit this for every single region.
|
|
69
|
+
//
|
|
70
|
+
// The test is against the image, not the atlas `size:` line: a page
|
|
71
|
+
// declared at the wrong size still goes through the cut, since the cut
|
|
72
|
+
// is what the region's coordinates actually describe.
|
|
73
|
+
if (region.degrees === 0 &&
|
|
74
|
+
atlasRegion.x === 0 &&
|
|
75
|
+
atlasRegion.y === 0 &&
|
|
76
|
+
w === image.naturalWidth &&
|
|
77
|
+
h === image.naturalHeight) {
|
|
78
|
+
put(atlasRegion.name, { url: image.src, width: w, height: h });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const canvas = document.createElement('canvas');
|
|
82
|
+
canvas.width = w;
|
|
83
|
+
canvas.height = h;
|
|
84
|
+
const ctx = canvas.getContext('2d');
|
|
85
|
+
if (!ctx)
|
|
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);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
ctx.drawImage(image, atlasRegion.x, atlasRegion.y, w, h, 0, 0, w, h);
|
|
96
|
+
}
|
|
97
|
+
const blob = await new Promise((resolve, reject) => {
|
|
98
|
+
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
|
|
99
|
+
});
|
|
100
|
+
const url = URL.createObjectURL(blob);
|
|
101
|
+
ownedUrls.add(url);
|
|
102
|
+
put(atlasRegion.name, { url, width: w, height: h });
|
|
44
103
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
revokeRegions(result);
|
|
107
|
+
throw error;
|
|
49
108
|
}
|
|
50
109
|
return result;
|
|
51
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Frees the blob URLs unpackRegions minted for `images` — the counterpart of
|
|
113
|
+
* unpackRegions, to call once the skeletons using them are gone (after
|
|
114
|
+
* `renderer.dispose()`, since a live <img> would keep pointing at a dead URL).
|
|
115
|
+
*
|
|
116
|
+
* Only URLs this module created are revoked: entries the caller supplied
|
|
117
|
+
* itself (a hand-built map of loose part PNGs) and page images reused
|
|
118
|
+
* verbatim by the whole-page pass-through are left alone. Idempotent —
|
|
119
|
+
* revoking twice is a no-op, so it is safe on a map that is shared or
|
|
120
|
+
* partially reused.
|
|
121
|
+
*
|
|
122
|
+
* The map keeps its entries; drop the reference (or reload) afterwards, since
|
|
123
|
+
* the URLs no longer resolve.
|
|
124
|
+
*/
|
|
125
|
+
export function revokeRegions(images) {
|
|
126
|
+
for (const image of images.values())
|
|
127
|
+
revokeOwned(image.url);
|
|
128
|
+
}
|
|
52
129
|
//# sourceMappingURL=DomTexture.js.map
|
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;
|
|
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;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAmB,EACnB,UAAyC;IAEzC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC9C,wEAAwE;IACxE,2CAA2C;IAC3C,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,KAAkB,EAAQ,EAAE;QACrD,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,QAAQ;YAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC1B,CAAC,CAAC;IAEF,IAAI,CAAC;QACH,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YACnC,MAAM,WAAW,GAAG,MAA4B,CAAC;YACjD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpD,IAAI,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YAE5E,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC;YACvB,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;YAExB,wEAAwE;YACxE,uEAAuE;YACvE,wEAAwE;YACxE,uEAAuE;YACvE,sEAAsE;YACtE,2DAA2D;YAC3D,EAAE;YACF,oEAAoE;YACpE,uEAAuE;YACvE,sDAAsD;YACtD,IACE,MAAM,CAAC,OAAO,KAAK,CAAC;gBACpB,WAAW,CAAC,CAAC,KAAK,CAAC;gBACnB,WAAW,CAAC,CAAC,KAAK,CAAC;gBACnB,CAAC,KAAK,KAAK,CAAC,YAAY;gBACxB,CAAC,KAAK,KAAK,CAAC,aAAa,EACzB,CAAC;gBACD,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;gBAC/D,SAAS;YACX,CAAC;YAED,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YAChD,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC;YACjB,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;YAClB,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;YAEpD,IAAI,MAAM,CAAC,OAAO,KAAK,EAAE,EAAE,CAAC;gBAC1B,qEAAqE;gBACrE,0DAA0D;gBAC1D,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBACpB,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;gBACzB,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YACvE,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YACvE,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACvD,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;YAC3F,CAAC,CAAC,CAAC;YACH,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;YACtC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,aAAa,CAAC,MAAM,CAAC,CAAC;QACtB,MAAM,KAAK,CAAC;IACd,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAAC,MAAgC;IAC5D,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared offscreen WebGL rasterizer for the mesh (deform) tier.
|
|
3
|
+
*
|
|
4
|
+
* One module-level context serves every renderer instance: browsers cap live
|
|
5
|
+
* WebGL contexts (16 on Chrome/Safari) and a page can hold many renderers, so
|
|
6
|
+
* per-renderer contexts would evict each other. Per flush, every dirty mesh is
|
|
7
|
+
* shelf-packed into a rect of the offscreen canvas, its triangles are drawn
|
|
8
|
+
* textured (atlas page texture, premultiplied alpha), and each rect is blitted
|
|
9
|
+
* onto the mesh's per-part 2d canvas with an unclipped drawImage rect copy.
|
|
10
|
+
*
|
|
11
|
+
* Why this exists: Safari antialiases canvas2d clip paths, so the standard
|
|
12
|
+
* per-triangle clip+transform+drawImage mapping pays a per-triangle AA-mask
|
|
13
|
+
* cost in the GPU process — invisible to in-callback JS timing, but it
|
|
14
|
+
* rAF-limits heavy scenes. GL rasterizes shared triangle edges seamlessly
|
|
15
|
+
* (no clip, no crack overdraw needed), and the remaining canvas2d work is a
|
|
16
|
+
* plain rect blit.
|
|
17
|
+
*
|
|
18
|
+
* The offscreen buffer is scratch space: rects are scissor-cleared and
|
|
19
|
+
* redrawn every flush and read back in the same task, so neither
|
|
20
|
+
* preserveDrawingBuffer nor cross-frame content is relied on. The buffer
|
|
21
|
+
* grows quantized and never shrinks, mirroring the per-part backing policy
|
|
22
|
+
* (reallocating GPU surfaces per frame is a Safari killer).
|
|
23
|
+
*/
|
|
24
|
+
export interface MeshBlitJob {
|
|
25
|
+
/** Destination per-part canvas; the blit clears its full backing first. */
|
|
26
|
+
canvas: HTMLCanvasElement;
|
|
27
|
+
/** Atlas page image the mesh samples (cached as a GL texture on first use). */
|
|
28
|
+
page: HTMLImageElement;
|
|
29
|
+
/** Bbox-relative vertices in CSS px, x/y interleaved (indexed via `triangles`). */
|
|
30
|
+
vertices: Float64Array;
|
|
31
|
+
/** Normalized page UVs aligned with `vertices`. */
|
|
32
|
+
uvs: ArrayLike<number>;
|
|
33
|
+
/** Triangle index list into vertex/UV pairs. */
|
|
34
|
+
triangles: ArrayLike<number>;
|
|
35
|
+
/** Backing-store pixels per CSS px. */
|
|
36
|
+
ratio: number;
|
|
37
|
+
/** Rasterized region in device px — the packed rect and the blit both use it. */
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
}
|
|
41
|
+
declare class MeshGlBlitter {
|
|
42
|
+
lost: boolean;
|
|
43
|
+
private readonly canvas;
|
|
44
|
+
private readonly gl;
|
|
45
|
+
private readonly uResolution;
|
|
46
|
+
private readonly textures;
|
|
47
|
+
private readonly maxSize;
|
|
48
|
+
private vertexData;
|
|
49
|
+
/** Per-job packed rect origins, filled by flush(). */
|
|
50
|
+
private packX;
|
|
51
|
+
private packY;
|
|
52
|
+
constructor();
|
|
53
|
+
/**
|
|
54
|
+
* Rasterize every job into the offscreen buffer, then blit each rect onto
|
|
55
|
+
* its per-part canvas. Returns false when the context is lost (or the jobs
|
|
56
|
+
* cannot fit) so the caller can rasterize the batch on the canvas2d path.
|
|
57
|
+
*/
|
|
58
|
+
flush(jobs: MeshBlitJob[]): boolean;
|
|
59
|
+
private compile;
|
|
60
|
+
private textureFor;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The module-level blitter, created on first use. Returns null when WebGL is
|
|
64
|
+
* unavailable or the shared context has been lost — callers then stay on the
|
|
65
|
+
* canvas2d path.
|
|
66
|
+
*/
|
|
67
|
+
export declare function getMeshGlBlitter(): MeshGlBlitter | null;
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=MeshGlBlitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MeshGlBlitter.d.ts","sourceRoot":"","sources":["../src/MeshGlBlitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;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;AA4BD,cAAM,aAAa;IACjB,IAAI,UAAS;IAEb,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;IAC3D,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAwB;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuB;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6C;IACtE,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,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,UAAU;CAmBnB;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CASvD"}
|