diff --git a/spine-ts/spine-canvaskit/example/index.html b/spine-ts/spine-canvaskit/example/index.html index c17155589..16f03d7f1 100644 --- a/spine-ts/spine-canvaskit/example/index.html +++ b/spine-ts/spine-canvaskit/example/index.html @@ -34,7 +34,6 @@ canvasElement.width = canvasElement.clientWidth * dpr; canvasElement.height = canvasElement.clientHeight * dpr; - // Initialize CanvasKit and create a surface from the Canvas element to draw to const ck = await CanvasKitInit(); const surface = ck.MakeCanvasSurface('foo'); diff --git a/spine-ts/spine-canvaskit/src/index.ts b/spine-ts/spine-canvaskit/src/index.ts index 86e3d3aa7..12d760be8 100644 --- a/spine-ts/spine-canvaskit/src/index.ts +++ b/spine-ts/spine-canvaskit/src/index.ts @@ -1,103 +1,170 @@ export * 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, Surface, 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; -type CanvasKitImage = { shaders: Shader[], paintPerBlendMode: Map, image: Image }; +type CanvasKitImage = { + shaders: Shader[]; + paintPerBlendMode: Map; + image: Image; +}; // CanvasKit blend modes for premultiplied alpha -function toCkBlendMode(ck: CanvasKit, blendMode: BlendMode) { - switch(blendMode) { - case BlendMode.Normal: return ck.BlendMode.SrcOver; - case BlendMode.Additive: return ck.BlendMode.Plus; - case BlendMode.Multiply: return ck.BlendMode.SrcOver; - case BlendMode.Screen: return ck.BlendMode.Screen; - default: return ck.BlendMode.SrcOver; - } +function toCkBlendMode (ck: CanvasKit, blendMode: BlendMode) { + switch (blendMode) { + case BlendMode.Normal: + return ck.BlendMode.SrcOver; + case BlendMode.Additive: + return ck.BlendMode.Plus; + case BlendMode.Multiply: + return ck.BlendMode.SrcOver; + case BlendMode.Screen: + return ck.BlendMode.Screen; + default: + return ck.BlendMode.SrcOver; + } } -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'); - } +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 { - return this._image; - } + getImage (): CanvasKitImage { + return this._image; + } - setFilters(minFilter: TextureFilter, magFilter: TextureFilter): void { - } + setFilters (minFilter: TextureFilter, magFilter: TextureFilter): void { } - setWraps(uWrap: TextureWrap, vWrap: TextureWrap): void { - } + setWraps (uWrap: TextureWrap, vWrap: TextureWrap): void { } - dispose(): void { - const data: CanvasKitImage = this._image; - for (const paint of data.paintPerBlendMode.values()) { - paint.delete(); - } - for (const shader of data.shaders) { - shader.delete(); - } - data.image.delete(); - this._image = null; - } + dispose (): void { + const data: CanvasKitImage = this._image; + for (const paint of data.paintPerBlendMode.values()) { + paint.delete(); + } + for (const shader of data.shaders) { + shader.delete(); + } + data.image.delete(); + this._image = null; + } - static async fromFile(ck: CanvasKit, path: string, readFile: (path: string) => Promise): Promise { - const imgData = await readFile(path); - if (!imgData) throw new Error(`Could not load image ${path}`); - const image = ck.MakeImageFromEncoded(imgData); - if (!image) throw new Error(`Could not load image ${path}`); - const paintPerBlendMode = new Map(); - const shaders: Shader[] = []; - for (const blendMode of [BlendMode.Normal, BlendMode.Additive, BlendMode.Multiply, BlendMode.Screen]) { - const paint = new ck.Paint(); - const shader = image.makeShaderOptions(ck.TileMode.Clamp, ck.TileMode.Clamp, ck.FilterMode.Linear, ck.MipmapMode.Linear); - paint.setShader(shader); - paint.setBlendMode(toCkBlendMode(ck, blendMode)); - paintPerBlendMode.set(blendMode, paint); - shaders.push(shader); - } - return new CanvasKitTexture({ shaders, paintPerBlendMode, image }); - } + static async fromFile ( + ck: CanvasKit, + path: string, + readFile: (path: string) => Promise + ): Promise { + const imgData = await readFile(path); + if (!imgData) throw new Error(`Could not load image ${path}`); + const image = ck.MakeImageFromEncoded(imgData); + if (!image) throw new Error(`Could not load image ${path}`); + const paintPerBlendMode = new Map(); + const shaders: Shader[] = []; + for (const blendMode of [ + BlendMode.Normal, + BlendMode.Additive, + BlendMode.Multiply, + BlendMode.Screen, + ]) { + const paint = new ck.Paint(); + const shader = image.makeShaderOptions( + ck.TileMode.Clamp, + ck.TileMode.Clamp, + ck.FilterMode.Linear, + ck.MipmapMode.Linear + ); + paint.setShader(shader); + paint.setBlendMode(toCkBlendMode(ck, blendMode)); + paintPerBlendMode.set(blendMode, paint); + shaders.push(shader); + } + return new CanvasKitTexture({ shaders, paintPerBlendMode, image }); + } } /** * Loads a {@link TextureAtlas} and its atlas page images from the given file path using the `readFile(path: string): Promise` 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): Promise { - const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile))); - const slashIndex = atlasFile.lastIndexOf("/"); - const parentDir = slashIndex >= 0 ? atlasFile.substring(0, slashIndex + 1) + "/" : ""; - for (const page of atlas.pages) { - const texture = await CanvasKitTexture.fromFile(ck, parentDir + page.name, readFile); - page.setTexture(texture); - } - return atlas; +export async function loadTextureAtlas ( + ck: CanvasKit, + atlasFile: string, + readFile: (path: string) => Promise +): Promise { + const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile))); + const slashIndex = atlasFile.lastIndexOf("/"); + const parentDir = + slashIndex >= 0 ? atlasFile.substring(0, slashIndex + 1) + "/" : ""; + for (const page of atlas.pages) { + const texture = await CanvasKitTexture.fromFile( + ck, + parentDir + page.name, + readFile + ); + page.setTexture(texture); + } + return atlas; } /** * Loads a {@link SkeletonData} from the given file path (`.json` or `.skel`) using the `readFile(path: string): Promise` function. * Attachments will be looked up in the provided atlas. */ -export async function loadSkeletonData(skeletonFile: string, atlas: TextureAtlas, readFile: (path: string) => Promise): Promise { - const attachmentLoader = new AtlasAttachmentLoader(atlas); - const loader = skeletonFile.endsWith(".json") ? new SkeletonJson(attachmentLoader) : new SkeletonBinary(attachmentLoader); - let data = await readFile(skeletonFile); - if (skeletonFile.endsWith(".json")) { - data = bufferToUtf8String(data); - } - const skeletonData = loader.readSkeletonData(data); - return skeletonData; +export async function loadSkeletonData ( + skeletonFile: string, + atlas: TextureAtlas, + readFile: (path: string) => Promise, + scale = 1 +): Promise { + const attachmentLoader = new AtlasAttachmentLoader(atlas); + const loader = skeletonFile.endsWith(".json") + ? new SkeletonJson(attachmentLoader) + : new SkeletonBinary(attachmentLoader); + loader.scale = scale; + let data = await readFile(skeletonFile); + if (skeletonFile.endsWith(".json")) { + data = bufferToUtf8String(data); + } + const skeletonData = loader.readSkeletonData(data); + return skeletonData; } /** @@ -105,58 +172,60 @@ export async function loadSkeletonData(skeletonFile: string, atlas: TextureAtlas * be shared by any number of drawables. */ export class SkeletonDrawable { - public readonly skeleton: Skeleton; - public readonly animationState: AnimationState; + 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)); - } + /** + * 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); - } + /** + * 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 { - 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 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); - /** - * Creates a new skeleton renderer. - * @param ck the {@link CanvasKit} instance returned by `CanvasKitInit()`. - */ - constructor(private ck: CanvasKit) {} + /** + * 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. - */ - render(canvas: Canvas, skeleton: Skeleton | SkeletonDrawable) { - if (skeleton instanceof SkeletonDrawable) skeleton = skeleton.skeleton; - let clipper = this.clipper; + /** + * 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 drawOrder = skeleton.drawOrder; let skeletonColor = skeleton.color; @@ -165,20 +234,20 @@ export class SkeletonRenderer { if (!slot.bone.active) { clipper.clipEndWithSlot(slot); continue; - } + } let attachment = slot.getAttachment(); - let positions = this.scratchPositions; - let colors = this.scratchColors; - let uvs: NumberArrayLike; + let positions = this.scratchPositions; + let colors = this.scratchColors; + let uvs: NumberArrayLike; let texture: CanvasKitTexture; - let triangles: Array; - let attachmentColor: Color; - let numVertices = 0; + let triangles: Array; + let attachmentColor: Color; + let numVertices = 0; if (attachment instanceof RegionAttachment) { let region = attachment as RegionAttachment; - positions = positions.length < 8 ? Utils.newFloatArray(8) : positions; - numVertices = 4; + positions = positions.length < 8 ? Utils.newFloatArray(8) : positions; + numVertices = 4; region.computeWorldVertices(slot, positions, 0, 2); triangles = SkeletonRenderer.QUAD_TRIANGLES; uvs = region.uvs as Float32Array; @@ -186,9 +255,19 @@ export class SkeletonRenderer { attachmentColor = region.color; } else if (attachment instanceof MeshAttachment) { let mesh = attachment as MeshAttachment; - positions = positions.length < mesh.worldVerticesLength ? Utils.newFloatArray(mesh.worldVerticesLength) : positions; - numVertices = mesh.worldVerticesLength >> 1; - mesh.computeWorldVertices(slot, 0, mesh.worldVerticesLength, positions, 0, 2); + positions = + positions.length < mesh.worldVerticesLength + ? Utils.newFloatArray(mesh.worldVerticesLength) + : positions; + numVertices = mesh.worldVerticesLength >> 1; + mesh.computeWorldVertices( + slot, + 0, + mesh.worldVerticesLength, + positions, + 0, + 2 + ); triangles = mesh.triangles; texture = mesh.region?.texture as CanvasKitTexture; uvs = mesh.uvs as Float32Array; @@ -204,43 +283,63 @@ export class SkeletonRenderer { if (texture) { if (clipper.isClipping()) { - clipper.clipTrianglesUnpacked(positions, triangles, triangles.length, uvs); - positions = clipper.clippedVertices; - uvs = clipper.clippedUVs; - triangles = clipper.clippedTriangles; + clipper.clipTrianglesUnpacked( + positions, + triangles, + triangles.length, + uvs + ); + positions = clipper.clippedVertices; + uvs = clipper.clippedUVs; + triangles = clipper.clippedTriangles; } - let slotColor = slot.color; + let slotColor = slot.color; let 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; - if (colors.length / 4 < numVertices) colors = Utils.newFloatArray(numVertices * 4); - 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; - } + if (colors.length / 4 < numVertices) + colors = Utils.newFloatArray(numVertices * 4); + 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; + } - const scaledUvs = this.scratchUVs.length < uvs.length ? Utils.newFloatArray(uvs.length) : this.scratchUVs; - const width = texture.getImage().image.width(); - const height = texture.getImage().image.height(); - for (let i = 0; i < uvs.length; i+=2) { - scaledUvs[i] = uvs[i] * width; - scaledUvs[i + 1] = uvs[i + 1] * height; - } + const scaledUvs = + this.scratchUVs.length < uvs.length + ? Utils.newFloatArray(uvs.length) + : this.scratchUVs; + const width = texture.getImage().image.width(); + const height = texture.getImage().image.height(); + for (let i = 0; i < uvs.length; i += 2) { + scaledUvs[i] = uvs[i] * width; + scaledUvs[i + 1] = uvs[i + 1] * height; + } - const blendMode = slot.data.blendMode; - const vertices = this.ck.MakeVertices(this.ck.VertexMode.Triangles, positions, scaledUvs, colors, triangles, false); - canvas.drawVertices(vertices, this.ck.BlendMode.Modulate, texture.getImage().paintPerBlendMode.get(blendMode)!); - vertices.delete(); + const blendMode = slot.data.blendMode; + const vertices = this.ck.MakeVertices( + this.ck.VertexMode.Triangles, + positions, + scaledUvs, + colors, + triangles, + false + ); + canvas.drawVertices( + vertices, + this.ck.BlendMode.Modulate, + texture.getImage().paintPerBlendMode.get(blendMode)! + ); + vertices.delete(); } clipper.clipEndWithSlot(slot); } clipper.clipEnd(); - } -} \ No newline at end of file + } +} diff --git a/spine-ts/spine-core/src/SkeletonBinary.ts b/spine-ts/spine-core/src/SkeletonBinary.ts index 7e6621a84..518d1ec51 100644 --- a/spine-ts/spine-core/src/SkeletonBinary.ts +++ b/spine-ts/spine-core/src/SkeletonBinary.ts @@ -64,7 +64,7 @@ export class SkeletonBinary { this.attachmentLoader = attachmentLoader; } - readSkeletonData (binary: Uint8Array | ArrayBuffer): SkeletonData { + readSkeletonData (binary: Uint8Array | ArrayBuffer): SkeletonData { let scale = this.scale; let skeletonData = new SkeletonData(); diff --git a/spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineAtlasAsset.cpp b/spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineAtlasAsset.cpp index f8bd0f901..8409b6120 100644 --- a/spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineAtlasAsset.cpp +++ b/spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineAtlasAsset.cpp @@ -95,7 +95,7 @@ void USpineAtlasAsset::BeginDestroy() { class UETextureLoader : public TextureLoader { void load(AtlasPage &page, const String &path) { - page.texture = (void*)(uintptr_t)page.index; + page.texture = (void *) (uintptr_t) page.index; } void unload(void *texture) { diff --git a/spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonRendererComponent.cpp b/spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonRendererComponent.cpp index 6ac48ea76..b317eaf35 100644 --- a/spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonRendererComponent.cpp +++ b/spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonRendererComponent.cpp @@ -268,7 +268,7 @@ void USpineSkeletonRendererComponent::UpdateMesh(USpineSkeletonComponent *compon // to the correct skeleton data yet, we won't find any regions. // ignore regions for which we can't find a material UMaterialInstanceDynamic *material = nullptr; - int foundPageIndex = (int)(intptr_t)attachmentAtlasRegion->rendererObject; + int foundPageIndex = (int) (intptr_t) attachmentAtlasRegion->rendererObject; if (foundPageIndex == -1) { clipper.clipEnd(*slot); continue;