/* ========================================================================== 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(); })();