Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/examples/src/examples/spine/ExampleSpine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions packages/spine-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/spine-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 2 additions & 2 deletions packages/spine-plugin/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 11 additions & 1 deletion packages/spine-plugin/src/SkeletonRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Comment on lines 148 to +152
);
Comment on lines +150 to +153

if (triangles) {
this.drawMesh(renderer, image, worldVertices, triangles);
Expand Down
66 changes: 37 additions & 29 deletions packages/spine-plugin/src/Spine.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export default class Spine extends Renderable {
runtime;
skeleton;
plugin;
renderer;
animationState;
skeletonRenderer;
root;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
}
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);

Comment on lines +392 to +395
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(
Expand All @@ -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);
Expand All @@ -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();
Expand Down
Loading