[canvaskit] SkeletonDrawble, simplified examples.

This commit is contained in:
Mario Zechner 2024-07-05 19:03:25 +02:00
parent 40ce74ff2d
commit f80ac86bbf
3 changed files with 106 additions and 53 deletions

View File

@ -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 // Load the skeleton data
const binary = new SkeletonBinary(new AtlasAttachmentLoader(atlas)); const skeletonData = await loadSkeletonData(__dirname + "/assets/spineboy-pro.skel", atlas, async (path) => fs.readFileSync(path));
const skeletonData = binary.readSkeletonData(fs.readFileSync(__dirname + "/assets/spineboy-pro.skel"));
// Create a skeleton and scale and position it. // Create a SkeletonDrawable
const skeleton = new Skeleton(skeletonData); const drawable = new SkeletonDrawable(skeletonData);
skeleton.scaleX = skeleton.scaleY = 0.5;
skeleton.x = 300;
skeleton.y = 380;
// Create an animation state to apply and mix one or more animations // Scale and position the skeleton
const animationState = new AnimationState(new AnimationStateData(skeletonData)); drawable.skeleton.x = 300;
animationState.setAnimation(0, "hoverboard", true); 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 // Update the drawable, which will advance the animation(s)
animationState.update(deltaTime); // apply them to the skeleton, and update the skeleton's pose.
animationState.apply(skeleton); drawable.update(deltaTime);
// Update the skeleton time for physics, and its world transforms
skeleton.update(deltaTime);
skeleton.updateWorldTransform(Physics.update);
// 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);

View File

@ -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 = await spine.loadSkeletonData("assets/spineboy-pro.skel", atlas, readFile);
const skeletonData = binary.readSkeletonData(await readFile("assets/spineboy-pro.skel"));
// Create a skeleton and scale and position it. // Create a drawable and scale and position the skeleton
const skeleton = new spine.Skeleton(skeletonData); const drawable = new spine.SkeletonDrawable(skeletonData);
skeleton.scaleX = skeleton.scaleY = 0.4; drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.4;
skeleton.x = 300; drawable.skeleton.x = 300;
skeleton.y = 380; drawable.skeleton.y = 380;
skeleton.setToSetupPose();
// Create an animation state to apply and mix one or more animations // Set the "hoverboard" animation on the first track of the animation state.
const animationState = new spine.AnimationState(new spine.AnimationStateData(skeletonData)); drawable.animationState.setAnimation(0, "hoverboard", true);
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 // Update the drawable, which will advance the animation(s)
animationState.update(deltaTime); // apply them to the skeleton, and update the skeleton's pose.
animationState.apply(skeleton); drawable.update(deltaTime);
// Update the skeleton time for physics, and its world transforms // Render the skeleton to the canvas
skeleton.update(deltaTime); renderer.render(canvas, drawable);
skeleton.updateWorldTransform(spine.Physics.update);
renderer.render(canvas, skeleton); // Request the next frame
surface.requestAnimationFrame(drawFrame); surface.requestAnimationFrame(drawFrame);
} }
surface.requestAnimationFrame(drawFrame); surface.requestAnimationFrame(drawFrame);

View File

@ -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 { 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, CanvasKit, Image, Paint, Shader, BlendMode as CanvasKitBlendMode } from "canvaskit-wasm"; 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') { * Loads a {@link TextureAtlas} and its atlas page images from the given file path using the `readFile(path: string): Promise<Buffer>` function.
return buffer.toString('utf-8'); * Throws an `Error` if the file or one of the atlas page images could not be loaded.
} else if (typeof TextDecoder !== 'undefined') { */
return new TextDecoder('utf-8').decode(buffer);
} else {
throw new Error('Unsupported environment');
}
}
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;