Skip to content

Commit

Permalink
[ts][pixi] Add feature to attach pixi objects to slots
Browse files Browse the repository at this point in the history
  • Loading branch information
davidetan committed Jun 5, 2024
1 parent c79f89b commit 04736e9
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 3 deletions.
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.scaleX, slot.bone.scaleY, 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

0 comments on commit 04736e9

Please sign in to comment.