From 51df5dfd4ffff11859fa70f6712db8005d9daf5c Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Wed, 15 Apr 2026 18:41:44 +0800 Subject: [PATCH] Spine plugin 2.2.0: fix Canvas scale, tint support, PMA blend modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../src/examples/spine/ExampleSpine.tsx | 2 +- packages/spine-plugin/CHANGELOG.md | 11 ++++ packages/spine-plugin/README.md | 1 + packages/spine-plugin/package.json | 4 +- packages/spine-plugin/src/SkeletonRenderer.js | 12 +++- packages/spine-plugin/src/Spine.js | 66 +++++++++++-------- 6 files changed, 63 insertions(+), 33 deletions(-) diff --git a/packages/examples/src/examples/spine/ExampleSpine.tsx b/packages/examples/src/examples/spine/ExampleSpine.tsx index 8d14a53c1..3b69ab710 100644 --- a/packages/examples/src/examples/spine/ExampleSpine.tsx +++ b/packages/examples/src/examples/spine/ExampleSpine.tsx @@ -60,7 +60,7 @@ const createGame = () => { // register plugins plugin.register(DebugPanelPlugin); - plugin.get(DebugPanelPlugin)?.show(); + (plugin.get(DebugPanelPlugin) as DebugPanelPlugin)?.show(); plugin.register(SpinePlugin); // set cross-origin diff --git a/packages/spine-plugin/CHANGELOG.md b/packages/spine-plugin/CHANGELOG.md index 7a961e832..30127ac49 100644 --- a/packages/spine-plugin/CHANGELOG.md +++ b/packages/spine-plugin/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2.2.0 - 2026-04-15 + +### Added +- `setTint()` now applies to `skeleton.color` — RGB tinting works on WebGL, Canvas is limited to alpha only +- Canvas `SkeletonRenderer` now passes `premultipliedAlpha` to `setBlendMode()` for correct blending with PMA textures + +### Fixed +- fix `scale()` double-applying on Canvas — was scaling through both root bone and canvas context +- fix `skin.attachments.entries()` crash in mesh detection — inner attachments are plain objects, not Maps +- fix potential crash when `draw()` is called before `setSkeleton` completes + ## 2.1.0 ### Added diff --git a/packages/spine-plugin/README.md b/packages/spine-plugin/README.md index 6d82cdc59..d3adef4c5 100644 --- a/packages/spine-plugin/README.md +++ b/packages/spine-plugin/README.md @@ -125,6 +125,7 @@ me.loader.preload(DataManifest, function() { | @melonjs/spine-plugin | melonJS | spine-runtime | |---|---|---| +| v2.2.0 | v18.3.0 (or higher) | v4.2.x | | v2.1.0 | v18.3.0 (or higher) | v4.2.x | | v2.0.1 | v18.2.1 (or higher) | v4.2.x | | v2.0.0 | v18.2.0 | v4.2.x | diff --git a/packages/spine-plugin/package.json b/packages/spine-plugin/package.json index f9e5f949e..fdbb2f204 100644 --- a/packages/spine-plugin/package.json +++ b/packages/spine-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@melonjs/spine-plugin", - "version": "2.1.0", + "version": "2.2.0", "description": "melonJS Spine plugin", "homepage": "https://www.npmjs.com/package/@melonjs/spine-plugin", "type": "module", @@ -56,7 +56,7 @@ }, "devDependencies": { "concurrently": "^9.2.1", - "esbuild": "^0.27.3", + "esbuild": "^0.28.0", "melonjs": "workspace:*", "tsconfig": "workspace:*", "tsx": "^4.21.0", diff --git a/packages/spine-plugin/src/SkeletonRenderer.js b/packages/spine-plugin/src/SkeletonRenderer.js index e0aaf8667..6cd6e7ae9 100644 --- a/packages/spine-plugin/src/SkeletonRenderer.js +++ b/packages/spine-plugin/src/SkeletonRenderer.js @@ -49,6 +49,13 @@ export default class SkeletonRenderer { */ debugRendering = false; + /** + * Whether textures use premultiplied alpha + * @type {boolean} + * @default false + */ + premultipliedAlpha = false; + // reusable color instances to avoid allocations tintColor = new MColor(); tempColor = new MColor(); @@ -140,7 +147,10 @@ export default class SkeletonRenderer { renderer.setGlobalAlpha(color.a); renderer.setTint(color); - renderer.setBlendMode(BLEND_MODES[slot.data.blendMode]); + renderer.setBlendMode( + BLEND_MODES[slot.data.blendMode], + this.premultipliedAlpha, + ); if (triangles) { this.drawMesh(renderer, image, worldVertices, triangles); diff --git a/packages/spine-plugin/src/Spine.js b/packages/spine-plugin/src/Spine.js index d4412ace9..fa50f015d 100644 --- a/packages/spine-plugin/src/Spine.js +++ b/packages/spine-plugin/src/Spine.js @@ -18,7 +18,6 @@ export default class Spine extends Renderable { runtime; skeleton; plugin; - renderer; animationState; skeletonRenderer; root; @@ -96,23 +95,19 @@ export default class Spine extends Renderable { "Spine plugin: plugin needs to be registered first using plugin.register", ); } - this.renderer = this.plugin.app.renderer; + const renderer = this.plugin.app.renderer; - if (this.renderer.WebGLVersion >= 1) { - this.runtime = spineWebGL; - this.gl = this.renderer.gl; - this.canvas = this.renderer.renderTarget.canvas; - this.context = this.renderer; - this.twoColorTint = true; + /** @ignore */ + this.isWebGL = renderer.WebGLVersion >= 1; + if (this.isWebGL) { + this.runtime = spineWebGL; + this.canvas = renderer.renderTarget.canvas; // register the Spine batcher with the melonJS renderer (once) - if (!this.renderer.batchers.has("spine")) { - this.renderer.addBatcher( - new SpineBatcher(this.renderer, this.canvas), - "spine", - ); + if (!renderer.batchers.has("spine")) { + renderer.addBatcher(new SpineBatcher(renderer, this.canvas), "spine"); } - this.spineBatcher = this.renderer.batchers.get("spine"); + this.spineBatcher = renderer.batchers.get("spine"); // spine skeleton renderer this.skeletonRenderer = new this.runtime.SkeletonRenderer( @@ -197,21 +192,21 @@ export default class Spine extends Renderable { this.premultipliedAlpha = atlas.pages.some((page) => { return page.pma; }); - if (this.renderer.WebGLVersion >= 1) { - this.skeletonRenderer.premultipliedAlpha = this.premultipliedAlpha; - } + this.skeletonRenderer.premultipliedAlpha = this.premultipliedAlpha; // Instantiate a new skeleton based on the atlas and skeleton data. this.skeleton = new this.runtime.Skeleton(skeletonData); // auto-detect if the skeleton uses mesh attachments for canvas renderer - if (this.skeletonRenderer instanceof SkeletonRenderer) { + if (!this.isWebGL) { this.skeletonRenderer.triangleRendering = skeletonData.skins.some( (skin) => { - for (const [, attachments] of skin.attachments.entries()) { - for (const [, attachment] of attachments.entries()) { - if (attachment instanceof MeshAttachment) { - return true; + for (const attachments of skin.attachments) { + if (attachments) { + for (const attachment of Object.values(attachments)) { + if (attachment instanceof MeshAttachment) { + return true; + } } } } @@ -274,7 +269,7 @@ export default class Spine extends Renderable { * @returns {Spine} Reference to this object for method chaining */ rotate(angle, v) { - if (this.renderer.WebGLVersion >= 1) { + if (this.isWebGL) { this.skeleton.getRootBone().rotation -= Math.radToDeg(angle); } else { // rotation for rootBone is in degrees (anti-clockwise) @@ -291,8 +286,13 @@ export default class Spine extends Renderable { * @returns {Spine} Reference to this object for method chaining */ scale(x, y = x) { - this.root.scaleX *= x; - this.root.scaleY *= y; + if (this.isWebGL) { + // WebGL: SpineBatcher ignores currentTransform, scale through root bone + this.root.scaleX *= x; + this.root.scaleY *= y; + } + // Canvas: scale through currentTransform only (applied by preDraw), + // which scales both region bone transforms and mesh world vertices uniformly return super.scale(x, y); } @@ -385,9 +385,17 @@ export default class Spine extends Renderable { * @param {CanvasRenderer|WebGLRenderer} renderer - A renderer instance. */ draw(renderer) { - if (this.renderer.WebGLVersion >= 1) { + if (typeof this.skeleton === "undefined") { + return; + } + + // apply melonJS tint to Spine skeleton color + const t = this.tint.toArray(); + this.skeleton.color.set(t[0], t[1], t[2], this.skeleton.color.a); + + if (this.isWebGL) { // switch to the Spine batcher via melonJS batcher system - this.renderer.setBatcher("spine"); + renderer.setBatcher("spine"); // draw the skeleton — SkeletonRenderer calls spineBatcher.draw() this.skeletonRenderer.draw( @@ -406,7 +414,7 @@ export default class Spine extends Renderable { this.shapesShader.bind(); this.shapesShader.setUniform4x4f( this.runtime.Shader.MVP_MATRIX, - this.context.projectionMatrix.val, + renderer.projectionMatrix.toArray(), ); this.shapes.begin(this.shapesShader); this.skeletonDebugRenderer.draw(this.shapes, this.skeleton); @@ -425,7 +433,7 @@ export default class Spine extends Renderable { * Called automatically when the renderable is removed from the world. */ dispose() { - if (this.renderer.WebGLVersion >= 1) { + if (this.isWebGL) { this.shapes.dispose(); this.shapesShader.dispose(); this.skeletonDebugRenderer.dispose();