Skip to content

Commit a78a790

Browse files
obiotclaude
andauthored
Spine plugin 2.2.0: fix Canvas scale, tint support, PMA blend modes
- setTint() applies to skeleton.color — RGB tinting on WebGL, alpha-only on Canvas - Canvas SkeletonRenderer passes premultipliedAlpha to setBlendMode() - fix scale() double-applying on Canvas (root bone + currentTransform) - fix skin.attachments.entries() crash — plain objects, not Maps - fix draw() crash when skeleton not yet initialized - Cached isWebGL flag, removed unused properties (this.gl, this.renderer, this.context, this.twoColorTint) - Fix DebugPanelPlugin type cast in spine example Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a36a29f commit a78a790

6 files changed

Lines changed: 63 additions & 33 deletions

File tree

packages/examples/src/examples/spine/ExampleSpine.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const createGame = () => {
6060

6161
// register plugins
6262
plugin.register(DebugPanelPlugin);
63-
plugin.get(DebugPanelPlugin)?.show();
63+
(plugin.get(DebugPanelPlugin) as DebugPanelPlugin)?.show();
6464
plugin.register(SpinePlugin);
6565

6666
// set cross-origin

packages/spine-plugin/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## 2.2.0 - 2026-04-15
4+
5+
### Added
6+
- `setTint()` now applies to `skeleton.color` — RGB tinting works on WebGL, Canvas is limited to alpha only
7+
- Canvas `SkeletonRenderer` now passes `premultipliedAlpha` to `setBlendMode()` for correct blending with PMA textures
8+
9+
### Fixed
10+
- fix `scale()` double-applying on Canvas — was scaling through both root bone and canvas context
11+
- fix `skin.attachments.entries()` crash in mesh detection — inner attachments are plain objects, not Maps
12+
- fix potential crash when `draw()` is called before `setSkeleton` completes
13+
314
## 2.1.0
415

516
### Added

packages/spine-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ me.loader.preload(DataManifest, function() {
125125

126126
| @melonjs/spine-plugin | melonJS | spine-runtime |
127127
|---|---|---|
128+
| v2.2.0 | v18.3.0 (or higher) | v4.2.x |
128129
| v2.1.0 | v18.3.0 (or higher) | v4.2.x |
129130
| v2.0.1 | v18.2.1 (or higher) | v4.2.x |
130131
| v2.0.0 | v18.2.0 | v4.2.x |

packages/spine-plugin/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@melonjs/spine-plugin",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"description": "melonJS Spine plugin",
55
"homepage": "https://www.npmjs.com/package/@melonjs/spine-plugin",
66
"type": "module",
@@ -56,7 +56,7 @@
5656
},
5757
"devDependencies": {
5858
"concurrently": "^9.2.1",
59-
"esbuild": "^0.27.3",
59+
"esbuild": "^0.28.0",
6060
"melonjs": "workspace:*",
6161
"tsconfig": "workspace:*",
6262
"tsx": "^4.21.0",

packages/spine-plugin/src/SkeletonRenderer.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export default class SkeletonRenderer {
4949
*/
5050
debugRendering = false;
5151

52+
/**
53+
* Whether textures use premultiplied alpha
54+
* @type {boolean}
55+
* @default false
56+
*/
57+
premultipliedAlpha = false;
58+
5259
// reusable color instances to avoid allocations
5360
tintColor = new MColor();
5461
tempColor = new MColor();
@@ -140,7 +147,10 @@ export default class SkeletonRenderer {
140147

141148
renderer.setGlobalAlpha(color.a);
142149
renderer.setTint(color);
143-
renderer.setBlendMode(BLEND_MODES[slot.data.blendMode]);
150+
renderer.setBlendMode(
151+
BLEND_MODES[slot.data.blendMode],
152+
this.premultipliedAlpha,
153+
);
144154

145155
if (triangles) {
146156
this.drawMesh(renderer, image, worldVertices, triangles);

packages/spine-plugin/src/Spine.js

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export default class Spine extends Renderable {
1818
runtime;
1919
skeleton;
2020
plugin;
21-
renderer;
2221
animationState;
2322
skeletonRenderer;
2423
root;
@@ -96,23 +95,19 @@ export default class Spine extends Renderable {
9695
"Spine plugin: plugin needs to be registered first using plugin.register",
9796
);
9897
}
99-
this.renderer = this.plugin.app.renderer;
98+
const renderer = this.plugin.app.renderer;
10099

101-
if (this.renderer.WebGLVersion >= 1) {
102-
this.runtime = spineWebGL;
103-
this.gl = this.renderer.gl;
104-
this.canvas = this.renderer.renderTarget.canvas;
105-
this.context = this.renderer;
106-
this.twoColorTint = true;
100+
/** @ignore */
101+
this.isWebGL = renderer.WebGLVersion >= 1;
107102

103+
if (this.isWebGL) {
104+
this.runtime = spineWebGL;
105+
this.canvas = renderer.renderTarget.canvas;
108106
// register the Spine batcher with the melonJS renderer (once)
109-
if (!this.renderer.batchers.has("spine")) {
110-
this.renderer.addBatcher(
111-
new SpineBatcher(this.renderer, this.canvas),
112-
"spine",
113-
);
107+
if (!renderer.batchers.has("spine")) {
108+
renderer.addBatcher(new SpineBatcher(renderer, this.canvas), "spine");
114109
}
115-
this.spineBatcher = this.renderer.batchers.get("spine");
110+
this.spineBatcher = renderer.batchers.get("spine");
116111

117112
// spine skeleton renderer
118113
this.skeletonRenderer = new this.runtime.SkeletonRenderer(
@@ -197,21 +192,21 @@ export default class Spine extends Renderable {
197192
this.premultipliedAlpha = atlas.pages.some((page) => {
198193
return page.pma;
199194
});
200-
if (this.renderer.WebGLVersion >= 1) {
201-
this.skeletonRenderer.premultipliedAlpha = this.premultipliedAlpha;
202-
}
195+
this.skeletonRenderer.premultipliedAlpha = this.premultipliedAlpha;
203196

204197
// Instantiate a new skeleton based on the atlas and skeleton data.
205198
this.skeleton = new this.runtime.Skeleton(skeletonData);
206199

207200
// auto-detect if the skeleton uses mesh attachments for canvas renderer
208-
if (this.skeletonRenderer instanceof SkeletonRenderer) {
201+
if (!this.isWebGL) {
209202
this.skeletonRenderer.triangleRendering = skeletonData.skins.some(
210203
(skin) => {
211-
for (const [, attachments] of skin.attachments.entries()) {
212-
for (const [, attachment] of attachments.entries()) {
213-
if (attachment instanceof MeshAttachment) {
214-
return true;
204+
for (const attachments of skin.attachments) {
205+
if (attachments) {
206+
for (const attachment of Object.values(attachments)) {
207+
if (attachment instanceof MeshAttachment) {
208+
return true;
209+
}
215210
}
216211
}
217212
}
@@ -274,7 +269,7 @@ export default class Spine extends Renderable {
274269
* @returns {Spine} Reference to this object for method chaining
275270
*/
276271
rotate(angle, v) {
277-
if (this.renderer.WebGLVersion >= 1) {
272+
if (this.isWebGL) {
278273
this.skeleton.getRootBone().rotation -= Math.radToDeg(angle);
279274
} else {
280275
// rotation for rootBone is in degrees (anti-clockwise)
@@ -291,8 +286,13 @@ export default class Spine extends Renderable {
291286
* @returns {Spine} Reference to this object for method chaining
292287
*/
293288
scale(x, y = x) {
294-
this.root.scaleX *= x;
295-
this.root.scaleY *= y;
289+
if (this.isWebGL) {
290+
// WebGL: SpineBatcher ignores currentTransform, scale through root bone
291+
this.root.scaleX *= x;
292+
this.root.scaleY *= y;
293+
}
294+
// Canvas: scale through currentTransform only (applied by preDraw),
295+
// which scales both region bone transforms and mesh world vertices uniformly
296296
return super.scale(x, y);
297297
}
298298

@@ -385,9 +385,17 @@ export default class Spine extends Renderable {
385385
* @param {CanvasRenderer|WebGLRenderer} renderer - A renderer instance.
386386
*/
387387
draw(renderer) {
388-
if (this.renderer.WebGLVersion >= 1) {
388+
if (typeof this.skeleton === "undefined") {
389+
return;
390+
}
391+
392+
// apply melonJS tint to Spine skeleton color
393+
const t = this.tint.toArray();
394+
this.skeleton.color.set(t[0], t[1], t[2], this.skeleton.color.a);
395+
396+
if (this.isWebGL) {
389397
// switch to the Spine batcher via melonJS batcher system
390-
this.renderer.setBatcher("spine");
398+
renderer.setBatcher("spine");
391399

392400
// draw the skeleton — SkeletonRenderer calls spineBatcher.draw()
393401
this.skeletonRenderer.draw(
@@ -406,7 +414,7 @@ export default class Spine extends Renderable {
406414
this.shapesShader.bind();
407415
this.shapesShader.setUniform4x4f(
408416
this.runtime.Shader.MVP_MATRIX,
409-
this.context.projectionMatrix.val,
417+
renderer.projectionMatrix.toArray(),
410418
);
411419
this.shapes.begin(this.shapesShader);
412420
this.skeletonDebugRenderer.draw(this.shapes, this.skeleton);
@@ -425,7 +433,7 @@ export default class Spine extends Renderable {
425433
* Called automatically when the renderable is removed from the world.
426434
*/
427435
dispose() {
428-
if (this.renderer.WebGLVersion >= 1) {
436+
if (this.isWebGL) {
429437
this.shapes.dispose();
430438
this.shapesShader.dispose();
431439
this.skeletonDebugRenderer.dispose();

0 commit comments

Comments
 (0)