This commit is contained in:
tianmo committed 2026-09-11 12:53:36 +08:00
1 parent 6f7230458d
commit ee7b411a01
1 file changed
+35 -12
+35 -12
View File
@@ -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) => {