From f80ac86bbfeb0430df9ddb60573fa46ebf564c2f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 5 Jul 2024 19:03:25 +0200 Subject: [PATCH] [canvaskit] SkeletonDrawble, simplified examples. --- spine-ts/spine-canvaskit/example/headless.js | 38 ++++----- spine-ts/spine-canvaskit/example/index.html | 33 ++++---- spine-ts/spine-canvaskit/src/index.ts | 88 ++++++++++++++++---- 3 files changed, 106 insertions(+), 53 deletions(-) diff --git a/spine-ts/spine-canvaskit/example/headless.js b/spine-ts/spine-canvaskit/example/headless.js index 46c67afda..82fd7396f 100644 --- a/spine-ts/spine-canvaskit/example/headless.js +++ b/spine-ts/spine-canvaskit/example/headless.js @@ -3,7 +3,7 @@ import { fileURLToPath } from 'url'; import path from 'path'; import CanvasKitInit from "canvaskit-wasm/bin/canvaskit.js"; import UPNG from "@pdf-lib/upng" -import {loadTextureAtlas, SkeletonRenderer, Skeleton, SkeletonBinary, AnimationState, AnimationStateData, AtlasAttachmentLoader, Physics} from "../dist/index.js" +import {loadTextureAtlas, SkeletonRenderer, Skeleton, SkeletonBinary, AnimationState, AnimationStateData, AtlasAttachmentLoader, Physics, loadSkeletonData, SkeletonDrawable} from "../dist/index.js" // Get the current directory const __filename = fileURLToPath(import.meta.url); @@ -22,21 +22,21 @@ async function main() { // Load atlas const atlas = await loadTextureAtlas(ck, __dirname + "/assets/spineboy.atlas", async (path) => fs.readFileSync(path)); - // Load skeleton data - const binary = new SkeletonBinary(new AtlasAttachmentLoader(atlas)); - const skeletonData = binary.readSkeletonData(fs.readFileSync(__dirname + "/assets/spineboy-pro.skel")); + // Load the skeleton data + const skeletonData = await loadSkeletonData(__dirname + "/assets/spineboy-pro.skel", atlas, async (path) => fs.readFileSync(path)); - // Create a skeleton and scale and position it. - const skeleton = new Skeleton(skeletonData); - skeleton.scaleX = skeleton.scaleY = 0.5; - skeleton.x = 300; - skeleton.y = 380; + // Create a SkeletonDrawable + const drawable = new SkeletonDrawable(skeletonData); - // Create an animation state to apply and mix one or more animations - const animationState = new AnimationState(new AnimationStateData(skeletonData)); - animationState.setAnimation(0, "hoverboard", true); + // Scale and position the skeleton + drawable.skeleton.x = 300; + drawable.skeleton.y = 380; + drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.5; - // Create a skeleton renderer to render the skeleton with to the canvas + // Set the "hoverboard" animation on track one + drawable.animationState.setAnimation(0, "hoverboard", true); + + // Create a skeleton renderer to render the skeleton to the canvas with const renderer = new SkeletonRenderer(ck); // Render the full animation in 1/30 second steps (30fps) and save it to an APNG @@ -50,16 +50,12 @@ async function main() { // Clear the canvas canvas.clear(ck.WHITE); - // Update and apply the animations to the skeleton - animationState.update(deltaTime); - animationState.apply(skeleton); - - // Update the skeleton time for physics, and its world transforms - skeleton.update(deltaTime); - skeleton.updateWorldTransform(Physics.update); + // Update the drawable, which will advance the animation(s) + // apply them to the skeleton, and update the skeleton's pose. + drawable.update(deltaTime); // Render the skeleton to the canvas - renderer.render(canvas, skeleton) + renderer.render(canvas, drawable) // Read the pixels of the current frame and store it. canvas.readPixels(0, 0, imageInfo, pixelArray); diff --git a/spine-ts/spine-canvaskit/example/index.html b/spine-ts/spine-canvaskit/example/index.html index ca87197da..5bbb10009 100644 --- a/spine-ts/spine-canvaskit/example/index.html +++ b/spine-ts/spine-canvaskit/example/index.html @@ -37,19 +37,16 @@ const atlas = await spine.loadTextureAtlas(ck, "assets/spineboy.atlas", readFile); // Load skeleton data - const binary = new spine.SkeletonBinary(new spine.AtlasAttachmentLoader(atlas)); - const skeletonData = binary.readSkeletonData(await readFile("assets/spineboy-pro.skel")); + const skeletonData = await spine.loadSkeletonData("assets/spineboy-pro.skel", atlas, readFile); - // Create a skeleton and scale and position it. - const skeleton = new spine.Skeleton(skeletonData); - skeleton.scaleX = skeleton.scaleY = 0.4; - skeleton.x = 300; - skeleton.y = 380; - skeleton.setToSetupPose(); + // Create a drawable and scale and position the skeleton + const drawable = new spine.SkeletonDrawable(skeletonData); + drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.4; + drawable.skeleton.x = 300; + drawable.skeleton.y = 380; - // Create an animation state to apply and mix one or more animations - const animationState = new spine.AnimationState(new spine.AnimationStateData(skeletonData)); - animationState.setAnimation(0, "hoverboard", true); + // Set the "hoverboard" animation on the first track of the animation state. + drawable.animationState.setAnimation(0, "hoverboard", true); // Create a skeleton renderer to render the skeleton with to the canvas const renderer = new spine.SkeletonRenderer(ck); @@ -64,14 +61,14 @@ const deltaTime = (now - lastTime) / 1000; lastTime = now; - // Update and apply the animations to the skeleton - animationState.update(deltaTime); - animationState.apply(skeleton); + // Update the drawable, which will advance the animation(s) + // apply them to the skeleton, and update the skeleton's pose. + drawable.update(deltaTime); - // Update the skeleton time for physics, and its world transforms - skeleton.update(deltaTime); - skeleton.updateWorldTransform(spine.Physics.update); - renderer.render(canvas, skeleton); + // Render the skeleton to the canvas + renderer.render(canvas, drawable); + + // Request the next frame surface.requestAnimationFrame(drawFrame); } surface.requestAnimationFrame(drawFrame); diff --git a/spine-ts/spine-canvaskit/src/index.ts b/spine-ts/spine-canvaskit/src/index.ts index 794819bd7..f05f77168 100644 --- a/spine-ts/spine-canvaskit/src/index.ts +++ b/spine-ts/spine-canvaskit/src/index.ts @@ -1,7 +1,7 @@ export * from "@esotericsoftware/spine-core"; -import { BlendMode, ClippingAttachment, Color, MeshAttachment, NumberArrayLike, RegionAttachment, Skeleton, SkeletonClipping, Texture, TextureAtlas, TextureFilter, TextureWrap, Utils } from "@esotericsoftware/spine-core"; -import { Canvas, CanvasKit, Image, Paint, Shader, BlendMode as CanvasKitBlendMode } from "canvaskit-wasm"; +import { AnimationState, AnimationStateData, AtlasAttachmentLoader, BlendMode, ClippingAttachment, Color, MeshAttachment, NumberArrayLike, Physics, RegionAttachment, Skeleton, SkeletonBinary, SkeletonClipping, SkeletonData, SkeletonJson, Texture, TextureAtlas, TextureFilter, TextureWrap, Utils } from "@esotericsoftware/spine-core"; +import { Canvas, Surface, CanvasKit, Image, Paint, Shader, BlendMode as CanvasKitBlendMode } from "canvaskit-wasm"; Skeleton.yDown = true; @@ -18,7 +18,17 @@ function toCkBlendMode(ck: CanvasKit, blendMode: BlendMode) { } } -export class CanvasKitTexture extends Texture { +function bufferToUtf8String(buffer: any) { + if (typeof Buffer !== 'undefined') { + return buffer.toString('utf-8'); + } else if (typeof TextDecoder !== 'undefined') { + return new TextDecoder('utf-8').decode(buffer); + } else { + throw new Error('Unsupported environment'); + } +} + +class CanvasKitTexture extends Texture { getImage(): CanvasKitImage { return this._image; } @@ -60,16 +70,10 @@ export class CanvasKitTexture extends Texture { } } -function bufferToUtf8String(buffer: any) { - if (typeof Buffer !== 'undefined') { - return buffer.toString('utf-8'); - } else if (typeof TextDecoder !== 'undefined') { - return new TextDecoder('utf-8').decode(buffer); - } else { - throw new Error('Unsupported environment'); - } -} - +/** + * Loads a {@link TextureAtlas} and its atlas page images from the given file path using the `readFile(path: string): Promise` function. + * Throws an `Error` if the file or one of the atlas page images could not be loaded. + */ export async function loadTextureAtlas(ck: CanvasKit, atlasFile: string, readFile: (path: string) => Promise): Promise { const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile))); const slashIndex = atlasFile.lastIndexOf("/"); @@ -81,6 +85,51 @@ export async function loadTextureAtlas(ck: CanvasKit, atlasFile: string, readFil return atlas; } +/** + * Loads a {@link SkeletonData} from the given file path (`.json` or `.skel`) using the `readFile(path: string): Promise` function. + * Attachments will be looked up in the provided atlas. + */ +export async function loadSkeletonData(skeletonFile: string, atlas: TextureAtlas, readFile: (path: string) => Promise): Promise { + const attachmentLoader = new AtlasAttachmentLoader(atlas); + const loader = skeletonFile.endsWith(".json") ? new SkeletonJson(attachmentLoader) : new SkeletonBinary(attachmentLoader); + const skeletonData = loader.readSkeletonData(await readFile(skeletonFile)); + return skeletonData; +} + +/** + * Manages a {@link Skeleton} and its associated {@link AnimationState}. A drawable is constructed from a {@link SkeletonData}, which can + * be shared by any number of drawables. + */ +export class SkeletonDrawable { + public readonly skeleton: Skeleton; + public readonly animationState: AnimationState; + + /** + * Constructs a new drawble from the skeleton data. + */ + constructor(skeletonData: SkeletonData) { + this.skeleton = new Skeleton(skeletonData); + this.animationState = new AnimationState(new AnimationStateData(skeletonData)); + } + + /** + * Updates the animation state and skeleton time by the delta time. Applies the + * animations to the skeleton and calculates the final pose of the skeleton. + * + * @param deltaTime the time since the last update in seconds + * @param physicsUpdate optional {@link Physics} update mode. + */ + update(deltaTime: number, physicsUpdate: Physics = Physics.update) { + this.animationState.update(deltaTime); + this.skeleton.update(deltaTime); + this.animationState.apply(this.skeleton); + this.skeleton.updateWorldTransform(physicsUpdate); + } +} + +/** + * Renders a {@link Skeleton} or {@link SkeletonDrawable} to a CanvasKit {@link Canvas}. + */ export class SkeletonRenderer { private clipper = new SkeletonClipping(); private tempColor = new Color(); @@ -88,9 +137,20 @@ export class SkeletonRenderer { private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0]; private scratchPositions = Utils.newFloatArray(100); private scratchColors = Utils.newFloatArray(100); + + /** + * Creates a new skeleton renderer. + * @param ck the {@link CanvasKit} instance returned by `CanvasKitInit()`. + */ constructor(private ck: CanvasKit) {} - render(canvas: Canvas, skeleton: Skeleton) { + /** + * Renders a skeleton or skeleton drawable in its current pose to the canvas. + * @param canvas the canvas to render to. + * @param skeleton the skeleton or drawable to render. + */ + render(canvas: Canvas, skeleton: Skeleton | SkeletonDrawable) { + if (skeleton instanceof SkeletonDrawable) skeleton = skeleton.skeleton; let clipper = this.clipper; let drawOrder = skeleton.drawOrder; let skeletonColor = skeleton.color;