2025-12-05 16:29:18 +01:00

425 lines
15 KiB
TypeScript

/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { type BlendMode, type Bone, ClippingAttachment, MathUtils, MeshAttachment, PathAttachment, RegionAttachment, type Skeleton, SkeletonRendererCore, Utils, Vector2 } from "@esotericsoftware/spine-core";
import type { C3Matrix } from "./C3Matrix";
import { BlendingModeSpineToC3, type C3TextureEditor, type C3TextureRuntime } from "./C3Texture";
type C3Renderer = IRenderer | SDK.Gfx.IWebGLRenderer;
type C3Texture = C3TextureRuntime | C3TextureEditor;
type C3Quad = DOMQuad | SDK.Quad;
abstract class C3SkeletonRenderer<
Renderer extends C3Renderer,
Texture extends C3Texture,
> extends SkeletonRendererCore {
private tempVertices = new Float32Array(4096);
private tempColors = new Float32Array(4096);
private tempPoint = new Vector2();
private tempArray = [] as number[];
private inv255 = 1 / 255;
constructor (
protected renderer: Renderer,
protected matrix: C3Matrix,
) {
super();
}
draw (skeleton: Skeleton, inColors: [number, number, number], opacity = 1) {
const { matrix, inv255 } = this;
let command = this.render(skeleton, true, [...inColors, opacity]);
while (command) {
const { numVertices, positions, uvs, colors, indices, numIndices, blendMode } = command;
const vertices = this.tempVertices.length < numVertices * 3
? (this.tempVertices = new Float32Array(numVertices * 3))
: this.tempVertices;
const c3colors = this.tempColors.length < numVertices * 4
? (this.tempColors = new Float32Array(numVertices * 4))
: this.tempColors;
for (let i = 0; i < numVertices; i++) {
const srcIndex = i * 2;
const { x, y } = matrix.skeletonToGame(positions[srcIndex], positions[srcIndex + 1]);
const dstIndex = i * 3;
vertices[dstIndex] = x;
vertices[dstIndex + 1] = y;
vertices[dstIndex + 2] = 0;
const color = colors[i];
const colorDst = i * 4;
const alpha = (color >>> 24) * inv255;
const inv255Alpha = alpha * inv255;
c3colors[colorDst] = (color >>> 16 & 0xFF) * inv255Alpha;
c3colors[colorDst + 1] = (color >>> 8 & 0xFF) * inv255Alpha;
c3colors[colorDst + 2] = (color & 0xFF) * inv255Alpha;
c3colors[colorDst + 3] = alpha;
}
this.renderSkeleton(
vertices.subarray(0, numVertices * 3),
uvs.subarray(0, numVertices * 2),
indices.subarray(0, numIndices),
c3colors.subarray(0, numVertices * 4),
command.texture,
blendMode)
command = command.next;
}
}
drawDebug (skeleton: Skeleton, x: number, y: number, quad: C3Quad) {
const { matrix } = this;
this.setColorFillMode();
this.setBlendMode();
// bones
const bones = skeleton.bones;
for (let i = 0, n = bones.length; i < n; i++) {
const bone = bones[i];
if (!bone.parent) continue;
const boneApplied = bone.applied;
const { x: x1, y: y1 } = matrix.skeletonToGame(boneApplied.worldX, boneApplied.worldY);
const x2 = bone.data.length * boneApplied.a + x1;
const y2 = bone.data.length * boneApplied.c + y1;
this.setColor(1, 0, 0, 1);
this.setColorFillMode();
const t = this.tempPoint.set(y2 - y1, x1 - x2);
t.normalize();
const width = 1 * 0.5;
const tx = t.x * width;
const ty = t.y * width;
this.poly([
x1 + tx, y1 + ty,
x1 - tx, y1 - ty,
x2 + tx, y2 + ty,
x2 - tx, y2 - ty,
x2 + tx, y2 + ty,
x1 - tx, y1 - ty,
]);
this.setColor(0, 1, 0, 1);
this.poly(this.circle(x1, y1, 2));
}
// regions
this.setColor(0, 0, 1, 0.5);
const slots = skeleton.slots;
for (let i = 0, n = slots.length; i < n; i++) {
const slot = slots[i];
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
if (attachment instanceof RegionAttachment) {
const vertices = this.tempVertices;
attachment.computeWorldVertices(slot, vertices, 0, 2);
this.line(vertices[0], vertices[1], vertices[2], vertices[3], x, y);
this.line(vertices[2], vertices[3], vertices[4], vertices[5], x, y);
this.line(vertices[4], vertices[5], vertices[6], vertices[7], x, y);
this.line(vertices[6], vertices[7], vertices[0], vertices[1], x, y);
}
}
// meshes
for (let i = 0, n = slots.length; i < n; i++) {
const slot = slots[i];
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
if (!(attachment instanceof MeshAttachment)) continue;
const vertices = this.tempVertices;
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, vertices, 0, 2);
const triangles = attachment.triangles;
let hullLength = attachment.hullLength;
// mesh triangles
this.setColor(1, 0.64, 0, 0.5);
for (let ii = 0, nn = triangles.length; ii < nn; ii += 3) {
const v1 = triangles[ii] * 2, v2 = triangles[ii + 1] * 2, v3 = triangles[ii + 2] * 2;
this.triangle(
vertices[v1], vertices[v1 + 1],
vertices[v2], vertices[v2 + 1],
vertices[v3], vertices[v3 + 1],
x, y,
);
}
// mesh hulls
if (hullLength > 0) {
this.setColor(0, 0, 1, 0.5);
hullLength = (hullLength >> 1) * 2;
let lastX = vertices[hullLength - 2], lastY = vertices[hullLength - 1];
for (let ii = 0, nn = hullLength; ii < nn; ii += 2) {
const x1 = vertices[ii], y1 = vertices[ii + 1];
this.line(x1, y1, lastX, lastY, x, y);
lastX = x1;
lastY = y1;
}
}
}
// paths
for (let i = 0, n = slots.length; i < n; i++) {
const slot = slots[i];
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
if (!(attachment instanceof PathAttachment)) continue;
let nn = attachment.worldVerticesLength;
const world = this.tempArray = Utils.setArraySize(this.tempArray, nn, 0);
attachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2);
let x1 = world[2], y1 = world[3], x2 = 0, y2 = 0;
if (attachment.closed) {
this.setColor(1, 0.5, 0, 1);
const cx1 = world[0], cy1 = world[1], cx2 = world[nn - 2], cy2 = world[nn - 1];
x2 = world[nn - 4];
y2 = world[nn - 3];
this.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32, x, y);
this.setColor(.75, .75, .75, 1);
this.line(x1, y1, cx1, cy1, x, y);
this.line(x2, y2, cx2, cy2, x, y);
}
nn -= 4;
for (let ii = 4; ii < nn; ii += 6) {
const cx1 = world[ii], cy1 = world[ii + 1], cx2 = world[ii + 2], cy2 = world[ii + 3];
x2 = world[ii + 4];
y2 = world[ii + 5];
this.setColor(1, 0.5, 0, 1);
this.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32, x, y);
this.setColor(.75, .75, .75, 1);
this.line(x1, y1, cx1, cy1, x, y);
this.line(x2, y2, cx2, cy2, x, y);
x1 = x2;
y1 = y2;
}
}
// clipping
this.setColor(0.8, 0, 0, 1)
for (let i = 0, n = slots.length; i < n; i++) {
const slot = slots[i];
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
if (!(attachment instanceof ClippingAttachment)) continue;
const nn = attachment.worldVerticesLength;
const world = this.tempArray = Utils.setArraySize(this.tempArray, nn, 0);
attachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2);
for (let i = 0, n = world.length; i < n; i += 2) {
const x1 = world[i];
const y1 = world[i + 1];
const x2 = world[(i + 2) % world.length];
const y2 = world[(i + 3) % world.length];
this.line(x1, y1, x2, y2, x, y);
}
}
this.renderGameObjectBounds(x, y, quad);
}
protected abstract setColor (r: number, g: number, b: number, a: number): void;
protected abstract setColorFillMode (): void;
protected abstract setBlendMode (): void;
protected abstract poly (points: number[]): void;
protected abstract lineInternal (x: number, y: number, x2: number, y2: number): void;
protected line (x: number, y: number, x2: number, y2: number, offsetX = 0, offsetY = 0) {
this.lineInternal(x + offsetX, y + offsetY, x2 + offsetX, y2 + offsetY);
};
protected abstract renderSkeleton (vertices: Float32Array, uvs: Float32Array, indices: Uint16Array, colors: Float32Array, texture: Texture, blendMode: BlendMode): void;
public abstract renderGameObjectBounds (x: number, y: number, quad: DOMQuad | SDK.Quad): void;
protected circle (x: number, y: number, radius: number) {
let segments = Math.max(1, (6 * MathUtils.cbrt(radius)) | 0);
if (segments <= 0) throw new Error("segments must be > 0.");
const angle = 2 * MathUtils.PI / segments;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let cx = radius, cy = 0;
segments--;
const poly = [];
for (let i = 0; i < segments; i++) {
poly.push(x, y);
poly.push(x + cx, y + cy);
const temp = cx;
cx = cos * cx - sin * cy;
cy = sin * temp + cos * cy;
poly.push(x + cx, y + cy);
}
poly.push(x, y, x + cx, y + cy);
cx = radius;
cy = 0;
poly.push(x + cx, y + cy);
return poly;
}
protected triangle (x: number, y: number, x2: number, y2: number, x3: number, y3: number, offsetX = 0, offsetY = 0) {
this.line(x, y, x2, y2, offsetX, offsetY);
this.line(x2, y2, x3, y3, offsetX, offsetY);
this.line(x3, y3, x, y, offsetX, offsetY);
}
protected curve (x1: number, y1: number, cx1: number, cy1: number, cx2: number, cy2: number, x2: number, y2: number, segments: number, offsetX = 0, offsetY = 0) {
x1 += offsetX;
y1 += offsetY;
cx1 += offsetX;
cy1 += offsetY;
x2 += offsetX;
y2 += offsetY;
cx2 += offsetX;
cy2 += offsetY;
// Algorithm from: http://www.antigrain.com/research/bezier_interpolation/index.html#PAGE_BEZIER_INTERPOLATION
const subdiv_step = 1 / segments;
const subdiv_step2 = subdiv_step * subdiv_step;
const subdiv_step3 = subdiv_step * subdiv_step * subdiv_step;
const pre1 = 3 * subdiv_step;
const pre2 = 3 * subdiv_step2;
const pre4 = 6 * subdiv_step2;
const pre5 = 6 * subdiv_step3;
const tmp1x = x1 - cx1 * 2 + cx2;
const tmp1y = y1 - cy1 * 2 + cy2;
const tmp2x = (cx1 - cx2) * 3 - x1 + x2;
const tmp2y = (cy1 - cy2) * 3 - y1 + y2;
let fx = x1;
let fy = y1;
let dfx = (cx1 - x1) * pre1 + tmp1x * pre2 + tmp2x * subdiv_step3;
let dfy = (cy1 - y1) * pre1 + tmp1y * pre2 + tmp2y * subdiv_step3;
let ddfx = tmp1x * pre4 + tmp2x * pre5;
let ddfy = tmp1y * pre4 + tmp2y * pre5;
const dddfx = tmp2x * pre5;
const dddfy = tmp2y * pre5;
while (segments-- > 0) {
this.line(fx, fy, fx + dfx, fy + dfy);
fx += dfx;
fy += dfy;
dfx += ddfx;
dfy += ddfy;
ddfx += dddfx;
ddfy += dddfy;
}
this.line(fx, fy, x2, y2);
}
}
export class C3RendererRuntime extends C3SkeletonRenderer<IRenderer, C3TextureRuntime> {
protected setColor (r: number, g: number, b: number, a: number): void {
this.renderer.setColor([r, g, b, a]);
}
protected setColorFillMode (): void {
this.renderer.setColorFillMode();
}
protected setBlendMode (blenMode: BlendModeParameter = "normal"): void {
this.renderer.setBlendMode(blenMode);
}
protected poly (points: number[]): void {
this.renderer.convexPoly(points);
}
protected lineInternal (x: number, y: number, x2: number, y2: number): void {
this.renderer.line(x, y, x2, y2);
}
protected renderSkeleton (vertices: Float32Array, uvs: Float32Array, indices: Uint16Array, colors: Float32Array, texture: C3TextureRuntime, blendMode: BlendMode) {
this.renderer.setTexture(texture.texture);
this.renderer.setBlendMode(BlendingModeSpineToC3[blendMode]);
this.renderer.drawMesh(vertices, uvs, indices, colors);
};
public renderDragHandles (bone: Bone, radius: number) {
const boneApplied = bone.applied;
const { x: x1, y: y1 } = this.matrix.skeletonToGame(boneApplied.worldX, boneApplied.worldY);
this.renderer.setColorFillMode();
this.renderer.setColor([1, 0, 0, .2]);
this.renderer.convexPoly(this.circle(x1, y1, radius));
}
public renderGameObjectBounds (x: number, y: number, quad: DOMQuad) {
const { renderer, matrix } = this;
renderer.setAlphaBlendMode();
renderer.setColorFillMode();
renderer.setColorRgba(0.25, 0, 0, 0.25);
renderer.lineQuad(quad);
renderer.line(x, y, matrix.tx, matrix.ty);
};
}
export class C3RendererEditor extends C3SkeletonRenderer<SDK.Gfx.IWebGLRenderer, C3TextureEditor> {
protected setColor (r: number, g: number, b: number, a: number): void {
this.renderer.SetColorRgba(r, g, b, a);
}
protected setColorFillMode (): void {
this.renderer.SetColorFillMode();
}
protected setBlendMode (blenMode: BlendModeParameter = "normal"): void {
this.renderer.SetBlendMode(blenMode);
}
protected poly (points: number[]): void {
this.renderer.ConvexPoly(points);
}
protected lineInternal (x: number, y: number, x2: number, y2: number): void {
this.renderer.Line(x, y, x2, y2);
}
protected renderSkeleton (vertices: Float32Array, uvs: Float32Array, indices: Uint16Array, colors: Float32Array, texture: C3TextureEditor, blendMode: BlendMode) {
this.renderer.ResetColor();
this.renderer.SetBlendMode(BlendingModeSpineToC3[blendMode]);
this.renderer.SetTextureFillMode();
this.renderer.SetTexture(texture.texture);
this.renderer.DrawMesh(vertices, uvs, indices, colors);
};
public renderGameObjectBounds (x: number, y: number, quad: SDK.Quad): void {
const { renderer, matrix } = this;
renderer.SetAlphaBlend();
renderer.SetColorFillMode();
renderer.SetColorRgba(0.25, 0, 0, 0.25);
renderer.LineQuad(quad);
renderer.Line(x, y, matrix.tx, matrix.ty);
}
}