From 0b3dba8998e8a72ecb0c02b7c856788f56fa0a6e Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Tue, 27 Jan 2026 15:59:36 +0100 Subject: [PATCH] [ts][pixi-v7][pixi-v8] Slot objects follow bone using local matrix making its transform equivalent to the one of the slot. See #3015. --- .../example/slot-objects-scale-rotation.html | 15 ++++++++--- spine-ts/spine-pixi-v7/src/Spine.ts | 26 +++++++----------- .../example/slot-objects-scale-rotation.html | 17 +++++++++--- spine-ts/spine-pixi-v8/src/Spine.ts | 27 +++++++------------ 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/spine-ts/spine-pixi-v7/example/slot-objects-scale-rotation.html b/spine-ts/spine-pixi-v7/example/slot-objects-scale-rotation.html index 21c7ed04f..d5116f840 100644 --- a/spine-ts/spine-pixi-v7/example/slot-objects-scale-rotation.html +++ b/spine-ts/spine-pixi-v7/example/slot-objects-scale-rotation.html @@ -55,25 +55,34 @@ const bone = spineGO.skeleton.findBone("replaceMe"); const myObject = { - parentScaleX: 1, parentScaleY: 1, parentRotation: 0, - parent2ScaleX: 1, parent2ScaleY: 1, parent2Rotation: 0, + parentScaleX: 1, parentScaleY: 1, parentRotation: 0, parentShearX: 0, parentShearY: 0, + parent2ScaleX: 1, parent2ScaleY: 1, parent2Rotation: 0, parent2ShearX: 0, parent2ShearY: 0, scaleX: 1, scaleY: 1, rotation: 0, + shearX: 0, shearY: 0, + spriteAlpha: 1, }; const gui = new lil.GUI({}); gui.add(myObject, 'parentScaleX').min(-3).max(3).step(0.1).name('parentScaleX').onChange(value => parentBone.scaleX = value); gui.add(myObject, 'parentScaleY').min(-3).max(3).step(0.1).name('parentScaleY').onChange(value => parentBone.scaleY = value); gui.add(myObject, 'parentRotation').min(-180).max(180).step(1).name('parentRotation').onChange(value => parentBone.rotation = value); + gui.add(myObject, 'parentShearX').min(-180).max(180).step(1).name('parentShearX').onChange(value => parentBone.shearX = value); + gui.add(myObject, 'parentShearY').min(-180).max(180).step(1).name('parentShearY').onChange(value => parentBone.shearY = value); gui.add(myObject, 'parent2ScaleX').min(-3).max(3).step(0.1).name('parent2ScaleX').onChange(value => parent2Bone.scaleX = value); gui.add(myObject, 'parent2ScaleY').min(-3).max(3).step(0.1).name('parent2ScaleY').onChange(value => parent2Bone.scaleY = value); gui.add(myObject, 'parent2Rotation').min(-180).max(180).step(1).name('parent2Rotation').onChange(value => parent2Bone.rotation = value); + gui.add(myObject, 'parent2ShearX').min(-180).max(180).step(1).name('parent2ShearX').onChange(value => parent2Bone.shearX = value); + gui.add(myObject, 'parent2ShearY').min(-180).max(180).step(1).name('parent2ShearY').onChange(value => parent2Bone.shearY = value); gui.add(myObject, 'scaleX').min(-3).max(3).step(0.1).name('scaleX').onChange(value => bone.scaleX = value); gui.add(myObject, 'scaleY').min(-3).max(3).step(0.1).name('scaleY').onChange(value => bone.scaleY = value); gui.add(myObject, 'rotation').min(-180).max(180).step(1).name('rotation').onChange(value => bone.rotation = value); + gui.add(myObject, 'shearX').min(-180).max(180).step(1).name('shearX').onChange(value => bone.shearX = value); + gui.add(myObject, 'shearY').min(-180).max(180).step(1).name('shearY').onChange(value => bone.shearY = value); + gui.add(myObject, 'spriteAlpha').min(0).max(1).step(0.01).name('spriteAlpha').onChange(value => sprite.alpha = value); // add instructions const basicText = new PIXI.Text( - "This example shows that slot objects follow scale and rotation from ancestors too, but if a rotation on a bone occurs scale won't work anymore on ancestors.", + "This example shows that slot objects can follow scale, shear and rotation from ancestors too.", { fontSize: 20, fill: "white", wordWrap: true, wordWrapWidth: 300 } ); basicText.position.set(10, 10); diff --git a/spine-ts/spine-pixi-v7/src/Spine.ts b/spine-ts/spine-pixi-v7/src/Spine.ts index 36bd381f0..6a80862d2 100644 --- a/spine-ts/spine-pixi-v7/src/Spine.ts +++ b/spine-ts/spine-pixi-v7/src/Spine.ts @@ -578,24 +578,16 @@ export class Spine extends Container { slotObject.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue; if (slotObject.visible) { - slotObject.position.set(slot.bone.worldX, slot.bone.worldY); - slotObject.angle = slot.bone.getWorldRotationX(); + let bone = slot.bone; - let bone: Bone | null = slot.bone; - let cumulativeScaleX = 1; - let cumulativeScaleY = 1; - while (bone) { - cumulativeScaleX *= bone.scaleX; - cumulativeScaleY *= bone.scaleY; - bone = bone.parent; - }; - - if (cumulativeScaleX < 0) slotObject.angle -= 180; - - slotObject.scale.set( - slot.bone.getWorldScaleX() * Math.sign(cumulativeScaleX), - slot.bone.getWorldScaleY() * Math.sign(cumulativeScaleY), - ); + const matrix = slotObject.localTransform; + matrix.a = bone.a; + matrix.b = bone.c; + matrix.c = -bone.b; + matrix.d = -bone.d; + matrix.tx = bone.worldX; + matrix.ty = bone.worldY; + slotObject.transform.setFromMatrix(matrix); slotObject.zIndex = zIndex + 1; slotObject.alpha = this.skeleton.color.a * slot.color.a; diff --git a/spine-ts/spine-pixi-v8/example/slot-objects-scale-rotation.html b/spine-ts/spine-pixi-v8/example/slot-objects-scale-rotation.html index fd79dcb36..c160f0a2f 100644 --- a/spine-ts/spine-pixi-v8/example/slot-objects-scale-rotation.html +++ b/spine-ts/spine-pixi-v8/example/slot-objects-scale-rotation.html @@ -2,7 +2,7 @@ spine-pixi - + @@ -54,25 +54,34 @@ const bone = spineGO.skeleton.findBone("replaceMe"); const myObject = { - parentScaleX: 1, parentScaleY: 1, parentRotation: 0, - parent2ScaleX: 1, parent2ScaleY: 1, parent2Rotation: 0, + parentScaleX: 1, parentScaleY: 1, parentRotation: 0, parentShearX: 0, parentShearY: 0, + parent2ScaleX: 1, parent2ScaleY: 1, parent2Rotation: 0, parent2ShearX: 0, parent2ShearY: 0, scaleX: 1, scaleY: 1, rotation: 0, + shearX: 0, shearY: 0, + spriteAlpha: 1, }; const gui = new lil.GUI({}); gui.add(myObject, 'parentScaleX').min(-3).max(3).step(0.1).name('parentScaleX').onChange(value => parentBone.scaleX = value); gui.add(myObject, 'parentScaleY').min(-3).max(3).step(0.1).name('parentScaleY').onChange(value => parentBone.scaleY = value); gui.add(myObject, 'parentRotation').min(-180).max(180).step(1).name('parentRotation').onChange(value => parentBone.rotation = value); + gui.add(myObject, 'parentShearX').min(-180).max(180).step(1).name('parentShearX').onChange(value => parentBone.shearX = value); + gui.add(myObject, 'parentShearY').min(-180).max(180).step(1).name('parentShearY').onChange(value => parentBone.shearY = value); gui.add(myObject, 'parent2ScaleX').min(-3).max(3).step(0.1).name('parent2ScaleX').onChange(value => parent2Bone.scaleX = value); gui.add(myObject, 'parent2ScaleY').min(-3).max(3).step(0.1).name('parent2ScaleY').onChange(value => parent2Bone.scaleY = value); gui.add(myObject, 'parent2Rotation').min(-180).max(180).step(1).name('parent2Rotation').onChange(value => parent2Bone.rotation = value); + gui.add(myObject, 'parent2ShearX').min(-180).max(180).step(1).name('parent2ShearX').onChange(value => parent2Bone.shearX = value); + gui.add(myObject, 'parent2ShearY').min(-180).max(180).step(1).name('parent2ShearY').onChange(value => parent2Bone.shearY = value); gui.add(myObject, 'scaleX').min(-3).max(3).step(0.1).name('scaleX').onChange(value => bone.scaleX = value); gui.add(myObject, 'scaleY').min(-3).max(3).step(0.1).name('scaleY').onChange(value => bone.scaleY = value); gui.add(myObject, 'rotation').min(-180).max(180).step(1).name('rotation').onChange(value => bone.rotation = value); + gui.add(myObject, 'shearX').min(-180).max(180).step(1).name('shearX').onChange(value => bone.shearX = value); + gui.add(myObject, 'shearY').min(-180).max(180).step(1).name('shearY').onChange(value => bone.shearY = value); + gui.add(myObject, 'spriteAlpha').min(0).max(1).step(0.01).name('spriteAlpha').onChange(value => sprite.alpha = value); // add instructions const basicText = new PIXI.Text({ - text: "This example shows that slot objects follow scale and rotation from ancestors too, but if a rotation on a bone occurs scale won't work anymore on ancestors.", + text: "This example shows that slot objects can follow scale, shear and rotation from ancestors too.", style: { fontSize: 20, fill: "white", wordWrap: true, wordWrapWidth: 300 } }); basicText.position.set(10, 10); diff --git a/spine-ts/spine-pixi-v8/src/Spine.ts b/spine-ts/spine-pixi-v8/src/Spine.ts index 0d00137e4..5d21efbe4 100644 --- a/spine-ts/spine-pixi-v8/src/Spine.ts +++ b/spine-ts/spine-pixi-v8/src/Spine.ts @@ -792,25 +792,16 @@ export class Spine extends ViewContainer { container.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue; if (container.visible) { - let bone: Bone | null = slot.bone; + let bone = slot.bone; - container.position.set(bone.worldX, bone.worldY); - container.angle = bone.getWorldRotationX(); - - let cumulativeScaleX = 1; - let cumulativeScaleY = 1; - while (bone) { - cumulativeScaleX *= bone.scaleX; - cumulativeScaleY *= bone.scaleY; - bone = bone.parent; - }; - - if (cumulativeScaleX < 0) container.angle -= 180; - - container.scale.set( - slot.bone.getWorldScaleX() * Math.sign(cumulativeScaleX), - slot.bone.getWorldScaleY() * Math.sign(cumulativeScaleY), - ); + const matrix = container.localTransform; + matrix.a = bone.a; + matrix.b = bone.c; + matrix.c = -bone.b; + matrix.d = -bone.d; + matrix.tx = bone.worldX; + matrix.ty = bone.worldY; + container.setFromMatrix(matrix); container.alpha = this.skeleton.color.a * slot.color.a; }