Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ts][pixi] Add feature to attach pixi objects to slots #2541

Merged
merged 1 commit into from
Jun 5, 2024
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
1 change: 1 addition & 0 deletions spine-ts/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ <h1>spine-ts Examples</h1>
<li><a href="/spine-pixi/example/physics2.html">Physics II</a></li>
<li><a href="/spine-pixi/example/physics3.html">Physics III</a></li>
<li><a href="/spine-pixi/example/physics4.html">Physics IV</a></li>
<li><a href="/spine-pixi/example/slot-objects.html">Slot Objects</a></li>
</ul>
<li>Phaser</li>
<ul>
Expand Down
Binary file added spine-ts/spine-pixi/example/assets/spine_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
125 changes: 125 additions & 0 deletions spine-ts/spine-pixi/example/slot-objects.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<html>
<head>
<meta charset="UTF-8" />
<title>spine-pixi</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/pixi.min.js"></script>
<script src="../dist/iife/spine-pixi.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/tweakpane.min.js"></script>
<link rel="stylesheet" href="../../index.css">
</head>

<body>
<script>
(async function () {
var app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
resizeTo: window,
backgroundColor: 0x2c3e50,
hello: true,
});
document.body.appendChild(app.view);

// Pre-load the skeleton data and atlas. You can also load .json skeleton data.
PIXI.Assets.add("spineboyData", "./assets/spineboy-pro.skel");
PIXI.Assets.add("spineboyAtlas", "./assets/spineboy-pma.atlas");
await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]);

// Create the spine display object
const spineboy = spine.Spine.from("spineboyData", "spineboyAtlas", {
scale: 0.5,
});

// Set the default mix time to use when transitioning
// from one animation to the next.
spineboy.state.data.defaultMix = 0.2;

// Center the spine object on screen.
spineboy.x = window.innerWidth / 2;
spineboy.y = window.innerHeight / 2 + spineboy.getBounds().height / 2;

// Set animation "run" on track 0, looped.
spineboy.state.setAnimation(0, "walk", true);

// Add the display object to the stage.
app.stage.addChild(spineboy);

const logo1 = PIXI.Sprite.from('assets/spine_logo.png');
const logo2 = PIXI.Sprite.from('assets/spine_logo.png');
const logo3 = PIXI.Sprite.from('assets/spine_logo.png');
const logo4 = PIXI.Sprite.from('assets/spine_logo.png');
const text = new PIXI.Text('Spine Text');

// putting logo1 on top of the gun
spineboy.addSlotObject("gun", logo1);

// putting logo2 on top of the hand using slot directly and remove the attachment hand
let frontFist;
setTimeout(() => {
frontFist = spineboy.skeleton.findSlot("front-fist");
spineboy.addSlotObject(frontFist, logo2);
frontFist.setAttachment(null);
}, 2000)

// scaling the bone, will scale the pixi object too
setTimeout(() => {
frontFist.bone.scaleX = .5
frontFist.bone.scaleY = .5
}, 3000)

// adding a pixi text in a slot using slot index
let mouth;
setTimeout(() => {
mouth = spineboy.skeleton.findSlot("mouth");
spineboy.addSlotObject(mouth, text);
}, 4000)

// adding one display object to an already "occupied" slot will remove the old one,
// and move the given one to the slot
setTimeout(() => {
spineboy.addSlotObject(mouth, logo1);
}, 5000)

// adding multiple DisplayObjects to a slot using a Container to control their offset, size, ...
setTimeout(() => {
const container = new PIXI.Container();
container.addChild(logo3, logo4);
logo3.y = 20;
logo3.scale.set(.5);
logo4.scale.set(.5);
logo4.tint = 0xFF5500;
spineboy.addSlotObject("gun", container);
}, 6000)

// removing the container won't automatically destroy the displayObject contained, so take care of them
setTimeout(() => {
const container = new PIXI.Container();
spineboy.removeSlotObject("gun");
logo3.destroy();
logo4.destroy();
}, 7000)

// removing a specific slot object, that is not in that slot do nothing
setTimeout(() => {
const container = new PIXI.Container();
spineboy.removeSlotObject(frontFist, text);
text.destroy();
}, 8000)

// removing a specific slot object
setTimeout(() => {
const container = new PIXI.Container();
spineboy.removeSlotObject(frontFist, logo2);
logo2.destroy();
}, 9000)

// resetting the slot with the original attachment
setTimeout(() => {
frontFist.setToSetupPose();
}, 10000)
})();
</script>
</body>
</html>
87 changes: 84 additions & 3 deletions spine-ts/spine-pixi/src/Spine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
AtlasAttachmentLoader,
ClippingAttachment,
Color,
MathUtils,
MeshAttachment,
Physics,
RegionAttachment,
Expand Down Expand Up @@ -224,17 +225,97 @@ export class Spine extends Container {
}
}

