网站页面

This commit is contained in:
tianmo committed 2026-09-12 18:10:52 +08:00
1 parent 55f811ef4a
commit f0470410df
18 files changed
+25943

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
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:
https://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.
+12
View File
@@ -0,0 +1,12 @@
// 官方 Raptor 示例(Spine 4.3.75-beta 导出)的 JS 内嵌副本。
// 用途:以 file:// 直接打开页面时,浏览器不允许 fetch 本地文本,本地图片也会污染画布,
// 这里预先内嵌 JSON、Atlas 与贴图的 data URL,保证离线与 file:// 场景下同样可以渲染。
// 数据来源:Esoteric Software spine-runtimes 仓库 examples/raptor/export
// 固定提交:ba17cf88b198c6531c6c51d8d4e1e4b3c3ab47dd(Spine Runtimes License,见 LICENSE.txt)。
window.SPINE_DEMO_DATA = {
name: "Raptor",
version: "4.3.75-beta",
data: {"skeleton":{"hash":"OKE722Fi+rc","spine":"4.3.75-beta","x":-809.163,"y":-73.5388,"width":1287.646,"height":1039.443,"images":"./images/","audio":null},"bones":[{"name":"root"},{"name":"hip","parent":"root","rotation":8.95755,"x":-122.52013,"y":392.648,"color":"fbff00ff"},{"name":"torso1","parent":"hip","length":126.26,"rotation":-4.98,"x":30.04,"y":-0.4,"color":"eaff00ff"},{"name":"saddle","parent":"torso1","length":50.92,"rotation":91.8,"x":25.310295,"y":70.64713,"color":"ff7300ff"},{"name":"spineboy-hip","parent":"saddle","length":0.53,"rotation":90.02,"x":81.88,"y":2.69,"color":"e8ff00ff"},{"name":"spineboy-torso","parent":"spineboy-hip","length":122.45,"rotation":-75.86,"x":1.05,"y":-2.11,"color":"e8ff00ff"},{"name":"torso2","parent":"torso1","length":121.2,"rotation":39.85,"x":126.26,"y":-0.38,"color":"e9ff00ff"},{"name":"neck","parent":"torso2","length":70.6,"rotation":41.38,"x":121.2,"y":0.35,"color":"eaff00ff"},{"name":"head","parent":"neck","length":105.51,"rotation":9.83,"x":70.6,"y":0.04,"color":"eaff00ff"},{"name":"horn-back","parent":"head","length":73.78,"rotation":44.32,"x":104.76395,"y":-242.01355,"color":"e07800ff"},{"name":"spineboy-back-arm-target","parent":"horn-back","rotation":-133.55,"x":232.68,"y":245.85,"inherit":"noScale","color":"ff3f00ff"},{"name":"back-arm","parent":"spineboy-torso","length":67.21,"rotation":-120.9,"x":96.33,"y":-38.47,"color":"e07800ff"},{"name":"back-bracer","parent":"back-arm","length":43.69,"rotation":17.48,"x":67.22,"y":-0.32,"color":"e07800ff"},{"name":"back-arm1","parent":"torso2","length":109.56,"rotation":-124.72,"x":83.67854,"y":-83.23534,"color":"e07800ff"},{"name":"back-arm2","parent":"back-arm1","length":85.8,"rotation":123.56,"x":109.57,"y":-0.01,"color":"e07800ff"},{"name":"back-foot-target","parent":"root","x":33.44,"y":30.82,"color":"ff3f00ff","icon":"ik"},{"name":"back-leg-target","parent":"back-foot-target","x":-127.51,"y":75.99,"color":"ff3f00ff","icon":"ik"},{"name":"back-leg1","parent":"hip","length":226.28,"rotation":-54.76,"x":55.19,"y":-71.25,"color":"e07800ff"},{"name":"back-leg2","parent":"back-leg1","length":172.59,"rotation":-92.25,"x":226.32,"y":0.23,"color":"e07800ff"},{"name":"back-leg3","parent":"back-leg2","length":103.05,"rotation":82.82,"x":172.32,"y":2.21,"color":"e07800ff"},{"name":"back-foot1","parent":"back-leg3","length":84.51,"rotation":75.43,"x":102.38,"y":-0.03,"color":"e07800ff"},{"name":"back-foot2","parent":"back-foot1","length":102.31,"rotation":-6.14,"x":84.5,"y":-0.35,"inherit":"noRotationOrReflection","color":"e07800ff"},{"name":"back-hand","parent":"back-arm2","length":45.81,"rotation":-76.28,"x":85.8,"y":0.11,"color":"e07800ff"},{"name":"back-hand2","parent":"spineboy-back-arm-target","length":41.98,"rotation":27.77738,"x":-0.02857208,"y":0.05377197,"inherit":"noRotationOrReflection","color":"e07800ff"},{"name":"spineboy-back-foot-target","parent":"saddle","x":-30.44,"y":-100.08,"color":"ff3f00ff"},{"name":"back-thigh","parent":"spineboy-hip","length":71.16,"rotation":160.75,"x":-9.57,"y":2.32,"color":"e07800ff"},{"name":"back-knee","parent":"back-thigh","length":97.17,"rotation":-54.98,"x":71.16,"y":-0.28,"color":"e07800ff"},{"name":"neck2","parent":"spineboy-torso","length":32.05,"rotation":-45.23,"x":113.44,"y":-15.22,"color":"e8ff00ff"},{"name":"head2","parent":"neck2","length":167.1939,"rotation":11.66,"x":25.68,"y":-0.77,"inherit":"noScale","color":"e7ff00ff"},{"name":"bone","parent":"head2","length":39.91506,"rotation":-35.22991,"x":166.09326,"y":-79.27026,"color":"e7ff00ff"},{"name":"bone2","parent":"bone","length":47.4192,"rotation":51.7975,"x":39.9151,"color":"e7ff00ff"},{"name":"bone3","parent":"head2","length":45.40962,"rotation":-12.343472,"x":179.80286,"y":-20.90898,"color":"e7ff00ff"},{"name":"bone4","parent":"bone3","length":43.31327,"rotation":42.0083,"x":44.63544,"y":0.303009,"color":"e7ff00ff"},{"name":"bone5","parent":"bone4","length":44.87501,"rotation":48.80142,"x":43.31311,"color":"e7ff00ff"},{"name":"horn-front","parent":"head","length":87.48,"rotation":49.36,"x":87.97336,"y":-235.14723,"color":"15ff00ff"},{"name":"spineboy-front-arm-target","parent":"horn-front","rotation":-138.6,"x":294.58,"y":234.18,"color":"ff3f00ff"},{"name":"front-arm","parent":"spineboy-torso","length":74.52,"rotation":-118.17,"x":101.38,"y":9.79,"color":"14ff00ff"},{"name":"front-bracer","parent":"front-arm","length":39.85,"rotation":20.31,"x":74.52,"y":-0.42,"color":"14ff00ff"},{"name":"front-arm1","parent":"torso2","length":109.99,"rotation":224.54,"x":73.00035,"y":-72.46397,"color":"15ff00ff"},{"name":"front-arm2","parent":"front-arm1","length":86.33,"rotation":105.24,"x":109.99,"y":0.2,"color":"15ff00ff"},{"name":"front-foot-target","parent":"root","rotation":-6.956577,"x":-45.8,"y":-28.67,"color":"ff3f00ff","icon":"ik"},{"name":"front-leg-target","parent":"front-foot-target","x":-106.06,"y":115.58,"color":"ff3f00ff","icon":"ik"},{"name":"front-leg1","parent":"hip","length":251.75,"rotation":-51.51,Line truncated
atlas: "raptor.png\n\tsize: 1024, 512\n\tfilter: Linear, Linear\n\tscale: 0.5\nback-arm\n\tbounds: 206, 49, 46, 25\nback-bracer\n\tbounds: 498, 42, 39, 28\nback-hand\n\tbounds: 400, 55, 36, 34\nback-knee\n\tbounds: 498, 72, 49, 67\nback-thigh\n\tbounds: 254, 40, 39, 24\neyes-open\n\tbounds: 232, 328, 47, 45\nfront-arm\n\tbounds: 350, 63, 48, 26\nfront-bracer\n\tbounds: 163, 56, 41, 29\nfront-hand\n\tbounds: 956, 231, 41, 38\nfront-open-hand\n\tbounds: 770, 329, 43, 44\nfront-thigh\n\tbounds: 897, 240, 57, 29\ngun\n\tbounds: 915, 106, 107, 103\ngun-nohand\n\tbounds: 916, 271, 105, 102\nhead\n\tbounds: 626, 81, 136, 149\nlower-leg\n\tbounds: 551, 42, 73, 98\nmouth-grind\n\tbounds: 2, 64, 47, 30\nmouth-smile\n\tbounds: 841, 127, 47, 30\nneck\n\tbounds: 281, 352, 18, 21\nraptor-back-arm\n\tbounds: 80, 11, 82, 86\nraptor-body\n\tbounds: 2, 140, 632, 233\nraptor-front-arm\n\tbounds: 911, 2, 81, 102\nraptor-front-leg\n\tbounds: 524, 182, 191, 257\n\trotate: 90\nraptor-hindleg-back\n\tbounds: 745, 158, 169, 215\n\trotate: 180\nraptor-horn\n\tbounds: 2, 293, 182, 80\nraptor-horn-back\n\tbounds: 174, 123, 176, 77\nraptor-jaw\n\tbounds: 772, 35, 126, 138\n\trotate: 270\nraptor-jaw-tooth\n\tbounds: 686, 242, 37, 48\nraptor-mouth-inside\n\tbounds: 755, 39, 36, 41\nraptor-saddle-strap-back\n\tbounds: 626, 26, 54, 74\nraptor-saddle-strap-front\n\tbounds: 2, 96, 57, 95\nraptor-saddle-w-shadow\n\tbounds: 2, 88, 162, 171\n\trotate: 90\nraptor-tail-shadow\n\tbounds: 159, 58, 189, 63\nraptor-tongue\n\tbounds: 685, 33, 86, 64\nstirrup-back\n\tbounds: 783, 204, 44, 35\nstirrup-front\n\tbounds: 185, 323, 45, 50\nstirrup-strap\n\tbounds: 636, 217, 49, 46\ntorso\n\tbounds: 442, 48, 54, 91\nvisor\n\tbounds: 356, 91, 131, 84\n\trotate: 90\n",
texture: "data:image/png;base64,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 truncated
};
File diff suppressed because it is too large. Load diff
+85
View File
@@ -0,0 +1,85 @@
raptor.png
size: 1024, 512
filter: Linear, Linear
scale: 0.5
back-arm
bounds: 206, 49, 46, 25
back-bracer
bounds: 498, 42, 39, 28
back-hand
bounds: 400, 55, 36, 34
back-knee
bounds: 498, 72, 49, 67
back-thigh
bounds: 254, 40, 39, 24
eyes-open
bounds: 232, 328, 47, 45
front-arm
bounds: 350, 63, 48, 26
front-bracer
bounds: 163, 56, 41, 29
front-hand
bounds: 956, 231, 41, 38
front-open-hand
bounds: 770, 329, 43, 44
front-thigh
bounds: 897, 240, 57, 29
gun
bounds: 915, 106, 107, 103
gun-nohand
bounds: 916, 271, 105, 102
head
bounds: 626, 81, 136, 149
lower-leg
bounds: 551, 42, 73, 98
mouth-grind
bounds: 2, 64, 47, 30
mouth-smile
bounds: 841, 127, 47, 30
neck
bounds: 281, 352, 18, 21
raptor-back-arm
bounds: 80, 11, 82, 86
raptor-body
bounds: 2, 140, 632, 233
raptor-front-arm
bounds: 911, 2, 81, 102
raptor-front-leg
bounds: 524, 182, 191, 257
rotate: 90
raptor-hindleg-back
bounds: 745, 158, 169, 215
rotate: 180
raptor-horn
bounds: 2, 293, 182, 80
raptor-horn-back
bounds: 174, 123, 176, 77
raptor-jaw
bounds: 772, 35, 126, 138
rotate: 270
raptor-jaw-tooth
bounds: 686, 242, 37, 48
raptor-mouth-inside
bounds: 755, 39, 36, 41
raptor-saddle-strap-back
bounds: 626, 26, 54, 74
raptor-saddle-strap-front
bounds: 2, 96, 57, 95
raptor-saddle-w-shadow
bounds: 2, 88, 162, 171
rotate: 90
raptor-tail-shadow
bounds: 159, 58, 189, 63
raptor-tongue
bounds: 685, 33, 86, 64
stirrup-back
bounds: 783, 204, 44, 35
stirrup-front
bounds: 185, 323, 45, 50
stirrup-strap
bounds: 636, 217, 49, 46
torso
bounds: 442, 48, 54, 91
visor
bounds: 356, 91, 131, 84
rotate: 90
Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+25
View File
@@ -0,0 +1,25 @@
'use strict';
const patterns = new WeakMap();
// Called with the same world-to-canvas transform used to draw the skeleton.
function drawCheckerboard(ctx, width, height) {
let pattern = patterns.get(ctx);
if (!pattern) {
const tile = ctx.canvas.ownerDocument.createElement('canvas');
tile.width = tile.height = 100;
const paint = tile.getContext('2d');
paint.fillStyle = '#505052'; paint.fillRect(0, 0, 100, 100);
paint.fillStyle = '#5a5a5c'; paint.fillRect(0, 0, 50, 50); paint.fillRect(50, 50, 50, 50);
pattern = ctx.createPattern(tile, 'repeat'); patterns.set(ctx, pattern);
}
const m = ctx.getTransform().inverse();
const corners = [[0,0], [width,0], [width,height], [0,height]].map(([x,y]) => [m.a*x+m.c*y+m.e, m.b*x+m.d*y+m.f]);
const xs = corners.map(p => p[0]), ys = corners.map(p => p[1]);
const x = Math.min(...xs), y = Math.min(...ys);
if (!Number.isFinite(x + y)) return;
ctx.save();
try { ctx.imageSmoothingEnabled = false; ctx.fillStyle = pattern; ctx.fillRect(x, y, Math.max(...xs)-x, Math.max(...ys)-y); }
finally { ctx.restore(); }
}
module.exports = { drawCheckerboard };
File diff suppressed because it is too large. Load diff
+243
View File
@@ -0,0 +1,243 @@
'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;
return require('./spine-webgl-rasterizer.js').attach(renderer, context);
}
module.exports = { createRenderer };
+177
View File
@@ -0,0 +1,177 @@
'use strict';
// Decode file bytes without the Canvas 2D premultiply/unpremultiply round trip.
// In low-alpha PMA pixels that round trip destroys RGB precision before blending.
const preparedTextures = new WeakMap();
async function prepareTexture(image, blob) {
if (typeof createImageBitmap !== 'function') return;
const options = { colorSpaceConversion: 'none' };
const [pma, straight] = await Promise.all([
createImageBitmap(blob, { ...options, premultiplyAlpha: 'none' }),
createImageBitmap(blob, { ...options, premultiplyAlpha: 'premultiply' })
]);
preparedTextures.set(image, { pma, straight });
}
// Rasterize adjacent mesh triangles together, with GPU edge ownership rules.
// Canvas clip()+drawImage() anti-aliases each triangle separately and exposes
// the background along shared edges, which looks like a wireframe overlay.
function createRasterizer(context) {
const document = context.canvas && context.canvas.ownerDocument;
if (!document || typeof context.getTransform !== 'function') return null;
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl', {
alpha: true, premultipliedAlpha: true, antialias: true,
preserveDrawingBuffer: true, depth: false, stencil: false
});
if (!gl) return null;
function shader(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source); gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader));
return shader;
}
const vertex = shader(gl.VERTEX_SHADER, `
attribute vec2 position;
attribute vec2 uv;
attribute vec4 light;
attribute vec4 dark;
uniform vec2 resolution;
varying vec2 textureUV;
varying vec4 lightColor;
varying vec4 darkColor;
void main() {
vec2 p = position / resolution * 2.0 - 1.0;
gl_Position = vec4(p.x, -p.y, 0.0, 1.0);
textureUV = uv;
lightColor = light; darkColor = dark;
}`);
const fragment = shader(gl.FRAGMENT_SHADER, `
precision mediump float;
uniform sampler2D image;
varying vec2 textureUV;
varying vec4 lightColor;
varying vec4 darkColor;
void main() {
vec4 color = texture2D(image, textureUV);
float alpha = color.a * lightColor.a;
// Textures are PMA in GPU memory. Light/dark colors arrive straight.
vec3 rgb = (color.rgb * lightColor.rgb + (color.a - color.rgb) * darkColor.rgb) * lightColor.a;
gl_FragColor = vec4(rgb, alpha);
}`);
const program = gl.createProgram(); gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
gl.deleteShader(vertex); gl.deleteShader(fragment);
const buffer = gl.createBuffer();
const position = gl.getAttribLocation(program, 'position'), uv = gl.getAttribLocation(program, 'uv');
const resolution = gl.getUniformLocation(program, 'resolution');
const light = gl.getAttribLocation(program, 'light'), dark = gl.getAttribLocation(program, 'dark');
const textures = new Map();
let coordinates = [], currentImage = null, currentAlpha = 1, currentMode = 'source-over', transform;
let disposed = false, lost = false;
let premultipliedAlpha = false;
canvas.addEventListener('webglcontextlost', event => { event.preventDefault(); lost = true; });
function texture(image) {
const pma = image !== context.canvas && premultipliedAlpha;
let pair = textures.get(image);
if (!pair) { pair = {}; textures.set(image, pair); }
const key = pma ? 'pma' : 'straight';
if (pair[key] && image !== context.canvas) return pair[key];
const t = pair[key] || gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t);
// Premultiply before interpolation so translucent edges do not darken.
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, !pma);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const prepared = preparedTextures.get(image);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, prepared ? prepared[key] : image);
pair[key] = t; return t;
}
function flush() {
if (!coordinates.length) return;
gl.bindTexture(gl.TEXTURE_2D, texture(currentImage));
// Premultiplied RGB factors; keep source-over alpha independently.
const factors = currentMode === 'lighter' ? [gl.ONE, gl.ONE]
: currentMode === 'multiply' ? [gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA]
: currentMode === 'screen' ? [gl.ONE, gl.ONE_MINUS_SRC_COLOR]
: [gl.ONE, gl.ONE_MINUS_SRC_ALPHA];
gl.blendFuncSeparate(factors[0], factors[1], gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(coordinates), gl.STREAM_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, coordinates.length / 12);
coordinates.length = 0;
}
return {
begin(pma = false) {
if (disposed || lost || gl.isContextLost()) return false;
premultipliedAlpha = pma;
const target = context.canvas;
if (canvas.width !== target.width) canvas.width = target.width;
if (canvas.height !== target.height) canvas.height = target.height;
transform = context.getTransform(); coordinates.length = 0; currentImage = null;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program); gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 48, 0);
gl.enableVertexAttribArray(uv); gl.vertexAttribPointer(uv, 2, gl.FLOAT, false, 48, 8);
gl.enableVertexAttribArray(light); gl.vertexAttribPointer(light, 4, gl.FLOAT, false, 48, 16);
gl.enableVertexAttribArray(dark); gl.vertexAttribPointer(dark, 4, gl.FLOAT, false, 48, 32);
gl.uniform2f(resolution, canvas.width, canvas.height);
gl.activeTexture(gl.TEXTURE0); gl.uniform1i(gl.getUniformLocation(program, 'image'), 0);
gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
// Seed the same framebuffer with the actual Canvas pixels (solid or
// checkerboard background). CSS backgrounds cannot participate in blending.
currentImage = target; currentAlpha = 1; currentMode = 'source-over';
const w = canvas.width, h = canvas.height;
for (const vertex of [[0,0,0,0],[w,0,1,0],[w,h,1,1],[0,0,0,0],[w,h,1,1],[0,h,0,1]])
coordinates.push(...vertex, 1,1,1,1, 0,0,0,0);
flush(); currentImage = null;
return true;
},
triangle(image, x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2, colors, darkColors) {
const alpha = context.globalAlpha;
const mode = context.globalCompositeOperation || 'source-over';
if (image !== currentImage || alpha !== currentAlpha || mode !== currentMode) { flush(); currentImage = image; currentAlpha = alpha; currentMode = mode; }
const t = transform;
const points = [[x0,y0,u0,v0],[x1,y1,u1,v1],[x2,y2,u2,v2]];
for (let i=0; i<3; i++) {
const [x,y,u,v] = points[i], j=i*4;
coordinates.push(t.a*x+t.c*y+t.e, t.b*x+t.d*y+t.f, u,v,
...(colors ? colors.slice(j,j+4) : [1,1,1,alpha]),
...(darkColors ? darkColors.slice(j,j+4) : [0,0,0,0]));
}
},
end() {
flush();
context.save();
try { context.setTransform(1, 0, 0, 1, 0, 0); context.globalAlpha = 1; context.globalCompositeOperation = 'copy'; context.drawImage(canvas, 0, 0); }
finally { context.restore(); }
},
dispose() {
if (disposed) return; disposed = true;
for (const pair of textures.values()) for (const value of Object.values(pair)) gl.deleteTexture(value);
textures.clear(); gl.deleteBuffer(buffer); gl.deleteProgram(program);
const extension = gl.getExtension('WEBGL_lose_context'); if (extension) extension.loseContext();
}
};
}
function attach(renderer, context) {
let rasterizer;
try { rasterizer = createRasterizer(context); } catch (e) { return renderer; }
if (!rasterizer) return renderer;
const draw = renderer.draw.bind(renderer), triangle = renderer.drawTriangle.bind(renderer);
renderer.draw = skeleton => {
if (!rasterizer.begin(!!renderer.premultipliedAlpha)) return draw(skeleton);
renderer.gpuDrawing = true;
renderer.drawTriangle = rasterizer.triangle;
try { draw(skeleton); rasterizer.end(); }
finally { renderer.drawTriangle = triangle; renderer.gpuDrawing = false; }
};
renderer.dispose = rasterizer.dispose;
renderer.rasterization = 'webgl';
return renderer;
}
module.exports = { createRasterizer, attach, prepareTexture };
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB