diff --git a/examples/export/runtimes.sh b/examples/export/runtimes.sh index aa6f7e351..ba42a27f9 100755 --- a/examples/export/runtimes.sh +++ b/examples/export/runtimes.sh @@ -489,6 +489,7 @@ rm "$ROOT/spine-ts/spine-pixi/example/assets/"* cp -f ../raptor/export/raptor-pro.json "$ROOT/spine-ts/spine-pixi/example/assets/" cp -f ../raptor/export/raptor.atlas "$ROOT/spine-ts/spine-pixi/example/assets/" cp -f ../raptor/export/raptor.png "$ROOT/spine-ts/spine-pixi/example/assets/" +cp -f ../raptor/images/raptor-jaw-tooth.png "$ROOT/spine-ts/spine-pixi/example/assets/" cp -f ../spineboy/export/spineboy-pro.json "$ROOT/spine-ts/spine-pixi/example/assets/" cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-pixi/example/assets/" diff --git a/spine-ts/spine-pixi/example/assets/raptor-jaw-tooth.png b/spine-ts/spine-pixi/example/assets/raptor-jaw-tooth.png new file mode 100644 index 000000000..59b3b69c7 Binary files /dev/null and b/spine-ts/spine-pixi/example/assets/raptor-jaw-tooth.png differ diff --git a/spine-ts/spine-pixi/example/assets/spine_logo.png b/spine-ts/spine-pixi/example/assets/spine_logo.png deleted file mode 100644 index 40e65c5c9..000000000 Binary files a/spine-ts/spine-pixi/example/assets/spine_logo.png and /dev/null differ diff --git a/spine-ts/spine-pixi/example/slot-objects.html b/spine-ts/spine-pixi/example/slot-objects.html index 493d374fd..7f787bdc3 100644 --- a/spine-ts/spine-pixi/example/slot-objects.html +++ b/spine-ts/spine-pixi/example/slot-objects.html @@ -29,7 +29,7 @@ // Create the spine display object const spineboy = spine.Spine.from("spineboyData", "spineboyAtlas", { - scale: 0.5, + scale: 0.25, }); // Set the default mix time to use when transitioning @@ -46,79 +46,96 @@ // 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'); + const tooth1 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png'); + const tooth2 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png'); + const text = new PIXI.Text('Text GUN'); - // putting logo1 on top of the gun - spineboy.addSlotObject("gun", logo1); + const toothContainer = new PIXI.Container(); + toothContainer.addChild(tooth1); + toothContainer.name = "tooth"; + text.name = "text"; // 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 = spineboy.skeleton.findSlot("front-foot"); + tooth1.x = -10; + tooth1.y = -70; + spineboy.addSlotObject(frontFist, toothContainer); frontFist.setAttachment(null); - }, 2000) + }, 1000); // scaling the bone, will scale the pixi object too setTimeout(() => { - frontFist.bone.scaleX = .5 - frontFist.bone.scaleY = .5 - }, 3000) + frontFist.bone.scaleX = .5; + frontFist.bone.scaleY = .5; + }, 2000); // adding a pixi text in a slot using slot index let mouth; setTimeout(() => { - mouth = spineboy.skeleton.findSlot("mouth"); - spineboy.addSlotObject(mouth, text); - }, 4000) + spineboy.addSlotObject("gun", text); + }, 4000); - // adding one display object to an already "occupied" slot will remove the old one, + // adding one pixi 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) + spineboy.addSlotObject("gun", toothContainer); + }, 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) + toothContainer.addChild(tooth2); + tooth2.x = 30; + tooth2.y = -70; + tooth2.angle = 30; + tooth2.tint = 0xFF5500; + }, 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) + console.log(toothContainer.destroyed) + console.log(tooth1.destroyed) + console.log(tooth2.destroyed) + toothContainer.destroy(); + tooth1.destroy(); + console.log(toothContainer.destroyed) + console.log(tooth1.destroyed) + console.log(tooth2.destroyed) + }, 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) + spineboy.addSlotObject("gun", tooth2); + spineboy.removeSlotObject("gun", text); + }, 8000); // removing a specific slot object setTimeout(() => { - const container = new PIXI.Container(); - spineboy.removeSlotObject(frontFist, logo2); - logo2.destroy(); - }, 9000) + spineboy.removeSlotObject("gun", tooth2); + tooth2.destroy(); + }, 9000); // resetting the slot with the original attachment setTimeout(() => { frontFist.setToSetupPose(); - }, 10000) + frontFist.bone.setToSetupPose(); + }, 10000); + + // showing an animation with clipping -> Pixi masks will be created + // for clipping attachments having slot objects + setTimeout(() => { + spineboy.state.setAnimation(0, "portal", true) + const tooth3 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png'); + tooth3.scale.set(2); + tooth3.x = -60; + tooth3.y = 120; + tooth3.angle = 180; + const foot1 = new PIXI.Container(); + foot1.addChild(tooth3); + spineboy.addSlotObject("rear-foot", foot1); + }, 11000); })(); diff --git a/spine-ts/spine-pixi/src/Spine.ts b/spine-ts/spine-pixi/src/Spine.ts index 4f383db9d..1fae0201b 100644 --- a/spine-ts/spine-pixi/src/Spine.ts +++ b/spine-ts/spine-pixi/src/Spine.ts @@ -54,6 +54,7 @@ import type { IPointData } from "@pixi/core"; import { Ticker } from "@pixi/core"; import type { IDestroyOptions, DisplayObject } from "@pixi/display"; import { Container } from "@pixi/display"; +import { Graphics } from "@pixi/graphics"; /** * Options to configure a {@link Spine} game object. @@ -205,6 +206,13 @@ export class Spine extends Container { this.debug = undefined; this.meshesCache.clear(); this.slotsObject.clear(); + + for (let maskKey in this.clippingSlotToPixiMasks) { + const mask = this.clippingSlotToPixiMasks[maskKey]; + mask.destroy(); + delete this.clippingSlotToPixiMasks[maskKey]; + } + super.destroy(options); } @@ -231,7 +239,7 @@ export class Spine extends Container { } } - private slotsObject = new Map(); + private slotsObject = new Map(); private getSlotFromRef (slotRef: number | string | Slot): Slot { let slot: Slot | null; if (typeof slotRef === 'number') slot = this.skeleton.slots[slotRef]; @@ -243,54 +251,52 @@ export class Spine extends Container { 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 + * Add a pixi Container as a child of the Spine object. + * The Container will be rendered coherently with the draw order of the slot. + * If an attachment is active on the slot, the pixi Container will be rendered on top of it. + * If the Container is already attached to the given slot, nothing will happen. + * If the Container 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 + * If another Container 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. + * @param pixiObject - The pixi Container to add. */ - addSlotObject (slotRef: number | string | Slot, pixiObject: DisplayObject): void { + addSlotObject (slotRef: number | string | Slot, pixiObject: Container): void { let slot = this.getSlotFromRef(slotRef); let oldPixiObject = this.slotsObject.get(slot); + if (oldPixiObject === pixiObject) return; // 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; - } + for (const [otherSlot, oldPixiObjectAnotherSlot] of this.slotsObject) { + if (otherSlot !== slot && oldPixiObjectAnotherSlot === pixiObject) { + this.removeSlotObject(otherSlot, 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. + * Return the Container 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. + * @param pixiObject - The slot index, or the slot name, or the Slot to get the Container from. + * @returns a Container if any, undefined otherwise. */ - getSlotObject (slotRef: number | string | Slot): DisplayObject | undefined { + getSlotObject (slotRef: number | string | Slot): Container | 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. + * If `pixiObject` is not passed and the given slot has an attached Container, 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. + * @param pixiObject - Optional, The pixi Container to remove. */ - removeSlotObject (slotRef: number | string | Slot, pixiObject?: DisplayObject): void { + removeSlotObject (slotRef: number | string | Slot, pixiObject?: Container): void { let slot = this.getSlotFromRef(slotRef); let slotObject = this.slotsObject.get(slot); if (!slotObject) return; @@ -303,11 +309,45 @@ export class Spine extends Container { } private verticesCache: NumberArrayLike = Utils.newFloatArray(1024); + private clippingSlotToPixiMasks: Record = {}; + private pixiMaskCleanup (slot: Slot) { + let mask = this.clippingSlotToPixiMasks[slot.data.name]; + if (mask) { + delete this.clippingSlotToPixiMasks[slot.data.name]; + mask.destroy(); + } + } + private updatePixiObject (pixiObject: Container, slot: Slot, zIndex: number) { + pixiObject.setTransform(slot.bone.worldX, slot.bone.worldY, slot.bone.getWorldScaleX(), slot.bone.getWorldScaleX(), slot.bone.getWorldRotationX() * MathUtils.degRad); + pixiObject.zIndex = zIndex + 1; + pixiObject.alpha = this.skeleton.color.a * slot.color.a; + } + private updateAndSetPixiMask (pixiMaskSource: PixiMaskSource | null, pixiObject: Container) { + if (Spine.clipper.isClipping() && pixiMaskSource) { + let mask = this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] as Graphics; + if (!mask) { + mask = new Graphics(); + this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] = mask; + this.addChild(mask); + } + if (!pixiMaskSource.computed) { + pixiMaskSource.computed = true; + const clippingAttachment = pixiMaskSource.slot.attachment as ClippingAttachment; + const world = Array.from(clippingAttachment.vertices); + clippingAttachment.computeWorldVertices(pixiMaskSource.slot, 0, clippingAttachment.worldVerticesLength, world, 0, 2); + mask.clear().lineStyle(0).beginFill(0x000000).drawPolygon(world); + } + pixiObject.mask = mask; + } else if (pixiObject.mask) { + pixiObject.mask = null; + } + } private renderMeshes (): void { this.resetMeshes(); let triangles: Array | null = null; let uvs: NumberArrayLike | null = null; + let pixiMaskSource: PixiMaskSource | null = null; const drawOrder = this.skeleton.drawOrder; for (let i = 0, n = drawOrder.length, slotObjectsCounter = 0; i < n; i++) { @@ -317,15 +357,16 @@ export class Spine extends Container { 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; + this.updatePixiObject(pixiObject, slot, zIndex + 1); slotObjectsCounter++; + this.updateAndSetPixiMask(pixiMaskSource, pixiObject); } const useDarkColor = slot.darkColor != null; const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE; if (!slot.bone.active) { Spine.clipper.clipEndWithSlot(slot); + this.pixiMaskCleanup(slot); continue; } const attachment = slot.getAttachment(); @@ -353,9 +394,11 @@ export class Spine extends Container { texture = mesh.region?.texture; } else if (attachment instanceof ClippingAttachment) { Spine.clipper.clipStart(slot, attachment); + pixiMaskSource = { slot, computed: false }; continue; } else { Spine.clipper.clipEndWithSlot(slot); + this.pixiMaskCleanup(slot); continue; } if (texture != null) { @@ -423,6 +466,7 @@ export class Spine extends Container { } Spine.clipper.clipEndWithSlot(slot); + this.pixiMaskCleanup(slot); } Spine.clipper.clipEnd(); } @@ -542,6 +586,11 @@ export class Spine extends Container { } } +type PixiMaskSource = { + slot: Slot, + computed: boolean, // prevent to reculaculate vertices for a mask clipping multiple pixi objects +} + Skeleton.yDown = true; /**