private verticesCache: NumberArrayLike = Utils.newFloatArray(1024);
private slotsObject = new Map<Slot, DisplayObject>();
private getSlotFromRef (slotRef: number | string | Slot): Slot {
let slot: Slot | null;
if (typeof slotRef === 'number') slot = this.skeleton.slots[slotRef];
else if (typeof slotRef === 'string') slot = this.skeleton.findSlot(slotRef);
else slot = slotRef;

if (!slot) throw new Error(`No slot found with the given slot reference: ${slotRef}`);

return slot;
}
/**
* Add a pixi DisplayObject as a child of the Spine object.
* The DisplayObject will be rendered coherently with the draw order of the slot.
* If an attachment is active on the slot, the pixi DisplayObject will be rendered on top of it.
* If the DisplayObject is already attached to the given slot, nothing will happen.
* If the DisplayObject is already attached to another slot, it will be removed from that slot
* before adding it to the given one.
* If another DisplayObject is already attached to this slot, the old one will be removed from this
* slot before adding it to the current one.
* @param slotRef - The slot index, or the slot name, or the Slot where the pixi object will be added to.
* @param pixiObject - The pixi DisplayObject to add.
*/
addSlotObject (slotRef: number | string | Slot, pixiObject: DisplayObject): void {
let slot = this.getSlotFromRef(slotRef);
let oldPixiObject = this.slotsObject.get(slot);

// search if the pixiObject was already in another slotObject
if (!oldPixiObject) {
for (const [slot, oldPixiObjectAnotherSlot] of this.slotsObject) {
if (oldPixiObjectAnotherSlot === pixiObject) {
this.removeSlotObject(slot, pixiObject);
break;
}
}
}

if (oldPixiObject === pixiObject) return;
if (oldPixiObject) this.removeChild(oldPixiObject);

this.slotsObject.set(slot, pixiObject);
this.addChild(pixiObject);
}
/**
* Return the DisplayObject connected to the given slot, if any.
* Otherwise return undefined
* @param pixiObject - The slot index, or the slot name, or the Slot to get the DisplayObject from.
* @returns a DisplayObject if any, undefined otherwise.
*/
getSlotObject (slotRef: number | string | Slot): DisplayObject | undefined {
return this.slotsObject.get(this.getSlotFromRef(slotRef));
}
/**
* Remove a slot object from the given slot.
* If `pixiObject` is passed and attached to the given slot, remove it from the slot.
* If `pixiObject` is not passed and the given slot has an attached DisplayObject, remove it from the slot.
* @param slotRef - The slot index, or the slot name, or the Slot where the pixi object will be remove from.
* @param pixiObject - Optional, The pixi DisplayObject to remove.
*/
removeSlotObject (slotRef: number | string | Slot, pixiObject?: DisplayObject): void {
let slot = this.getSlotFromRef(slotRef);
let slotObject = this.slotsObject.get(slot);
if (!slotObject) return;

// if pixiObject is passed, remove only if it is equal to the given one
if (pixiObject && pixiObject !== slotObject) return;

this.removeChild(slotObject);
this.slotsObject.delete(slot);
}

private verticesCache: NumberArrayLike = Utils.newFloatArray(1024);
private renderMeshes (): void {
this.resetMeshes();

let triangles: Array<number> | null = null;
let uvs: NumberArrayLike | null = null;
const drawOrder = this.skeleton.drawOrder;

for (let i = 0, n = drawOrder.length; i < n; i++) {
for (let i = 0, n = drawOrder.length, slotObjectsCounter = 0; i < n; i++) {
const slot = drawOrder[i];

// render pixi object on the current slot on top of the slot attachment
let pixiObject = this.slotsObject.get(slot);
let zIndex = i + slotObjectsCounter;
if (pixiObject) {
pixiObject.setTransform(slot.bone.worldX, slot.bone.worldY, slot.bone.getWorldScaleX(), slot.bone.getWorldScaleX(), slot.bone.getWorldRotationX() * MathUtils.degRad);
pixiObject.zIndex = zIndex + 1;
slotObjectsCounter++;
}

const useDarkColor = slot.darkColor != null;
const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE;
if (!slot.bone.active) {
Expand Down Expand Up @@ -331,7 +412,7 @@ export class Spine extends Container {
}

const mesh = this.getMeshForSlot(slot);
mesh.zIndex = i;
mesh.zIndex = zIndex;
mesh.updateFromSpineData(texture, slot.data.blendMode, slot.data.name, finalVertices, finalVerticesLength, finalIndices, finalIndicesLength, useDarkColor);
}

Expand Down
Loading