Skip to content

Commit 87a1aff

Browse files
committed
[ts][pixi] clipping + alpha for pixi objects added to slots
1 parent 4cdee31 commit 87a1aff

File tree

5 files changed

+135
-68
lines changed

5 files changed

+135
-68
lines changed

examples/export/runtimes.sh

+1
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ rm "$ROOT/spine-ts/spine-pixi/example/assets/"*
489489
cp -f ../raptor/export/raptor-pro.json "$ROOT/spine-ts/spine-pixi/example/assets/"
490490
cp -f ../raptor/export/raptor.atlas "$ROOT/spine-ts/spine-pixi/example/assets/"
491491
cp -f ../raptor/export/raptor.png "$ROOT/spine-ts/spine-pixi/example/assets/"
492+
cp -f ../raptor/images/raptor-jaw-tooth.png "$ROOT/spine-ts/spine-pixi/example/assets/"
492493

493494
cp -f ../spineboy/export/spineboy-pro.json "$ROOT/spine-ts/spine-pixi/example/assets/"
494495
cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-pixi/example/assets/"
Loading
-3.41 KB
Binary file not shown.

spine-ts/spine-pixi/example/slot-objects.html

+58-41
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
// Create the spine display object
3131
const spineboy = spine.Spine.from("spineboyData", "spineboyAtlas", {
32-
scale: 0.5,
32+
scale: 0.25,
3333
});
3434

3535
// Set the default mix time to use when transitioning
@@ -46,79 +46,96 @@
4646
// Add the display object to the stage.
4747
app.stage.addChild(spineboy);
4848

49-
const logo1 = PIXI.Sprite.from('assets/spine_logo.png');
50-
const logo2 = PIXI.Sprite.from('assets/spine_logo.png');
51-
const logo3 = PIXI.Sprite.from('assets/spine_logo.png');
52-
const logo4 = PIXI.Sprite.from('assets/spine_logo.png');
53-
const text = new PIXI.Text('Spine Text');
49+
const tooth1 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png');
50+
const tooth2 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png');
51+
const text = new PIXI.Text('Text GUN');
5452

55-
// putting logo1 on top of the gun
56-
spineboy.addSlotObject("gun", logo1);
53+
const toothContainer = new PIXI.Container();
54+
toothContainer.addChild(tooth1);
55+
toothContainer.name = "tooth";
56+
text.name = "text";
5757

5858
// putting logo2 on top of the hand using slot directly and remove the attachment hand
5959
let frontFist;
6060
setTimeout(() => {
61-
frontFist = spineboy.skeleton.findSlot("front-fist");
62-
spineboy.addSlotObject(frontFist, logo2);
61+
frontFist = spineboy.skeleton.findSlot("front-foot");
62+
tooth1.x = -10;
63+
tooth1.y = -70;
64+
spineboy.addSlotObject(frontFist, toothContainer);
6365
frontFist.setAttachment(null);
64-
}, 2000)
66+
}, 1000);
6567

6668
// scaling the bone, will scale the pixi object too
6769
setTimeout(() => {
68-
frontFist.bone.scaleX = .5
69-
frontFist.bone.scaleY = .5
70-
}, 3000)
70+
frontFist.bone.scaleX = .5;
71+
frontFist.bone.scaleY = .5;
72+
}, 2000);
7173

7274
// adding a pixi text in a slot using slot index
7375
let mouth;
7476
setTimeout(() => {
75-
mouth = spineboy.skeleton.findSlot("mouth");
76-
spineboy.addSlotObject(mouth, text);
77-
}, 4000)
77+
spineboy.addSlotObject("gun", text);
78+
}, 4000);
7879

79-
// adding one display object to an already "occupied" slot will remove the old one,
80+
// adding one pixi object to an already "occupied" slot will remove the old one,
8081
// and move the given one to the slot
8182
setTimeout(() => {
82-
spineboy.addSlotObject(mouth, logo1);
83-
}, 5000)
83+
spineboy.addSlotObject("gun", toothContainer);
84+
}, 5000);
8485

