254 lines
10 KiB
JavaScript
254 lines
10 KiB
JavaScript
// Node 里用 require,插件页面里用 <script> 标签挂到 window。
|
|
(function () {
|
|
'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 BLEND_MODES = ['source-over', 'lighter', 'multiply', 'screen'];
|
|
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 (!image.width || !image.height) return image;
|
|
let canvas;
|
|
if (doc) canvas = doc.createElement('canvas');
|
|
else {
|
|
try { canvas = require('@napi-rs/canvas').createCanvas(image.width, image.height); }
|
|
catch (e) { return image; }
|
|
}
|
|
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, darkColor) {
|
|
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],
|
|
[...vertices.slice(a+2,a+6), ...vertices.slice(b+2,b+6), ...vertices.slice(c+2,c+6)],
|
|
darkColor ? Array.from({length:3}, () => [darkColor.r,darkColor.g,darkColor.b,0]).flat() : undefined);
|
|
}
|
|
}
|
|
|
|
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 composite = context.globalCompositeOperation;
|
|
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, false);
|
|
triangles = Renderer.QUAD_TRIANGLES || QUAD_TRIANGLES;
|
|
vertexCount = 4;
|
|
} else if (attachment instanceof spine.MeshAttachment) {
|
|
vertices = base.computeMeshVertices(slot, attachment, false);
|
|
triangles = attachment.triangles;
|
|
vertexCount = attachment.worldVerticesLength >> 1;
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
const sourceImage = imageFromAttachment(attachment);
|
|
const image = base.gpuDrawing ? sourceImage : imageForCanvas(sourceImage, context, pma);
|
|
if (!image) continue;
|
|
context.globalAlpha = base.tempColor.a;
|
|
context.globalCompositeOperation = BLEND_MODES[slot.data.blendMode] || 'source-over';
|
|
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, slot.darkColor);
|
|
} else {
|
|
drawInterleaved(base, image, vertices, triangles, slot.darkColor);
|
|
}
|
|
} finally {
|
|
clipper.clipEndWithSlot(slot);
|
|
}
|
|
}
|
|
} finally {
|
|
clipper.clipEnd();
|
|
context.globalAlpha = 1;
|
|
context.globalCompositeOperation = composite;
|
|
}
|
|
};
|
|
base.clipper = clipper;
|
|
return base;
|
|
}
|
|
|
|
// 3.4/3.5 predate clipping attachments and compute packed vertices on attachments.
|
|
function create34Renderer(spine, context) {
|
|
const base = new (rendererConstructor(spine))(context);
|
|
base.triangleRendering = true;
|
|
base.premultipliedAlpha = false;
|
|
base.draw = skeleton => {
|
|
const composite = context.globalCompositeOperation;
|
|
try {
|
|
for (const slot of skeleton.drawOrder) {
|
|
const attachment = slot.getAttachment();
|
|
if (!(attachment instanceof spine.RegionAttachment) && !(attachment instanceof spine.MeshAttachment)) continue;
|
|
const vertices = attachment.updateWorldVertices(slot, false);
|
|
const sourceImage = imageFromAttachment(attachment);
|
|
const image = base.gpuDrawing ? sourceImage : imageForCanvas(sourceImage, context, base.premultipliedAlpha);
|
|
if (!image) continue;
|
|
context.globalAlpha = vertices[5];
|
|
context.globalCompositeOperation = BLEND_MODES[slot.data.blendMode] || 'source-over';
|
|
drawInterleaved(base, image, vertices, attachment instanceof spine.MeshAttachment ? attachment.triangles : QUAD_TRIANGLES);
|
|
}
|
|
} finally { context.globalAlpha = 1; context.globalCompositeOperation = composite; }
|
|
};
|
|
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) {
|
|
const composite = context.globalCompositeOperation;
|
|
try {
|
|
const pma = !!base.premultipliedAlpha;
|
|
for (let command = core.render(skeleton, false); command; command = command.next) {
|
|
const sourceImage = command.texture && command.texture.getImage && command.texture.getImage();
|
|
const image = base.gpuDrawing ? sourceImage : imageForCanvas(sourceImage, context, pma);
|
|
if (!image) continue;
|
|
context.globalAlpha = ((command.colors[0] >>> 24) & 255) / 255;
|
|
context.globalCompositeOperation = BLEND_MODES[command.blendMode] || 'source-over';
|
|
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;
|
|
const unpack = packed => [((packed>>>16)&255)/255,((packed>>>8)&255)/255,(packed&255)/255,((packed>>>24)&255)/255];
|
|
const colors = [a,b,c].flatMap(index => unpack(command.colors[index/2]));
|
|
const darkColors = [a,b,c].flatMap(index => unpack(command.darkColors[index/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], colors, darkColors);
|
|
}
|
|
}
|
|
} finally {
|
|
context.globalAlpha = 1;
|
|
context.globalCompositeOperation = composite;
|
|
}
|
|
};
|
|
base.clipper = core.clipping;
|
|
return base;
|
|
}
|
|
|
|
function createRenderer(spine, context, runtimeDir) {
|
|
const renderer = (runtimeDir === '3.4' || runtimeDir === '3.5') ? create34Renderer(spine, context) : runtimeDir === '4.3'
|
|
? create43Renderer(spine, context)
|
|
: createLegacyRenderer(spine, context);
|
|
// Explicitly disable debug drawing in every supported runtime.
|
|
renderer.debugRendering = false;
|
|
// 页面里 rasterizer 已经由 <script> 提前加载并挂在 window 上,Node 里才走 require。
|
|
const rasterizer = typeof window !== 'undefined' && window.SpineWebglRasterizer
|
|
? window.SpineWebglRasterizer
|
|
: require('./spine-webgl-rasterizer.js');
|
|
return rasterizer.attach(renderer, context);
|
|
}
|
|
|
|
const api = { createRenderer };
|
|
if (typeof window !== 'undefined') window.SpineCanvasClipping = api;
|
|
else if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
|
|
})();
|