0.11.1: drawer mouse trigger and clip track / audio behavior
This commit is contained in:
1 parent
8d987d1526
commit
fa66ba69bb
9 files changed
+258
-31
No files matched your search
+160
-21
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user