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.
@@ -0,0 +1,265 @@
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
+ const VERTEX_SHADER = `
25
+ attribute vec2 aPos;
26
+ attribute vec2 aUV;
27
+ uniform vec2 uResolution;
28
+ varying vec2 vUV;
29
+ void main() {
30
+ // aPos is in offscreen-canvas device px, top-left origin (matching the
31
+ // top-left-origin drawImage read of the same rect).
32
+ vec2 clip = aPos / uResolution * 2.0 - 1.0;
33
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
34
+ vUV = aUV;
35
+ }`;
36
+ const FRAGMENT_SHADER = `
37
+ precision mediump float;
38
+ uniform sampler2D uTex;
39
+ varying vec2 vUV;
40
+ void main() {
41
+ gl_FragColor = texture2D(uTex, vUV);
42
+ }`;
43
+ /** Gap between packed rects; blits are 1:1 unfiltered, so 1px is plenty. */
44
+ const GUTTER = 1;
45
+ /** Drawing-buffer growth quantum (device px). */
46
+ const GROW_STEP = 256;
47
+ class MeshGlBlitter {
48
+ lost = false;
49
+ canvas = document.createElement('canvas');
50
+ gl;
51
+ uResolution;
52
+ textures = new Map();
53
+ maxSize;
54
+ vertexData = new Float32Array(8192);
55
+ /** Per-job packed rect origins, filled by flush(). */
56
+ packX = [];
57
+ packY = [];
58
+ constructor() {
59
+ const gl = this.canvas.getContext('webgl', {
60
+ alpha: true,
61
+ premultipliedAlpha: true,
62
+ antialias: false,
63
+ depth: false,
64
+ stencil: false,
65
+ preserveDrawingBuffer: false,
66
+ });
67
+ if (!gl)
68
+ throw new Error('WebGL unavailable');
69
+ this.gl = gl;
70
+ // No restore attempt: on loss the renderer falls back to canvas2d and the
71
+ // backend signature re-dirties every mesh, so frames stay complete.
72
+ this.canvas.addEventListener('webglcontextlost', () => {
73
+ this.lost = true;
74
+ });
75
+ const program = gl.createProgram();
76
+ if (!program)
77
+ throw new Error('createProgram failed');
78
+ gl.attachShader(program, this.compile(gl.VERTEX_SHADER, VERTEX_SHADER));
79
+ gl.attachShader(program, this.compile(gl.FRAGMENT_SHADER, FRAGMENT_SHADER));
80
+ gl.linkProgram(program);
81
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
82
+ throw new Error(`program link failed: ${gl.getProgramInfoLog(program)}`);
83
+ }
84
+ gl.useProgram(program);
85
+ const uResolution = gl.getUniformLocation(program, 'uResolution');
86
+ if (!uResolution)
87
+ throw new Error('uResolution not found');
88
+ this.uResolution = uResolution;
89
+ gl.uniform1i(gl.getUniformLocation(program, 'uTex'), 0);
90
+ // Static state: this context does exactly one thing, set it up once.
91
+ // Interleaved [x, y, u, v] vertices in a single dynamic buffer.
92
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
93
+ const aPos = gl.getAttribLocation(program, 'aPos');
94
+ const aUV = gl.getAttribLocation(program, 'aUV');
95
+ gl.enableVertexAttribArray(aPos);
96
+ gl.enableVertexAttribArray(aUV);
97
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0);
98
+ gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 16, 8);
99
+ // Premultiplied source-over, matching canvas2d triangle compositing.
100
+ gl.enable(gl.BLEND);
101
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
102
+ gl.enable(gl.SCISSOR_TEST);
103
+ gl.clearColor(0, 0, 0, 0);
104
+ gl.activeTexture(gl.TEXTURE0);
105
+ this.maxSize = Math.min(gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) || 4096, 8192);
106
+ }
107
+ /**
108
+ * Rasterize every job into the offscreen buffer, then blit each rect onto
109
+ * its per-part canvas. Returns false when the context is lost (or the jobs
110
+ * cannot fit) so the caller can rasterize the batch on the canvas2d path.
111
+ */
112
+ flush(jobs) {
113
+ const gl = this.gl;
114
+ if (this.lost || gl.isContextLost())
115
+ return false;
116
+ // Shelf-pack, tallest first (indices keep job order for the draw pass).
117
+ let width = Math.max(512, this.canvas.width);
118
+ for (const job of jobs)
119
+ width = Math.max(width, job.width + GUTTER * 2);
120
+ if (width > this.maxSize)
121
+ return false;
122
+ const order = jobs.map((_, i) => i).sort((a, b) => jobs[b].height - jobs[a].height);
123
+ const packX = (this.packX = new Array(jobs.length));
124
+ const packY = (this.packY = new Array(jobs.length));
125
+ let shelfX = GUTTER;
126
+ let shelfY = GUTTER;
127
+ let shelfH = 0;
128
+ let height = GUTTER;
129
+ for (const i of order) {
130
+ const job = jobs[i];
131
+ if (shelfX + job.width + GUTTER > width) {
132
+ shelfY += shelfH + GUTTER;
133
+ shelfX = GUTTER;
134
+ shelfH = 0;
135
+ }
136
+ packX[i] = shelfX;
137
+ packY[i] = shelfY;
138
+ shelfX += job.width + GUTTER;
139
+ if (job.height > shelfH)
140
+ shelfH = job.height;
141
+ if (shelfY + job.height + GUTTER > height)
142
+ height = shelfY + job.height + GUTTER;
143
+ }
144
+ if (height > this.maxSize)
145
+ return false;
146
+ // Grow-only quantized drawing buffer. Resizing clears it, which is fine:
147
+ // every rect below is cleared and redrawn anyway.
148
+ if (width > this.canvas.width || height > this.canvas.height) {
149
+ this.canvas.width = Math.min(this.maxSize, Math.ceil(width / GROW_STEP) * GROW_STEP);
150
+ this.canvas.height = Math.min(this.maxSize, Math.ceil(Math.max(height, this.canvas.height) / GROW_STEP) * GROW_STEP);
151
+ }
152
+ const bufW = this.canvas.width;
153
+ const bufH = this.canvas.height;
154
+ gl.viewport(0, 0, bufW, bufH);
155
+ gl.uniform2f(this.uResolution, bufW, bufH);
156
+ // Build one interleaved vertex array for the whole batch (unindexed —
157
+ // meshes are a few hundred triangles, expansion is cheaper than managing
158
+ // index buffers).
159
+ let floats = 0;
160
+ for (const job of jobs)
161
+ floats += job.triangles.length * 4;
162
+ if (this.vertexData.length < floats) {
163
+ this.vertexData = new Float32Array(1 << Math.ceil(Math.log2(floats)));
164
+ }
165
+ const data = this.vertexData;
166
+ let f = 0;
167
+ for (let i = 0; i < jobs.length; i++) {
168
+ const { vertices, uvs, triangles, ratio } = jobs[i];
169
+ const ox = packX[i];
170
+ const oy = packY[i];
171
+ for (let t = 0; t < triangles.length; t++) {
172
+ const vi = triangles[t] * 2;
173
+ data[f++] = ox + vertices[vi] * ratio;
174
+ data[f++] = oy + vertices[vi + 1] * ratio;
175
+ data[f++] = uvs[vi];
176
+ data[f++] = uvs[vi + 1];
177
+ }
178
+ }
179
+ gl.bufferData(gl.ARRAY_BUFFER, data.subarray(0, floats), gl.DYNAMIC_DRAW);
180
+ let boundPage = null;
181
+ let first = 0;
182
+ for (let i = 0; i < jobs.length; i++) {
183
+ const job = jobs[i];
184
+ if (job.page !== boundPage) {
185
+ gl.bindTexture(gl.TEXTURE_2D, this.textureFor(job.page));
186
+ boundPage = job.page;
187
+ }
188
+ // Scissor is bottom-left origin; pack coords are top-left origin.
189
+ gl.scissor(packX[i], bufH - packY[i] - job.height, job.width, job.height);
190
+ gl.clear(gl.COLOR_BUFFER_BIT);
191
+ gl.drawArrays(gl.TRIANGLES, first, job.triangles.length);
192
+ first += job.triangles.length;
193
+ }
194
+ // If the context died mid-batch the draws above were no-ops; report it
195
+ // before blitting stale/blank rects onto the part canvases.
196
+ if (gl.isContextLost()) {
197
+ this.lost = true;
198
+ return false;
199
+ }
200
+ // Blit each rect onto its per-part canvas: same-task read (no
201
+ // preserveDrawingBuffer needed), 1:1 device px (no filtering).
202
+ for (let i = 0; i < jobs.length; i++) {
203
+ const job = jobs[i];
204
+ const ctx = job.canvas.getContext('2d');
205
+ if (!ctx)
206
+ continue;
207
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
208
+ // Clear the full backing: the previous raster may have been larger.
209
+ ctx.clearRect(0, 0, job.canvas.width, job.canvas.height);
210
+ ctx.drawImage(this.canvas, packX[i], packY[i], job.width, job.height, 0, 0, job.width, job.height);
211
+ }
212
+ return true;
213
+ }
214
+ compile(type, source) {
215
+ const gl = this.gl;
216
+ const shader = gl.createShader(type);
217
+ if (!shader)
218
+ throw new Error('createShader failed');
219
+ gl.shaderSource(shader, source);
220
+ gl.compileShader(shader);
221
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
222
+ throw new Error(`shader compile failed: ${gl.getShaderInfoLog(shader)}`);
223
+ }
224
+ return shader;
225
+ }
226
+ textureFor(page) {
227
+ let texture = this.textures.get(page);
228
+ if (texture)
229
+ return texture;
230
+ const gl = this.gl;
231
+ texture = gl.createTexture();
232
+ if (!texture)
233
+ throw new Error('createTexture failed');
234
+ gl.bindTexture(gl.TEXTURE_2D, texture);
235
+ // Premultiply at upload so blending and the premultiplied canvas agree.
236
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
237
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
238
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, page);
239
+ // Linear, no mips, clamped — NPOT-safe in WebGL1.
240
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
241
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
242
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
243
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
244
+ this.textures.set(page, texture);
245
+ return texture;
246
+ }
247
+ }
248
+ let shared;
249
+ /**
250
+ * The module-level blitter, created on first use. Returns null when WebGL is
251
+ * unavailable or the shared context has been lost — callers then stay on the
252
+ * canvas2d path.
253
+ */
254
+ export function getMeshGlBlitter() {
255
+ if (shared === undefined) {
256
+ try {
257
+ shared = new MeshGlBlitter();
258
+ }
259
+ catch {
260
+ shared = null;
261
+ }
262
+ }
263
+ return shared && !shared.lost ? shared : null;
264
+ }
265
+ //# sourceMappingURL=MeshGlBlitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MeshGlBlitter.js","sourceRoot":"","sources":["../src/MeshGlBlitter.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,7 @@
1
1
  import { type Skeleton } from '@esotericsoftware/spine-core';