8586
// adding multiple DisplayObjects to a slot using a Container to control their offset, size, ...
8687
setTimeout(() => {
87-
const container = new PIXI.Container();
88-
container.addChild(logo3, logo4);
89-
logo3.y = 20;
90-
logo3.scale.set(.5);
91-
logo4.scale.set(.5);
92-
logo4.tint = 0xFF5500;
93-
spineboy.addSlotObject("gun", container);
94-
}, 6000)
88+
toothContainer.addChild(tooth2);
89+
tooth2.x = 30;
90+
tooth2.y = -70;
91+
tooth2.angle = 30;
92+
tooth2.tint = 0xFF5500;
93+
}, 6000);
9594

9695
// removing the container won't automatically destroy the displayObject contained, so take care of them
9796
setTimeout(() => {
98-
const container = new PIXI.Container();
9997
spineboy.removeSlotObject("gun");
100-
logo3.destroy();
101-
logo4.destroy();
102-
}, 7000)
98+
console.log(toothContainer.destroyed)
99+
console.log(tooth1.destroyed)
100+
console.log(tooth2.destroyed)
101+
toothContainer.destroy();
102+
tooth1.destroy();
103+
console.log(toothContainer.destroyed)
104+
console.log(tooth1.destroyed)
105+
console.log(tooth2.destroyed)
106+
}, 7000);
103107

104108
// removing a specific slot object, that is not in that slot do nothing
105109
setTimeout(() => {
106-
const container = new PIXI.Container();
107-
spineboy.removeSlotObject(frontFist, text);
108-
text.destroy();
109-
}, 8000)
110+
spineboy.addSlotObject("gun", tooth2);
111+
spineboy.removeSlotObject("gun", text);
112+
}, 8000);
110113

111114
// removing a specific slot object
112115
setTimeout(() => {
113-
const container = new PIXI.Container();
114-
spineboy.removeSlotObject(frontFist, logo2);
115-
logo2.destroy();
116-
}, 9000)
116+
spineboy.removeSlotObject("gun", tooth2);
117+
tooth2.destroy();
118+
}, 9000);
117119

118120
// resetting the slot with the original attachment
119121
setTimeout(() => {
120122
frontFist.setToSetupPose();
121-
}, 10000)
123+
frontFist.bone.setToSetupPose();
124+
}, 10000);
125+
126+
// showing an animation with clipping -> Pixi masks will be created
127+
// for clipping attachments having slot objects
128+
setTimeout(() => {
129+
spineboy.state.setAnimation(0, "portal", true)
130+
const tooth3 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png');
131+
tooth3.scale.set(2);
132+
tooth3.x = -60;
133+
tooth3.y = 120;
134+
tooth3.angle = 180;
135+
const foot1 = new PIXI.Container();
136+
foot1.addChild(tooth3);
137+
spineboy.addSlotObject("rear-foot", foot1);
138+
}, 11000);
122139
})();
123140
</script>
124141
</body>

spine-ts/spine-pixi/src/Spine.ts

+76-27
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import type { IPointData } from "@pixi/core";
5454
import { Ticker } from "@pixi/core";
5555
import type { IDestroyOptions, DisplayObject } from "@pixi/display";
5656
import { Container } from "@pixi/display";
57+
import { Graphics } from "@pixi/graphics";
5758

