Files
SpineViewer/js/spine-canvas-clipping.js
T
2026-09-09 15:58:56 +08:00

199 lines
7.1 KiB
JavaScript

'use strict';
// The official spine-canvas SkeletonRenderer does not process clipping
// attachments. Keep its triangle rasterizer, but feed it geometry produced by
// SkeletonClipping so the viewer matches Spine's draw order and clipping rules.
const QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
const unpremultipliedImages = new WeakMap();
function rendererConstructor(spine) {
return (spine.canvas && spine.canvas.SkeletonRenderer) || spine.SkeletonRenderer;
}
function imageFromAttachment(attachment) {
const region = attachment && attachment.region;
// 4.0 stores the texture on the atlas page, while the other bundled
// runtimes expose it directly on the attachment region.
const renderRegion = region && region.renderObject;
const texture = region && (region.texture
|| (region.page && region.page.texture)
|| (renderRegion && renderRegion.texture)
|| (renderRegion && renderRegion.page && renderRegion.page.texture));
return texture && typeof texture.getImage === 'function' ? texture.getImage() : null;
}
// Canvas 2D always applies source-alpha blending. A Spine texture exported
// with PMA already has RGB multiplied by alpha, so drawing it directly would
// multiply translucent edge pixels a second time. Convert it once to straight
// alpha and cache the result; Canvas can then blend it correctly.
function imageForCanvas(image, context, premultipliedAlpha) {
if (!premultipliedAlpha || !image || (typeof image !== 'object' && typeof image !== 'function')) return image;
const cached = unpremultipliedImages.get(image);
if (cached) return cached;
try {
const ownerDocument = context.canvas && context.canvas.ownerDocument;
const doc = ownerDocument || (typeof document !== 'undefined' ? document : null);
if (!doc || !image.width || !image.height) return image;
const canvas = doc.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(image, 0, 0);
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = pixels.data;
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3];
if (alpha > 0 && alpha < 255) {
const factor = 255 / alpha;
data[i] = Math.min(255, Math.round(data[i] * factor));
data[i + 1] = Math.min(255, Math.round(data[i + 1] * factor));
data[i + 2] = Math.min(255, Math.round(data[i + 2] * factor));
}
}
ctx.putImageData(pixels, 0, 0);
unpremultipliedImages.set(image, canvas);
return canvas;
} catch (e) {
// If pixel access is unavailable, retain the original texture so preview
// remains usable instead of failing the whole frame.
return image;
}
}
function unpackVertices(vertices, count) {
const positions = new Array(count * 2);
const uvs = new Array(count * 2);
for (let i = 0, p = 0; i < count; i++, p += 2) {
const source = i * 8;
positions[p] = vertices[source];
positions[p + 1] = vertices[source + 1];
uvs[p] = vertices[source + 6];
uvs[p + 1] = vertices[source + 7];
}
return { positions, uvs };
}
function drawInterleaved(base, image, vertices, triangles) {
for (let i = 0; i < triangles.length; i += 3) {
const a = triangles[i] * 8;
const b = triangles[i + 1] * 8;
const c = triangles[i + 2] * 8;
base.drawTriangle(image,
vertices[a], vertices[a + 1], vertices[a + 6], vertices[a + 7],
vertices[b], vertices[b + 1], vertices[b + 6], vertices[b + 7],
vertices[c], vertices[c + 1], vertices[c + 6], vertices[c + 7]);
}
}
function createLegacyRenderer(spine, context) {
const Renderer = rendererConstructor(spine);
if (!Renderer || !spine.SkeletonClipping) throw new Error('Spine Canvas 渲染器不完整');
const base = new Renderer(context);
base.triangleRendering = true;
base.premultipliedAlpha = false;
const clipper = new spine.SkeletonClipping();
const dark = new spine.Color(0, 0, 0, 0);
base.draw = function drawWithClipping(skeleton) {
const drawOrder = skeleton.drawOrder;
const pma = !!base.premultipliedAlpha;
try {
for (let i = 0; i < drawOrder.length; i++) {
const slot = drawOrder[i];
if (slot.bone && slot.bone.active === false) {
clipper.clipEndWithSlot(slot);
continue;
}
const attachment = slot.getAttachment();
if (attachment instanceof spine.ClippingAttachment) {
clipper.clipStart(slot, attachment);
continue;
}
try {
if (!attachment) continue;
let vertices;
let triangles;
let vertexCount;
if (attachment instanceof spine.RegionAttachment) {
vertices = base.computeRegionVertices(slot, attachment, pma);
triangles = Renderer.QUAD_TRIANGLES || QUAD_TRIANGLES;
vertexCount = 4;
} else if (attachment instanceof spine.MeshAttachment) {
vertices = base.computeMeshVertices(slot, attachment, pma);
triangles = attachment.triangles;
vertexCount = attachment.worldVerticesLength >> 1;
} else {
continue;
}
const image = imageForCanvas(imageFromAttachment(attachment), context, pma);
if (!image) continue;
context.globalAlpha = base.tempColor.a;
if (clipper.isClipping()) {
const unpacked = unpackVertices(vertices, vertexCount);
clipper.clipTriangles(unpacked.positions, unpacked.positions.length,
triangles, triangles.length, unpacked.uvs, base.tempColor, dark, false);
drawInterleaved(base, image, clipper.clippedVertices, clipper.clippedTriangles);
} else {
drawInterleaved(base, image, vertices, triangles);
}
} finally {
clipper.clipEndWithSlot(slot);
}
}
} finally {
clipper.clipEnd();
context.globalAlpha = 1;
}
};
base.clipper = clipper;
return base;
}
function create43Renderer(spine, context) {
const Renderer = rendererConstructor(spine);
if (!Renderer || !spine.SkeletonRendererCore) throw new Error('Spine 4.3 Canvas 渲染器不完整');
const base = new Renderer(context);
base.triangleRendering = true;
base.premultipliedAlpha = false;
const core = new spine.SkeletonRendererCore();
base.draw = function drawWithCoreClipping(skeleton) {
try {
const pma = !!base.premultipliedAlpha;
for (let command = core.render(skeleton, pma); command; command = command.next) {
const sourceImage = command.texture && command.texture.getImage && command.texture.getImage();
const image = imageForCanvas(sourceImage, context, pma);
if (!image) continue;
context.globalAlpha = ((command.colors[0] >>> 24) & 255) / 255;
const positions = command.positions;
const uvs = command.uvs;
const indices = command.indices;
for (let i = 0; i < command.numIndices; i += 3) {
const a = indices[i] * 2;
const b = indices[i + 1] * 2;
const c = indices[i + 2] * 2;
base.drawTriangle(image,
positions[a], positions[a + 1], uvs[a], uvs[a + 1],
positions[b], positions[b + 1], uvs[b], uvs[b + 1],
positions[c], positions[c + 1], uvs[c], uvs[c + 1]);
}
}
} finally {
context.globalAlpha = 1;
}
};
base.clipper = core.clipping;
return base;
}
function createRenderer(spine, context, runtimeDir) {
return runtimeDir === '4.3'
? create43Renderer(spine, context)
: createLegacyRenderer(spine, context);
}
module.exports = { createRenderer };