rig-play 0.8.0 → 1.0.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/NOTICE.md +50 -23
- package/README.md +354 -461
- package/dist/CanvasRenderer.d.ts +139 -0
- package/dist/CanvasRenderer.d.ts.map +1 -0
- package/dist/{SpineHtmlRenderer.d.ts → DomRenderer.d.ts} +70 -65
- package/dist/DomRenderer.d.ts.map +1 -0
- package/dist/DomTexture.d.ts +34 -14
- package/dist/DomTexture.d.ts.map +1 -1
- package/dist/MeshGlBlitter.d.ts +67 -1
- package/dist/MeshGlBlitter.d.ts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7271 -5
- package/dist/index.js.map +1 -1
- package/dist/loadModel.d.ts +92 -0
- package/dist/loadModel.d.ts.map +1 -0
- package/dist/player.d.ts +151 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/posedFrame.d.ts +99 -0
- package/dist/posedFrame.d.ts.map +1 -0
- package/package.json +13 -19
- package/dist/DomTexture.js +0 -520
- package/dist/DomTexture.js.map +0 -1
- package/dist/MeshGlBlitter.js +0 -365
- package/dist/MeshGlBlitter.js.map +0 -1
- package/dist/SpineHtmlRenderer.d.ts.map +0 -1
- package/dist/SpineHtmlRenderer.js +0 -1107
- package/dist/SpineHtmlRenderer.js.map +0 -1
- package/dist/binary.d.ts +0 -48
- package/dist/binary.d.ts.map +0 -1
- package/dist/binary.js +0 -29
- package/dist/binary.js.map +0 -1
- package/dist/coreCompat.d.ts +0 -153
- package/dist/coreCompat.d.ts.map +0 -1
- package/dist/coreCompat.js +0 -136
- package/dist/coreCompat.js.map +0 -1
- package/dist/loadAtlasAssets.d.ts +0 -73
- package/dist/loadAtlasAssets.d.ts.map +0 -1
- package/dist/loadAtlasAssets.js +0 -75
- package/dist/loadAtlasAssets.js.map +0 -1
- package/dist/loadSkeletonAssets.d.ts +0 -88
- package/dist/loadSkeletonAssets.d.ts.map +0 -1
- package/dist/loadSkeletonAssets.js +0 -79
- package/dist/loadSkeletonAssets.js.map +0 -1
|
@@ -1,1107 +0,0 @@
|
|
|
1
|
-
import { BlendMode, ClippingAttachment, MeshAttachment, RegionAttachment, } from '@esotericsoftware/spine-core';
|
|
2
|
-
import { coreCompatFor } from './coreCompat.js';
|
|
3
|
-
import { straightAlphaSource } from './DomTexture.js';
|
|
4
|
-
import { getMeshGlBlitter } from './MeshGlBlitter.js';
|
|
5
|
-
const regionVertices = new Float32Array(8);
|
|
6
|
-
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
7
|
-
/** Unique tint-filter ids across renderer instances (ids are document-global). */
|
|
8
|
-
let tintFilterSeq = 0;
|
|
9
|
-
/** Edge length of the throwaway box syncPixelRatio() measures the root with. */
|
|
10
|
-
const SCALE_PROBE_PX = 100;
|
|
11
|
-
/**
|
|
12
|
-
* Relative change below which syncPixelRatio() leaves `pixelRatio` alone.
|
|
13
|
-
* Layout reads are quantized (1/64 px in Blink), so a 100 px probe carries
|
|
14
|
-
* ~0.016% of measurement noise — 6× under this band, which in turn is 10×
|
|
15
|
-
* under the smallest zoom step a UI realistically takes (1%). Inside the band
|
|
16
|
-
* the sampling error is worth ~0.2% of backing pixels; outside it, a rewrite
|
|
17
|
-
* of `pixelRatio` recreates every mesh canvas's GPU surface.
|
|
18
|
-
*/
|
|
19
|
-
const PIXEL_RATIO_DEADBAND = 1e-3;
|
|
20
|
-
/** Push a point away from (cx, cy) by `amount` pixels. */
|
|
21
|
-
function expandPoint(x, y, cx, cy, amount) {
|
|
22
|
-
const dx = x - cx;
|
|
23
|
-
const dy = y - cy;
|
|
24
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
25
|
-
if (len < 1e-6)
|
|
26
|
-
return [x, y];
|
|
27
|
-
const s = amount / len;
|
|
28
|
-
return [x + dx * s, y + dy * s];
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Formats one clip-path coordinate, rounded to 1/1000 of the element's local
|
|
32
|
-
* unit so that a pose which recomputes to a bit-different float does not
|
|
33
|
-
* defeat the write cache.
|
|
34
|
-
*
|
|
35
|
-
* Why 1/1000: the local unit is an atlas unit for a rigid `<img>` (the matrix
|
|
36
|
-
* denominator) and a CSS pixel for a mesh canvas, so the worst error this can
|
|
37
|
-
* add on screen is half a quantum times the element's local-to-screen scale.
|
|
38
|
-
* At a 50× upscale — far past anything a skeleton is posed at — that is
|
|
39
|
-
* 0.025 px, an order of magnitude under one device pixel at dpr 3, while
|
|
40
|
-
* still sitting three orders of magnitude above the f32 recomputation jitter
|
|
41
|
-
* of coordinates that run 10²–10³ units. Shortest-round-trip number
|
|
42
|
-
* formatting then keeps every coordinate at three decimals or fewer.
|
|
43
|
-
*/
|
|
44
|
-
function clipCoord(v) {
|
|
45
|
-
return `${clipNum(v)}px`;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* The same quantized number without a unit, for `path()` data — SVG path
|
|
49
|
-
* coordinates are user units and a `px` suffix there is a parse error.
|
|
50
|
-
*/
|
|
51
|
-
function clipNum(v) {
|
|
52
|
-
return Math.round(v * 1000) / 1000;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Bound past which a local coordinate is treated as degenerate rather than
|
|
56
|
-
* written. Two reasons, and the second is the hard one: a near-singular
|
|
57
|
-
* element matrix sends the inverse — and with it these coordinates — toward
|
|
58
|
-
* infinity, and JavaScript switches to exponential notation at 1e21, which is
|
|
59
|
-
* not valid CSS. The browser would then drop the whole declaration and the
|
|
60
|
-
* element would render UNCLIPPED, which is the one failure mode a clip must
|
|
61
|
-
* never have. A billion local units is already far outside anything a
|
|
62
|
-
* skeleton poses, so nothing expressible is lost by stopping here.
|
|
63
|
-
*/
|
|
64
|
-
const CLIP_COORD_LIMIT = 1e9;
|
|
65
|
-
/**
|
|
66
|
-
* Does `endSlot` still lie ahead of `index` in the draw order? A null end slot
|
|
67
|
-
* never does, and neither does the clip's own slot (the scan starts past it)
|
|
68
|
-
* nor one already passed — which is exactly why each of those clips runs to
|
|
69
|
-
* the end of the draw order instead of ending.
|
|
70
|
-
*/
|
|
71
|
-
function endsAhead(drawOrder, index, endSlot) {
|
|
72
|
-
if (!endSlot)
|
|
73
|
-
return false;
|
|
74
|
-
for (let i = index + 1, n = drawOrder.length; i < n; i++) {
|
|
75
|
-
if (drawOrder[i].data === endSlot)
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
const BLEND_CSS = {
|
|
81
|
-
[BlendMode.Normal]: '',
|
|
82
|
-
[BlendMode.Additive]: 'plus-lighter',
|
|
83
|
-
[BlendMode.Multiply]: 'multiply',
|
|
84
|
-
[BlendMode.Screen]: 'screen',
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
|
-
* Renders a spine-core Skeleton as DOM, split by slot type:
|
|
88
|
-
*
|
|
89
|
-
* - Region attachments (rigid parts) become one absolutely-positioned <img>
|
|
90
|
-
* posed with a single CSS matrix() write per frame — exact, since bone
|
|
91
|
-
* transforms are affine.
|
|
92
|
-
* - Mesh attachments (deform parts) each get a small per-part <canvas> sized
|
|
93
|
-
* to the mesh's world bounding box, redrawn per frame — either with the
|
|
94
|
-
* standard per-triangle clip+transform+drawImage mapping (default), or via
|
|
95
|
-
* a shared offscreen WebGL canvas that rect-blits into the same per-part
|
|
96
|
-
* canvases (meshBackend = 'webgl'). Frames where the canvas-space vertices
|
|
97
|
-
* are unchanged reuse the previous raster on both backends.
|
|
98
|
-
*
|
|
99
|
-
* Both element kinds share one stacking context, so draw order interleaves
|
|
100
|
-
* freely via z-index (rear hair canvas < torso img < front hair canvas).
|
|
101
|
-
* RGB tint (skeleton × slot × attachment color) is applied per element with
|
|
102
|
-
* an SVG feColorMatrix reference filter — exact channel multiply, works the
|
|
103
|
-
* same on <img> and <canvas>. Dark (two-color) tint is not expressible that
|
|
104
|
-
* way and is unsupported. Clipping attachments become a CSS clip-path per
|
|
105
|
-
* element, in that element's own local frame, on spine-core's semantics — one
|
|
106
|
-
* active clip at a time, ending at the end slot inclusive (see `clipping`).
|
|
107
|
-
*
|
|
108
|
-
* Coordinate mapping: Spine is Y-up, CSS is Y-down — world Y is negated.
|
|
109
|
-
* spine-core computes everything (bones, constraints, physics, deformed
|
|
110
|
-
* vertices) on the CPU; this class only draws.
|
|
111
|
-
*/
|
|
112
|
-
export class SpineHtmlRenderer {
|
|
113
|
-
root;
|
|
114
|
-
regionImages;
|
|
115
|
-
/**
|
|
116
|
-
* Apply clipping attachments (default). Each element drawn inside an active
|
|
117
|
-
* clip gets a CSS `clip-path` in its own local frame — an element-level
|
|
118
|
-
* feature, so both mesh backends see exactly the same thing and neither
|
|
119
|
-
* raster path knows clipping exists.
|
|
120
|
-
*
|
|
121
|
-
* spine-core's semantics, not an approximation of them: one clip is active
|
|
122
|
-
* at a time (a second clipping attachment met while one is active is
|
|
123
|
-
* ignored, as SkeletonClipping.clipStart does), a clip applies to the slots
|
|
124
|
-
* drawn after its own slot through its end slot inclusive, and an end slot
|
|
125
|
-
* that is the clip's own slot — or one already passed — never ends it, so
|
|
126
|
-
* the clip runs to the end of the draw order. That last case is the
|
|
127
|
-
* whole-skeleton clip, and it takes a fast path: one `clip-path` on the root
|
|
128
|
-
* instead of one per element. The root is the caller's element, so its
|
|
129
|
-
* inline `clip-path` is borrowed — saved on the first write and restored
|
|
130
|
-
* when the clip stops covering the frame, when `clipping` goes false, and by
|
|
131
|
-
* `dispose()`.
|
|
132
|
-
*
|
|
133
|
-
* False restores the pre-0.6 behaviour: clips are counted in clipSkipCount
|
|
134
|
-
* and nothing is clipped, with any clip-path this renderer wrote removed.
|
|
135
|
-
*/
|
|
136
|
-
clipping = true;
|
|
137
|
-
/** Clipping attachments applied last frame. */
|
|
138
|
-
clipCount = 0;
|
|
139
|
-
/**
|
|
140
|
-
* Clipping attachments NOT applied last frame: `clipping` is false, another
|
|
141
|
-
* clip was already active (spine-core ignores the second one), the slot's
|
|
142
|
-
* bone is inactive, or the polygon has fewer than three points.
|
|
143
|
-
*/
|
|
144
|
-
clipSkipCount = 0;
|
|
145
|
-
/**
|
|
146
|
-
* `clip-path` style writes performed last frame, across slot elements and
|
|
147
|
-
* the root, clears included. A static polygon over static elements settles
|
|
148
|
-
* at zero: every clip-path is cached per element exactly as `transform` is.
|
|
149
|
-
*/
|
|
150
|
-
clipWriteCount = 0;
|
|
151
|
-
/** Mesh canvases rasterized last frame. */
|
|
152
|
-
meshCount = 0;
|
|
153
|
-
/** Mesh canvases that reused their previous raster last frame. */
|
|
154
|
-
meshReuseCount = 0;
|
|
155
|
-
/**
|
|
156
|
-
* Mesh canvas backing stores (re)allocated last frame. Should drop to zero
|
|
157
|
-
* once an animation reaches its steady state; a persistent nonzero value
|
|
158
|
-
* means GPU surfaces are being recreated per frame (a Safari killer).
|
|
159
|
-
*/
|
|
160
|
-
canvasReallocCount = 0;
|
|
161
|
-
/** Triangles rasterized last frame. */
|
|
162
|
-
triangleCount = 0;
|
|
163
|
-
/**
|
|
164
|
-
* How far (px) each triangle's clip polygon is expanded from its centroid.
|
|
165
|
-
* Closes antialiased-clip cracks between adjacent triangles (Safari);
|
|
166
|
-
* set to 0 to see them.
|
|
167
|
-
*/
|
|
168
|
-
triangleExpand = 0.5;
|
|
169
|
-
/**
|
|
170
|
-
* Mesh-canvas backing-store pixels per CSS pixel. Defaults to the device
|
|
171
|
-
* pixel ratio. If the caller scales the root element, fold that scale in
|
|
172
|
-
* (e.g. devicePixelRatio * rootScale) so the raster matches the on-screen
|
|
173
|
-
* resolution instead of over- or under-sampling — syncPixelRatio() measures
|
|
174
|
-
* that scale and folds it in for you.
|
|
175
|
-
*/
|
|
176
|
-
pixelRatio = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;
|
|
177
|
-
/**
|
|
178
|
-
* Rasterizer for the mesh (deform) tier. 'canvas2d' (default) maps each
|
|
179
|
-
* triangle with clip+transform+drawImage directly on the per-part canvas.
|
|
180
|
-
* 'webgl' rasterizes every dirty mesh into one shared offscreen WebGL
|
|
181
|
-
* canvas and rect-blits each mesh back onto its per-part canvas — the DOM
|
|
182
|
-
* structure and all element-level behavior (z-index interleave, tint
|
|
183
|
-
* filter, mix-blend-mode, dirty-skip) are identical, only the raster step
|
|
184
|
-
* changes. Motivation: Safari antialiases canvas2d clip paths, so the
|
|
185
|
-
* per-triangle clip mapping pays a per-triangle AA-mask cost in the GPU
|
|
186
|
-
* process that rAF-limits heavy scenes; GL rasterizes shared edges
|
|
187
|
-
* seamlessly (no clip, no crack overdraw) and the blit is an unclipped
|
|
188
|
-
* rect copy. Falls back to 'canvas2d' when WebGL is unavailable or the
|
|
189
|
-
* shared context is lost — see meshBackendActive.
|
|
190
|
-
*/
|
|
191
|
-
meshBackend = 'canvas2d';
|
|
192
|
-
/** Backend that actually rasterized the mesh tier during the last render(). */
|
|
193
|
-
meshBackendActive = 'canvas2d';
|
|
194
|
-
/**
|
|
195
|
-
* How this renderer reaches spine-core, picked from the first skeleton it is
|
|
196
|
-
* handed and then reused — see coreCompat.ts. Null until then, because the
|
|
197
|
-
* constructor has no skeleton to look at and the installed core is not
|
|
198
|
-
* knowable from the root element.
|
|
199
|
-
*/
|
|
200
|
-
core = null;
|
|
201
|
-
views = new Map();
|
|
202
|
-
pendingJobs = [];
|
|
203
|
-
pendingViews = [];
|
|
204
|
-
/**
|
|
205
|
-
* Atlas page images this renderer has retained from the shared blitter,
|
|
206
|
-
* one retain each. Allocated on the first webgl mesh job, so a renderer
|
|
207
|
-
* that never leaves canvas2d holds nothing and gives nothing back.
|
|
208
|
-
*/
|
|
209
|
-
glPages = null;
|
|
210
|
-
scratchVertices = new Float32Array(256);
|
|
211
|
-
tintDefs = null;
|
|
212
|
-
/** The clip in force at the current point of the draw order, if any. */
|
|
213
|
-
clipAttachment = null;
|
|
214
|
-
clipEndSlot = null;
|
|
215
|
-
clipInverse = false;
|
|
216
|
-
/** True while the active clip is served by one clip-path on the root. */
|
|
217
|
-
clipOnRoot = false;
|
|
218
|
-
/** Whether any element has been drawn yet this frame (the fast-path test). */
|
|
219
|
-
drewAnything = false;
|
|
220
|
-
/** The active clip polygon in CSS space (Y already negated), flat x,y. */
|
|
221
|
-
clipPolygon = new Float64Array(0);
|
|
222
|
-
clipPolygonLength = 0;
|
|
223
|
-
/**
|
|
224
|
-
* The root's inline `clip-path` before this renderer borrowed it; null means
|
|
225
|
-
* not borrowed. The root belongs to the caller, so the fast path gives back
|
|
226
|
-
* exactly the string it found.
|
|
227
|
-
*/
|
|
228
|
-
rootClipSaved = null;
|
|
229
|
-
/** Last clip-path string written to the root, to skip no-op style writes. */
|
|
230
|
-
rootClipWritten = '';
|
|
231
|
-
/**
|
|
232
|
-
* @param root Positioned element (e.g. position:absolute) that becomes the
|
|
233
|
-
* skeleton origin. The renderer only appends slot elements to it — layout
|
|
234
|
-
* and scaling of the root belong to the caller.
|
|
235
|
-
* @param regionImages Unpacked per-region bitmaps (for the rigid tier).
|
|
236
|
-
*/
|
|
237
|
-
constructor(root, regionImages) {
|
|
238
|
-
this.root = root;
|
|
239
|
-
this.regionImages = regionImages;
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Backing-store pixels currently allocated across the mesh canvases
|
|
243
|
-
* (Σ width × height). Computed on demand from the views — nothing is
|
|
244
|
-
* tracked per frame — so reading it is cheap but not free; the demo reads it
|
|
245
|
-
* with the other counters, twice a second.
|
|
246
|
-
*
|
|
247
|
-
* This is the quantity `pixelRatio` moves quadratically, and the one that
|
|
248
|
-
* makes an oversampling stage visible before the frame rate does: a root
|
|
249
|
-
* scaled down by CSS without that scale folded into `pixelRatio` holds
|
|
250
|
-
* 1/scale² of the backing it needs. Hidden slots are included — their
|
|
251
|
-
* canvases keep their allocation.
|
|
252
|
-
*/
|
|
253
|
-
get meshBackingPixels() {
|
|
254
|
-
let total = 0;
|
|
255
|
-
for (const view of this.views.values()) {
|
|
256
|
-
if (view.kind === 'canvas')
|
|
257
|
-
total += view.canvasW * view.canvasH;
|
|
258
|
-
}
|
|
259
|
-
return total;
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Re-derives `pixelRatio` from the root's effective on-screen scale, so the
|
|
263
|
-
* mesh tier rasters at the resolution the screen actually shows.
|
|
264
|
-
*
|
|
265
|
-
* Mesh canvases are sized in the root's own coordinates, and a CSS transform
|
|
266
|
-
* anywhere above the root — the natural way to build a pan/zoom stage is
|
|
267
|
-
* `transform: scale(zoom)` on an ancestor — rescales them on screen without
|
|
268
|
-
* the renderer seeing it. At zoom z the raster is then oversampled by 1/z per
|
|
269
|
-
* axis, 1/z² in backing pixels: GPU memory and fill rate, not correctness.
|
|
270
|
-
* This folds the measured scale in, `devicePixelRatio × scale`.
|
|
271
|
-
*
|
|
272
|
-
* One layout read per call. The root itself is usually 0×0 (slot elements are
|
|
273
|
-
* absolutely positioned and posed by transforms), so there is no box to
|
|
274
|
-
* measure: a hidden 100×100 px child is appended, measured once with
|
|
275
|
-
* getBoundingClientRect(), and removed again — nothing of it is left behind.
|
|
276
|
-
* It cannot disturb the slot elements either: they interleave by z-index,
|
|
277
|
-
* which applyCommon() writes explicitly on every visible one, so DOM order is
|
|
278
|
-
* not what orders them; and the probe is `visibility: hidden` and gone before
|
|
279
|
-
* anything can paint.
|
|
280
|
-
*
|
|
281
|
-
* The scale is the larger axis of the measured box. An ancestor rotation
|
|
282
|
-
* inflates that axis-aligned box, so the ratio errs high — the safe side:
|
|
283
|
-
* oversampling costs pixels, undersampling costs picture.
|
|
284
|
-
*
|
|
285
|
-
* A move smaller than 0.1% (see PIXEL_RATIO_DEADBAND) changes nothing, and a
|
|
286
|
-
* root that is not laid out (a `display: none` ancestor — the probe measures
|
|
287
|
-
* 0) changes nothing either. Both matter because writing `pixelRatio`
|
|
288
|
-
* reallocates every mesh canvas on the next frame.
|
|
289
|
-
*
|
|
290
|
-
* The renderer never calls this itself. It is a forced synchronous layout,
|
|
291
|
-
* which has no business in a render path — and there is no per-frame layout
|
|
292
|
-
* read anywhere in this class. Call it when a zoom or a layout settles: after
|
|
293
|
-
* a wheel/pinch gesture ends, on a debounced resize, not during the drag.
|
|
294
|
-
*
|
|
295
|
-
* @returns the pixel ratio now in effect (the current one if nothing moved).
|
|
296
|
-
*/
|
|
297
|
-
syncPixelRatio() {
|
|
298
|
-
const probe = document.createElement('div');
|
|
299
|
-
probe.style.position = 'absolute';
|
|
300
|
-
probe.style.left = '0';
|
|
301
|
-
probe.style.top = '0';
|
|
302
|
-
probe.style.width = `${SCALE_PROBE_PX}px`;
|
|
303
|
-
probe.style.height = `${SCALE_PROBE_PX}px`;
|
|
304
|
-
probe.style.visibility = 'hidden';
|
|
305
|
-
probe.style.pointerEvents = 'none';
|
|
306
|
-
this.root.appendChild(probe);
|
|
307
|
-
let width = 0;
|
|
308
|
-
let height = 0;
|
|
309
|
-
try {
|
|
310
|
-
const rect = probe.getBoundingClientRect();
|
|
311
|
-
width = rect.width;
|
|
312
|
-
height = rect.height;
|
|
313
|
-
}
|
|
314
|
-
finally {
|
|
315
|
-
probe.remove();
|
|
316
|
-
}
|
|
317
|
-
const scale = Math.max(width, height) / SCALE_PROBE_PX;
|
|
318
|
-
// Not laid out (display:none ancestor, detached root): no measurement, so
|
|
319
|
-
// no decision — keep whatever the caller has.
|
|
320
|
-
if (!(scale > 0))
|
|
321
|
-
return this.pixelRatio;
|
|
322
|
-
const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;
|
|
323
|
-
const next = dpr * scale;
|
|
324
|
-
const current = this.pixelRatio;
|
|
325
|
-
if (current > 0 && Math.abs(next - current) < current * PIXEL_RATIO_DEADBAND)
|
|
326
|
-
return current;
|
|
327
|
-
this.pixelRatio = next;
|
|
328
|
-
return next;
|
|
329
|
-
}
|
|
330
|
-
render(skeleton) {
|
|
331
|
-
// One feature detection per renderer, never per slot and never per frame.
|
|
332
|
-
const core = (this.core ??= coreCompatFor(skeleton));
|
|
333
|
-
this.clipCount = 0;
|
|
334
|
-
this.clipSkipCount = 0;
|
|
335
|
-
this.clipWriteCount = 0;
|
|
336
|
-
this.meshCount = 0;
|
|
337
|
-
this.meshReuseCount = 0;
|
|
338
|
-
this.canvasReallocCount = 0;
|
|
339
|
-
this.triangleCount = 0;
|
|
340
|
-
this.clipAttachment = null;
|
|
341
|
-
this.clipEndSlot = null;
|
|
342
|
-
this.clipInverse = false;
|
|
343
|
-
this.clipOnRoot = false;
|
|
344
|
-
this.drewAnything = false;
|
|
345
|
-
const blitter = this.meshBackend === 'webgl' ? getMeshGlBlitter() : null;
|
|
346
|
-
this.meshBackendActive = blitter ? 'webgl' : 'canvas2d';
|
|
347
|
-
const drawOrder = core.drawOrder(skeleton);
|
|
348
|
-
for (let i = 0, n = drawOrder.length; i < n; i++) {
|
|
349
|
-
const slot = drawOrder[i];
|
|
350
|
-
const pose = core.pose(slot);
|
|
351
|
-
const attachment = pose.attachment;
|
|
352
|
-
// The official draw loops take the clipping attachment before anything
|
|
353
|
-
// else and in this order: end whatever clip this slot closes, start this
|
|
354
|
-
// slot's clip, then `continue` — skipping the per-slot clipEnd at the
|
|
355
|
-
// bottom. That `continue` is the whole own-slot rule: the slot that
|
|
356
|
-
// STARTS a clip is never offered to clipEnd afterwards, so an end slot
|
|
357
|
-
// equal to the clip's own slot ends nothing and the clip runs to the end
|
|
358
|
-
// of the draw order. [spine-core 4.3.13 dist/SkeletonRendererCore.js,
|
|
359
|
-
// and dist/SkeletonRenderer.js of @esotericsoftware/spine-webgl 4.3.13.]
|
|
360
|
-
if (attachment instanceof ClippingAttachment) {
|
|
361
|
-
this.clipEnd(slot);
|
|
362
|
-
// An inactive bone means the official loop never reaches the clipping
|
|
363
|
-
// branch at all, so no clip starts.
|
|
364
|
-
if (slot.bone.active)
|
|
365
|
-
this.clipStart(skeleton, slot, attachment, drawOrder, i);
|
|
366
|
-
else
|
|
367
|
-
this.clipSkipCount++;
|
|
368
|
-
this.hide(slot);
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
if (!slot.bone.active) {
|
|
372
|
-
this.hide(slot);
|
|
373
|
-
this.clipEnd(slot);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (attachment instanceof RegionAttachment) {
|
|
377
|
-
this.renderRegion(skeleton, slot, pose, attachment, i);
|
|
378
|
-
}
|
|
379
|
-
else if (attachment instanceof MeshAttachment) {
|
|
380
|
-
this.renderMesh(skeleton, slot, pose, attachment, i);
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
this.hide(slot);
|
|
384
|
-
}
|
|
385
|
-
// After the slot is drawn: the end slot is inside its own clip.
|
|
386
|
-
this.clipEnd(slot);
|
|
387
|
-
}
|
|
388
|
-
// Only a clip that covered the whole frame keeps the root's style; every
|
|
389
|
-
// other frame hands the caller's inline value back.
|
|
390
|
-
if (!this.clipOnRoot)
|
|
391
|
-
this.releaseRootClip();
|
|
392
|
-
if (this.pendingJobs.length) {
|
|
393
|
-
if (!blitter || !blitter.flush(this.pendingJobs)) {
|
|
394
|
-
// Context lost mid-frame: rasterize this batch on the 2d path so the
|
|
395
|
-
// frame stays complete; the next render() re-selects the backend.
|
|
396
|
-
for (let i = 0; i < this.pendingJobs.length; i++) {
|
|
397
|
-
const job = this.pendingJobs[i];
|
|
398
|
-
this.pendingViews[i].meshBackendDrawn = 'canvas2d';
|
|
399
|
-
this.rasterizeMesh2d(job.canvas,
|
|
400
|
-
// The 2d path wants straight alpha where the upload wanted it
|
|
401
|
-
// premultiplied; the derivation is cached, so this costs a lookup.
|
|
402
|
-
straightAlphaSource(job.page, job.pma), job.vertices, job.uvs, job.triangles, job.ratio);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
this.pendingJobs.length = 0;
|
|
406
|
-
this.pendingViews.length = 0;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Removes every element this renderer added to the root (slot elements and
|
|
411
|
-
* the tint filter defs), restores the root's own inline `clip-path` if the
|
|
412
|
-
* whole-skeleton clip path borrowed it, and hands the atlas pages it
|
|
413
|
-
* uploaded back to the shared GL blitter — the one resource here that outlives the instance, so
|
|
414
|
-
* the one that has to be given back explicitly (GPU memory, and no garbage
|
|
415
|
-
* collector feels pressure from it). The pages are reference-counted there:
|
|
416
|
-
* a page another live renderer still draws stays uploaded, and the texture
|
|
417
|
-
* is deleted only when the last user of it is disposed.
|
|
418
|
-
*
|
|
419
|
-
* The region bitmaps are deliberately untouched: the map is the caller's,
|
|
420
|
-
* and one map is normally shared by many renderers (disposing one instance
|
|
421
|
-
* must not blind the others). Free the unpacked blob URLs with
|
|
422
|
-
* revokeRegions() once no renderer needs them.
|
|
423
|
-
*
|
|
424
|
-
* Idempotent: the retained-page ledger is cleared here, so a second call
|
|
425
|
-
* releases nothing a second time.
|
|
426
|
-
*/
|
|
427
|
-
dispose() {
|
|
428
|
-
// The root is the caller's element and outlives this renderer, so the
|
|
429
|
-
// borrowed inline clip-path goes back before the slot elements go.
|
|
430
|
-
this.releaseRootClip();
|
|
431
|
-
for (const view of this.views.values())
|
|
432
|
-
view.el.remove();
|
|
433
|
-
this.views.clear();
|
|
434
|
-
this.tintDefs?.remove();
|
|
435
|
-
this.tintDefs = null;
|
|
436
|
-
if (this.glPages) {
|
|
437
|
-
const blitter = getMeshGlBlitter();
|
|
438
|
-
if (blitter)
|
|
439
|
-
for (const page of this.glPages)
|
|
440
|
-
blitter.release(page);
|
|
441
|
-
this.glPages = null;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// --- rigid tier -----------------------------------------------------------
|
|
445
|
-
renderRegion(skeleton, slot, pose, attachment, zIndex) {
|
|
446
|
-
const core = this.core;
|
|
447
|
-
const region = core.regionAt(attachment, slot, core.sequenceIndex(attachment, pose));
|
|
448
|
-
const regionImage = region && this.regionImages.get(region.name);
|
|
449
|
-
if (!regionImage) {
|
|
450
|
-
this.hide(slot);
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
const view = this.view(slot, 'image');
|
|
454
|
-
const img = view.el;
|
|
455
|
-
if (view.regionName !== region.name) {
|
|
456
|
-
view.regionName = region.name;
|
|
457
|
-
img.src = regionImage.url;
|
|
458
|
-
img.width = regionImage.width;
|
|
459
|
-
img.height = regionImage.height;
|
|
460
|
-
}
|
|
461
|
-
core.regionWorldVertices(attachment, slot, pose, regionVertices);
|
|
462
|
-
// Corner order from spine-core is BL, UL, UR, BR — derived from
|
|
463
|
-
// computeUVs, whose per-vertex UVs are (u,v2), (u,v), (u2,v), (u2,v2).
|
|
464
|
-
// (The br/bl/ul/ur comments inside computeWorldVertices are stale.)
|
|
465
|
-
// Flip Y for CSS (Spine is Y-up).
|
|
466
|
-
const blx = regionVertices[0], bly = -regionVertices[1];
|
|
467
|
-
const ulx = regionVertices[2], uly = -regionVertices[3];
|
|
468
|
-
const urx = regionVertices[4], ury = -regionVertices[5];
|
|
469
|
-
const w = regionImage.width;
|
|
470
|
-
const h = regionImage.height;
|
|
471
|
-
const a = (urx - ulx) / w;
|
|
472
|
-
const b = (ury - uly) / w;
|
|
473
|
-
const c = (blx - ulx) / h;
|
|
474
|
-
const d = (bly - uly) / h;
|
|
475
|
-
this.setTransform(view, `matrix(${a},${b},${c},${d},${ulx},${uly})`);
|
|
476
|
-
// Rigid elements carry a full affine, so the clip polygon rides its
|
|
477
|
-
// inverse into the local (atlas-unit) frame the <img> box spans.
|
|
478
|
-
this.applyClip(view, a, b, c, d, ulx, uly, w, h);
|
|
479
|
-
this.applyCommon(view, slot, pose, attachment.color, skeleton, zIndex);
|
|
480
|
-
}
|
|
481
|
-
// --- deform tier ----------------------------------------------------------
|
|
482
|
-
renderMesh(skeleton, slot, pose, attachment, zIndex) {
|
|
483
|
-
const core = this.core;
|
|
484
|
-
const sequenceIndex = core.sequenceIndex(attachment, pose);
|
|
485
|
-
const region = core.regionAt(attachment, slot, sequenceIndex);
|
|
486
|
-
// The page image comes off the *page*, not off the region. A region's own
|
|
487
|
-
// `texture` is a 4.1-and-later convenience — `TextureAtlasPage.setTexture`
|
|
488
|
-
// copies the page's texture onto each of its regions — and 4.0 has no such
|
|
489
|
-
// field at all (it carries a `renderObject` back-reference instead, which
|
|
490
|
-
// is why spine-webgl 4.0 reads `region.renderObject.page.texture`). The
|
|
491
|
-
// page is where the handle has always lived, so reading it there needs no
|
|
492
|
-
// seam entry and is the same object on every supported generation.
|
|
493
|
-
const page = region?.page.texture?.getImage();
|
|
494
|
-
if (!region || !page) {
|
|
495
|
-
this.hide(slot);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
// Which alpha convention the page's texels are in. The two backends consume
|
|
499
|
-
// opposite ones — GL wants them premultiplied, `drawImage` wants them
|
|
500
|
-
// straight — so this decides what each is handed, never what it does.
|
|
501
|
-
const pma = region.page.pma;
|
|
502
|
-
const count = attachment.worldVerticesLength;
|
|
503
|
-
if (this.scratchVertices.length < count)
|
|
504
|
-
this.scratchVertices = new Float32Array(count);
|
|
505
|
-
const vertices = this.scratchVertices;
|
|
506
|
-
core.vertexWorldVertices(attachment, skeleton, slot, 0, count, vertices, 0, 2);
|
|
507
|
-
// World bounds (in CSS coords: Y negated).
|
|
508
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
509
|
-
for (let v = 0; v < count; v += 2) {
|
|
510
|
-
const x = vertices[v];
|
|
511
|
-
const y = -vertices[v + 1];
|
|
512
|
-
vertices[v + 1] = y;
|
|
513
|
-
if (x < minX)
|
|
514
|
-
minX = x;
|
|
515
|
-
if (x > maxX)
|
|
516
|
-
maxX = x;
|
|
517
|
-
if (y < minY)
|
|
518
|
-
minY = y;
|
|
519
|
-
if (y > maxY)
|
|
520
|
-
maxY = y;
|
|
521
|
-
}
|
|
522
|
-
const pad = 1;
|
|
523
|
-
minX = Math.floor(minX) - pad;
|
|
524
|
-
minY = Math.floor(minY) - pad;
|
|
525
|
-
const w = Math.ceil(maxX) + pad - minX;
|
|
526
|
-
const h = Math.ceil(maxY) + pad - minY;
|
|
527
|
-
if (w <= 0 || h <= 0) {
|
|
528
|
-
this.hide(slot);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
const view = this.view(slot, 'canvas');
|
|
532
|
-
const canvas = view.el;
|
|
533
|
-
const ratio = this.pixelRatio;
|
|
534
|
-
let dirty = view.meshAttachment !== attachment ||
|
|
535
|
-
view.meshSequenceIndex !== sequenceIndex ||
|
|
536
|
-
view.meshExpand !== this.triangleExpand ||
|
|
537
|
-
view.meshVertexCount !== count ||
|
|
538
|
-
view.meshBackendDrawn !== this.meshBackendActive;
|
|
539
|
-
// The backing store only grows, in 32-device-px steps. Setting
|
|
540
|
-
// canvas.width recreates the GPU surface, and a deforming mesh changes
|
|
541
|
-
// its bbox every frame — reallocating every mesh canvas per frame
|
|
542
|
-
// stalled real Safari to ~3 fps while the JS split showed ~4 ms (the
|
|
543
|
-
// cost lives in the compositor, invisible to in-callback timing). With
|
|
544
|
-
// grow-only quantized backing, steady-state animation reallocates
|
|
545
|
-
// nothing. A pixelRatio change is the one exception to grow-only: it
|
|
546
|
-
// reallocates every mesh canvas anyway, so the new size comes from the
|
|
547
|
-
// new need alone. Keeping the old size as a floor there would make a
|
|
548
|
-
// ratio *drop* pay the reallocation and keep every oversized pixel — the
|
|
549
|
-
// backing would never come back down to what that ratio needs. The CSS
|
|
550
|
-
// size mirrors the whole backing so the pixel mapping stays 1:1; the mesh
|
|
551
|
-
// draws into the top-left w×h logical region and the rest stays
|
|
552
|
-
// transparent.
|
|
553
|
-
const needW = Math.max(1, Math.round(w * ratio));
|
|
554
|
-
const needH = Math.max(1, Math.round(h * ratio));
|
|
555
|
-
const ratioChanged = view.meshRatio !== ratio;
|
|
556
|
-
if (needW > view.canvasW || needH > view.canvasH || ratioChanged) {
|
|
557
|
-
// 25% slack: at low fps the animation is sampled sparsely, so new bbox
|
|
558
|
-
// maxima keep being discovered for many seconds — allocate ahead of the
|
|
559
|
-
// curve instead of chasing it.
|
|
560
|
-
const step = 32;
|
|
561
|
-
const floorW = ratioChanged ? 0 : view.canvasW;
|
|
562
|
-
const floorH = ratioChanged ? 0 : view.canvasH;
|
|
563
|
-
view.canvasW = Math.ceil(Math.max(needW * 1.25, floorW) / step) * step;
|
|
564
|
-
view.canvasH = Math.ceil(Math.max(needH * 1.25, floorH) / step) * step;
|
|
565
|
-
view.meshRatio = ratio;
|
|
566
|
-
canvas.width = view.canvasW;
|
|
567
|
-
canvas.height = view.canvasH;
|
|
568
|
-
canvas.style.width = `${view.canvasW / ratio}px`;
|
|
569
|
-
canvas.style.height = `${view.canvasH / ratio}px`;
|
|
570
|
-
this.canvasReallocCount++;
|
|
571
|
-
dirty = true;
|
|
572
|
-
}
|
|
573
|
-
// Canvas-space vertices: identical values mean last frame's raster is
|
|
574
|
-
// still exact (static pose, or the part moved by whole pixels), so the
|
|
575
|
-
// per-triangle redraw can be skipped — the CSS translate below keeps
|
|
576
|
-
// tracking the part. This is the main WebKit cost lever: idle skeletons
|
|
577
|
-
// stop paying the raster entirely.
|
|
578
|
-
if (view.meshVertices.length < count) {
|
|
579
|
-
view.meshVertices = new Float64Array(count);
|
|
580
|
-
dirty = true;
|
|
581
|
-
}
|
|
582
|
-
const rel = view.meshVertices;
|
|
583
|
-
for (let v = 0; v < count; v += 2) {
|
|
584
|
-
const x = vertices[v] - minX;
|
|
585
|
-
const y = vertices[v + 1] - minY;
|
|
586
|
-
if (rel[v] !== x || rel[v + 1] !== y)
|
|
587
|
-
dirty = true;
|
|
588
|
-
rel[v] = x;
|
|
589
|
-
rel[v + 1] = y;
|
|
590
|
-
}
|
|
591
|
-
this.setTransform(view, `translate(${minX}px,${minY}px)`);
|
|
592
|
-
// A mesh canvas is posed by a translate only, so its local frame is the
|
|
593
|
-
// CSS-space polygon minus that translate. The clip never reaches the
|
|
594
|
-
// raster: it is not part of the dirty signature, and a mesh whose vertices
|
|
595
|
-
// held still still reuses its raster under a moving clip.
|
|
596
|
-
this.applyClip(view, 1, 0, 0, 1, minX, minY, view.canvasW / ratio, view.canvasH / ratio);
|
|
597
|
-
if (!dirty) {
|
|
598
|
-
this.meshReuseCount++;
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
view.meshAttachment = attachment;
|
|
602
|
-
view.meshSequenceIndex = sequenceIndex;
|
|
603
|
-
view.meshExpand = this.triangleExpand;
|
|
604
|
-
view.meshVertexCount = count;
|
|
605
|
-
view.meshBackendDrawn = this.meshBackendActive;
|
|
606
|
-
const uvs = core.meshUVs(attachment, sequenceIndex);
|
|
607
|
-
const triangles = attachment.triangles;
|
|
608
|
-
if (this.meshBackendActive === 'webgl') {
|
|
609
|
-
// Queued, not drawn: render() flushes the whole batch through the
|
|
610
|
-
// shared GL context once the slot loop is done. `rel` is this view's
|
|
611
|
-
// own signature array — nothing mutates it before the flush.
|
|
612
|
-
this.pendingJobs.push({
|
|
613
|
-
canvas,
|
|
614
|
-
page,
|
|
615
|
-
pma,
|
|
616
|
-
vertices: rel,
|
|
617
|
-
uvs,
|
|
618
|
-
triangles,
|
|
619
|
-
ratio,
|
|
620
|
-
width: Math.min(view.canvasW, Math.ceil(w * ratio)),
|
|
621
|
-
height: Math.min(view.canvasH, Math.ceil(h * ratio)),
|
|
622
|
-
});
|
|
623
|
-
this.pendingViews.push(view);
|
|
624
|
-
this.retainPage(page);
|
|
625
|
-
}
|
|
626
|
-
else {
|
|
627
|
-
this.rasterizeMesh2d(canvas, straightAlphaSource(page, pma), rel, uvs, triangles, ratio);
|
|
628
|
-
}
|
|
629
|
-
this.meshCount++;
|
|
630
|
-
this.triangleCount += triangles.length / 3;
|
|
631
|
-
}
|
|
632
|
-
this.applyCommon(view, slot, pose, attachment.color, skeleton, zIndex);
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Declares this renderer a user of `page` with the shared blitter, once per
|
|
636
|
-
* page — the counterpart of the release in dispose(). The set is this
|
|
637
|
-
* renderer's own ledger, so sharing one page across renderers works by
|
|
638
|
-
* counting rather than by guessing. On the render path this costs one
|
|
639
|
-
* Set.has per queued mesh job and allocates nothing after the first page.
|
|
640
|
-
*/
|
|
641
|
-
retainPage(page) {
|
|
642
|
-
const pages = (this.glPages ??= new Set());
|
|
643
|
-
if (pages.has(page))
|
|
644
|
-
return;
|
|
645
|
-
// Null only if the context died between the backend pick and here; the
|
|
646
|
-
// flush then fails too and the batch falls back to canvas2d.
|
|
647
|
-
const blitter = getMeshGlBlitter();
|
|
648
|
-
if (!blitter)
|
|
649
|
-
return;
|
|
650
|
-
pages.add(page);
|
|
651
|
-
blitter.retain(page);
|
|
652
|
-
}
|
|
653
|
-
/** The canvas2d raster path: clear the backing, map each triangle. */
|
|
654
|
-
rasterizeMesh2d(canvas, page, vertices, uvs, triangles, ratio) {
|
|
655
|
-
const ctx = canvas.getContext('2d');
|
|
656
|
-
if (!ctx)
|
|
657
|
-
return;
|
|
658
|
-
// Clear the full backing: the previous frame's bbox (and so its drawn
|
|
659
|
-
// region) may have been larger than today's.
|
|
660
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
661
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
662
|
-
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
663
|
-
// Texel addressing, shared with the GL blitter: a normalized UV maps to
|
|
664
|
-
// `uv * size` in the continuous texture frame where the page spans
|
|
665
|
-
// [0, size] and texel i covers [i, i+1). drawImage-under-a-transform and
|
|
666
|
-
// texture2D() both read that frame, so both backends land on the same
|
|
667
|
-
// source pixels. (No half-texel term: the affine below *maps* corners, it
|
|
668
|
-
// does not sample — see drawTriangle.) `page` is the page image, or its
|
|
669
|
-
// straight-alpha derivation on a premultiplied one; the derivation is the
|
|
670
|
-
// image's own natural size, so the frame is the same either way.
|
|
671
|
-
const uw = page.width;
|
|
672
|
-
const uh = page.height;
|
|
673
|
-
for (let t = 0; t < triangles.length; t += 3) {
|
|
674
|
-
const i0 = triangles[t] * 2;
|
|
675
|
-
const i1 = triangles[t + 1] * 2;
|
|
676
|
-
const i2 = triangles[t + 2] * 2;
|
|
677
|
-
this.drawTriangle(ctx, page, vertices[i0], vertices[i0 + 1], uvs[i0] * uw, uvs[i0 + 1] * uh, vertices[i1], vertices[i1 + 1], uvs[i1] * uw, uvs[i1 + 1] * uh, vertices[i2], vertices[i2 + 1], uvs[i2] * uw, uvs[i2 + 1] * uh);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* Standard canvas triangle texture mapping (same math as the official
|
|
682
|
-
* spine-canvas renderer): derive the affine that sends the triangle's
|
|
683
|
-
* texture-space corners to its screen-space corners, clip, draw the page —
|
|
684
|
-
* but only the part of the page this triangle can show (see below).
|
|
685
|
-
*
|
|
686
|
-
* The clip polygon is expanded outward from the centroid by a fraction of
|
|
687
|
-
* a pixel. Browsers that antialias clip paths (Safari) otherwise leave
|
|
688
|
-
* hairline cracks between adjacent triangles; the expanded clips overlap
|
|
689
|
-
* into the neighbouring triangle, and since the texture is continuous
|
|
690
|
-
* across the shared edge the overlap draws the same pixels — cracks close
|
|
691
|
-
* with no visible cost. The texture-mapping affine itself stays exact.
|
|
692
|
-
*
|
|
693
|
-
* ## Why a source sub-rect instead of the whole page
|
|
694
|
-
*
|
|
695
|
-
* The draw is a 9-argument `drawImage` whose source rect covers exactly what
|
|
696
|
-
* the clip can reveal, placed at the same texture-space position, so the
|
|
697
|
-
* mapping is identical to drawing the whole page — the same affine, the same
|
|
698
|
-
* texels, only fewer of them offered to the rasterizer.
|
|
699
|
-
*
|
|
700
|
-
* That is not an optimization. Linux WebKit — the software rasterizer, the
|
|
701
|
-
* only engine this has been observed on; Chromium and macOS WebKit never show
|
|
702
|
-
* it — garbles individual triangles when the whole page is drawn under a
|
|
703
|
-
* steep per-triangle affine: read off the parity dump, head/goggles/foot
|
|
704
|
-
* triangles land with displaced texture while the webgl capture of the same
|
|
705
|
-
* pose stays clean. Switching this one call to a source sub-rect — the only
|
|
706
|
-
* variable changed — took that platform's bad-pixel counts from 1042 to 2
|
|
707
|
-
* (hoverboard), 231 to 4 (portal) and 803 to 3 (walk + tint), and left every
|
|
708
|
-
* Chromium capture byte-identical. [measured on the ubuntu CI runner through
|
|
709
|
-
* PARITY_DUMP: first with a fixed 2-texel pad (run 35209262693), then with
|
|
710
|
-
* the derived rect below — the same counts (run 35213286993).] WHY that
|
|
711
|
-
* rasterizer garbles the whole-page form is not identified; steep affines
|
|
712
|
-
* making whole-page coordinates large is a suspicion, not a measurement. The
|
|
713
|
-
* sub-rect sidesteps it. (Moving `clip()` before `transform()` was tried as a
|
|
714
|
-
* separate one-variable experiment: every number came back identical, so the
|
|
715
|
-
* clip path is not the cause.)
|
|
716
|
-
*
|
|
717
|
-
* ## How the sub-rect is derived
|
|
718
|
-
*
|
|
719
|
-
* It must provably cover everything the clip can reveal, which is the
|
|
720
|
-
* *expanded* polygon, not the triangle. So each clip corner's canvas-space
|
|
721
|
-
* displacement is pushed back through the inverse of the 2×2 affine into
|
|
722
|
-
* texture space, the bounding box is taken over the displaced corners, one
|
|
723
|
-
* texel of bilinear support is added, and the result is floor/ceil'd to texel
|
|
724
|
-
* boundaries (so the sub-rect itself resamples nothing) and clamped to the
|
|
725
|
-
* page. A fixed pad would not do: the texture-space reach of the expansion is
|
|
726
|
-
* `triangleExpand` times the local texels-per-canvas-unit, which on the
|
|
727
|
-
* demo's own meshes ranges from ~0.26 texels on a typical triangle to ~8.6 on
|
|
728
|
-
* a foreshortened one. Under-padding lets the rim of the overdraw sample
|
|
729
|
-
* nothing, which brings back the very seams the overdraw exists to close.
|
|
730
|
-
*
|
|
731
|
-
* No half-texel offset is applied to the incoming u/v: the `(i + 0.5) / size`
|
|
732
|
-
* texel-centre convention belongs to *sampling* (landing inside the intended
|
|
733
|
-
* texel instead of on a filter boundary). This solves for the affine that
|
|
734
|
-
* sends three source corners to three destination corners; the rasterizer
|
|
735
|
-
* then samples continuously along that map. Biasing all three corners by
|
|
736
|
-
* half a texel would translate the texture against the geometry — the very
|
|
737
|
-
* class of offset the `uv * size` addressing removes.
|
|
738
|
-
*/
|
|
739
|
-
drawTriangle(ctx, img, x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2) {
|
|
740
|
-
const cx = (x0 + x1 + x2) / 3;
|
|
741
|
-
const cy = (y0 + y1 + y2) / 3;
|
|
742
|
-
const expand = this.triangleExpand;
|
|
743
|
-
const [px0, py0] = expandPoint(x0, y0, cx, cy, expand);
|
|
744
|
-
const [px1, py1] = expandPoint(x1, y1, cx, cy, expand);
|
|
745
|
-
const [px2, py2] = expandPoint(x2, y2, cx, cy, expand);
|
|
746
|
-
ctx.beginPath();
|
|
747
|
-
ctx.moveTo(px0, py0);
|
|
748
|
-
ctx.lineTo(px1, py1);
|
|
749
|
-
ctx.lineTo(px2, py2);
|
|
750
|
-
ctx.closePath();
|
|
751
|
-
// Edge vectors, in canvas space and in texture space.
|
|
752
|
-
const ex1 = x1 - x0, ey1 = y1 - y0;
|
|
753
|
-
const ex2 = x2 - x0, ey2 = y2 - y0;
|
|
754
|
-
const eu1 = u1 - u0, ev1 = v1 - v0;
|
|
755
|
-
const eu2 = u2 - u0, ev2 = v2 - v0;
|
|
756
|
-
let det = eu1 * ev2 - eu2 * ev1;
|
|
757
|
-
if (det === 0)
|
|
758
|
-
return;
|
|
759
|
-
det = 1 / det;
|
|
760
|
-
const a = (ev2 * ex1 - ev1 * ex2) * det;
|
|
761
|
-
const b = (ev2 * ey1 - ev1 * ey2) * det;
|
|
762
|
-
const c = (eu1 * ex2 - eu2 * ex1) * det;
|
|
763
|
-
const d = (eu1 * ey2 - eu2 * ey1) * det;
|
|
764
|
-
const e = x0 - a * u0 - c * v0;
|
|
765
|
-
const f = y0 - b * u0 - d * v0;
|
|
766
|
-
// Texture-space bounds of the clip polygon. The triangle's own corners
|
|
767
|
-
// first, then the three expansion offsets carried back through the inverse
|
|
768
|
-
// of [[a, c], [b, d]] — the clip reaches exactly that far and no further.
|
|
769
|
-
let minU = Math.min(u0, u1, u2);
|
|
770
|
-
let maxU = Math.max(u0, u1, u2);
|
|
771
|
-
let minV = Math.min(v0, v1, v2);
|
|
772
|
-
let maxV = Math.max(v0, v1, v2);
|
|
773
|
-
const detM = a * d - b * c;
|
|
774
|
-
if (detM !== 0) {
|
|
775
|
-
const im = 1 / detM;
|
|
776
|
-
const ia = d * im, ic = -c * im, ib = -b * im, id = a * im;
|
|
777
|
-
let gx = px0 - x0, gy = py0 - y0;
|
|
778
|
-
let tu = u0 + ia * gx + ic * gy, tv = v0 + ib * gx + id * gy;
|
|
779
|
-
minU = Math.min(minU, tu);
|
|
780
|
-
maxU = Math.max(maxU, tu);
|
|
781
|
-
minV = Math.min(minV, tv);
|
|
782
|
-
maxV = Math.max(maxV, tv);
|
|
783
|
-
gx = px1 - x1;
|
|
784
|
-
gy = py1 - y1;
|
|
785
|
-
tu = u1 + ia * gx + ic * gy;
|
|
786
|
-
tv = v1 + ib * gx + id * gy;
|
|
787
|
-
minU = Math.min(minU, tu);
|
|
788
|
-
maxU = Math.max(maxU, tu);
|
|
789
|
-
minV = Math.min(minV, tv);
|
|
790
|
-
maxV = Math.max(maxV, tv);
|
|
791
|
-
gx = px2 - x2;
|
|
792
|
-
gy = py2 - y2;
|
|
793
|
-
tu = u2 + ia * gx + ic * gy;
|
|
794
|
-
tv = v2 + ib * gx + id * gy;
|
|
795
|
-
minU = Math.min(minU, tu);
|
|
796
|
-
maxU = Math.max(maxU, tu);
|
|
797
|
-
minV = Math.min(minV, tv);
|
|
798
|
-
maxV = Math.max(maxV, tv);
|
|
799
|
-
}
|
|
800
|
-
// One texel of bilinear support on every side, snapped outward to texel
|
|
801
|
-
// boundaries, clamped to the page. Source rect and destination rect are the
|
|
802
|
-
// same rect in texture space, so the affine below maps it exactly as it
|
|
803
|
-
// mapped the whole page.
|
|
804
|
-
const sx = Math.max(0, Math.floor(minU - 1));
|
|
805
|
-
const sy = Math.max(0, Math.floor(minV - 1));
|
|
806
|
-
const sw = Math.min(img.width, Math.ceil(maxU + 1)) - sx;
|
|
807
|
-
const sh = Math.min(img.height, Math.ceil(maxV + 1)) - sy;
|
|
808
|
-
if (!(sw > 0) || !(sh > 0))
|
|
809
|
-
return;
|
|
810
|
-
ctx.save();
|
|
811
|
-
ctx.transform(a, b, c, d, e, f);
|
|
812
|
-
ctx.clip();
|
|
813
|
-
ctx.drawImage(img, sx, sy, sw, sh, sx, sy, sw, sh);
|
|
814
|
-
ctx.restore();
|
|
815
|
-
}
|
|
816
|
-
// --- clipping tier ---------------------------------------------------------
|
|
817
|
-
/**
|
|
818
|
-
* Begins a clip, mirroring SkeletonClipping.clipStart — whose first line is
|
|
819
|
-
* `if (this.clipAttachment) return`, so a second clipping attachment met
|
|
820
|
-
* while one is active is ignored rather than nested.
|
|
821
|
-
*
|
|
822
|
-
* The polygon is spine-core's: computeWorldVertices gives it in world space
|
|
823
|
-
* each frame, and the only thing done to it here is the Y negation the rest
|
|
824
|
-
* of the renderer already applies. It is NOT convexified or triangulated the
|
|
825
|
-
* way the CPU clipper does — CSS `polygon()` takes a concave polygon
|
|
826
|
-
* directly, so the decomposition that exists to feed a triangle rasterizer
|
|
827
|
-
* has no job here.
|
|
828
|
-
*/
|
|
829
|
-
clipStart(skeleton, slot, clip, drawOrder, index) {
|
|
830
|
-
const count = clip.worldVerticesLength;
|
|
831
|
-
if (this.clipAttachment || !this.clipping || count < 6) {
|
|
832
|
-
this.clipSkipCount++;
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
const core = this.core;
|
|
836
|
-
if (this.scratchVertices.length < count)
|
|
837
|
-
this.scratchVertices = new Float32Array(count);
|
|
838
|
-
const world = this.scratchVertices;
|
|
839
|
-
core.vertexWorldVertices(clip, skeleton, slot, 0, count, world, 0, 2);
|
|
840
|
-
if (this.clipPolygon.length < count)
|
|
841
|
-
this.clipPolygon = new Float64Array(count);
|
|
842
|
-
const poly = this.clipPolygon;
|
|
843
|
-
for (let v = 0; v < count; v += 2) {
|
|
844
|
-
poly[v] = world[v];
|
|
845
|
-
poly[v + 1] = -world[v + 1]; // Spine is Y-up, CSS is Y-down.
|
|
846
|
-
}
|
|
847
|
-
this.clipPolygonLength = count;
|
|
848
|
-
this.clipAttachment = clip;
|
|
849
|
-
this.clipEndSlot = clip.endSlot;
|
|
850
|
-
this.clipInverse = core.inverse(clip);
|
|
851
|
-
this.clipCount++;
|
|
852
|
-
// Whole-skeleton fast path: nothing was drawn before this clip started and
|
|
853
|
-
// it never ends, so every element of this frame is inside it — one
|
|
854
|
-
// clip-path on the root beats one per element. An inverse clip is
|
|
855
|
-
// excluded: its CSS form needs an outer ring around the element's own box,
|
|
856
|
-
// and the root is an origin element with no box to use.
|
|
857
|
-
this.clipOnRoot =
|
|
858
|
-
!this.drewAnything && !this.clipInverse && !endsAhead(drawOrder, index, clip.endSlot);
|
|
859
|
-
if (this.clipOnRoot)
|
|
860
|
-
this.writeRootClip(this.rootClipPath());
|
|
861
|
-
}
|
|
862
|
-
/**
|
|
863
|
-
* Ends the active clip if this slot is its end slot, called after the slot
|
|
864
|
-
* has been drawn so that the end slot is itself clipped. Every other slot is
|
|
865
|
-
* ignored, exactly as SkeletonClipping.clipEnd ignores it.
|
|
866
|
-
*
|
|
867
|
-
* This cannot fire for a clip on the root fast path: that path is only taken
|
|
868
|
-
* when no slot ahead of the clip carries its end slot.
|
|
869
|
-
*/
|
|
870
|
-
clipEnd(slot) {
|
|
871
|
-
if (this.clipAttachment && this.clipEndSlot === slot.data) {
|
|
872
|
-
this.clipAttachment = null;
|
|
873
|
-
this.clipEndSlot = null;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Writes (or clears) this element's clip-path. `a…f` is the element's own
|
|
878
|
-
* CSS transform, and the world polygon is carried through its INVERSE:
|
|
879
|
-
* clip-path is resolved in the element's local frame and then transformed
|
|
880
|
-
* along with the element, so the polygon has to arrive there already.
|
|
881
|
-
*
|
|
882
|
-
* `transform-origin` is `0 0` on every slot element (see view()), so the
|
|
883
|
-
* matrix maps local (0,0) to (e,f) with no origin term to undo, and
|
|
884
|
-
* `left/top: 0` puts the border box — clip-path's reference box — on that
|
|
885
|
-
* same local origin.
|
|
886
|
-
*/
|
|
887
|
-
applyClip(view, a, b, c, d, e, f, boxW, boxH) {
|
|
888
|
-
if (!this.clipAttachment || this.clipOnRoot) {
|
|
889
|
-
this.setClipPath(view, '');
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
const det = a * d - b * c;
|
|
893
|
-
// Singular: the element is collapsed onto a line or a point and draws
|
|
894
|
-
// nothing, so there is no clip to express and no style write to pay for.
|
|
895
|
-
if (det === 0)
|
|
896
|
-
return;
|
|
897
|
-
const inv = 1 / det;
|
|
898
|
-
const poly = this.clipPolygon;
|
|
899
|
-
const n = this.clipPolygonLength;
|
|
900
|
-
// An inverse clip shows what is OUTSIDE the polygon. That shape has a hole
|
|
901
|
-
// in it, so it needs TWO rings — an outer one and the polygon — and
|
|
902
|
-
// `polygon()` cannot carry two: it is one closed ring, and listing a box
|
|
903
|
-
// and a polygon inside it just makes one self-intersecting ring whose
|
|
904
|
-
// even-odd fill leaves a wedge of the seam between them (measured, on a
|
|
905
|
-
// corner of spineboy's boot). `path()` takes subpaths, so an inverse clip
|
|
906
|
-
// is written as two of them with the even-odd rule. The outer ring is the
|
|
907
|
-
// element's own box: the element paints nothing outside it anyway, so
|
|
908
|
-
// box-minus-polygon is exactly the visible part of the inverse region.
|
|
909
|
-
const inverse = this.clipInverse;
|
|
910
|
-
let s = inverse
|
|
911
|
-
? `path(evenodd,'M0 0H${clipNum(boxW)}V${clipNum(boxH)}H0Z`
|
|
912
|
-
: 'polygon(';
|
|
913
|
-
for (let v = 0; v < n; v += 2) {
|
|
914
|
-
const dx = poly[v] - e;
|
|
915
|
-
const dy = poly[v + 1] - f;
|
|
916
|
-
const lx = (d * dx - c * dy) * inv;
|
|
917
|
-
const ly = (a * dy - b * dx) * inv;
|
|
918
|
-
// Near-singular matrices blow the inverse up; see CLIP_COORD_LIMIT for
|
|
919
|
-
// why an unwritable coordinate must not become a written-and-ignored one.
|
|
920
|
-
if (!(Math.abs(lx) <= CLIP_COORD_LIMIT) || !(Math.abs(ly) <= CLIP_COORD_LIMIT))
|
|
921
|
-
return;
|
|
922
|
-
if (inverse) {
|
|
923
|
-
s += `${v > 0 ? 'L' : 'M'}${clipNum(lx)} ${clipNum(ly)}`;
|
|
924
|
-
}
|
|
925
|
-
else {
|
|
926
|
-
s += `${v > 0 ? ',' : ''}${clipCoord(lx)} ${clipCoord(ly)}`;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
this.setClipPath(view, inverse ? `${s}Z')` : `${s})`);
|
|
930
|
-
}
|
|
931
|
-
setClipPath(view, clipPath) {
|
|
932
|
-
if (view.clipPath === clipPath)
|
|
933
|
-
return;
|
|
934
|
-
view.clipPath = clipPath;
|
|
935
|
-
view.el.style.clipPath = clipPath;
|
|
936
|
-
this.clipWriteCount++;
|
|
937
|
-
}
|
|
938
|
-
/** The active polygon in the root's own frame — no element transform. */
|
|
939
|
-
rootClipPath() {
|
|
940
|
-
const poly = this.clipPolygon;
|
|
941
|
-
let s = 'polygon(';
|
|
942
|
-
for (let v = 0, n = this.clipPolygonLength; v < n; v += 2) {
|
|
943
|
-
if (v > 0)
|
|
944
|
-
s += ',';
|
|
945
|
-
s += `${clipCoord(poly[v])} ${clipCoord(poly[v + 1])}`;
|
|
946
|
-
}
|
|
947
|
-
return `${s})`;
|
|
948
|
-
}
|
|
949
|
-
writeRootClip(clipPath) {
|
|
950
|
-
// Borrow the caller's inline value once, so releaseRootClip gives back
|
|
951
|
-
// exactly what was there. Reading `.style` is CSSOM, not a layout read.
|
|
952
|
-
if (this.rootClipSaved === null)
|
|
953
|
-
this.rootClipSaved = this.root.style.clipPath;
|
|
954
|
-
if (this.rootClipWritten === clipPath)
|
|
955
|
-
return;
|
|
956
|
-
this.rootClipWritten = clipPath;
|
|
957
|
-
this.root.style.clipPath = clipPath;
|
|
958
|
-
this.clipWriteCount++;
|
|
959
|
-
}
|
|
960
|
-
releaseRootClip() {
|
|
961
|
-
if (this.rootClipSaved === null)
|
|
962
|
-
return;
|
|
963
|
-
this.root.style.clipPath = this.rootClipSaved;
|
|
964
|
-
this.rootClipSaved = null;
|
|
965
|
-
this.rootClipWritten = '';
|
|
966
|
-
this.clipWriteCount++;
|
|
967
|
-
}
|
|
968
|
-
// --- shared plumbing -------------------------------------------------------
|
|
969
|
-
setTransform(view, transform) {
|
|
970
|
-
if (view.transform !== transform) {
|
|
971
|
-
view.transform = transform;
|
|
972
|
-
view.el.style.transform = transform;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
applyCommon(view, slot, pose, attachmentColor, skeleton, zIndex) {
|
|
976
|
-
// This element is about to be visible: the whole-skeleton fast path is
|
|
977
|
-
// only open to a clip that starts before anything has been drawn.
|
|
978
|
-
this.drewAnything = true;
|
|
979
|
-
const sc = skeleton.color;
|
|
980
|
-
const pc = pose.color;
|
|
981
|
-
if (view.zIndex !== zIndex) {
|
|
982
|
-
view.zIndex = zIndex;
|
|
983
|
-
view.el.style.zIndex = String(zIndex);
|
|
984
|
-
}
|
|
985
|
-
const alpha = sc.a * pc.a * attachmentColor.a;
|
|
986
|
-
if (view.opacity !== alpha) {
|
|
987
|
-
view.opacity = alpha;
|
|
988
|
-
view.el.style.opacity = alpha === 1 ? '' : String(alpha);
|
|
989
|
-
}
|
|
990
|
-
// RGB tint (uniform per slot) rides an feColorMatrix reference filter:
|
|
991
|
-
// scaling the channels is an exact multiply, applies to <img> and
|
|
992
|
-
// <canvas> alike, and never touches the raster. Needs reference-filter
|
|
993
|
-
// support (Safari 15+).
|
|
994
|
-
const r = sc.r * pc.r * attachmentColor.r;
|
|
995
|
-
const g = sc.g * pc.g * attachmentColor.g;
|
|
996
|
-
const b = sc.b * pc.b * attachmentColor.b;
|
|
997
|
-
if (view.tintR !== r || view.tintG !== g || view.tintB !== b) {
|
|
998
|
-
view.tintR = r;
|
|
999
|
-
view.tintG = g;
|
|
1000
|
-
view.tintB = b;
|
|
1001
|
-
if (r === 1 && g === 1 && b === 1) {
|
|
1002
|
-
view.el.style.filter = '';
|
|
1003
|
-
}
|
|
1004
|
-
else {
|
|
1005
|
-
this.tintMatrixFor(view).setAttribute('values', `${r} 0 0 0 0 0 ${g} 0 0 0 0 0 ${b} 0 0 0 0 0 1 0`);
|
|
1006
|
-
view.el.style.filter = `url(#${view.tintId})`;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
const blendMode = slot.data.blendMode;
|
|
1010
|
-
if (view.blendMode !== blendMode) {
|
|
1011
|
-
view.blendMode = blendMode;
|
|
1012
|
-
view.el.style.mixBlendMode = BLEND_CSS[blendMode];
|
|
1013
|
-
}
|
|
1014
|
-
if (!view.visible) {
|
|
1015
|
-
view.visible = true;
|
|
1016
|
-
view.el.style.display = '';
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
tintMatrixFor(view) {
|
|
1020
|
-
if (view.tintMatrix)
|
|
1021
|
-
return view.tintMatrix;
|
|
1022
|
-
if (!this.tintDefs) {
|
|
1023
|
-
const svg = document.createElementNS(SVG_NS, 'svg');
|
|
1024
|
-
svg.setAttribute('width', '0');
|
|
1025
|
-
svg.setAttribute('height', '0');
|
|
1026
|
-
svg.style.position = 'absolute';
|
|
1027
|
-
this.root.appendChild(svg);
|
|
1028
|
-
this.tintDefs = svg;
|
|
1029
|
-
}
|
|
1030
|
-
const filter = document.createElementNS(SVG_NS, 'filter');
|
|
1031
|
-
view.tintId = `spine-html-tint-${tintFilterSeq++}`;
|
|
1032
|
-
filter.setAttribute('id', view.tintId);
|
|
1033
|
-
// Filter math must happen in sRGB to match Spine's color multiply
|
|
1034
|
-
// (SVG filters default to linearRGB).
|
|
1035
|
-
filter.setAttribute('color-interpolation-filters', 'sRGB');
|
|
1036
|
-
const matrix = document.createElementNS(SVG_NS, 'feColorMatrix');
|
|
1037
|
-
matrix.setAttribute('in', 'SourceGraphic');
|
|
1038
|
-
matrix.setAttribute('type', 'matrix');
|
|
1039
|
-
filter.appendChild(matrix);
|
|
1040
|
-
this.tintDefs.appendChild(filter);
|
|
1041
|
-
view.tintMatrix = matrix;
|
|
1042
|
-
return matrix;
|
|
1043
|
-
}
|
|
1044
|
-
view(slot, kind) {
|
|
1045
|
-
let view = this.views.get(slot);
|
|
1046
|
-
if (view && view.kind !== kind) {
|
|
1047
|
-
view.el.remove();
|
|
1048
|
-
view.tintMatrix?.parentElement?.remove();
|
|
1049
|
-
view = undefined;
|
|
1050
|
-
}
|
|
1051
|
-
if (!view) {
|
|
1052
|
-
const el = kind === 'image' ? document.createElement('img') : document.createElement('canvas');
|
|
1053
|
-
if (el instanceof HTMLImageElement)
|
|
1054
|
-
el.draggable = false;
|
|
1055
|
-
el.style.position = 'absolute';
|
|
1056
|
-
el.style.left = '0';
|
|
1057
|
-
el.style.top = '0';
|
|
1058
|
-
el.style.transformOrigin = '0 0';
|
|
1059
|
-
el.style.pointerEvents = 'none';
|
|
1060
|
-
el.style.userSelect = 'none';
|
|
1061
|
-
el.style.display = 'none';
|
|
1062
|
-
view = {
|
|
1063
|
-
kind,
|
|
1064
|
-
el,
|
|
1065
|
-
regionName: '',
|
|
1066
|
-
visible: false,
|
|
1067
|
-
zIndex: -1,
|
|
1068
|
-
opacity: 1,
|
|
1069
|
-
blendMode: BlendMode.Normal,
|
|
1070
|
-
transform: '',
|
|
1071
|
-
clipPath: '',
|
|
1072
|
-
tintR: 1,
|
|
1073
|
-
tintG: 1,
|
|
1074
|
-
tintB: 1,
|
|
1075
|
-
tintId: '',
|
|
1076
|
-
tintMatrix: null,
|
|
1077
|
-
canvasW: 0,
|
|
1078
|
-
canvasH: 0,
|
|
1079
|
-
meshRatio: -1,
|
|
1080
|
-
meshAttachment: null,
|
|
1081
|
-
meshSequenceIndex: -1,
|
|
1082
|
-
meshExpand: -1,
|
|
1083
|
-
meshVertexCount: -1,
|
|
1084
|
-
meshBackendDrawn: '',
|
|
1085
|
-
meshVertices: new Float64Array(0),
|
|
1086
|
-
};
|
|
1087
|
-
this.views.set(slot, view);
|
|
1088
|
-
this.root.appendChild(el);
|
|
1089
|
-
}
|
|
1090
|
-
return view;
|
|
1091
|
-
}
|
|
1092
|
-
hide(slot) {
|
|
1093
|
-
const view = this.views.get(slot);
|
|
1094
|
-
if (!view)
|
|
1095
|
-
return;
|
|
1096
|
-
if (view.visible) {
|
|
1097
|
-
view.visible = false;
|
|
1098
|
-
view.el.style.display = 'none';
|
|
1099
|
-
}
|
|
1100
|
-
// A hidden element keeps no clip-path: one left behind would sit there
|
|
1101
|
-
// stale until the slot draws again, and "nothing is clipped" has to be
|
|
1102
|
-
// true of the DOM, not only of what gets painted. Cached like every other
|
|
1103
|
-
// write, so the sweep costs one write per element, once.
|
|
1104
|
-
this.setClipPath(view, '');
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
//# sourceMappingURL=SpineHtmlRenderer.js.map
|