0.11.1: drawer mouse trigger and clip track / audio behavior

This commit is contained in:
th committed 2026-04-23 20:31:41 +02:00
1 parent 8d987d1526
commit fa66ba69bb
9 files changed
+258 -31

No files matched your search

+160 -21
View File
@@ -1835,6 +1835,24 @@ function clipPeaks(clip: AudioClip): Float32Array | undefined {
* the clip's current width (resizes on zoom + trim changes). Cheap
* because peaks are cached and the draw itself is a few hundred
* fillRects. */
function waveformColorFor(clip: AudioClip): string | undefined {
// Honor the track color so tinted audio tracks get a matching
// waveform fill. Accept #rgb/#rrggbb and append a fixed 55% alpha
// (matches the default sky-blue tint). Non-hex values fall back to
// the drawWaveform default.
const track = audioTrackById.value.get(clip.audioTrackId)
const color = track?.color
if (!color) return undefined
const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color)
if (short) {
const [, r, g, b] = short
return `#${r}${r}${g}${g}${b}${b}8c`
}
const full = /^#([0-9a-f]{6})$/i.exec(color)
if (full) return `#${full[1]}8c`
return color
}
function renderWaveformCanvas(clip: AudioClip) {
return (raw: Element | ComponentPublicInstance | null) => {
if (!raw || !(raw instanceof HTMLCanvasElement)) return
@@ -1852,7 +1870,7 @@ function renderWaveformCanvas(clip: AudioClip) {
if (el.height !== pxH) el.height = pxH
el.style.width = cssW + 'px'
el.style.height = cssH + 'px'
drawWaveform(el, peaks, { pixelRatio: pr })
drawWaveform(el, peaks, { pixelRatio: pr, color: waveformColorFor(clip) })
}
}
@@ -2099,7 +2117,78 @@ function onAudioClipPointerUp(e: PointerEvent) {
}
audioClipMove.value = null
audioClipGhost.value = null
if (wasMoved) e.preventDefault()
}
interface AudioClipTrimState {
clipId: string
side: 'start' | 'end'
pointerId: number
startX: number
initialStartTime: number
initialTrimStart: number
initialTrimEnd: number
}
const audioClipTrim = ref<AudioClipTrimState | null>(null)
function onAudioClipHandlePointerDown(
e: PointerEvent,
clip: AudioClip,
side: 'start' | 'end',
) {
if (e.button !== 0) return
const track = audioTrackById.value.get(clip.audioTrackId)
if (track?.locked) return
e.preventDefault()
e.stopPropagation()
const el = e.currentTarget as HTMLElement
el.setPointerCapture(e.pointerId)
audioClipTrim.value = {
clipId: clip.id,
side,
pointerId: e.pointerId,
startX: e.clientX,
initialStartTime: clip.startTime,
initialTrimStart: clip.trimStart ?? 0,
initialTrimEnd: clip.trimEnd ?? 0,
}
}
function onAudioClipHandlePointerMove(e: PointerEvent) {
const tr = audioClipTrim.value
if (!tr || tr.pointerId !== e.pointerId) return
const pps = effectivePxPerSec.value
if (pps <= 0) return
const rawDelta = (e.clientX - tr.startX) / pps
const clip = store.scene.audioClips?.find((c) => c.id === tr.clipId)
if (!clip) return
const asset = audioAssetById.value.get(clip.audioAssetId)
if (!asset) return
const MIN_VISIBLE = 0.05
const maxBudget = asset.duration - MIN_VISIBLE
if (tr.side === 'start') {
// Clamp locally so overshooting the natural start doesn't slide
// the whole clip — matches the regular-clip handle behavior.
const minDelta = Math.max(-tr.initialTrimStart, -tr.initialStartTime)
const maxDelta = maxBudget - tr.initialTrimStart - tr.initialTrimEnd
const delta = Math.min(Math.max(rawDelta, minDelta), maxDelta)
store.trimAudioClip(tr.clipId, {
trimStart: tr.initialTrimStart + delta,
startTime: tr.initialStartTime + delta,
})
} else {
store.trimAudioClip(tr.clipId, {
trimEnd: tr.initialTrimEnd - rawDelta,
})
}
}
function onAudioClipHandlePointerUp(e: PointerEvent) {
const tr = audioClipTrim.value
if (!tr || tr.pointerId !== e.pointerId) return
const el = e.currentTarget as HTMLElement
if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId)
audioClipTrim.value = null
}
// -------- Button asset edit bar --------------------------------------
@@ -2253,17 +2342,29 @@ function onClipHandlePointerMove(e: PointerEvent) {
if (!t || t.pointerId !== e.pointerId) return
const pps = effectivePxPerSec.value
if (pps <= 0) return
const deltaSec = (e.clientX - t.startX) / pps
const rawDelta = (e.clientX - t.startX) / pps
if (t.side === 'start') {
// shift block right = trim more from start + shift startTime
// Clamp the delta locally so pulling the handle past the natural
// start doesn't shift startTime — previously trimStart clamped to
// 0 in the store but startTime still moved, which slid the whole
// clip. Bounds: trimStart stays in [0, maxBudget - trimEnd] and
// we also can't shift startTime below 0.
const inst = store.scene.clipInstances?.find((ci) => ci.id === t.instanceId)
const clip = inst ? store.scene.clips?.find((c) => c.id === inst.clipId) : null
if (!clip) return
const MIN_VISIBLE = 0.05
const maxBudget = clip.duration - MIN_VISIBLE
const minDelta = Math.max(-t.initialTrimStart, -t.initialStartTime)
const maxDelta = maxBudget - t.initialTrimStart - t.initialTrimEnd
const delta = Math.min(Math.max(rawDelta, minDelta), maxDelta)
store.trimClipInstance(t.instanceId, {
trimStart: t.initialTrimStart + deltaSec,
startTime: t.initialStartTime + deltaSec,
trimStart: t.initialTrimStart + delta,
startTime: t.initialStartTime + delta,
})
} else {
// shift right edge right = less trimEnd (reveal more)
store.trimClipInstance(t.instanceId, {
trimEnd: t.initialTrimEnd - deltaSec,
trimEnd: t.initialTrimEnd - rawDelta,
})
}
}
@@ -3310,7 +3411,12 @@ onBeforeUnmount(() => {
v-for="clip in audioClipsByTrack[track.audioTrackId] ?? []"
:key="clip.id"
class="audio-clip-block"
:class="{ moving: audioClipMove?.clipId === clip.id }"
:class="{
moving: audioClipMove?.clipId === clip.id,
'trimmed-start': (clip.trimStart ?? 0) > 0,
'trimmed-end': (clip.trimEnd ?? 0) > 0,
trimming: audioClipTrim?.clipId === clip.id,
}"
:style="audioClipBlockStyle(clip)"
v-tooltip="audioClipTooltip(clip)"
@pointerdown="onAudioClipPointerDown($event, clip)"
@@ -3319,6 +3425,17 @@ onBeforeUnmount(() => {
@pointercancel="onAudioClipPointerUp"
@contextmenu="onAudioClipContextMenu($event, clip)"
>
<span
class="clip-handle start"
draggable="false"
@pointerdown="onAudioClipHandlePointerDown($event, clip, 'start')"
@pointermove="onAudioClipHandlePointerMove"
@pointerup="onAudioClipHandlePointerUp"
@pointercancel="onAudioClipHandlePointerUp"
@dragstart.prevent.stop
@dblclick.stop
@click.stop
/>
<canvas
v-if="clipPeaks(clip)"
class="audio-clip-wave"
@@ -3327,6 +3444,17 @@ onBeforeUnmount(() => {
<span class="audio-clip-label">
{{ audioAssetById.get(clip.audioAssetId)?.name ?? '—' }}
</span>
<span
class="clip-handle end"
draggable="false"
@pointerdown="onAudioClipHandlePointerDown($event, clip, 'end')"
@pointermove="onAudioClipHandlePointerMove"
@pointerup="onAudioClipHandlePointerUp"
@pointercancel="onAudioClipHandlePointerUp"
@dragstart.prevent.stop
@dblclick.stop
@click.stop
/>
</span>
<span
v-if="audioClipGhost && audioClipGhost.targetTrackId === track.audioTrackId"
@@ -4811,15 +4939,15 @@ onBeforeUnmount(() => {
width: 9px;
height: 9px;
border-radius: 50%;
background: #f59e0b;
background: var(--track-color, #f59e0b);
box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.35);
}
.script-frame-count {
font-size: 9px;
font-weight: 600;
color: #f59e0b;
background: rgba(245, 158, 11, 0.15);
color: var(--track-color, #f59e0b);
background: color-mix(in srgb, var(--track-color, #f59e0b) 18%, transparent);
border-radius: 999px;
padding: 0 5px;
line-height: 13px;
@@ -4833,8 +4961,8 @@ onBeforeUnmount(() => {
height: 30px;
background: linear-gradient(
to bottom,
rgba(14, 165, 233, 0.08) 0%,
rgba(14, 165, 233, 0.02) 100%
color-mix(in srgb, var(--track-color, #0ea5e9) 8%, transparent) 0%,
color-mix(in srgb, var(--track-color, #0ea5e9) 2%, transparent) 100%
);
}
@@ -4876,10 +5004,10 @@ onBeforeUnmount(() => {
bottom: 4px;
background: linear-gradient(
to bottom,
rgba(14, 165, 233, 0.35) 0%,
rgba(14, 165, 233, 0.15) 100%
color-mix(in srgb, var(--track-color, #0ea5e9) 35%, transparent) 0%,
color-mix(in srgb, var(--track-color, #0ea5e9) 15%, transparent) 100%
);
border: 1px solid rgba(14, 165, 233, 0.6);
border: 1px solid color-mix(in srgb, var(--track-color, #0ea5e9) 60%, transparent);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
@@ -5034,29 +5162,40 @@ onBeforeUnmount(() => {
right: 2px;
}
.clip-block:hover .clip-handle {
.clip-block:hover .clip-handle,
.audio-clip-block:hover .clip-handle {
opacity: 1;
}
.clip-block.trimmed-start .clip-handle.start,
.clip-block.trimmed-end .clip-handle.end {
.clip-block.trimmed-end .clip-handle.end,
.audio-clip-block.trimmed-start .clip-handle.start,
.audio-clip-block.trimmed-end .clip-handle.end {
opacity: 0.75;
}
.clip-block.trimmed-start .clip-handle.start::after,
.clip-block.trimmed-end .clip-handle.end::after {
.clip-block.trimmed-end .clip-handle.end::after,
.audio-clip-block.trimmed-start .clip-handle.start::after,
.audio-clip-block.trimmed-end .clip-handle.end::after {
background: #ffc857;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
}
.clip-block.trimming .clip-handle {
.clip-block.trimming .clip-handle,
.audio-clip-block.trimming .clip-handle {
opacity: 1;
}
.clip-block.trimming {
.clip-block.trimming,
.audio-clip-block.trimming {
box-shadow: 0 0 0 1px #ffc857;
}
.track.audio-track.locked .clip-handle {
display: none;
}
.clip-block.moving {
box-shadow: 0 0 0 1px var(--track-color, var(--accent)), 0 4px 12px rgba(0, 0, 0, 0.3);
cursor: grabbing;