默认预览setup pose、修复播放动画时的画布缩放问题

This commit is contained in:
tianmo committed 2026-09-18 17:50:54 +08:00
1 parent a4e86b37fc
commit 52bdbc9f3c
19 files changed
+166 -40

No files matched your search

@@ -39,6 +39,7 @@
"reloadTitle": "Ordner neu laden",
"animationList": "Animationsliste",
"animations": "Animationen",
"setupPose": "Setup Pose",
"errorTitle": "Vorschau nicht möglich",
"durationPlaceholder": "Dauer --",
"keyframesLegend": "Blaue Marken: Keyframes",
@@ -41,6 +41,7 @@
"reloadTitle": "Reload folder",
"animationList": "Animation list",
"animations": "Animations",
"setupPose": "Setup Pose",
"errorTitle": "Cannot preview",
"durationPlaceholder": "Duration --",
"keyframesLegend": "Blue ticks: keyframes",
@@ -39,6 +39,7 @@
"reloadTitle": "Recargar carpeta",
"animationList": "Lista de animaciones",
"animations": "Animaciones",
"setupPose": "Setup Pose",
"errorTitle": "No se puede previsualizar",
"durationPlaceholder": "Duración --",
"keyframesLegend": "Marcas azules: fotogramas clave",
@@ -39,6 +39,7 @@
"reloadTitle": "フォルダーを再読み込み",
"animationList": "アニメーション一覧",
"animations": "アニメーション",
"setupPose": "Setup Pose",
"errorTitle": "プレビューできません",
"durationPlaceholder": "長さ --",
"keyframesLegend": "青い線:キーフレーム",
@@ -39,6 +39,7 @@
"reloadTitle": "폴더 다시 불러오기",
"animationList": "애니메이션 목록",
"animations": "애니메이션",
"setupPose": "Setup Pose",
"errorTitle": "미리볼 수 없음",
"durationPlaceholder": "길이 --",
"keyframesLegend": "파란 선: 키프레임",
@@ -39,6 +39,7 @@
"reloadTitle": "Перезагрузить папку",
"animationList": "Список анимаций",
"animations": "Анимации",
"setupPose": "Setup Pose",
"errorTitle": "Просмотр невозможен",
"durationPlaceholder": "Длительность --",
"keyframesLegend": "Синие метки: ключевые кадры",
@@ -41,6 +41,7 @@
"reloadTitle": "重新加载目录",
"animationList": "动画列表",
"animations": "动画",
"setupPose": "Setup Pose",
"errorTitle": "无法预览",
"durationPlaceholder": "时长 --",
"keyframesLegend": "蓝色短线:关键帧",
@@ -41,6 +41,7 @@
"reloadTitle": "重新載入目錄",
"animationList": "動畫清單",
"animations": "動畫",
"setupPose": "Setup Pose",
"errorTitle": "無法預覽",
"durationPlaceholder": "時長 --",
"keyframesLegend": "藍色短線:關鍵影格",
+53 -20
View File
@@ -250,8 +250,9 @@
let renderer = null;
let activeRuntimeDir = null;
let activeVersion = null;
let currentAnim = 0, currentSkin = null;
let playing = true, loop = true, speed = 1;
const SETUP_POSE_INDEX = -1;
let currentAnim = SETUP_POSE_INDEX, currentSkin = null;
let playing = false, loop = true, speed = 1;
let premultipliedAlpha = false;
let previewSettings = null;
let timeKeeper = null, lastTime = 0, running = false;
@@ -265,6 +266,7 @@
// 画布平移 / 缩放状态(左键拖拽平移,滚轮缩放,右键回中心)
let panX = 0, panY = 0, zoom = 1;
let baseFitScale = 1, baseFitCx = 0, baseFitCy = 0;
let stableFitBounds = null;
let dragging = false; // 是否正在拖拽
let dragStartX = 0, dragStartY = 0, dragStartPanX = 0, dragStartPanY = 0;
@@ -460,8 +462,17 @@
// baseFitScale 为自动适配用的基准缩放;panX/panY/zoom 为用户平移/缩放叠加。
// fitScale = baseFitScale * zoom;fitCx/fitCy 是骨骼包围盒中心(世界坐标)。
let fitScale = 1, fitCx = 0, fitCy = 0;
function fitToCanvas() {
function captureStableFitBounds() {
const bounds = getBounds(skeleton);
stableFitBounds = bounds && bounds.width > 0 && bounds.height > 0
? { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }
: null;
}
function fitToCanvas() {
// 自动适配始终使用加载完成时的 Setup Pose 包围盒。动画播放会改变当前帧
// 的包围盒,但不应改变用户滚轮缩放所依赖的基准比例。
const bounds = stableFitBounds;
const cw = canvas.clientWidth, ch = canvas.clientHeight;
if (!bounds || bounds.width <= 0 || bounds.height <= 0) {
baseFitScale = 1; baseFitCx = 0; baseFitCy = 0; fitScale = 1; fitCx = 0; fitCy = 0; return;
@@ -981,16 +992,25 @@
function fillAnimations() {
animationList.innerHTML = '';
const animations = skeletonData.animations || [];
animationPanel.style.display = animations.length ? 'flex' : 'none';
animationPanel.style.display = 'flex';
const setupItem = document.createElement('button');
setupItem.type = 'button';
setupItem.className = 'animation-item active';
setupItem.dataset.index = String(SETUP_POSE_INDEX);
setupItem.textContent = t('viewer.setupPose');
setupItem.title = t('viewer.setupPose');
setupItem.setAttribute('role', 'option');
setupItem.setAttribute('aria-selected', 'true');
animationList.appendChild(setupItem);
animations.forEach((a, i) => {
const item = document.createElement('button');
item.type = 'button';
item.className = 'animation-item' + (i === 0 ? ' active' : '');
item.className = 'animation-item';
item.dataset.index = String(i);
item.textContent = a.name;
item.title = a.name;
item.setAttribute('role', 'option');
item.setAttribute('aria-selected', i === 0 ? 'true' : 'false');
item.setAttribute('aria-selected', 'false');
animationList.appendChild(item);
});
}
@@ -1026,6 +1046,7 @@
const resolvedIndex = Number.isInteger(index) ? index : (skeletonData.animations || []).indexOf(anim);
if (resolvedIndex >= 0) selectAnimationItem(resolvedIndex);
state.setAnimation(0, name, loop);
playing = true;
updateTimelineDuration(anim);
state.update(0);
state.apply(skeleton);
@@ -1033,6 +1054,23 @@
draw();
}
function showSetupPose() {
if (!state || !skeleton) return;
selectAnimationItem(SETUP_POSE_INDEX);
playing = false;
if (typeof state.clearTracks === 'function') state.clearTracks();
else if (typeof state.clearTrack === 'function') state.clearTrack(0);
if (typeof skeleton.setToSetupPose === 'function') skeleton.setToSetupPose();
else {
if (typeof skeleton.setBonesToSetupPose === 'function') skeleton.setBonesToSetupPose();
if (typeof skeleton.setSlotsToSetupPose === 'function') skeleton.setSlotsToSetupPose();
}
updateWorld(skeleton);
updateTimelineDuration(null, true);
setStatus(t('viewer.setupPose'));
draw();
}
// 跨版本地通过名字查找动画(Spine 3.x / 4.x 都有 animations 数组)
function findAnimByName(name) {
const arr = (skeletonData && skeletonData.animations) || [];
@@ -1043,9 +1081,9 @@
}
// 更新时间轴范围(max = 动画时长),并把滑块归零,同时渲染关键帧标记
function updateTimelineDuration(anim) {
function updateTimelineDuration(anim, forceStatic = false) {
duration = animationDuration(anim);
isStaticAnimation = !!anim && duration <= STATIC_EPSILON;
isStaticAnimation = forceStatic || (!!anim && duration <= STATIC_EPSILON);
timeline.max = String(isStaticAnimation ? 1 : Math.max(duration, 0.001));
timeline.step = String(isStaticAnimation ? 1 : Math.min(1 / FPS, Math.max(duration / 1000, 0.001)));
timeline.value = '0';
@@ -1287,18 +1325,9 @@
fillAnimations();
fillSkins(currentSkin);
// 默认使用导出顺序中的第一个动画;duration=0 的动画按静帧处理。
if (skeletonData.animations.length > 0) {
const defaultAnim = skeletonData.animations[0];
state.setAnimation(0, defaultAnim.name, loop);
const idx = skeletonData.animations.indexOf(defaultAnim);
if (idx >= 0) selectAnimationItem(idx);
updateTimelineDuration(defaultAnim);
} else {
updateTimelineDuration(null);
}
updateWorld(skeleton);
// 默认显示 Setup Pose 静帧,由用户从列表中主动选择动画。
showSetupPose();
captureStableFitBounds();
fitToCanvas();
draw();
@@ -1330,6 +1359,10 @@
const item = e.target.closest('.animation-item');
if (!item) return;
const idx = parseInt(item.dataset.index, 10);
if (idx === SETUP_POSE_INDEX) {
showSetupPose();
return;
}
const anims = skeletonData.animations;
if (anims && anims[idx]) setAnimationByName(anims[idx].name, idx);
});