From c46d28891fee62ca2d3132da876af6411a41d9da Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 15 Apr 2026 15:45:50 +0200 Subject: feat: text / custom masks, fix preview pan, refactor selection --- .../editor/panels/timeline/audio-waveform.tsx | 48 +++++++++++++++++++++- .../editor/panels/timeline/timeline-element.tsx | 18 ++++++-- 2 files changed, 61 insertions(+), 5 deletions(-) (limited to 'apps/web/src/components/editor/panels/timeline') diff --git a/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx b/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx index a481f1c2..430575e1 100644 --- a/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx +++ b/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx @@ -9,9 +9,34 @@ import { cn } from "@/utils/ui"; const BAR_WIDTH = 2; const BAR_GAP = 1; const BAR_STEP = BAR_WIDTH + BAR_GAP; +export const WAVEFORM_GAIN_SAMPLE_COUNT = 200; + +function sampleGain({ + samples, + startFraction, + endFraction, + barIndex, + barCount, +}: { + samples: number[]; + startFraction: number; + endFraction: number; + barIndex: number; + barCount: number; +}): number { + if (samples.length === 0) return 1; + const progress = + startFraction + + ((barIndex + 0.5) / barCount) * (endFraction - startFraction); + const rawIndex = Math.max(0, Math.min(1, progress)) * (samples.length - 1); + const lo = Math.floor(rawIndex); + const hi = Math.min(samples.length - 1, lo + 1); + return samples[lo] + (samples[hi] - samples[lo]) * (rawIndex - lo); +} interface AudioWaveformProps { audioUrl?: string; audioBuffer?: AudioBuffer; + gainSamples?: number[]; color?: string; className?: string; } @@ -19,6 +44,7 @@ interface AudioWaveformProps { export function AudioWaveform({ audioUrl, audioBuffer, + gainSamples, color = "rgba(255, 255, 255, 0.7)", className = "", }: AudioWaveformProps) { @@ -26,6 +52,7 @@ export function AudioWaveform({ const containerRef = useRef(null); const bufferRef = useRef(null); const globalMaxRef = useRef(1); + const gainSamplesRef = useRef(gainSamples); const scrollParentRef = useRef(null); const heightRef = useRef(0); @@ -99,20 +126,37 @@ export function AudioWaveform({ const maxBarHeight = height * 0.7; + const samples = gainSamplesRef.current; for (let i = 0; i < barCount; i++) { - const scaled = Math.log1p(peaks[i]) / Math.log1p(1); + const gain = + samples != null + ? sampleGain({ + samples, + startFraction, + endFraction, + barIndex: i, + barCount, + }) + : 1; + const scaledPeak = Math.min(1, Math.max(0, peaks[i] * gain)); + const scaled = Math.log1p(scaledPeak) / Math.log1p(1); const barH = Math.max(1, scaled * maxBarHeight); ctx.fillRect(i * BAR_STEP, height - barH, BAR_WIDTH, barH); } }, [color]); + useEffect(() => { + gainSamplesRef.current = gainSamples; + drawVisible(); + }, [gainSamples, drawVisible]); + useEffect(() => { let isCancelled = false; async function load() { let buffer = audioBuffer ?? null; - if (!buffer && audioUrl) { + if (!buffer && audioUrl) { try { const resp = await fetch(audioUrl); const arrayBuffer = await resp.arrayBuffer(); diff --git a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx index 644dbc56..9283c651 100644 --- a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx +++ b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx @@ -2,7 +2,7 @@ import { useEditor } from "@/hooks/use-editor"; import { useAssetsPanelStore } from "@/stores/assets-panel-store"; -import { AudioWaveform } from "./audio-waveform"; +import { AudioWaveform, WAVEFORM_GAIN_SAMPLE_COUNT } from "./audio-waveform"; import { useElementPreview } from "@/hooks/use-element-preview"; import { useKeyframeDrag, @@ -44,6 +44,7 @@ import { getSourceAudioActionLabel, isSourceAudioSeparated, } from "@/lib/timeline/audio-separation"; +import { buildWaveformGainSamples } from "@/lib/timeline/audio-state"; import { getActionDefinition, type TAction, @@ -382,6 +383,7 @@ export function TimelineElement({ > 0} @@ -498,6 +500,7 @@ export function TimelineElement({ function ElementInner({ element, + displayElement, track, isSelected, isExpanded, @@ -509,6 +512,7 @@ function ElementInner({ isDropTarget = false, }: { element: TimelineElementType; + displayElement?: TimelineElementType; track: TimelineTrack; isSelected: boolean; isExpanded: boolean; @@ -530,8 +534,10 @@ function ElementInner({ }) => void; isDropTarget?: boolean; }) { + const visibleElement = displayElement ?? element; const isReducedOpacity = - (canElementBeHidden(element) && element.hidden) || isDropTarget; + (canElementBeHidden(visibleElement) && visibleElement.hidden) || + isDropTarget; return (
- +
{expandedContent} @@ -979,6 +985,11 @@ function AudioElementContent({ element }: { element: AudioElement }) { const audioUrl = element.sourceType === "library" ? element.sourceUrl : mediaAsset?.url; const mediaLabel = mediaAsset?.name ?? element.name; + const gainSamples = useMemo( + () => + buildWaveformGainSamples({ element, count: WAVEFORM_GAIN_SAMPLE_COUNT }), + [element], + ); if (audioBuffer || audioUrl) { return ( @@ -986,6 +997,7 @@ function AudioElementContent({ element }: { element: AudioElement }) { -- cgit v1.3.1