2
2
  import type { RegionImage } from './DomTexture';
3
+ /** Rasterizer used for the mesh (deform) tier. */
4
+ export type MeshBackend = 'canvas2d' | 'webgl';
3
5
  /**
4
6
  * Renders a spine-core Skeleton as DOM, split by slot type:
5
7
  *
@@ -7,9 +9,11 @@ import type { RegionImage } from './DomTexture';
7
9
  * posed with a single CSS matrix() write per frame — exact, since bone
8
10
  * transforms are affine.
9
11
  * - Mesh attachments (deform parts) each get a small per-part <canvas> sized
10
- * to the mesh's world bounding box, redrawn per frame with the standard
11
- * per-triangle clip+transform+drawImage mapping. Frames where the
12
- * canvas-space vertices are unchanged reuse the previous raster.
12
+ * to the mesh's world bounding box, redrawn per frame — either with the
13
+ * standard per-triangle clip+transform+drawImage mapping (default), or via
14
+ * a shared offscreen WebGL canvas that rect-blits into the same per-part
15
+ * canvases (meshBackend = 'webgl'). Frames where the canvas-space vertices
16
+ * are unchanged reuse the previous raster on both backends.
13
17
  *
14
18
  * Both element kinds share one stacking context, so draw order interleaves
15
19
  * freely via z-index (rear hair canvas < torso img < front hair canvas).
@@ -33,6 +37,12 @@ export declare class SpineHtmlRenderer {
33
37
  meshCount: number;
34
38
  /** Mesh canvases that reused their previous raster last frame. */
