77 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as fs from "fs"
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"
// Get the current directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// This app loads the Spineboy skeleton and its atlas, then renders Spineboy's "portal" animation
// at 30 fps to individual frames, which are then encoded as an animated PNG (APNG), which is
// written to "output.png"
async function main() {
// Initialize CanvasKit and create a surface and canvas.
const ck = await CanvasKitInit();
const surface = ck.MakeSurface(600, 400);
if (!surface) throw new Error();
const canvas = surface.getCanvas();
// 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"));
// 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 an animation state to apply and mix one or more animations
const animationState = new AnimationState(new AnimationStateData(skeletonData));
animationState.setAnimation(0, "hoverboard", true);
// Create a skeleton renderer to render the skeleton with to the canvas
const renderer = new SkeletonRenderer(ck);
// Render the full animation in 1/30 second steps (30fps) and save it to an APNG
const animationDuration = skeletonData.findAnimation("hoverboard")?.duration ?? 0;
const FRAME_TIME = 1 / 30; // 30 FPS
let deltaTime = 0;
const frames = [];
const imageInfo = { width: 600, height: 400, colorType: ck.ColorType.RGBA_8888, alphaType: ck.AlphaType.Unpremul, colorSpace: ck.ColorSpace.SRGB };
const pixelArray = ck.Malloc(Uint8Array, imageInfo.width * imageInfo.height * 4);
for (let time = 0; time <= animationDuration; time += deltaTime) {
// 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);
// Render the skeleton to the canvas
renderer.render(canvas, skeleton)
// Read the pixels of the current frame and store it.
canvas.readPixels(0, 0, imageInfo, pixelArray);
frames.push(new Uint8Array(pixelArray.toTypedArray()).buffer.slice(0));
// First frame has deltaTime 0, subsequent use FRAME_TIME
deltaTime = FRAME_TIME;
}
const apng = UPNG.default.encode(frames, 600, 400, 0, frames.map(() => FRAME_TIME * 1000));
fs.writeFileSync('output.png', Buffer.from(apng));
}
main();