Nate
Thanks Nate! And thanks for the Web Components pointer — the 4.3 blog post with
readable source is a great reference.
Funnily enough, I just learned first-hand why your web integrations render with
WebGL. On real Safari (Apple silicon) the DOM tier holds up great — 10 rigid-only
spineboys run at 60 fps — but the per-part canvas2d mesh tier stalls in the
compositor: 1 meshed skeleton ≈ 37 fps, 10 ≈ 3–4 fps, while in-callback JS time
stays under 5 ms. The clip()-per-triangle path seems to be the cost (Safari
antialiases clip paths — the same trait that produced the hairline cracks I had to
overdraw; Chromium runs the same scene fine). So the plan is exactly your route:
keep the DOM stage, render the deforming meshes in one offscreen WebGL context, and
blit each part's rect into its small canvas — the interleaved stacking context
stays, only the triangles leave canvas2d.
On the corner order — I might be misreading something, but the 4.3.13 (and 4.2.98)
code seems to preserve the offsets order rather than reorder it:
computeWorldVertices reads vertexOffsets[0,2,4,6] in index order and writes
worldVertices in that same order. A minimal check with an identity bone pose and
a 2×1 region (spine-core 4.3.13):
offsets: (-1,-0.5) (-1,0.5) (1,0.5) (1,-0.5) // BL UL UR BR (as you said)
world : (-1,-0.5) (-1,0.5) (1,0.5) (1,-0.5) // same order → BL UL UR BR
uvs : (0,1) (0,0) (1,0) (1,1) // (u,v2) (u,v) (u2,v) (u2,v2)
The renderer only matched the official player pixel-for-pixel once I mapped the
corners as BL/UL/UR/BR — with the br/bl/ul/ur reading the parts come out rotated.
Happy to be corrected if there's a subtlety I'm missing!