35
39
  meshReuseCount: number;
40
+ /**
41
+ * Mesh canvas backing stores (re)allocated last frame. Should drop to zero
42
+ * once an animation reaches its steady state; a persistent nonzero value
43
+ * means GPU surfaces are being recreated per frame (a Safari killer).
44
+ */
45
+ canvasReallocCount: number;
36
46
  /** Triangles rasterized last frame. */
37
47
  triangleCount: number;
38
48
  /**
@@ -48,7 +58,26 @@ export declare class SpineHtmlRenderer {
48
58
  * resolution instead of over- or under-sampling.
49
59
  */
50
60
  pixelRatio: number;
61
+ /**
62
+ * Rasterizer for the mesh (deform) tier. 'canvas2d' (default) maps each
63
+ * triangle with clip+transform+drawImage directly on the per-part canvas.
64
+ * 'webgl' rasterizes every dirty mesh into one shared offscreen WebGL
65
+ * canvas and rect-blits each mesh back onto its per-part canvas — the DOM
66
+ * structure and all element-level behavior (z-index interleave, tint
67
+ * filter, mix-blend-mode, dirty-skip) are identical, only the raster step
68
+ * changes. Motivation: Safari antialiases canvas2d clip paths, so the
69
+ * per-triangle clip mapping pays a per-triangle AA-mask cost in the GPU
70
+ * process that rAF-limits heavy scenes; GL rasterizes shared edges
71
+ * seamlessly (no clip, no crack overdraw) and the blit is an unclipped
72
+ * rect copy. Falls back to 'canvas2d' when WebGL is unavailable or the
73
+ * shared context is lost — see meshBackendActive.
74
+ */
75
+ meshBackend: MeshBackend;
76
+ /** Backend that actually rasterized the mesh tier during the last render(). */
77
+ meshBackendActive: MeshBackend;
51
78
  private readonly views;
