diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-15 15:45:50 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-15 15:45:50 +0200 |
| commit | c46d28891fee62ca2d3132da876af6411a41d9da (patch) | |
| tree | f856f71d04c69628f26b904088d675dd298a5080 /apps/web/src/components/editor/panels/timeline | |
| parent | 66e4367bc4727f1d0bcf8f869529c7518812277e (diff) | |
| download | opencut-c46d28891fee62ca2d3132da876af6411a41d9da.tar.gz | |
feat: text / custom masks, fix preview pan, refactor selection
Diffstat (limited to 'apps/web/src/components/editor/panels/timeline')
| -rw-r--r-- | apps/web/src/components/editor/panels/timeline/audio-waveform.tsx | 48 | ||||
| -rw-r--r-- | apps/web/src/components/editor/panels/timeline/timeline-element.tsx | 18 |
2 files changed, 61 insertions, 5 deletions
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<HTMLDivElement>(null); const bufferRef = useRef<AudioBuffer | null>(null); const globalMaxRef = useRef<number>(1); + const gainSamplesRef = useRef<number[] | undefined>(gainSamples); const scrollParentRef = useRef<HTMLElement | null>(null); const heightRef = useRef<number>(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({ > <ElementInner element={element} + displayElement={renderElement} track={track} isSelected={isSelected} isExpanded={expandedRows.length > 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 ( <div className="absolute top-0 bottom-0" @@ -576,7 +582,7 @@ function ElementInner({ style={{ height: `${baseTrackHeight}px` }} > <div className="flex flex-1 min-h-0 h-full items-center overflow-hidden"> - <ElementContent element={element} track={track} /> + <ElementContent element={visibleElement} track={track} /> </div> </div> {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 }) { <AudioWaveform audioBuffer={audioBuffer} audioUrl={audioUrl} + gainSamples={gainSamples} color={TIMELINE_TRACK_THEME.audio.waveformColor} /> <MediaElementHeader name={mediaLabel} hasFade={false} /> |
