headless.js 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. import CanvasKitInit from "canvaskit-wasm";
  2. import UPNG from "@pdf-lib/upng"
  3. import path from "node:path";
  4. import { fileURLToPath } from "node:url";
  5. import { readFileSync, writeFileSync } from "node:fs"
  6. import { loadTextureAtlas, SkeletonRenderer, loadSkeletonData, SkeletonDrawable } from "../dist/index.js"
  7. // Get the current directory
  8. const __filename = fileURLToPath(import.meta.url);
  9. const __dirname = path.dirname(__filename);
  10. // This app loads the Spineboy skeleton and its atlas, then renders Spineboy's "portal" animation
  11. // at 30 fps to individual frames, which are then encoded as an animated PNG (APNG), which is
  12. // written to "output.png"
  13. async function main() {
  14. // Initialize CanvasKit and create a surface and canvas.
  15. const ck = await CanvasKitInit();
  16. const surface = ck.MakeSurface(600, 400);
  17. if (!surface) throw new Error();
  18. // Load atlas
  19. const atlas = await loadTextureAtlas(ck, `${__dirname}/../../assets/spineboy.atlas`, async (path) => readFileSync(path));
  20. // Load the skeleton data
  21. const skeletonData = await loadSkeletonData(`${__dirname}/../../assets/spineboy-pro.skel`, atlas, async (path) => readFileSync(path));
  22. // Create a SkeletonDrawable
  23. const drawable = new SkeletonDrawable(skeletonData);
  24. // Scale and position the skeleton
  25. drawable.skeleton.x = 300;
  26. drawable.skeleton.y = 380;
  27. drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.5;
  28. // Set the "hoverboard" animation on track one
  29. drawable.animationState.setAnimation(0, "hoverboard", true);
  30. // Create a skeleton renderer to render the skeleton to the canvas with
  31. const renderer = new SkeletonRenderer(ck);
  32. // Render the full animation in 1/30 second steps (30fps) and save it to an APNG
  33. const animationDuration = skeletonData.findAnimation("hoverboard")?.duration ?? 0;
  34. const FRAME_TIME = 1 / 30; // 30 FPS
  35. let deltaTime = 0;
  36. const frames = [];
  37. const imageInfo = { width: 600, height: 400, colorType: ck.ColorType.RGBA_8888, alphaType: ck.AlphaType.Unpremul, colorSpace: ck.ColorSpace.SRGB };
  38. const pixelArray = ck.Malloc(Uint8Array, imageInfo.width * imageInfo.height * 4);
  39. for (let time = 0; time <= animationDuration; time += deltaTime) {
  40. // Get the canvas object to render to the surface to
  41. const canvas = surface.getCanvas();
  42. // Clear the canvas
  43. canvas.clear(ck.WHITE);
  44. // Update the drawable, which will advance the animation(s)
  45. // apply them to the skeleton, and update the skeleton's pose.
  46. drawable.update(deltaTime);
  47. // Render the skeleton to the canvas
  48. renderer.render(canvas, drawable)
  49. // Read the pixels of the current frame and store it.
  50. canvas.readPixels(0, 0, imageInfo, pixelArray);
  51. frames.push(new Uint8Array(pixelArray.toTypedArray()).buffer.slice(0));
  52. // First frame has deltaTime 0, subsequent use FRAME_TIME
  53. deltaTime = FRAME_TIME;
  54. }
  55. const apng = UPNG.default.encode(frames, 600, 400, 0, frames.map(() => FRAME_TIME * 1000));
  56. writeFileSync('output.png', Buffer.from(apng));
  57. }
  58. main();