79
+ private readonly pendingJobs;
80
+ private readonly pendingViews;
52
81
  private scratchVertices;
53
82
  private tintDefs;
54
83
  /**
@@ -59,9 +88,18 @@ export declare class SpineHtmlRenderer {
59
88
  */
60
89
  constructor(root: HTMLElement, regionImages: Map<string, RegionImage>);
61
90
  render(skeleton: Skeleton): void;
91
+ /**
92
+ * Removes every element this renderer added to the root (slot elements and
93
+ * the tint filter defs). The region bitmaps are deliberately untouched: the
94
+ * map is the caller's, and one map is normally shared by many renderers
95
+ * (disposing one instance must not blind the others). Free the unpacked
96
+ * blob URLs with revokeRegions() once no renderer needs them.
97
+ */
62
98
  dispose(): void;
63
99
  private renderRegion;
64
100
  private renderMesh;
101
+ /** The canvas2d raster path: clear the backing, map each triangle. */
102
+ private rasterizeMesh2d;
65
103
  /**
66
104
  * Standard canvas triangle texture mapping (same math as the official
67
105
  * spine-canvas renderer): derive the affine that sends the triangle's
@@ -1 +1 @@
1
- {"version":3,"file":"SpineHtmlRenderer.d.ts","sourceRoot":"","sources":["../src/SpineHtmlRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,QAAQ,EAId,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AA6DhD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,iBAAiB;IAkC1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAlC/B,kEAAkE;IAClE,aAAa,SAAK;IAClB,2CAA2C;IAC3C,SAAS,SAAK;IACd,kEAAkE;IAClE,cAAc,SAAK;IACnB,uCAAuC;IACvC,aAAa,SAAK;IAClB;;;;OAIG;IACH,cAAc,SAAO;IACrB;;;;;OAKG;IACH,UAAU,SAA+D;IAEzE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,QAAQ,CAA8B;IAE9C;;;;;OAKG;gBAEgB,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC;IAGzD,MAAM,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IA2BhC,OAAO,IAAI,IAAI;IASf,OAAO,CAAC,YAAY;IA8CpB,OAAO,CAAC,UAAU;IAsHlB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,YAAY;IAwCpB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;IAmDnB,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,IAAI;IA6CZ,OAAO,CAAC,IAAI;CAOb"}
1
+ {"version":3,"file":"SpineHtmlRenderer.d.ts","sourceRoot":"","sources":["../src/SpineHtmlRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,QAAQ,EAId,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,kDAAkD;AAClD,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;AAiE/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,iBAAiB;IA2D1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,YAAY;IA3D/B,kEAAkE;IAClE,aAAa,SAAK;IAClB,2CAA2C;IAC3C,SAAS,SAAK;IACd,kEAAkE;IAClE,cAAc,SAAK;IACnB;;;;OAIG;IACH,kBAAkB,SAAK;IACvB,uCAAuC;IACvC,aAAa,SAAK;IAClB;;;;OAIG;IACH,cAAc,SAAO;IACrB;;;;;OAKG;IACH,UAAU,SAA+D;IACzE;;;;;;;;;;;;;OAaG;IACH,WAAW,EAAE,WAAW,CAAc;IACtC,+EAA+E;IAC/E,iBAAiB,EAAE,WAAW,CAAc;IAE5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkB;IAC/C,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,QAAQ,CAA8B;IAE9C;;;;;OAKG;gBAEgB,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC;IAGzD,MAAM,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IA4ChC;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IASf,OAAO,CAAC,YAAY;IA8CpB,OAAO,CAAC,UAAU;IAsIlB,sEAAsE;IACtE,OAAO,CAAC,eAAe;IA+BvB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,YAAY;IAwCpB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;IAmDnB,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,IAAI;IA+CZ,OAAO,CAAC,IAAI;CAOb"}
@@ -1,4 +1,5 @@
1
1
  import { BlendMode, ClippingAttachment, MeshAttachment, RegionAttachment, } from '@esotericsoftware/spine-core';
2
+ import { getMeshGlBlitter } from './MeshGlBlitter';
2
3
  const regionVertices = new Float32Array(8);
3
4
  const SVG_NS = 'http://www.w3.org/2000/svg';
4
5
  /** Unique tint-filter ids across renderer instances (ids are document-global). */
