Browse Source

[canvaskit] SkeletonDrawble, simplified examples.

Mario Zechner 1 year ago
parent
commit
f80ac86bbf

+ 17 - 21
spine-ts/spine-canvaskit/example/headless.js

@@ -3,7 +3,7 @@ import { fileURLToPath } from 'url';
 import path from 'path';
 import path from 'path';
 import CanvasKitInit from "canvaskit-wasm/bin/canvaskit.js";
 import CanvasKitInit from "canvaskit-wasm/bin/canvaskit.js";
 import UPNG from "@pdf-lib/upng"
 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
 // Get the current directory
 const __filename = fileURLToPath(import.meta.url);
 const __filename = fileURLToPath(import.meta.url);
@@ -22,21 +22,21 @@ async function main() {
     // Load atlas
     // Load atlas
     const atlas = await loadTextureAtlas(ck, __dirname + "/assets/spineboy.atlas", async (path) => fs.readFileSync(path));
     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);
     const renderer = new SkeletonRenderer(ck);
 
 
     // Render the full animation in 1/30 second steps (30fps) and save it to an APNG
     // 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
         // Clear the canvas
         canvas.clear(ck.WHITE);
         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
         // Render the skeleton to the canvas
-        renderer.render(canvas, skeleton)
+        renderer.render(canvas, drawable)
 
 
         // Read the pixels of the current frame and store it.
         // Read the pixels of the current frame and store it.
         canvas.readPixels(0, 0, imageInfo, pixelArray);
         canvas.readPixels(0, 0, imageInfo, pixelArray);

+ 15 - 18
spine-ts/spine-canvaskit/example/index.html

@@ -37,19 +37,16 @@
     const atlas = await spine.loadTextureAtlas(ck, "assets/spineboy.atlas", readFile);
     const atlas = await spine.loadTextureAtlas(ck, "assets/spineboy.atlas", readFile);
 
 
     // Load skeleton data
     // 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
     // Create a skeleton renderer to render the skeleton with to the canvas
     const renderer = new spine.SkeletonRenderer(ck);
     const renderer = new spine.SkeletonRenderer(ck);
@@ -64,14 +61,14 @@
         const deltaTime = (now - lastTime) / 1000;
         const deltaTime = (now - lastTime) / 1000;
         lastTime = now;
         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);
     }
     }
     surface.requestAnimationFrame(drawFrame);
     surface.requestAnimationFrame(drawFrame);

+ 74 - 14
spine-ts/spine-canvaskit/src/index.ts

@@ -1,7 +1,7 @@
 export * from "@esotericsoftware/spine-core";
 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;
 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 {
     getImage(): CanvasKitImage {
         return this._image;
         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<Buffer>` 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<Buffer>): Promise<TextureAtlas> {
 export async function loadTextureAtlas(ck: CanvasKit, atlasFile: string, readFile: (path: string) => Promise<Buffer>): Promise<TextureAtlas> {
     const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile)));
     const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile)));
     const slashIndex = atlasFile.lastIndexOf("/");
     const slashIndex = atlasFile.lastIndexOf("/");
@@ -81,6 +85,51 @@ export async function loadTextureAtlas(ck: CanvasKit, atlasFile: string, readFil
     return atlas;
     return atlas;
 }
 }
 
 
+/**
+ * Loads a {@link SkeletonData} from the given file path (`.json` or `.skel`) using the `readFile(path: string): Promise<Buffer>` function.
+ * Attachments will be looked up in the provided atlas.
+ */
+export async function loadSkeletonData(skeletonFile: string, atlas: TextureAtlas, readFile: (path: string) => Promise<Buffer>): Promise<SkeletonData> {
+    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 {
 export class SkeletonRenderer {
     private clipper = new SkeletonClipping();
     private clipper = new SkeletonClipping();
     private tempColor = new Color();
     private tempColor = new Color();
@@ -88,9 +137,20 @@ export class SkeletonRenderer {
     private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
     private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
     private scratchPositions = Utils.newFloatArray(100);
     private scratchPositions = Utils.newFloatArray(100);
     private scratchColors = 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) {}
     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 clipper = this.clipper;
 		let drawOrder = skeleton.drawOrder;
 		let drawOrder = skeleton.drawOrder;
 		let skeletonColor = skeleton.color;
 		let skeletonColor = skeleton.color;