From ee7b411a01f33db4b22ad783dee9a7ae3732075d Mon Sep 17 00:00:00 2001 From: tianmo Date: Fri, 11 Sep 2026 12:53:36 +0800 Subject: [PATCH] =?UTF-8?q?ui=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- viewer/spine.html | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/viewer/spine.html b/viewer/spine.html index a48a7af..c8e1913 100644 --- a/viewer/spine.html +++ b/viewer/spine.html @@ -25,7 +25,11 @@ --border: #ddd; --error: #d13438; } - * { box-sizing: border-box; } + * { + box-sizing: border-box; + -webkit-user-select: none; + user-select: none; + } html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; @@ -135,6 +139,11 @@ #timelineBar .tl-wrap { position: relative; flex: 1; padding-top: 20px; } #timelineBar input[type="range"] { display: block; width: 100%; margin: 0; accent-color: var(--accent); + outline: none; box-shadow: none; -webkit-tap-highlight-color: transparent; + } + #timelineBar input[type="range"]:focus, + #timelineBar input[type="range"]:focus-visible { + outline: none; box-shadow: none; border-color: var(--border); } #timeRuler { position: absolute; top: 0; left: 7px; right: 7px; height: 19px; } #timeRuler .tick { @@ -324,7 +333,6 @@ let seeking = false; // 用户正在拖动时间轴 let duration = 0; // 当前动画时长 let isStaticAnimation = false; - let wasPlayingBeforeSeek = false; // 记录拖动前是否在播放 const FPS = 30; const STATIC_EPSILON = 1e-6; let statusHoldUntil = 0; @@ -634,6 +642,12 @@ return Math.round((seconds || 0) * FPS); } + function snapTimeToFrame(seconds) { + const lastWholeFrame = Math.max(0, Math.floor((duration + 1e-8) * FPS)); + const frame = Math.max(0, Math.min(Math.round((Number(seconds) || 0) * FPS), lastWholeFrame)); + return frame / FPS; + } + function formatSeconds(seconds) { const value = Math.max(0, Number(seconds) || 0); if (value < 10) return value.toFixed(2) + ' 秒'; @@ -1116,29 +1130,38 @@ window.location.reload(); }); document.getElementById('thumbBtn').addEventListener('click', generateThumbnail); - // 时间轴:拖动时实时跳帧 + function seekTimelineToNearestFrame() { + const snappedTime = snapTimeToFrame(parseFloat(timeline.value)); + timeline.value = String(snappedTime); + seekTo(snappedTime); + } + + // 时间轴:拖动时按 30 FPS 整数帧实时预览。 timeline.addEventListener('input', (e) => { - const t = parseFloat(e.target.value); - if (state) seekTo(t); + if (state) seekTimelineToNearestFrame(); }); - // 时间轴:按下时暂停动画,拖动结束(松开)恢复播放(若之前是播放状态) + // 时间轴:开始拖动即暂停,松开后吸附到最近的 30 FPS 整数帧并保持暂停。 timeline.addEventListener('pointerdown', () => { seeking = true; - wasPlayingBeforeSeek = playing; playing = false; updatePlayButton(); }); const endTimelineSeek = () => { if (!seeking) return; + seekTimelineToNearestFrame(); seeking = false; - if (wasPlayingBeforeSeek) { - playing = true; - updatePlayButton(); - } + playing = false; + updatePlayButton(); + timeline.blur(); }; timeline.addEventListener('pointerup', endTimelineSeek); - // 即便指针移出滑块松开,也能恢复 + // 指针移出滑块后松开时也要完成吸附。 window.addEventListener('pointerup', endTimelineSeek); + timeline.addEventListener('change', () => { + if (seeking || timeline.disabled) return; + seekTimelineToNearestFrame(); + timeline.blur(); + }); // ---------- 画布交互:左键拖拽平移,右键回中心,滚轮缩放 ---------- canvas.addEventListener('pointerdown', (e) => {