@@ -26,9 +27,11 @@ const BLEND_CSS = {
26
27
  * posed with a single CSS matrix() write per frame — exact, since bone
27
28
  * transforms are affine.
28
29
  * - Mesh attachments (deform parts) each get a small per-part <canvas> sized
29
- * to the mesh's world bounding box, redrawn per frame with the standard
30
- * per-triangle clip+transform+drawImage mapping. Frames where the
31
- * canvas-space vertices are unchanged reuse the previous raster.
30
+ * to the mesh's world bounding box, redrawn per frame — either with the
31
+ * standard per-triangle clip+transform+drawImage mapping (default), or via
32
+ * a shared offscreen WebGL canvas that rect-blits into the same per-part
33
+ * canvases (meshBackend = 'webgl'). Frames where the canvas-space vertices
34
+ * are unchanged reuse the previous raster on both backends.
32
35
  *
33
36
  * Both element kinds share one stacking context, so draw order interleaves
34
37
  * freely via z-index (rear hair canvas < torso img < front hair canvas).
@@ -52,6 +55,12 @@ export class SpineHtmlRenderer {
52
55
  meshCount = 0;
53
56
  /** Mesh canvases that reused their previous raster last frame. */
54
57
  meshReuseCount = 0;
58
+ /**
59
+ * Mesh canvas backing stores (re)allocated last frame. Should drop to zero
60
+ * once an animation reaches its steady state; a persistent nonzero value
61
+ * means GPU surfaces are being recreated per frame (a Safari killer).
62
+ */
63
+ canvasReallocCount = 0;
55
64
  /** Triangles rasterized last frame. */
56
65
  triangleCount = 0;
57
66
  /**
@@ -67,7 +76,26 @@ export class SpineHtmlRenderer {
67
76
  * resolution instead of over- or under-sampling.
68
77
  */
69
78
  pixelRatio = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;
79
+ /**
80
+ * Rasterizer for the mesh (deform) tier. 'canvas2d' (default) maps each
81
+ * triangle with clip+transform+drawImage directly on the per-part canvas.
82
+ * 'webgl' rasterizes every dirty mesh into one shared offscreen WebGL
83
+ * canvas and rect-blits each mesh back onto its per-part canvas — the DOM
84
+ * structure and all element-level behavior (z-index interleave, tint
85
+ * filter, mix-blend-mode, dirty-skip) are identical, only the raster step
86
+ * changes. Motivation: Safari antialiases canvas2d clip paths, so the
87
+ * per-triangle clip mapping pays a per-triangle AA-mask cost in the GPU
88
+ * process that rAF-limits heavy scenes; GL rasterizes shared edges
89
+ * seamlessly (no clip, no crack overdraw) and the blit is an unclipped
90
+ * rect copy. Falls back to 'canvas2d' when WebGL is unavailable or the
91
+ * shared context is lost — see meshBackendActive.
92
+ */
93
+ meshBackend = 'canvas2d';
94
+ /** Backend that actually rasterized the mesh tier during the last render(). */
95
+ meshBackendActive = 'canvas2d';
70
96
  views = new Map();
97
+ pendingJobs = [];
98
+ pendingViews = [];
71
99
  scratchVertices = new Float32Array(256);
72
100
  tintDefs = null;
73
101
  /**
@@ -84,7 +112,10 @@ export class SpineHtmlRenderer {
84
112
  this.clipSkipCount = 0;
85
113
  this.meshCount = 0;
86
114
  this.meshReuseCount = 0;
115
+ this.canvasReallocCount = 0;
87
116
  this.triangleCount = 0;
117
+ const blitter = this.meshBackend === 'webgl' ? getMeshGlBlitter() : null;
118
+ this.meshBackendActive = blitter ? 'webgl' : 'canvas2d';
88
119
  const drawOrder = skeleton.drawOrder.appliedPose;
89
120
  for (let i = 0, n = drawOrder.length; i < n; i++) {
90
121
  const slot = drawOrder[i];
@@ -106,7 +137,27 @@ export class SpineHtmlRenderer {
106
137
  this.hide(slot);
107
138
  }
108
139
  }
140
+ if (this.pendingJobs.length) {
141
+ if (!blitter || !blitter.flush(this.pendingJobs)) {
142
+ // Context lost mid-frame: rasterize this batch on the 2d path so the
143
+ // frame stays complete; the next render() re-selects the backend.
144
+ for (let i = 0; i < this.pendingJobs.length; i++) {
145
+ const job = this.pendingJobs[i];
146
+ this.pendingViews[i].meshBackendDrawn = 'canvas2d';
147
+ this.rasterizeMesh2d(job.canvas, job.page, job.vertices, job.uvs, job.triangles, job.ratio);
148
+ }
149
+ }
150
+ this.pendingJobs.length = 0;
151
+ this.pendingViews.length = 0;
152
+ }
109
153
  }
154
+ /**
155
+ * Removes every element this renderer added to the root (slot elements and
156
+ * the tint filter defs). The region bitmaps are deliberately untouched: the
157
+ * map is the caller's, and one map is normally shared by many renderers
158
+ * (disposing one instance must not blind the others). Free the unpacked
159
+ * blob URLs with revokeRegions() once no renderer needs them.
160
+ */
110
161
  dispose() {
111
162
  for (const view of this.views.values())
112
163
  view.el.remove();
@@ -190,19 +241,35 @@ export class SpineHtmlRenderer {
190
241
  const view = this.view(slot, 'canvas');
191
242
  const canvas = view.el;
192
243
  const ratio = this.pixelRatio;
193
- const backingW = Math.max(1, Math.round(w * ratio));
194
- const backingH = Math.max(1, Math.round(h * ratio));
195
244
  let dirty = view.meshAttachment !== attachment ||
196
245
  view.meshSequenceIndex !== sequenceIndex ||
197
246
  view.meshExpand !== this.triangleExpand ||
198
- view.meshVertexCount !== count;
199
- if (view.canvasW !== backingW || view.canvasH !== backingH) {
200
- view.canvasW = backingW;
201
- view.canvasH = backingH;
202
- canvas.width = backingW; // resizing also clears the canvas
203
- canvas.height = backingH;
204
- canvas.style.width = `${w}px`;
205
- canvas.style.height = `${h}px`;
247
+ view.meshVertexCount !== count ||
248
+ view.meshBackendDrawn !== this.meshBackendActive;
249
+ // The backing store only grows, in 32-device-px steps. Setting
250
+ // canvas.width recreates the GPU surface, and a deforming mesh changes
251
+ // its bbox every frame reallocating every mesh canvas per frame
252
+ // stalled real Safari to ~3 fps while the JS split showed ~4 ms (the
253
+ // cost lives in the compositor, invisible to in-callback timing). With
254
+ // grow-only quantized backing, steady-state animation reallocates
255
+ // nothing. The CSS size mirrors the whole backing so the pixel mapping
256
+ // stays 1:1; the mesh draws into the top-left w×h logical region and
257
+ // the rest stays transparent.
258
+ const needW = Math.max(1, Math.round(w * ratio));
259
+ const needH = Math.max(1, Math.round(h * ratio));
260
+ if (needW > view.canvasW || needH > view.canvasH || view.meshRatio !== ratio) {
261
+ // 25% slack: at low fps the animation is sampled sparsely, so new bbox
262
+ // maxima keep being discovered for many seconds — allocate ahead of the
263
+ // curve instead of chasing it.
264
+ const step = 32;
265
+ view.canvasW = Math.ceil(Math.max(needW * 1.25, view.canvasW) / step) * step;
266
+ view.canvasH = Math.ceil(Math.max(needH * 1.25, view.canvasH) / step) * step;
267
+ view.meshRatio = ratio;
268
+ canvas.width = view.canvasW;
269
+ canvas.height = view.canvasH;
270
+ canvas.style.width = `${view.canvasW / ratio}px`;
271
+ canvas.style.height = `${view.canvasH / ratio}px`;
272
+ this.canvasReallocCount++;
206
273
  dirty = true;
207
274
  }
208
275
  // Canvas-space vertices: identical values mean last frame's raster is
@@ -232,26 +299,52 @@ export class SpineHtmlRenderer {
232
299
  view.meshSequenceIndex = sequenceIndex;
233
300
  view.meshExpand = this.triangleExpand;
234
301
  view.meshVertexCount = count;
235
- const ctx = canvas.getContext('2d');
236
- if (!ctx)
237
- return;
238
- ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
239
- ctx.clearRect(0, 0, w, h);
302
+ view.meshBackendDrawn = this.meshBackendActive;
240
303
  const uvs = sequence.getUVs(sequenceIndex);
241
304
  const triangles = attachment.triangles;
242
- const uw = page.width - 1;
243
- const uh = page.height - 1;
244
- for (let t = 0; t < triangles.length; t += 3) {
245
- const i0 = triangles[t] * 2;
246
- const i1 = triangles[t + 1] * 2;
247
- const i2 = triangles[t + 2] * 2;
248
- this.drawTriangle(ctx, page, rel[i0], rel[i0 + 1], uvs[i0] * uw, uvs[i0 + 1] * uh, rel[i1], rel[i1 + 1], uvs[i1] * uw, uvs[i1 + 1] * uh, rel[i2], rel[i2 + 1], uvs[i2] * uw, uvs[i2 + 1] * uh);
305
+ if (this.meshBackendActive === 'webgl') {
306
+ // Queued, not drawn: render() flushes the whole batch through the
307
+ // shared GL context once the slot loop is done. `rel` is this view's
308
+ // own signature array nothing mutates it before the flush.
309
+ this.pendingJobs.push({
310
+ canvas,
311
+ page,
312
+ vertices: rel,
313
+ uvs,
314
+ triangles,
315
+ ratio,
316
+ width: Math.min(view.canvasW, Math.ceil(w * ratio)),
317
+ height: Math.min(view.canvasH, Math.ceil(h * ratio)),
318
+ });
319
+ this.pendingViews.push(view);
320
+ }
321
+ else {
322
+ this.rasterizeMesh2d(canvas, page, rel, uvs, triangles, ratio);
249
323
  }
250
324
  this.meshCount++;
251
325
  this.triangleCount += triangles.length / 3;
252
326
  }
253
327
  this.applyCommon(view, slot, pose, attachment.color, skeleton, zIndex);
254
328
  }
329
+ /** The canvas2d raster path: clear the backing, map each triangle. */
330
+ rasterizeMesh2d(canvas, page, vertices, uvs, triangles, ratio) {
331
+ const ctx = canvas.getContext('2d');
332
+ if (!ctx)
333
+ return;
334
+ // Clear the full backing: the previous frame's bbox (and so its drawn
335
+ // region) may have been larger than today's.
336
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
337
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
338
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
339
+ const uw = page.width - 1;
340
+ const uh = page.height - 1;
341
+ for (let t = 0; t < triangles.length; t += 3) {
342
+ const i0 = triangles[t] * 2;
343
+ const i1 = triangles[t + 1] * 2;
344
+ const i2 = triangles[t + 2] * 2;
345
+ 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);
346
+ }
347
+ }
255
348
  /**
256
349
  * Standard canvas triangle texture mapping (same math as the official
257
350
  * spine-canvas renderer): derive the affine that sends the triangle's
@@ -404,10 +497,12 @@ export class SpineHtmlRenderer {
404
497
  tintMatrix: null,
405
498
  canvasW: 0,
406
499
  canvasH: 0,
500
+ meshRatio: -1,
407
501
  meshAttachment: null,
408
502
  meshSequenceIndex: -1,
409
503
  meshExpand: -1,
410
504
  meshVertexCount: -1,
505
+ meshBackendDrawn: '',
411
506
  meshVertices: new Float64Array(0),
412
507
  };
413
508
  this.views.set(slot, view);