diff --git a/spine-ts/spine-canvaskit/example/headless.js b/spine-ts/spine-canvaskit/example/headless.js index ef861a3de..57246d0d3 100644 --- a/spine-ts/spine-canvaskit/example/headless.js +++ b/spine-ts/spine-canvaskit/example/headless.js @@ -1,9 +1,9 @@ -import * as fs from "fs" -import { fileURLToPath } from 'url'; -import path from 'path'; import CanvasKitInit from "canvaskit-wasm"; import UPNG from "@pdf-lib/upng" -import {loadTextureAtlas, SkeletonRenderer, Skeleton, SkeletonBinary, AnimationState, AnimationStateData, AtlasAttachmentLoader, Physics, loadSkeletonData, SkeletonDrawable} from "../dist/index.js" +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { readFileSync, writeFileSync } from "node:fs" +import { loadTextureAtlas, SkeletonRenderer, loadSkeletonData, SkeletonDrawable } from "../dist/index.js" // Get the current directory const __filename = fileURLToPath(import.meta.url); @@ -19,10 +19,10 @@ async function main() { if (!surface) throw new Error(); // 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) => readFileSync(path)); // Load the skeleton data - const skeletonData = await loadSkeletonData(__dirname + "/../../assets/spineboy-pro.skel", atlas, async (path) => fs.readFileSync(path)); + const skeletonData = await loadSkeletonData(`${__dirname}/../../assets/spineboy-pro.skel`, atlas, async (path) => readFileSync(path)); // Create a SkeletonDrawable const drawable = new SkeletonDrawable(skeletonData); @@ -68,7 +68,7 @@ async function main() { } const apng = UPNG.default.encode(frames, 600, 400, 0, frames.map(() => FRAME_TIME * 1000)); - fs.writeFileSync('output.png', Buffer.from(apng)); + writeFileSync('output.png', Buffer.from(apng)); } main(); diff --git a/spine-ts/spine-canvaskit/src/index.ts b/spine-ts/spine-canvaskit/src/index.ts index 862c63cb1..68f0467a8 100644 --- a/spine-ts/spine-canvaskit/src/index.ts +++ b/spine-ts/spine-canvaskit/src/index.ts @@ -35,7 +35,8 @@ import { AtlasAttachmentLoader, BlendMode, ClippingAttachment, - Color, + type Color, + MathUtils, MeshAttachment, type NumberArrayLike, Physics, @@ -194,9 +195,9 @@ export class SkeletonDrawable { public readonly skeleton: Skeleton; public readonly animationState: AnimationState; - /** - * Constructs a new drawble from the skeleton data. - */ + /** + * Constructs a new drawble from the skeleton data. + */ constructor (skeletonData: SkeletonData) { this.skeleton = new Skeleton(skeletonData); this.animationState = new AnimationState( @@ -204,13 +205,13 @@ export class SkeletonDrawable { ); } - /** - * 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. - */ + /** + * 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); @@ -224,24 +225,22 @@ export class SkeletonDrawable { */ export class SkeletonRenderer { private clipper = new SkeletonClipping(); - private tempColor = new Color(); - private tempColor2 = new Color(); private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0]; private scratchPositions = Utils.newFloatArray(100); - private scratchColors = Utils.newFloatArray(100); private scratchUVs = Utils.newFloatArray(100); + private scratchColors = new Uint32Array(100 / 4); - /** - * Creates a new skeleton renderer. - * @param ck the {@link CanvasKit} instance returned by `CanvasKitInit()`. - */ + /** + * Creates a new skeleton renderer. + * @param ck the {@link CanvasKit} instance returned by `CanvasKitInit()`. + */ constructor (private ck: CanvasKit) { } - /** - * 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. - */ + /** + * 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; const clipper = this.clipper; @@ -257,50 +256,40 @@ export class SkeletonRenderer { const attachment = slot.getAttachment(); let positions = this.scratchPositions; - let colors = this.scratchColors; - let uvs: NumberArrayLike; - let texture: CanvasKitTexture; let triangles: Array; - let attachmentColor: Color; - let numVertices = 0; + let numVertices = 4; + if (attachment instanceof RegionAttachment) { - const region = attachment; - numVertices = 4; - region.computeWorldVertices(slot, positions, 0, 2); + attachment.computeWorldVertices(slot, positions, 0, 2); triangles = SkeletonRenderer.QUAD_TRIANGLES; - uvs = region.uvs as Float32Array; - texture = region.region ?.texture as CanvasKitTexture; - attachmentColor = region.color; } else if (attachment instanceof MeshAttachment) { - const mesh = attachment as MeshAttachment; - if (positions.length < mesh.worldVerticesLength) { - this.scratchPositions = Utils.newFloatArray(mesh.worldVerticesLength); + if (positions.length < attachment.worldVerticesLength) { + this.scratchPositions = Utils.newFloatArray(attachment.worldVerticesLength); positions = this.scratchPositions; } - numVertices = mesh.worldVerticesLength >> 1; - mesh.computeWorldVertices( + numVertices = attachment.worldVerticesLength >> 1; + attachment.computeWorldVertices( slot, 0, - mesh.worldVerticesLength, + attachment.worldVerticesLength, positions, 0, 2 ); - triangles = mesh.triangles; - texture = mesh.region ?.texture as CanvasKitTexture; - uvs = mesh.uvs as Float32Array; - attachmentColor = mesh.color; + triangles = attachment.triangles; } else if (attachment instanceof ClippingAttachment) { - const clip = attachment as ClippingAttachment; - clipper.clipStart(slot, clip); + clipper.clipStart(slot, attachment); continue; } else { clipper.clipEndWithSlot(slot); continue; } + const texture = attachment.region?.texture as CanvasKitTexture; if (texture) { + let uvs = attachment.uvs; let scaledUvs: NumberArrayLike; + let colors = this.scratchColors; if (clipper.isClipping()) { clipper.clipTrianglesUnpacked(positions, triangles, triangles.length, uvs); if (clipper.clippedVertices.length <= 0) { @@ -308,21 +297,16 @@ export class SkeletonRenderer { continue; } positions = clipper.clippedVertices; - uvs = clipper.clippedUVs; - scaledUvs = clipper.clippedUVs; + uvs = scaledUvs = clipper.clippedUVs; triangles = clipper.clippedTriangles; numVertices = clipper.clippedVertices.length / 2; - colors = Utils.newFloatArray(numVertices * 4); + colors = new Uint32Array(numVertices); } else { scaledUvs = this.scratchUVs; - if (this.scratchUVs.length < uvs.length) { - this.scratchUVs = Utils.newFloatArray(uvs.length); - scaledUvs = this.scratchUVs; - } - if (colors.length / 4 < numVertices) { - this.scratchColors = Utils.newFloatArray(numVertices * 4); - colors = this.scratchColors; - } + if (this.scratchUVs.length < uvs.length) + scaledUvs = this.scratchUVs = Utils.newFloatArray(uvs.length); + if (colors.length < numVertices) + colors = this.scratchColors = new Uint32Array(numVertices); } const ckImage = texture.getImage(); @@ -334,19 +318,19 @@ export class SkeletonRenderer { scaledUvs[i + 1] = uvs[i + 1] * height; } + const attachmentColor = attachment.color; const slotColor = slot.color; - const finalColor = this.tempColor; - finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r; - finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g; - finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b; - finalColor.a = skeletonColor.a * slotColor.a * attachmentColor.a; - for (let i = 0, n = numVertices * 4; i < n; i += 4) { - colors[i] = finalColor.r; - colors[i + 1] = finalColor.g; - colors[i + 2] = finalColor.b; - colors[i + 3] = finalColor.a; - } + // using Uint32Array for colors allows to avoid canvaskit to allocate one each time + // but colors need to be in canvaskit format. + // See: https://github.com/google/skia/blob/bb8c36fdf7b915a8c096e35e2f08109e477fe1b8/modules/canvaskit/color.js#L163 + const finalColor = ( + MathUtils.clamp(skeletonColor.a * slotColor.a * attachmentColor.a * 255, 0, 255) << 24 | + MathUtils.clamp(skeletonColor.r * slotColor.r * attachmentColor.r * 255, 0, 255) << 16 | + MathUtils.clamp(skeletonColor.g * slotColor.g * attachmentColor.g * 255, 0, 255) << 8 | + MathUtils.clamp(skeletonColor.b * slotColor.b * attachmentColor.b * 255, 0, 255) << 0 + ) >>> 0; + for (let i = 0, n = numVertices; i < n; i++) colors[i] = finalColor; const vertices = this.ck.MakeVertices( this.ck.VertexMode.Triangles,