时间轴交互与预览修正
- 界面文字全局禁止鼠标框选,输入类控件保留可选中 - 拖动时间轴立即暂停自动播放,避免与播放头抢时间轴 - 暂停时补齐未完成的帧缓存,拖动时间轴直接读取已烘焙帧,无需重新触发播放 - 循环持续的烘焙不再依赖从第 0 帧开始录制,避免整段缓存为空
This commit is contained in:
1 parent
d6232c9d66
commit
2ba2b9c08a
3 files changed
+59
-9
No files matched your search
@@ -13,6 +13,11 @@ body { position: fixed; inset: 0; }
|
||||
* { box-sizing: border-box; }
|
||||
#app { position: fixed; inset: 0; width: auto; height: auto; overflow: clip; }
|
||||
|
||||
/* 界面文字不参与鼠标框选:拖动滑块/手柄时不会误选中标签文字。
|
||||
输入类控件仍可选中,方便编辑和复制数值。 */
|
||||
html, body, #app { -webkit-user-select: none; user-select: none; }
|
||||
input, textarea, select, [contenteditable='true'] { -webkit-user-select: text; user-select: text; }
|
||||
|
||||
/* 数值由滑块、键盘或直接输入调整,统一隐藏浏览器自带的上下微调箭头。 */
|
||||
input[type='number'] { appearance: textfield; -moz-appearance: textfield; }
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
|
||||
+52
-9
@@ -192,6 +192,44 @@ function syncEmitters() {
|
||||
emitterSync.sync(store, world, dotTex, trailTex)
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步烘焙一个系统的整段帧缓存。
|
||||
* 与逐帧录制结果一致(同样的固定步长、同样的随机序列),因此不会改变预览内容,
|
||||
* 只是把"参数变动时就该生成好的烘焙结果"补齐。
|
||||
*/
|
||||
function bakeSystemFrames(system: ParticleSystem, emitter: ParticleEmitter, fps: number, totalFrames: number) {
|
||||
const frameCount = Math.max(2, Math.floor(totalFrames) || 2)
|
||||
// 循环持续本来就是一次性烘焙,直接复用同一套逻辑。
|
||||
if (system.config.mode === 'stream' && system.config.streamBehavior === 'loop') {
|
||||
bakeLoopFrames(system, emitter, fps, frameCount)
|
||||
return
|
||||
}
|
||||
emitter.reset()
|
||||
const frameDuration = 1 / Math.max(1, fps)
|
||||
const frames: ParticleState[][] = []
|
||||
for (let frame = 0; frame < frameCount; frame++) {
|
||||
// 与逐帧录制保持一致:第 0 帧保持空白,避免正式播放前提前发射。
|
||||
if (frame === 0) { emitter.apply([]); frames.push([]) } else frames.push(emitter.update(frameDuration))
|
||||
}
|
||||
system.frames = frames
|
||||
}
|
||||
|
||||
/**
|
||||
* 帧缓存缺失时补齐(例如参数刚改完就暂停/拖动时间轴)。
|
||||
* 缓存本来就完整时不做任何事,保证拖动时间轴不会重新生成预览。
|
||||
*/
|
||||
function ensureFramesBaked(fps: number) {
|
||||
const totalFrames = Math.max(2, Math.floor(store.timeline.totalFrames) || 2)
|
||||
let complete = true
|
||||
for (const system of store.systems) {
|
||||
if ((system.frames?.length || 0) >= totalFrames) continue
|
||||
const emitter = emitterSync.emitters.get(system.id)
|
||||
if (!emitter) { complete = false; continue }
|
||||
bakeSystemFrames(system as unknown as ParticleSystem, emitter, fps, totalFrames)
|
||||
}
|
||||
if (complete) store.timeline.recorded = true
|
||||
}
|
||||
|
||||
function loop() {
|
||||
raf = requestAnimationFrame(loop)
|
||||
const now = performance.now() / 1000
|
||||
@@ -323,6 +361,9 @@ function loop() {
|
||||
} else {
|
||||
// 暂停时清空余量,恢复播放后从完整的一帧时长重新计时。
|
||||
timelineAccumulator = 0
|
||||
// 暂停(含拖动时间轴)时把还没烤完的帧缓存补齐:否则画布只能停在上一次的状态,
|
||||
// 必须重新触发播放才能看到对应帧。缓存完整时这里不会做任何事。
|
||||
ensureFramesBaked(fps)
|
||||
}
|
||||
|
||||
if (tl.playing && advanceSteps > 0) {
|
||||
@@ -333,15 +374,17 @@ function loop() {
|
||||
total = 0
|
||||
if (!tl.recorded) {
|
||||
// 录制阶段:固定步长模拟并记录当前帧。
|
||||
if (tl.frame === 0) {
|
||||
for (const sys of store.systems) {
|
||||
const emitter = emitterSync.emitters.get(sys.id)
|
||||
if (!emitter) continue
|
||||
if (sys.config.mode === 'stream' && sys.config.streamBehavior === 'loop') {
|
||||
bakeLoopFrames(sys as unknown as ParticleSystem, emitter, fps, tl.totalFrames)
|
||||
} else {
|
||||
emitter.reset()
|
||||
}
|
||||
for (const sys of store.systems) {
|
||||
const emitter = emitterSync.emitters.get(sys.id)
|
||||
if (!emitter) continue
|
||||
const isLoopStream = sys.config.mode === 'stream' && sys.config.streamBehavior === 'loop'
|
||||
// 循环持续是一次性烘焙:只要缓存还没有就补上。录制不一定从第 0 帧开始
|
||||
// (播放头可能被播放范围钳到 A 帧),只按 tl.frame === 0 判断会漏烘焙,
|
||||
// 导致整段回放都没有可用帧。
|
||||
if (isLoopStream) {
|
||||
if (!sys.frames?.length) bakeLoopFrames(sys as unknown as ParticleSystem, emitter, fps, tl.totalFrames)
|
||||
} else if (tl.frame === 0) {
|
||||
emitter.reset()
|
||||
}
|
||||
}
|
||||
for (const [systemId, em] of emitterSync.emitters) {
|
||||
|
||||
@@ -355,6 +355,8 @@ function onWheel(e: WheelEvent) {
|
||||
// 拖动时间轴滑动(scrub):更新 frame(0..totalFrames-1)
|
||||
function onScrubDown(e: PointerEvent) {
|
||||
const vp = vpRef.value!
|
||||
// 用户接管时间轴时立即暂停播放,否则每帧推进会和拖动抢播放头。
|
||||
store.pause()
|
||||
const move = (ev: PointerEvent) => {
|
||||
const rect = vp.getBoundingClientRect()
|
||||
const f = Math.floor(((ev.clientX - rect.left) / rect.width) * viewFrames.value)
|
||||
|
||||
Reference in new issue
Block a user