网站页面

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

+842
View File
@@ -0,0 +1,842 @@
/* ==========================================================================
SpineViewer 插件介绍网站脚本
- 主题切换、图片放大、滚动出现
- 查看器交互演示:官方 Raptor 示例(Spine 4.3.75-beta 导出)
仅支持 Spine 4.3:使用 4.3 官方运行时 + 插件同款 WebGL 光栅化器,
WebGL 不可用时自动回退到 Canvas(与插件查看器的回退行为相同)。
========================================================================== */
(function () {
'use strict';
var root = document.documentElement;
var viewerEl = document.getElementById('viewerDemo');
function clamp(v, min, max) { return Math.min(max, Math.max(min, v)); }
/* ---------------- 主题切换 ---------------- */
var themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.addEventListener('click', function () {
var next = root.dataset.theme === 'dark' ? 'light' : 'dark';
root.dataset.theme = next;
try { localStorage.setItem('sv-site-theme', next); } catch (e) {}
refreshViewerColors();
});
}
/* ---------------- 滚动出现(带兜底,内容不会一直隐藏) ---------------- */
var revealEls = Array.prototype.slice.call(document.querySelectorAll('.reveal'));
var revealTick = 0;
function revealInViewport() {
var vh = window.innerHeight || document.documentElement.clientHeight;
for (var i = 0; i < revealEls.length; i++) {
var el = revealEls[i];
if (el.classList.contains('visible')) continue;
var rect = el.getBoundingClientRect();
if (rect.top < vh * 0.92 && rect.bottom > 0) el.classList.add('visible');
}
}
function revealAll() {
revealEls.forEach(function (el) { el.classList.add('visible'); });
}
function scheduleReveal() {
if (revealTick) return;
revealTick = window.requestAnimationFrame(function () {
revealTick = 0;
revealInViewport();
});
}
revealInViewport();
window.addEventListener('scroll', scheduleReveal, { passive: true });
window.addEventListener('resize', scheduleReveal);
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') revealInViewport();
});
// 兜底:后台标签页 / 未触发滚动时,稍后也把全部内容显示出来
setTimeout(revealAll, 2000);
/* ---------------- 图片放大 ---------------- */
var lightbox = document.getElementById('lightbox');
var lightboxImg = lightbox ? lightbox.querySelector('img') : null;
function closeLightbox() {
if (!lightbox || lightbox.hidden) return;
lightbox.hidden = true;
lightboxImg.removeAttribute('src');
document.body.style.overflow = '';
}
if (lightbox) {
Array.prototype.forEach.call(document.querySelectorAll('img.zoomable'), function (img) {
img.addEventListener('click', function () {
lightboxImg.src = img.currentSrc || img.src;
lightboxImg.alt = img.alt || '';
lightbox.hidden = false;
document.body.style.overflow = 'hidden';
});
});
lightbox.addEventListener('click', closeLightbox);
window.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeLightbox();
});
}
/* ====================================================================
查看器交互演示(官方 Raptor 示例 · Spine 4.3 运行时渲染)
==================================================================== */
if (!viewerEl) return;
var spine = window.spine;
var svLibs = window.__svLibs || {};
var FPS = 30;
var EPS = 1e-6;
var RUNTIME_DIR = '4.3'; // 演示只使用 4.3
var DEFAULT_ANIMATION = 'walk';
var DEMO_FILES = {
json: 'assets/demo/raptor/raptor-pro.json',
atlas: 'assets/demo/raptor/raptor.atlas',
texture: 'assets/demo/raptor/raptor.png'
};
var stageEl = document.getElementById('demoStage');
var canvas = document.getElementById('demoCanvas');
var ctx = canvas.getContext('2d');
var playBtn = document.getElementById('demoPlay');
var loopCheck = document.getElementById('demoLoop');
var speedRange = document.getElementById('demoSpeed');
var speedVal = document.getElementById('demoSpeedVal');
var bgCheck = document.getElementById('demoBg');
var reloadBtn = document.getElementById('demoReload');
var skinSelect = document.getElementById('demoSkin');
var animListEl = document.getElementById('demoAnimList');
var statusEl = document.getElementById('demoStatus');
var durationEl = document.getElementById('demoDuration');
var rulerEl = document.getElementById('demoRuler');
var timelineEl = document.getElementById('demoTimeline');
var keysEl = document.getElementById('demoKeys');
var readoutEl = document.getElementById('demoReadout');
var versionEl = document.getElementById('demoVersion');
var state = {
playing: true,
loop: true,
speed: 1,
background: false,
seeking: false,
animIndex: 0,
panX: 0,
panY: 0,
zoom: 1
};
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
state.playing = false;
}
var runtime = {
data: null,
skeleton: null,
animState: null,
atlas: null,
texture: null,
renderer: null,
canvasOnly: false,
ready: false,
loading: false
};
var colors = { bg: '#1e1e1e' };
var dpr = 1;
var fitScale = 1, fitCx = 0, fitCy = 0;
var statusTimer = 0;
var drawErrorReported = false;
var lastFrameTime = performance.now();
var needsDraw = true; // 暂停或没有交互时不再重绘
var frameHandle = 0; // rAF 句柄:空闲时停止循环,避免持续占用 CPU
// 需要重绘时唤醒渲染循环
function wake() {
needsDraw = true;
if (!frameHandle) frameHandle = requestAnimationFrame(tick);
}
function refreshViewerColors() {
var cs = getComputedStyle(viewerEl);
colors.bg = (cs.getPropertyValue('--v-bg') || '#1e1e1e').trim();
wake();
}
// 控制画布总像素,避免高分辨率屏上占用过多显存
function pickDpr(width, height) {
var value = Math.min(window.devicePixelRatio || 1, 2);
var maxPixels = 900000;
if (width > 0 && height > 0 && width * height * value * value > maxPixels) {
value = Math.max(1, Math.sqrt(maxPixels / (width * height)));
}
return value;
}
/* ---------------- 状态提示 ---------------- */
function setStatus(message, holdMs) {
statusEl.textContent = message;
statusEl.classList.add('show');
clearTimeout(statusTimer);
statusTimer = 0;
if (holdMs > 0 && isFinite(holdMs)) {
statusTimer = setTimeout(function () { statusEl.classList.remove('show'); }, holdMs);
}
}
/* ---------------- 时间与刻度(与查看器同算法) ---------------- */
function frameOf(seconds) { return Math.round((seconds || 0) * FPS); }
function formatSeconds(seconds) {
var value = Math.max(0, Number(seconds) || 0);
if (value < 10) return value.toFixed(2) + ' 秒';
if (value < 60) return value.toFixed(1) + ' 秒';
var minutes = Math.floor(value / 60);
return minutes + ':' + (value % 60).toFixed(1).padStart(4, '0');
}
function niceTickInterval(totalSeconds) {
var target = totalSeconds / 8;
var choices = [1 / 30, 0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];
for (var i = 0; i < choices.length; i++) {
if (choices[i] >= target) return choices[i];
}
return Math.pow(10, Math.ceil(Math.log10(target)));
}
function currentAnimation() {
if (!runtime.data || !runtime.data.animations) return null;
return runtime.data.animations[state.animIndex] || null;
}
function animationDuration(anim) {
if (!anim) return 0;
var declared = Number(anim.duration);
if (Number.isFinite(declared) && declared > EPS) return declared;
var times = collectKeyframeTimes(anim);
return times.length ? Math.max.apply(null, times) : 0;
}
function isStatic() { return animationDuration(currentAnimation()) <= EPS; }
function snapTimeToFrame(seconds) {
var duration = animationDuration(currentAnimation());
var lastFrame = Math.max(0, frameOf(duration));
var frame = clamp(Math.round((Number(seconds) || 0) * FPS), 0, lastFrame);
return frame / FPS;
}
// 与查看器一致:收集各 timeline 的关键帧时间
function collectKeyframeTimes(anim) {
if (!anim || !anim.timelines) return [];
var times = new Set();
for (var i = 0; i < anim.timelines.length; i++) {
var tl = anim.timelines[i];
var frames = tl && tl.frames;
if (!frames || !frames.length) continue;
try {
if (Array.isArray(frames[0])) {
for (var f = 0; f < frames.length; f++) {
var frame = frames[f];
if (frame && Number.isFinite(frame[0])) times.add(frame[0]);
}
continue;
}
var frameCount = typeof tl.getFrameCount === 'function' ? tl.getFrameCount() : 0;
var entries = typeof tl.getFrameEntries === 'function'
? tl.getFrameEntries()
: (frameCount > 0 ? Math.max(1, Math.round(frames.length / frameCount)) : 1);
for (var k = 0; k < frameCount; k++) {
var value = frames[k * entries];
if (Number.isFinite(value)) times.add(value);
}
} catch (e) { /* 忽略异常的 timeline */ }
}
return Array.from(times).filter(function (time) { return time >= 0; }).sort(function (a, b) { return a - b; });
}
function renderTimeRuler() {
rulerEl.innerHTML = '';
var duration = animationDuration(currentAnimation());
if (duration <= EPS) {
var staticLabel = document.createElement('span');
staticLabel.className = 'tick-label first';
staticLabel.style.left = '0%';
staticLabel.textContent = '0 秒(静帧)';
rulerEl.appendChild(staticLabel);
return;
}
var major = niceTickInterval(duration);
var minor = major / 5;
var count = Math.floor(duration / minor);
// 窄屏下隔一个主刻度显示标签,避免文字互相叠压
var skipLabels = rulerEl.clientWidth > 0 && rulerEl.clientWidth < 430 ? 2 : 1;
for (var i = 0; i <= count; i++) {
var value = Math.min(i * minor, duration);
var percent = (value / duration) * 100;
var isMajor = i % 5 === 0 || Math.abs(value - duration) < 1e-8;
var tick = document.createElement('span');
tick.className = 'tick' + (isMajor ? ' major' : '');
tick.style.left = percent + '%';
rulerEl.appendChild(tick);
if (isMajor) {
var isEnd = Math.abs(value - duration) < 1e-8;
if (!isEnd && (i / 5) % skipLabels !== 0) continue;
var label = document.createElement('span');
label.className = 'tick-label' + (i === 0 ? ' first' : (isEnd ? ' last' : ''));
label.style.left = percent + '%';
label.textContent = value < 10 ? value.toFixed(1) + 's' : Math.round(value) + 's';
rulerEl.appendChild(label);
}
if (Math.abs(value - duration) < 1e-8) break;
}
if (Math.abs(duration / minor - Math.round(duration / minor)) > 1e-8) {
var endTick = document.createElement('span');
endTick.className = 'tick major';
endTick.style.left = '100%';
rulerEl.appendChild(endTick);
var endLabel = document.createElement('span');
endLabel.className = 'tick-label last';
endLabel.style.left = '100%';
endLabel.textContent = duration < 10 ? duration.toFixed(2) + 's' : duration.toFixed(1) + 's';
rulerEl.appendChild(endLabel);
}
}
function renderKeyframes() {
keysEl.innerHTML = '';
var anim = currentAnimation();
var duration = animationDuration(anim);
if (!anim) return;
if (duration <= EPS) {
var span = document.createElement('span');
span.style.left = '0%';
span.title = '静帧姿势 · 第 1 帧';
keysEl.appendChild(span);
return;
}
var sorted = collectKeyframeTimes(anim).filter(function (time) { return time <= duration; });
// 限制数量,避免过多标记
var maxMarkers = 200;
var step = Math.max(1, Math.ceil(sorted.length / maxMarkers));
for (var i = 0; i < sorted.length; i += step) {
var keyframeTime = sorted[i];
var mark = document.createElement('span');
mark.style.left = (keyframeTime / duration) * 100 + '%';
mark.title = '关键帧 · ' + formatSeconds(keyframeTime) + ' · 第 ' + frameOf(keyframeTime) + ' 帧';
keysEl.appendChild(mark);
}
}
// 仅支持 4.3:轨道查询使用 getTrack
function trackEntry() {
return runtime.animState ? (runtime.animState.getTrack(0) || null) : null;
}
function currentTrackTime() {
var entry = trackEntry();
if (!entry) return 0;
var raw = Number.isFinite(entry.trackTime) ? entry.trackTime : 0;
var duration = animationDuration(currentAnimation());
if (state.loop && duration > EPS) return raw % duration;
return Math.max(0, Math.min(raw, duration));
}
function updateReadout() {
var duration = animationDuration(currentAnimation());
if (duration <= EPS) {
readoutEl.textContent = '静帧 · 1 / 1 帧';
if (!state.seeking) timelineEl.value = '0';
return;
}
var time = clamp(currentTrackTime(), 0, duration);
readoutEl.textContent = formatSeconds(time) + ' / ' + formatSeconds(duration) +
' · ' + frameOf(time) + ' / ' + frameOf(duration) + ' 帧';
if (!state.seeking) timelineEl.value = String(clamp(time / duration, 0, 1));
}
function updatePlayButton() {
if (!runtime.ready || isStatic()) {
playBtn.textContent = runtime.ready ? '▮ 静帧' : '⏸ 暂停';
playBtn.disabled = true;
return;
}
playBtn.disabled = false;
playBtn.textContent = state.playing ? '⏸ 暂停' : '▶ 播放';
}
function updateTimelineUI() {
var duration = animationDuration(currentAnimation());
var staticAnim = duration <= EPS;
durationEl.textContent = staticAnim
? '静帧 · 1 帧'
: '时长 ' + formatSeconds(duration) + ' · ' + frameOf(duration) + ' 帧';
timelineEl.disabled = staticAnim;
timelineEl.value = '0';
updatePlayButton();
renderTimeRuler();
renderKeyframes();
updateReadout();
}
function highlightAnimationItem() {
Array.prototype.forEach.call(animListEl.children, function (item, i) {
item.classList.toggle('active', i === state.animIndex);
item.setAttribute('aria-selected', i === state.animIndex ? 'true' : 'false');
});
}
function selectAnimation(index) {
state.animIndex = index;
var anim = currentAnimation();
if (runtime.animState && anim) {
var entry = runtime.animState.setAnimation(0, anim.name, state.loop);
if (entry) entry.loop = state.loop;
}
highlightAnimationItem();
updateTimelineUI();
wake();
}
/* ---------------- 骨架姿态与世界变换(4.3 API) ---------------- */
function updateWorld() {
if (runtime.skeleton) runtime.skeleton.updateWorldTransform(spine.Physics.update);
}
function applyPose() {
if (!runtime.animState || !runtime.skeleton) return;
runtime.animState.apply(runtime.skeleton);
updateWorld();
}
function getBounds(sk) {
if (!sk || typeof sk.getBoundsRect !== 'function') return null;
var rect = sk.getBoundsRect();
if (!rect || !(rect.width > 0) || !(rect.height > 0)) return null;
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
}
function fitToCanvas() {
if (!runtime.skeleton) return;
var bundle = getBounds(runtime.skeleton);
var cw = canvas.clientWidth, ch = canvas.clientHeight;
if (!bundle || !cw || !ch) {
fitScale = 1; fitCx = 0; fitCy = 0;
return;
}
// 自动适配并居中;顶部控制栏与底部时间轴是浮动面板,按其实际高度留出空间
var toolbarEl = viewerEl.querySelector('.v-toolbar');
var timelineBar = viewerEl.querySelector('.v-timeline');
var padTop = (toolbarEl ? toolbarEl.offsetHeight : 46) + 24;
var padBottom = (timelineBar ? timelineBar.offsetHeight : 68) + 24;
var pad = 0.06;
var availW = cw * (1 - pad * 2);
var availH = Math.max(60, ch - padTop - padBottom);
var scale = Math.min(availW / bundle.width, availH / bundle.height);
fitScale = (isFinite(scale) && scale > 0) ? scale : 1;
fitCx = bundle.x + bundle.width / 2;
fitCy = bundle.y + bundle.height / 2 - ((padBottom - padTop) / 2) / fitScale;
}
/* ---------------- 渲染 ---------------- */
function buildRenderer(context, allowWebGL) {
if (!svLibs.clipping || typeof svLibs.clipping.createRenderer !== 'function') throw new Error('渲染模块未加载');
var renderer;
if (allowWebGL) {
renderer = svLibs.clipping.createRenderer(spine, context, RUNTIME_DIR);
} else {
// 临时停用 WebGL 光栅化附着,得到纯 Canvas 渲染器
var rasterizer = svLibs.rasterizer || {};
var originalAttach = rasterizer.attach;
rasterizer.attach = function (r) { return r; };
try { renderer = svLibs.clipping.createRenderer(spine, context, RUNTIME_DIR); }
finally { if (originalAttach) rasterizer.attach = originalAttach; }
}
// 示例为普通 Alpha 导出(atlas 未标注 pma),预乘保持关闭
renderer.premultipliedAlpha = false;
return renderer;
}
function drawSkeletonSafely() {
try {
runtime.renderer.draw(runtime.skeleton);
} catch (e) {
if (!runtime.canvasOnly) {
// WebGL 光栅化不可用(缺少 WebGL、纹理受限等)时回退到 Canvas 路径
runtime.canvasOnly = true;
console.warn('WebGL 光栅化不可用,已回退到 Canvas 渲染:', (e && e.message) || e);
try { if (runtime.renderer && runtime.renderer.dispose) runtime.renderer.dispose(); } catch (e2) { /* ignore */ }
runtime.renderer = buildRenderer(ctx, false);
runtime.renderer.draw(runtime.skeleton);
return;
}
if (!drawErrorReported) {
drawErrorReported = true;
console.error(e);
setStatus('渲染失败:' + ((e && e.message) || e), 0);
}
}
}
function draw() {
var cw = canvas.clientWidth, ch = canvas.clientHeight;
if (!cw || !ch) return;
var pixelWidth = Math.round(cw * dpr), pixelHeight = Math.round(ch * dpr);
if (canvas.width !== pixelWidth) canvas.width = pixelWidth;
if (canvas.height !== pixelHeight) canvas.height = pixelHeight;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, cw, ch);
ctx.fillStyle = colors.bg;
ctx.fillRect(0, 0, cw, ch);
if (!runtime.ready || !runtime.skeleton || !runtime.renderer) return;
// 与查看器相同:居中 + 缩放 + Y 翻转;panX/panY 为屏幕坐标偏移
var scale = fitScale * state.zoom;
ctx.save();
ctx.translate(cw / 2 + state.panX, ch / 2 + state.panY);
ctx.scale(scale, -scale);
ctx.translate(-fitCx, -fitCy);
if (state.background && svLibs.background) {
svLibs.background.drawCheckerboard(ctx, canvas.width, canvas.height);
}
drawSkeletonSafely();
ctx.restore();
}
/* ---------------- 主循环 ---------------- */
function tick(now) {
frameHandle = 0;
var dt = Math.min(0.05, (now - lastFrameTime) / 1000);
lastFrameTime = now;
var advancing = false;
if (runtime.ready) {
var duration = animationDuration(currentAnimation());
advancing = state.playing && !state.seeking && state.speed > 0 && duration > EPS;
runtime.animState.timeScale = advancing ? state.speed : 0;
runtime.animState.update(dt);
runtime.animState.apply(runtime.skeleton);
updateWorld();
if (advancing) needsDraw = true;
// 非循环播放到结尾时停止推进(与查看器行为一致)
var entry = trackEntry();
if (entry && !state.loop && duration > EPS && entry.trackTime >= duration - 1e-4 && state.playing) {
state.playing = false;
updatePlayButton();
needsDraw = true;
advancing = false;
}
}
if (needsDraw) {
draw();
updateReadout();
needsDraw = false;
}
// 播放中持续刷新;暂停或空闲时停在这里,等待 wake() 唤醒
if (advancing) frameHandle = requestAnimationFrame(tick);
}
/* ---------------- 控件事件 ---------------- */
function togglePlay() {
if (!runtime.ready || isStatic()) return;
var duration = animationDuration(currentAnimation());
var entry = trackEntry();
// 播放到结尾后再次播放:从头发起
if (!state.playing && entry && duration > EPS && entry.trackTime >= duration - 1e-4) {
var anim = currentAnimation();
runtime.animState.setAnimation(0, anim.name, state.loop);
var next = trackEntry();
if (next) next.loop = state.loop;
}
state.playing = !state.playing;
updatePlayButton();
wake();
}
playBtn.addEventListener('click', togglePlay);
loopCheck.addEventListener('change', function () {
state.loop = loopCheck.checked;
var entry = trackEntry();
if (entry) entry.loop = state.loop;
});
speedRange.addEventListener('input', function () {
state.speed = Number(speedRange.value);
speedVal.textContent = state.speed.toFixed(1) + 'x';
});
bgCheck.addEventListener('change', function () {
state.background = bgCheck.checked;
wake();
});
skinSelect.addEventListener('change', function () {
if (!runtime.skeleton) return;
runtime.skeleton.setSkinByName(skinSelect.value);
runtime.skeleton.setupPoseSlots();
setStatus('皮肤:' + skinSelect.value, 1600);
wake();
});
reloadBtn.addEventListener('click', function () {
state.panX = 0;
state.panY = 0;
state.zoom = 1;
if (runtime.ready) {
selectAnimation(state.animIndex);
fitToCanvas();
}
setStatus('已重新加载目录', 1800);
});
// 时间轴:拖动时暂停推进,并按帧对齐(与查看器一致)
timelineEl.addEventListener('pointerdown', function () { state.seeking = true; });
timelineEl.addEventListener('input', function () {
if (!runtime.ready || isStatic()) return;
state.seeking = true;
var duration = animationDuration(currentAnimation());
var time = snapTimeToFrame(Number(timelineEl.value) * duration);
var entry = trackEntry();
if (entry) entry.trackTime = time;
applyPose();
updateReadout();
wake();
});
timelineEl.addEventListener('change', function () { state.seeking = false; });
window.addEventListener('pointerup', function () { state.seeking = false; });
// 画布:左键拖拽平移,滚轮缩放,右键回到中心
var dragging = false, dragX = 0, dragY = 0, startPanX = 0, startPanY = 0;
stageEl.addEventListener('pointerdown', function (e) {
if (e.button !== 0) return;
dragging = true;
dragX = e.clientX;
dragY = e.clientY;
startPanX = state.panX;
startPanY = state.panY;
stageEl.classList.add('dragging');
if (stageEl.setPointerCapture) stageEl.setPointerCapture(e.pointerId);
});
stageEl.addEventListener('pointermove', function (e) {
if (!dragging) return;
state.panX = startPanX + (e.clientX - dragX);
state.panY = startPanY + (e.clientY - dragY);
wake();
});
function endDrag() {
dragging = false;
stageEl.classList.remove('dragging');
}
stageEl.addEventListener('pointerup', endDrag);
stageEl.addEventListener('pointercancel', endDrag);
stageEl.addEventListener('wheel', function (e) {
e.preventDefault();
state.zoom = clamp(state.zoom * Math.exp(-e.deltaY * 0.0012), 0.3, 6);
wake();
}, { passive: false });
stageEl.addEventListener('contextmenu', function (e) {
e.preventDefault();
state.panX = 0;
state.panY = 0;
state.zoom = 1;
wake();
setStatus('已回到画布中心', 1600);
});
// 空格:鼠标悬停或演示区聚焦时播放/暂停
var hovering = false;
viewerEl.addEventListener('pointerenter', function () { hovering = true; });
viewerEl.addEventListener('pointerleave', function () { hovering = false; });
window.addEventListener('keydown', function (e) {
if (e.code !== 'Space' && e.key !== ' ') return;
var active = document.activeElement;
if (active && viewerEl.contains(active) && active !== viewerEl && active.closest('button, select, input, summary, a')) return;
if (!hovering && (!active || !viewerEl.contains(active))) return;
e.preventDefault();
togglePlay();
});
/* ---------------- 示例数据加载 ---------------- */
function loadImage(src) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = function () { resolve(image); };
image.onerror = function () { reject(new Error('贴图加载失败')); };
image.src = src;
});
}
function fetchText(url) {
return fetch(url).then(function (response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.text();
});
}
// 内嵌副本按需加载:file:// 或原始文件读取失败时才会用到
function ensureEmbeddedData() {
if (window.SPINE_DEMO_DATA) return Promise.resolve(window.SPINE_DEMO_DATA);
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = 'assets/demo/raptor/embedded.js';
script.onload = function () {
if (window.SPINE_DEMO_DATA) resolve(window.SPINE_DEMO_DATA);
else reject(new Error('缺少内嵌示例数据'));
};
script.onerror = function () { reject(new Error('缺少内嵌示例数据')); };
document.head.appendChild(script);
});
}
// http 环境读取同目录下的原始文件;file:// 下浏览器禁止 fetch 本地文本,改用内嵌副本
function loadDemoData() {
if (location.protocol === 'file:') {
return ensureEmbeddedData().then(function (embedded) {
return { data: embedded.data, atlas: embedded.atlas, texture: embedded.texture || DEMO_FILES.texture, source: 'embedded' };
});
}
return Promise.all([fetchText(DEMO_FILES.json), fetchText(DEMO_FILES.atlas)]).then(function (texts) {
return { data: JSON.parse(texts[0]), atlas: texts[1], texture: DEMO_FILES.texture, source: 'file' };
}).catch(function () {
return ensureEmbeddedData().then(function (embedded) {
return { data: embedded.data, atlas: embedded.atlas, texture: embedded.texture || DEMO_FILES.texture, source: 'embedded' };
});
});
}
function fillSkinSelect() {
skinSelect.innerHTML = '';
var skins = (runtime.data && runtime.data.skins) || [];
skins.forEach(function (skin) {
var option = document.createElement('option');
option.value = skin.name;
option.textContent = skin.name;
skinSelect.appendChild(option);
});
skinSelect.disabled = skins.length === 0;
}
function fillAnimationList() {
animListEl.innerHTML = '';
var animations = (runtime.data && runtime.data.animations) || [];
animations.forEach(function (anim, i) {
var item = document.createElement('button');
item.type = 'button';
item.className = 'v-anim-item';
item.textContent = anim.name;
item.title = anim.name;
item.setAttribute('role', 'option');
item.setAttribute('aria-selected', 'false');
item.addEventListener('click', function () { selectAnimation(i); });
animListEl.appendChild(item);
});
}
async function initDemo() {
if (runtime.ready || runtime.loading) return;
runtime.loading = true;
setStatus('正在加载官方示例 Raptor…', 0);
try {
if (!spine || typeof spine.SkeletonJson !== 'function') throw new Error('Spine 运行时未加载');
var payload = await loadDemoData();
var image = await loadImage(payload.texture);
runtime.texture = new spine.CanvasTexture(image);
// 仅支持 4.3:TextureAtlas 构造器只接收文本,纹理通过 page.setTexture 绑定
runtime.atlas = new spine.TextureAtlas(payload.atlas);
for (var p = 0; p < runtime.atlas.pages.length; p++) {
runtime.atlas.pages[p].setTexture(runtime.texture);
}
var skeletonJson = new spine.SkeletonJson(new spine.AtlasAttachmentLoader(runtime.atlas));
runtime.data = skeletonJson.readSkeletonData(payload.data);
runtime.skeleton = new spine.Skeleton(runtime.data);
runtime.animState = new spine.AnimationState(new spine.AnimationStateData(runtime.data));
runtime.renderer = buildRenderer(ctx, true);
// 默认皮肤:优先使用数据声明的默认皮肤,没有则用列表第一个
var defaultSkin = runtime.data.defaultSkin || (runtime.data.skins && runtime.data.skins[0]) || null;
fillSkinSelect();
if (defaultSkin) {
runtime.skeleton.setSkin(defaultSkin);
skinSelect.value = defaultSkin.name;
}
runtime.skeleton.setupPoseSlots();
fillAnimationList();
// 默认播放 walk
var walkIndex = 0;
for (var i = 0; i < runtime.data.animations.length; i++) {
if (runtime.data.animations[i].name === DEFAULT_ANIMATION) { walkIndex = i; break; }
}
state.animIndex = walkIndex;
selectAnimation(walkIndex);
// 与查看器一致:先应用姿态并更新世界变换,再按包围盒自动适配
applyPose();
versionEl.textContent = 'Spine ' + (runtime.data.version || '');
runtime.ready = true;
dpr = pickDpr(canvas.clientWidth, canvas.clientHeight);
fitToCanvas();
updateTimelineUI();
wake();
setStatus('已加载官方示例 Raptor · ' + runtime.data.animations.length + ' 个动画 · ' +
(payload.source === 'embedded' ? '内嵌数据' : '本地文件'), 3200);
} catch (e) {
console.error(e);
setStatus('示例加载失败:' + ((e && e.message) || e), 0);
} finally {
runtime.loading = false;
}
}
/* ---------------- 尺寸与启动 ---------------- */
function resize() {
dpr = pickDpr(canvas.clientWidth, canvas.clientHeight);
fitToCanvas();
renderTimeRuler(); // 宽度变化时重新决定刻度标签密度
wake();
}
if ('ResizeObserver' in window) {
new ResizeObserver(resize).observe(stageEl);
} else {
window.addEventListener('resize', resize);
}
refreshViewerColors();
dpr = pickDpr(canvas.clientWidth, canvas.clientHeight);
updatePlayButton();
wake();
initDemo();
})();