5859
/**
5960
* Options to configure a {@link Spine} game object.
@@ -205,6 +206,13 @@ export class Spine extends Container {
205206
this.debug = undefined;
206207
this.meshesCache.clear();
207208
this.slotsObject.clear();
209+
210+
for (let maskKey in this.clippingSlotToPixiMasks) {
211+
const mask = this.clippingSlotToPixiMasks[maskKey];
212+
mask.destroy();
213+
delete this.clippingSlotToPixiMasks[maskKey];
214+
}
215+
208216
super.destroy(options);
209217
}
210218

@@ -231,7 +239,7 @@ export class Spine extends Container {
231239
}
232240
}
233241

234-
private slotsObject = new Map<Slot, DisplayObject>();
242+
private slotsObject = new Map<Slot, Container>();
235243
private getSlotFromRef (slotRef: number | string | Slot): Slot {
236244
let slot: Slot | null;
237245
if (typeof slotRef === 'number') slot = this.skeleton.slots[slotRef];
@@ -243,54 +251,52 @@ export class Spine extends Container {
243251
return slot;
244252
}
245253
/**
246-
* Add a pixi DisplayObject as a child of the Spine object.
247-
* The DisplayObject will be rendered coherently with the draw order of the slot.
248-
* If an attachment is active on the slot, the pixi DisplayObject will be rendered on top of it.
249-
* If the DisplayObject is already attached to the given slot, nothing will happen.
250-
* If the DisplayObject is already attached to another slot, it will be removed from that slot
254+
* Add a pixi Container as a child of the Spine object.
255+
* The Container will be rendered coherently with the draw order of the slot.
256+
* If an attachment is active on the slot, the pixi Container will be rendered on top of it.
257+
* If the Container is already attached to the given slot, nothing will happen.
258+
* If the Container is already attached to another slot, it will be removed from that slot
251259
* before adding it to the given one.
252-
* If another DisplayObject is already attached to this slot, the old one will be removed from this
260+
* If another Container is already attached to this slot, the old one will be removed from this
253261
* slot before adding it to the current one.
254262
* @param slotRef - The slot index, or the slot name, or the Slot where the pixi object will be added to.
255-
* @param pixiObject - The pixi DisplayObject to add.
263+
* @param pixiObject - The pixi Container to add.
256264
*/
257-
addSlotObject (slotRef: number | string | Slot, pixiObject: DisplayObject): void {
265+
addSlotObject (slotRef: number | string | Slot, pixiObject: Container): void {
258266
let slot = this.getSlotFromRef(slotRef);
259267
let oldPixiObject = this.slotsObject.get(slot);
268+
if (oldPixiObject === pixiObject) return;
260269

261270
// search if the pixiObject was already in another slotObject
262-
if (!oldPixiObject) {
263-
for (const [slot, oldPixiObjectAnotherSlot] of this.slotsObject) {
264-
if (oldPixiObjectAnotherSlot === pixiObject) {
265-
this.removeSlotObject(slot, pixiObject);
266-
break;
267-
}
271+
for (const [otherSlot, oldPixiObjectAnotherSlot] of this.slotsObject) {
272+
if (otherSlot !== slot && oldPixiObjectAnotherSlot === pixiObject) {
273+
this.removeSlotObject(otherSlot, pixiObject);
274+
break;
268275
}
269276
}
270-
271-
if (oldPixiObject === pixiObject) return;
277+
272278
if (oldPixiObject) this.removeChild(oldPixiObject);
273279

274280
this.slotsObject.set(slot, pixiObject);
275281
this.addChild(pixiObject);
276282
}
277283
/**
278-
* Return the DisplayObject connected to the given slot, if any.
284+
* Return the Container connected to the given slot, if any.
279285
* Otherwise return undefined
280-
* @param pixiObject - The slot index, or the slot name, or the Slot to get the DisplayObject from.
281-
* @returns a DisplayObject if any, undefined otherwise.
286+
* @param pixiObject - The slot index, or the slot name, or the Slot to get the Container from.
287+
* @returns a Container if any, undefined otherwise.
282288
*/
283-
getSlotObject (slotRef: number | string | Slot): DisplayObject | undefined {
289+
getSlotObject (slotRef: number | string | Slot): Container | undefined {
284290
return this.slotsObject.get(this.getSlotFromRef(slotRef));
285291
}
286292
/**
287293
* Remove a slot object from the given slot.
288294
* If `pixiObject` is passed and attached to the given slot, remove it from the slot.
289-
* If `pixiObject` is not passed and the given slot has an attached DisplayObject, remove it from the slot.
295+
* If `pixiObject` is not passed and the given slot has an attached Container, remove it from the slot.
290296
* @param slotRef - The slot index, or the slot name, or the Slot where the pixi object will be remove from.
291-
* @param pixiObject - Optional, The pixi DisplayObject to remove.
297+
* @param pixiObject - Optional, The pixi Container to remove.
292298
*/
293-
removeSlotObject (slotRef: number | string | Slot, pixiObject?: DisplayObject): void {
299+
removeSlotObject (slotRef: number | string | Slot, pixiObject?: Container): void {
294300
let slot = this.getSlotFromRef(slotRef);
295301
let slotObject = this.slotsObject.get(slot);
296302
if (!slotObject) return;
@@ -303,29 +309,64 @@ export class Spine extends Container {
303309
}
304310

305311
private verticesCache: NumberArrayLike = Utils.newFloatArray(1024);
312+
private clippingSlotToPixiMasks: Record<string, Graphics> = {};
313+
private pixiMaskCleanup (slot: Slot) {
314+
let mask = this.clippingSlotToPixiMasks[slot.data.name];
315+
if (mask) {
316+
delete this.clippingSlotToPixiMasks[slot.data.name];
317+
mask.destroy();
318+
}
319+
}
320+
private updatePixiObject(pixiObject: Container, slot: Slot, zIndex: number) {
321+
pixiObject.setTransform(slot.bone.worldX, slot.bone.worldY, slot.bone.getWorldScaleX(), slot.bone.getWorldScaleX(), slot.bone.getWorldRotationX() * MathUtils.degRad);
322+
pixiObject.zIndex = zIndex + 1;
323+
pixiObject.alpha = this.skeleton.color.a * slot.color.a;
324+
}
325+
private updateAndSetPixiMask(pixiMaskSource: PixiMaskSource | null, pixiObject: Container) {
326+
if (Spine.clipper.isClipping() && pixiMaskSource) {
327+
let mask = this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] as Graphics;
328+
if (!mask) {
329+
mask = new Graphics();
330+
this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] = mask;
331+
this.addChild(mask);
332+
}
333+
if (!pixiMaskSource.computed) {
334+
pixiMaskSource.computed = true;
335+
const clippingAttachment = pixiMaskSource.slot.attachment as ClippingAttachment;
336+
const world = Array.from(clippingAttachment.vertices);
337+
clippingAttachment.computeWorldVertices(pixiMaskSource.slot, 0, clippingAttachment.worldVerticesLength, world, 0, 2);
338+
mask.clear().lineStyle(0).beginFill(0x000000).drawPolygon(world);
339+
}
340+
pixiObject.mask = mask;
341+
} else if (pixiObject.mask) {
342+
pixiObject.mask = null;
343+
}
344+
}
306345
private renderMeshes (): void {
307346
this.resetMeshes();
308347

309348
let triangles: Array<number> | null = null;
310349
let uvs: NumberArrayLike | null = null;
350+
let pixiMaskSource: PixiMaskSource | null = null;
311351
const drawOrder = this.skeleton.drawOrder;
312352

313353
for (let i = 0, n = drawOrder.length, slotObjectsCounter = 0; i < n; i++) {
314354
const slot = drawOrder[i];
315-
355+
316356
// render pixi object on the current slot on top of the slot attachment
317357
let pixiObject = this.slotsObject.get(slot);
318358
let zIndex = i + slotObjectsCounter;
319359
if (pixiObject) {
320-
pixiObject.setTransform(slot.bone.worldX, slot.bone.worldY, slot.bone.getWorldScaleX(), slot.bone.getWorldScaleX(), slot.bone.getWorldRotationX() * MathUtils.degRad);
321-
pixiObject.zIndex = zIndex + 1;
360+
this.updatePixiObject(pixiObject, slot, zIndex + 1);
322361
slotObjectsCounter++;
362+
this.updateAndSetPixiMask(pixiMaskSource, pixiObject);
323363
}
324364

325365
const useDarkColor = slot.darkColor != null;
326366
const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE;
327367
if (!slot.bone.active) {
328368
Spine.clipper.clipEndWithSlot(slot);
369+
this.pixiMaskCleanup(slot);
329370
continue;
330371
}
331372
const attachment = slot.getAttachment();
@@ -353,9 +394,11 @@ export class Spine extends Container {
353394
texture = <SpineTexture>mesh.region?.texture;
354395
} else if (attachment instanceof ClippingAttachment) {
355396
Spine.clipper.clipStart(slot, attachment);
397+
pixiMaskSource = { slot, computed: false };
356398
continue;
357399
} else {
358400
Spine.clipper.clipEndWithSlot(slot);
401+
this.pixiMaskCleanup(slot);
359402
continue;
360403
}
361404
if (texture != null) {
@@ -423,6 +466,7 @@ export class Spine extends Container {
423466
}
424467

425468
Spine.clipper.clipEndWithSlot(slot);
469+
this.pixiMaskCleanup(slot);
426470
}
427471
Spine.clipper.clipEnd();
428472
}
@@ -542,6 +586,11 @@ export class Spine extends Container {
542586
}
543587
}
544588

589+
type PixiMaskSource = {
590+
slot: Slot,
591+
computed: boolean, // prevent to reculaculate vertices for a mask clipping multiple pixi objects
592+
}
593+
545594
Skeleton.yDown = true;
546595

547596
/**

0 commit comments

Comments
 (0)