From 6e819e48f4c88402bcc2f24ce80c355986bef89e Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 02:10:22 +0800 Subject: [PATCH 01/30] feat: add continuous zoom curve visualization with visibility toggle --- apps/desktop/src/routes/editor/Header.tsx | 2 + .../src/routes/editor/Timeline/ZoomTrack.tsx | 52 ++++++++++++++++++- apps/desktop/src/routes/editor/context.ts | 1 + 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index c2e0a05b67b..c616dbfb7e2 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -35,6 +35,7 @@ import { import { serializeProjectConfiguration, useEditorContext } from "./context"; import OrganizationDropdown from "./OrganizationDropdown"; import PresetsDropdown from "./PresetsDropdown"; +import TimelineDropdown from "./TimelineDropdown"; import ShareButton from "./ShareButton"; import { Dialog, EditorButton, Input } from "./ui"; @@ -335,6 +336,7 @@ export function Header() { data-tauri-drag-region class="flex flex-row items-center justify-center gap-2 px-4 border-x border-black-transparent-10" > + diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index de8fb4808e2..eb9f610bf02 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -638,9 +638,59 @@ export function ZoomTrack(props: { > {(() => { const ctx = useSegmentContext(); + const isInstant = () => segment().instantAnimation; + + const prev = () => zoomSegments()[i - 1]; + const next = () => zoomSegments()[i + 1]; + + const isContiguousWithPrev = () => prev() && prev().end === segment().start; + const isContiguousWithNext = () => next() && next().start === segment().end; + + const prevAmt = () => isContiguousWithPrev() ? prev().amount : 1.0; + const currAmt = () => segment().amount; + const nextAmt = () => isContiguousWithNext() ? next().amount : 1.0; + + // Map amount to Y coordinate to increase amplitude (1.0 -> 90, 2.0 -> 40) + const getY = (amt: number) => Math.max(5, 90 - (amt - 1) * 50); + + const startY = () => getY(prevAmt()); + const currY = () => getY(currAmt()); + const endY = () => getY(nextAmt()); + + const W = () => Math.max(1, ctx.width()); + const rampUpPct = () => (Math.min(40, W() / 2) / W()) * 100; + const rampDownPct = () => (40 / W()) * 100; + + const d = () => { + if (isInstant()) { + return `M 0 ${startY()} L 0 ${currY()} L 100 ${currY()} ${ + !isContiguousWithNext() ? `L 100 ${endY()} L ${100 + rampDownPct()} ${endY()}` : "" + }`; + } + return `M 0 ${startY()} C ${rampUpPct() / 2} ${startY()}, ${rampUpPct() / 2} ${currY()}, ${rampUpPct()} ${currY()} L 100 ${currY()} ${ + !isContiguousWithNext() ? `C ${100 + rampDownPct() / 2} ${currY()}, ${100 + rampDownPct() / 2} ${endY()}, ${100 + rampDownPct()} ${endY()}` : "" + }`; + }; return ( - + <> + + + + + + +
diff --git a/apps/desktop/src/routes/editor/context.ts b/apps/desktop/src/routes/editor/context.ts index d85e2be6eeb..cab13fcdcbc 100644 --- a/apps/desktop/src/routes/editor/context.ts +++ b/apps/desktop/src/routes/editor/context.ts @@ -792,6 +792,7 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( staleDismissed: false, }, timeline: { + showZoomCurves: true, interactMode: "seek" as "seek" | "split", selection: null as | null From 6e835470bf92d14b2c9b2da980ed2af8b05be1b4 Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 02:12:30 +0800 Subject: [PATCH 02/30] fix: syntax error in ZoomTrack.tsx --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index eb9f610bf02..750c18f3c15 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -712,6 +712,7 @@ export function ZoomTrack(props: {
+ ); })()} From 55bbbc99ad78ed0c38786e132429da525cf204cc Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 02:17:23 +0800 Subject: [PATCH 03/30] fix: allow ZoomTrack segments to be overflow-visible to show ending curves --- apps/desktop/src/routes/editor/Timeline/Track.tsx | 4 +++- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Timeline/Track.tsx b/apps/desktop/src/routes/editor/Timeline/Track.tsx index 0953269d2b7..62cc1388fd3 100644 --- a/apps/desktop/src/routes/editor/Timeline/Track.tsx +++ b/apps/desktop/src/routes/editor/Timeline/Track.tsx @@ -59,6 +59,7 @@ export function SegmentRoot( onMouseDown?: ( e: MouseEvent & { currentTarget: HTMLDivElement; target: Element }, ) => void; + overflowVisible?: boolean; }, ) { const { editorState } = useEditorContext(); @@ -84,7 +85,8 @@ export function SegmentRoot( >
diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 750c18f3c15..057ba560d25 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -519,6 +519,7 @@ export function ZoomTrack(props: { return ( Date: Sun, 7 Jun 2026 02:19:37 +0800 Subject: [PATCH 04/30] style: use asymptotic curve scaling to prevent high zoom levels from hitting the top --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 057ba560d25..f220ab101cb 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -651,8 +651,8 @@ export function ZoomTrack(props: { const currAmt = () => segment().amount; const nextAmt = () => isContiguousWithNext() ? next().amount : 1.0; - // Map amount to Y coordinate to increase amplitude (1.0 -> 90, 2.0 -> 40) - const getY = (amt: number) => Math.max(5, 90 - (amt - 1) * 50); + // Map amount to Y coordinate asymptotically (1.0 -> 90, 2.0 -> 40, 4.5 -> 12) + const getY = (amt: number) => Math.max(5, 90 - 100 * (1 - 1 / Math.max(1, amt))); const startY = () => getY(prevAmt()); const currY = () => getY(currAmt()); From c933ba5c65ffc05ff542a38d61cc340e66feab4b Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 02:24:01 +0800 Subject: [PATCH 05/30] refactor: move Timeline settings to the top right of the Timeline panel --- apps/desktop/src/routes/editor/Header.tsx | 2 -- apps/desktop/src/routes/editor/Timeline/index.tsx | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index c616dbfb7e2..c2e0a05b67b 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -35,7 +35,6 @@ import { import { serializeProjectConfiguration, useEditorContext } from "./context"; import OrganizationDropdown from "./OrganizationDropdown"; import PresetsDropdown from "./PresetsDropdown"; -import TimelineDropdown from "./TimelineDropdown"; import ShareButton from "./ShareButton"; import { Dialog, EditorButton, Input } from "./ui"; @@ -336,7 +335,6 @@ export function Header() { data-tauri-drag-region class="flex flex-row items-center justify-center gap-2 px-4 border-x border-black-transparent-10" > -
diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 379d207ba2b..eef9ff8a285 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -44,6 +44,7 @@ import { type KeyboardSegmentDragState, KeyboardTrack } from "./KeyboardTrack"; import { type MaskSegmentDragState, MaskTrack } from "./MaskTrack"; import { type SceneSegmentDragState, SceneTrack } from "./SceneTrack"; import { type TextSegmentDragState, TextTrack } from "./TextTrack"; +import { TimelineDropdown } from "./TimelineDropdown"; import { TrackIcon, TrackManager } from "./TrackManager"; import { type ZoomSegmentDragState, ZoomTrack } from "./ZoomTrack"; @@ -833,6 +834,9 @@ export function Timeline(props: { } }} > +
+ +
Date: Sun, 7 Jun 2026 02:29:34 +0800 Subject: [PATCH 06/30] chore: fix TS types in TimelineDropdown --- packages/ui-solid/src/auto-imports.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index f30b9df566b..bd4d9c27902 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -107,6 +107,7 @@ declare global { const IconLucideSave: typeof import('~icons/lucide/save.jsx')['default'] const IconLucideSearch: typeof import('~icons/lucide/search.jsx')['default'] const IconLucideSettings: typeof import('~icons/lucide/settings.jsx')['default'] + const IconLucideSettings2: typeof import('~icons/lucide/settings2.jsx')['default'] const IconLucideSparkles: typeof import('~icons/lucide/sparkles.jsx')['default'] const IconLucideSquarePlay: typeof import('~icons/lucide/square-play.jsx')['default'] const IconLucideSubtitles: typeof import('~icons/lucide/subtitles.jsx')['default'] From 13956d3208f0f9be2b3d7782ad10f2b792d435d3 Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 23:06:47 +0800 Subject: [PATCH 07/30] refactor: extract zoom curves to dedicated sub-track toggled by track icon --- .../routes/editor/Timeline/TrackManager.tsx | 4 +- .../src/routes/editor/Timeline/ZoomTrack.tsx | 100 ++++++++++++++---- .../src/routes/editor/Timeline/index.tsx | 18 ++-- packages/ui-solid/src/auto-imports.d.ts | 1 + 4 files changed, 98 insertions(+), 25 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/TrackManager.tsx b/apps/desktop/src/routes/editor/Timeline/TrackManager.tsx index 8ece70cf03f..cd92633779f 100644 --- a/apps/desktop/src/routes/editor/Timeline/TrackManager.tsx +++ b/apps/desktop/src/routes/editor/Timeline/TrackManager.tsx @@ -76,13 +76,15 @@ export function TrackManager(props: { ); } -export function TrackIcon(props: { icon: JSX.Element; class?: string }) { +export function TrackIcon(props: { icon: JSX.Element; class?: string; onClick?: () => void }) { return (
e.stopPropagation()} > {props.icon} diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index f220ab101cb..952a5826e56 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -13,6 +13,7 @@ import { Match, Show, Switch, + For, } from "solid-js"; import { produce } from "solid-js/store"; import { commands } from "~/utils/tauri"; @@ -674,23 +675,6 @@ export function ZoomTrack(props: { }; return ( - <> - - - - - -
@@ -713,7 +697,6 @@ export function ZoomTrack(props: {
- ); })()} @@ -797,3 +780,84 @@ export function ZoomTrack(props: { ); } + +export function ZoomCurveTrack() { + const { project } = useEditorContext(); + + const zoomSegments = () => project.timeline?.zoomSegments ?? []; + + return ( + + + {(segment, i) => { + return ( + + + {(() => { + const ctx = useSegmentContext(); + const isInstant = () => segment.instantAnimation; + + const prev = () => zoomSegments()[i() - 1]; + const next = () => zoomSegments()[i() + 1]; + + const isContiguousWithPrev = () => prev() && prev().end === segment.start; + const isContiguousWithNext = () => next() && next().start === segment.end; + + const prevAmt = () => isContiguousWithPrev() ? prev().amount : 1.0; + const currAmt = () => segment.amount; + const nextAmt = () => isContiguousWithNext() ? next().amount : 1.0; + + // Map amount to Y coordinate asymptotically (1.0 -> 90, 2.0 -> 40, 4.5 -> 12) + const getY = (amt: number) => Math.max(5, 90 - 100 * (1 - 1 / Math.max(1, amt))); + + const startY = () => getY(prevAmt()); + const currY = () => getY(currAmt()); + const endY = () => getY(nextAmt()); + + const W = () => Math.max(1, ctx.width()); + const rampUpPct = () => (Math.min(40, W() / 2) / W()) * 100; + const rampDownPct = () => (40 / W()) * 100; + + const d = () => { + if (isInstant()) { + return `M 0 ${startY()} L 0 ${currY()} L 100 ${currY()} ${ + !isContiguousWithNext() ? `L 100 ${endY()} L ${100 + rampDownPct()} ${endY()}` : "" + }`; + } + return `M 0 ${startY()} C ${rampUpPct() / 2} ${startY()}, ${rampUpPct() / 2} ${currY()}, ${rampUpPct()} ${currY()} L 100 ${currY()} ${ + !isContiguousWithNext() ? `C ${100 + rampDownPct() / 2} ${currY()}, ${100 + rampDownPct() / 2} ${endY()}, ${100 + rampDownPct()} ${endY()}` : "" + }`; + }; + + return ( + + + + ); + })()} + + + ); + }} + + + ); +} + diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index eef9ff8a285..e2e543813cd 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -44,9 +44,8 @@ import { type KeyboardSegmentDragState, KeyboardTrack } from "./KeyboardTrack"; import { type MaskSegmentDragState, MaskTrack } from "./MaskTrack"; import { type SceneSegmentDragState, SceneTrack } from "./SceneTrack"; import { type TextSegmentDragState, TextTrack } from "./TextTrack"; -import { TimelineDropdown } from "./TimelineDropdown"; import { TrackIcon, TrackManager } from "./TrackManager"; -import { type ZoomSegmentDragState, ZoomTrack } from "./ZoomTrack"; +import { type ZoomSegmentDragState, ZoomTrack, ZoomCurveTrack } from "./ZoomTrack"; const TIMELINE_PADDING = 16; const TRACK_GUTTER = 64; @@ -834,9 +833,6 @@ export function Timeline(props: { } }} > -
- -
)} - + setEditorState("timeline", "showZoomCurves", (v) => !v)} + > { zoomSegmentDragState = v; @@ -992,6 +991,11 @@ export function Timeline(props: { handleUpdatePlayhead={handleUpdatePlayhead} /> + + }> + + + void; onContextMenu?: (e: MouseEvent) => void; + onIconClick?: () => void; }) { return (
Date: Sun, 7 Jun 2026 23:13:45 +0800 Subject: [PATCH 08/30] feat: refine zoom curves to be pure lines without blocks and add expansion animation --- .../src/routes/editor/Timeline/ZoomTrack.tsx | 41 ++++++++++--------- .../src/routes/editor/Timeline/index.tsx | 29 +++++++++++-- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 952a5826e56..3fc8e5d7699 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -783,25 +783,28 @@ export function ZoomTrack(props: { export function ZoomCurveTrack() { const { project } = useEditorContext(); + const { secsPerPixel } = useTimelineContext(); const zoomSegments = () => project.timeline?.zoomSegments ?? []; return ( - - {(segment, i) => { - return ( - - +
+ + {(segment, i) => { + const left = () => segment.start / secsPerPixel(); + const width = () => (segment.end - segment.start) / secsPerPixel(); + + return ( +
{(() => { - const ctx = useSegmentContext(); + const ctxWidth = width(); const isInstant = () => segment.instantAnimation; const prev = () => zoomSegments()[i() - 1]; @@ -821,7 +824,7 @@ export function ZoomCurveTrack() { const currY = () => getY(currAmt()); const endY = () => getY(nextAmt()); - const W = () => Math.max(1, ctx.width()); + const W = () => Math.max(1, ctxWidth); const rampUpPct = () => (Math.min(40, W() / 2) / W()) * 100; const rampDownPct = () => (40 / W()) * 100; @@ -852,11 +855,11 @@ export function ZoomCurveTrack() { ); })()} - - - ); - }} - +
+ ); + }} +
+
); } diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index e2e543813cd..58ab46c0d84 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -983,6 +983,11 @@ export function Timeline(props: { setEditorState("timeline", "showZoomCurves", (v) => !v)} + iconOverlay={() => + editorState.timeline.showZoomCurves ? + : + + } > { @@ -992,9 +997,11 @@ export function Timeline(props: { /> - }> - - +
+ }> + + +
@@ -1020,6 +1027,7 @@ function TrackRow(props: { onDelete?: () => void; onContextMenu?: (e: MouseEvent) => void; onIconClick?: () => void; + iconOverlay?: () => JSX.Element; }) { return (
+ + +
{props.children} From ad73d97fcafcab277bdc9a6677c725b3e26ea38f Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 23:18:10 +0800 Subject: [PATCH 09/30] fix: make zoom curve track move with timeline and add limit lines --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 3fc8e5d7699..026196b9a0b 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -782,24 +782,28 @@ export function ZoomTrack(props: { } export function ZoomCurveTrack() { - const { project } = useEditorContext(); + const { project, editorState } = useEditorContext(); const { secsPerPixel } = useTimelineContext(); const zoomSegments = () => project.timeline?.zoomSegments ?? []; return ( +
+
+
{(segment, i) => { - const left = () => segment.start / secsPerPixel(); + const base = () => editorState.timeline.transform.position; + const translateX = () => (segment.start - base()) / secsPerPixel(); const width = () => (segment.end - segment.start) / secsPerPixel(); return (
From cc0d44b5e3411d0c3851fd155ad943f0f8b1d2fd Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 23:20:31 +0800 Subject: [PATCH 10/30] feat: visually indicate subordinate track relation with tree branch connector UI --- .../src/routes/editor/Timeline/TrackManager.tsx | 16 +++++++++++++--- .../desktop/src/routes/editor/Timeline/index.tsx | 10 ++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/TrackManager.tsx b/apps/desktop/src/routes/editor/Timeline/TrackManager.tsx index cd92633779f..594af3614ff 100644 --- a/apps/desktop/src/routes/editor/Timeline/TrackManager.tsx +++ b/apps/desktop/src/routes/editor/Timeline/TrackManager.tsx @@ -76,18 +76,28 @@ export function TrackManager(props: { ); } -export function TrackIcon(props: { icon: JSX.Element; class?: string; onClick?: () => void }) { +import { Show } from "solid-js"; + +export function TrackIcon(props: { icon: JSX.Element; class?: string; onClick?: () => void; subordinate?: boolean }) { return (
e.stopPropagation()} > - {props.icon} + + {/* Tree connector L shape */} +
+
+ +
{props.icon}
); } diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 58ab46c0d84..0fc0a3c715c 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -998,7 +998,10 @@ export function Timeline(props: {
- }> + } + subordinate={true} + >
@@ -1028,16 +1031,19 @@ function TrackRow(props: { onContextMenu?: (e: MouseEvent) => void; onIconClick?: () => void; iconOverlay?: () => JSX.Element; + subordinate?: boolean; + class?: string; }) { return (
Date: Sun, 7 Jun 2026 23:23:23 +0800 Subject: [PATCH 11/30] feat: animate curve track collapse and fix alignment --- .../src/routes/editor/Timeline/index.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 0fc0a3c715c..536a8d490af 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -996,16 +996,19 @@ export function Timeline(props: { handleUpdatePlayhead={handleUpdatePlayhead} /> - -
- } - subordinate={true} - > - - -
-
+
+ } + subordinate={true} + > + + +
Date: Sun, 7 Jun 2026 23:25:52 +0800 Subject: [PATCH 12/30] fix: make curve width reactive so SVG scales correctly with timeline zoom --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 026196b9a0b..741c86acb98 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -808,7 +808,6 @@ export function ZoomCurveTrack() { }} > {(() => { - const ctxWidth = width(); const isInstant = () => segment.instantAnimation; const prev = () => zoomSegments()[i() - 1]; @@ -828,7 +827,7 @@ export function ZoomCurveTrack() { const currY = () => getY(currAmt()); const endY = () => getY(nextAmt()); - const W = () => Math.max(1, ctxWidth); + const W = () => Math.max(1, width()); const rampUpPct = () => (Math.min(40, W() / 2) / W()) * 100; const rampDownPct = () => (40 / W()) * 100; From 2c9215564398eddad0b149a9ae70394af22aceaa Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 23:29:25 +0800 Subject: [PATCH 13/30] fix: adjust ramp down curve to finish inside the zoom segment block --- .../desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 741c86acb98..bde3cbb818f 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -829,16 +829,20 @@ export function ZoomCurveTrack() { const W = () => Math.max(1, width()); const rampUpPct = () => (Math.min(40, W() / 2) / W()) * 100; - const rampDownPct = () => (40 / W()) * 100; + const rampDownPct = () => (Math.min(40, W() / 2) / W()) * 100; const d = () => { if (isInstant()) { return `M 0 ${startY()} L 0 ${currY()} L 100 ${currY()} ${ - !isContiguousWithNext() ? `L 100 ${endY()} L ${100 + rampDownPct()} ${endY()}` : "" + !isContiguousWithNext() ? `L 100 ${endY()}` : "" }`; } - return `M 0 ${startY()} C ${rampUpPct() / 2} ${startY()}, ${rampUpPct() / 2} ${currY()}, ${rampUpPct()} ${currY()} L 100 ${currY()} ${ - !isContiguousWithNext() ? `C ${100 + rampDownPct() / 2} ${currY()}, ${100 + rampDownPct() / 2} ${endY()}, ${100 + rampDownPct()} ${endY()}` : "" + return `M 0 ${startY()} C ${rampUpPct() / 2} ${startY()}, ${rampUpPct() / 2} ${currY()}, ${rampUpPct()} ${currY()} L ${ + !isContiguousWithNext() ? 100 - rampDownPct() : 100 + } ${currY()} ${ + !isContiguousWithNext() + ? `C ${100 - rampDownPct() / 2} ${currY()}, ${100 - rampDownPct() / 2} ${endY()}, 100 ${endY()}` + : "" }`; }; From 3c12a48b0a33434617848b7cd7dbec729de51f5c Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 23:32:23 +0800 Subject: [PATCH 14/30] fix: revert ramp-down bounds and change zoom mapping to be linear --- .../src/routes/editor/Timeline/ZoomTrack.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index bde3cbb818f..d19f7a2e9a4 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -820,8 +820,11 @@ export function ZoomCurveTrack() { const currAmt = () => segment.amount; const nextAmt = () => isContiguousWithNext() ? next().amount : 1.0; - // Map amount to Y coordinate asymptotically (1.0 -> 90, 2.0 -> 40, 4.5 -> 12) - const getY = (amt: number) => Math.max(5, 90 - 100 * (1 - 1 / Math.max(1, amt))); + // Map amount to Y coordinate linearly (1.0 -> 90, 5.0 -> 5) + const getY = (amt: number) => { + const p = Math.min(1, Math.max(0, (amt - 1) / 4)); + return 90 - 85 * p; + }; const startY = () => getY(prevAmt()); const currY = () => getY(currAmt()); @@ -829,19 +832,17 @@ export function ZoomCurveTrack() { const W = () => Math.max(1, width()); const rampUpPct = () => (Math.min(40, W() / 2) / W()) * 100; - const rampDownPct = () => (Math.min(40, W() / 2) / W()) * 100; + const rampDownPct = () => (40 / W()) * 100; const d = () => { if (isInstant()) { return `M 0 ${startY()} L 0 ${currY()} L 100 ${currY()} ${ - !isContiguousWithNext() ? `L 100 ${endY()}` : "" + !isContiguousWithNext() ? `L 100 ${endY()} L ${100 + rampDownPct()} ${endY()}` : "" }`; } - return `M 0 ${startY()} C ${rampUpPct() / 2} ${startY()}, ${rampUpPct() / 2} ${currY()}, ${rampUpPct()} ${currY()} L ${ - !isContiguousWithNext() ? 100 - rampDownPct() : 100 - } ${currY()} ${ + return `M 0 ${startY()} C ${rampUpPct() / 2} ${startY()}, ${rampUpPct() / 2} ${currY()}, ${rampUpPct()} ${currY()} L 100 ${currY()} ${ !isContiguousWithNext() - ? `C ${100 - rampDownPct() / 2} ${currY()}, ${100 - rampDownPct() / 2} ${endY()}, 100 ${endY()}` + ? `C ${100 + rampDownPct() / 2} ${currY()}, ${100 + rampDownPct() / 2} ${endY()}, ${100 + rampDownPct()} ${endY()}` : "" }`; }; From cdbfe8fad46961b0f2a7eac64a01e498126fd750 Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 23:41:17 +0800 Subject: [PATCH 15/30] fix: make zoom curve width match actual 1.0s video animation duration --- .../desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index d19f7a2e9a4..0dcbdd8b5d5 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -822,7 +822,8 @@ export function ZoomCurveTrack() { // Map amount to Y coordinate linearly (1.0 -> 90, 5.0 -> 5) const getY = (amt: number) => { - const p = Math.min(1, Math.max(0, (amt - 1) / 4)); + // Allow p to be negative so zoom-out (< 1.0) goes below the baseline + const p = Math.min(1, (amt - 1) / 4); return 90 - 85 * p; }; @@ -831,13 +832,16 @@ export function ZoomCurveTrack() { const endY = () => getY(nextAmt()); const W = () => Math.max(1, width()); - const rampUpPct = () => (Math.min(40, W() / 2) / W()) * 100; - const rampDownPct = () => (40 / W()) * 100; + // The video rendering engine uses exactly 1.0 second for the zoom transition + const rampDurationSecs = 1.0; + const rampPixels = () => rampDurationSecs / secsPerPixel(); + const rampUpPct = () => (Math.min(rampPixels(), W() / 2) / W()) * 100; + const rampDownPct = () => (rampPixels() / W()) * 100; const d = () => { if (isInstant()) { return `M 0 ${startY()} L 0 ${currY()} L 100 ${currY()} ${ - !isContiguousWithNext() ? `L 100 ${endY()} L ${100 + rampDownPct()} ${endY()}` : "" + !isContiguousWithNext() ? `L 100 ${endY()}` : "" }`; } return `M 0 ${startY()} C ${rampUpPct() / 2} ${startY()}, ${rampUpPct() / 2} ${currY()}, ${rampUpPct()} ${currY()} L 100 ${currY()} ${ From b10db4ebaefad4391ca0c421442c2cd715838679 Mon Sep 17 00:00:00 2001 From: yihuan Date: Sun, 7 Jun 2026 23:44:45 +0800 Subject: [PATCH 16/30] feat: make zoom curve continuous and color sloped sections --- .../src/routes/editor/Timeline/ZoomTrack.tsx | 74 +++++++++++++++---- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 0dcbdd8b5d5..91151cfb664 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -835,31 +835,77 @@ export function ZoomCurveTrack() { // The video rendering engine uses exactly 1.0 second for the zoom transition const rampDurationSecs = 1.0; const rampPixels = () => rampDurationSecs / secsPerPixel(); - const rampUpPct = () => (Math.min(rampPixels(), W() / 2) / W()) * 100; - const rampDownPct = () => (rampPixels() / W()) * 100; + + const toPct = (px: number) => (px / W()) * 100; + const rampUpPct = () => isInstant() ? 0 : toPct(Math.min(rampPixels(), W() / 2)); + const rampDownPct = () => isInstant() ? 0 : toPct(rampPixels()); - const d = () => { + const dGray = () => { + let parts = []; + + // 1. Gap before + if (i() === 0) { + parts.push(`M -100000 ${getY(1.0)} L 0 ${getY(1.0)}`); + } else { + const prevSeg = prev(); + const prevEndOffset = (prevSeg.end - segment.start) / secsPerPixel(); + const prevRampDownW = prevSeg.instantAnimation ? 0 : rampPixels(); + const gapStartX = isContiguousWithPrev() ? 0 : prevEndOffset + prevRampDownW; + if (gapStartX < 0) { + parts.push(`M ${toPct(gapStartX)} ${getY(1.0)} L 0 ${getY(1.0)}`); + } + } + + // 2. Flat top + parts.push(`M ${rampUpPct()} ${currY()} L 100 ${currY()}`); + + // 3. Gap after (if last) + if (i() === zoomSegments().length - 1) { + const afterXPct = 100 + (!isContiguousWithNext() ? rampDownPct() : 0); + parts.push(`M ${afterXPct} ${getY(1.0)} L 100000 ${getY(1.0)}`); + } + + return parts.join(" "); + }; + + const dColored = () => { + let parts = []; + + // 1. Ramp Up if (isInstant()) { - return `M 0 ${startY()} L 0 ${currY()} L 100 ${currY()} ${ - !isContiguousWithNext() ? `L 100 ${endY()}` : "" - }`; + parts.push(`M 0 ${startY()} L 0 ${currY()}`); + } else { + parts.push(`M 0 ${startY()} C ${rampUpPct() / 2} ${startY()}, ${rampUpPct() / 2} ${currY()}, ${rampUpPct()} ${currY()}`); + } + + // 2. Ramp Down + if (!isContiguousWithNext()) { + if (isInstant()) { + parts.push(`M 100 ${currY()} L 100 ${endY()}`); + } else { + parts.push(`M 100 ${currY()} C ${100 + rampDownPct() / 2} ${currY()}, ${100 + rampDownPct() / 2} ${endY()}, ${100 + rampDownPct()} ${endY()}`); + } } - return `M 0 ${startY()} C ${rampUpPct() / 2} ${startY()}, ${rampUpPct() / 2} ${currY()}, ${rampUpPct()} ${currY()} L 100 ${currY()} ${ - !isContiguousWithNext() - ? `C ${100 + rampDownPct() / 2} ${currY()}, ${100 + rampDownPct() / 2} ${endY()}, ${100 + rampDownPct()} ${endY()}` - : "" - }`; + + return parts.join(" "); }; return ( + Date: Sun, 7 Jun 2026 23:48:12 +0800 Subject: [PATCH 17/30] style: lighten zoom curve colors and update max zoom to 4.5 --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 91151cfb664..2a0a3760d2e 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -820,10 +820,10 @@ export function ZoomCurveTrack() { const currAmt = () => segment.amount; const nextAmt = () => isContiguousWithNext() ? next().amount : 1.0; - // Map amount to Y coordinate linearly (1.0 -> 90, 5.0 -> 5) + // Map amount to Y coordinate linearly (1.0 -> 90, 4.5 -> 5) const getY = (amt: number) => { // Allow p to be negative so zoom-out (< 1.0) goes below the baseline - const p = Math.min(1, (amt - 1) / 4); + const p = Math.min(1, (amt - 1) / 3.5); return 90 - 85 * p; }; @@ -898,14 +898,14 @@ export function ZoomCurveTrack() { > Date: Sun, 7 Jun 2026 23:49:43 +0800 Subject: [PATCH 18/30] style: increase transparency of gray zoom curve sections --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 2a0a3760d2e..c77c2dd5392 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -898,7 +898,7 @@ export function ZoomCurveTrack() { > Date: Sun, 7 Jun 2026 23:50:02 +0800 Subject: [PATCH 19/30] style: make blue curve color lighter (blue-300) --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index c77c2dd5392..69af93698f1 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -905,7 +905,7 @@ export function ZoomCurveTrack() { /> Date: Sun, 7 Jun 2026 23:51:59 +0800 Subject: [PATCH 20/30] style: make blue curve color even lighter (blue-200) --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 69af93698f1..f519e84fab9 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -905,7 +905,7 @@ export function ZoomCurveTrack() { /> Date: Sun, 7 Jun 2026 23:58:43 +0800 Subject: [PATCH 21/30] style: reduce gray curve transparency in dark mode for better visibility --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index f519e84fab9..b5593849f4e 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -898,7 +898,7 @@ export function ZoomCurveTrack() { > Date: Mon, 8 Jun 2026 00:01:32 +0800 Subject: [PATCH 22/30] style: make blue curve glow brightly with drop-shadow --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index b5593849f4e..cd7b52d0f16 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -905,7 +905,8 @@ export function ZoomCurveTrack() { /> Date: Mon, 8 Jun 2026 00:02:32 +0800 Subject: [PATCH 23/30] style: revert light mode blue to original and keep glow only in dark mode --- apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index cd7b52d0f16..4806a0340e1 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -905,8 +905,7 @@ export function ZoomCurveTrack() { /> Date: Mon, 8 Jun 2026 00:04:40 +0800 Subject: [PATCH 24/30] feat: add collapse overlay to zoom curve track icon --- apps/desktop/src/routes/editor/Timeline/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 536a8d490af..c12dce3c441 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -1005,6 +1005,8 @@ export function Timeline(props: { } subordinate={true} + onIconClick={() => setEditorState("timeline", "showZoomCurves", false)} + iconOverlay={() => } > From 84be3e7d738c34b5998f59edd919e63ef09663dd Mon Sep 17 00:00:00 2001 From: yihuan Date: Mon, 8 Jun 2026 00:07:47 +0800 Subject: [PATCH 25/30] feat: change zoom curve track icon to Expand and remove collapse overlay from it --- apps/desktop/src/routes/editor/Timeline/index.tsx | 4 +--- packages/ui-solid/src/auto-imports.d.ts | 1 + test_icon.tsx | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 test_icon.tsx diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index c12dce3c441..2e4561cbfaa 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -1003,10 +1003,8 @@ export function Timeline(props: { )} > } + icon={() => } subordinate={true} - onIconClick={() => setEditorState("timeline", "showZoomCurves", false)} - iconOverlay={() => } > diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index 9d500065ac4..68cde5e8dba 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -78,6 +78,7 @@ declare global { const IconLucideClock: typeof import('~icons/lucide/clock.jsx')['default'] const IconLucideColumns2: typeof import('~icons/lucide/columns2.jsx')['default'] const IconLucideEdit: typeof import('~icons/lucide/edit.jsx')['default'] + const IconLucideExpand: typeof import('~icons/lucide/expand.jsx')['default'] const IconLucideEyeOff: typeof import('~icons/lucide/eye-off.jsx')['default'] const IconLucideFastForward: typeof import('~icons/lucide/fast-forward.jsx')['default'] const IconLucideFolder: typeof import('~icons/lucide/folder.jsx')['default'] diff --git a/test_icon.tsx b/test_icon.tsx new file mode 100644 index 00000000000..05c9375b856 --- /dev/null +++ b/test_icon.tsx @@ -0,0 +1,3 @@ +import IconLucideScale from "~icons/lucide/scale" +import IconLucideMaximize from "~icons/lucide/maximize" +import IconLucideScaling from "~icons/lucide/scaling" From c4381b08a784b3b0d3262f1e4460a3c8c6292e3f Mon Sep 17 00:00:00 2001 From: yihuan Date: Mon, 8 Jun 2026 00:10:19 +0800 Subject: [PATCH 26/30] fix(ui): restrict track icon overlay hover state to icon container only feat(ui): change zoom curve track icon to Spline --- apps/desktop/src/routes/editor/Timeline/index.tsx | 6 +++--- packages/ui-solid/src/auto-imports.d.ts | 1 + test_icon.tsx | 3 --- 3 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 test_icon.tsx diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 2e4561cbfaa..24f90b1e904 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -1003,7 +1003,7 @@ export function Timeline(props: { )} > } + icon={() => } subordinate={true} > @@ -1039,10 +1039,10 @@ function TrackRow(props: { }) { return (
-
+
Date: Mon, 8 Jun 2026 00:14:00 +0800 Subject: [PATCH 27/30] fix(ui): gracefully interpolate zooming curves when segments are close to prevent overlap --- .../src/routes/editor/Timeline/ZoomTrack.tsx | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 4806a0340e1..b857cb795b2 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -810,21 +810,45 @@ export function ZoomCurveTrack() { {(() => { const isInstant = () => segment.instantAnimation; - const prev = () => zoomSegments()[i() - 1]; + const prev = () => i() > 0 ? zoomSegments()[i() - 1] : null; const next = () => zoomSegments()[i() + 1]; - const isContiguousWithPrev = () => prev() && prev().end === segment.start; - const isContiguousWithNext = () => next() && next().start === segment.end; + const gapBeforeSecs = () => prev() ? segment.start - prev()!.end : Infinity; + const gapAfterSecs = () => next() ? next()!.start - segment.end : Infinity; + + const isContiguousWithPrev = () => gapBeforeSecs() <= 0.001; + const isContiguousWithNext = () => gapAfterSecs() <= 0.001; + + const rampDurationSecs = 1.0; + + const prevAmt = () => { + const p = prev(); + if (!p) return 1.0; + if (isContiguousWithPrev()) return p.amount; + if (gapBeforeSecs() >= rampDurationSecs) return 1.0; + const t = gapBeforeSecs() / rampDurationSecs; + return p.amount + (1.0 - p.amount) * t; + }; - const prevAmt = () => isContiguousWithPrev() ? prev().amount : 1.0; const currAmt = () => segment.amount; - const nextAmt = () => isContiguousWithNext() ? next().amount : 1.0; + + const nextAmt = () => { + const n = next(); + if (!n) return 1.0; + if (isContiguousWithNext()) return n.amount; + if (gapAfterSecs() >= rampDurationSecs) return 1.0; + const t = gapAfterSecs() / rampDurationSecs; + return segment.amount + (1.0 - segment.amount) * t; + }; // Map amount to Y coordinate linearly (1.0 -> 90, 4.5 -> 5) const getY = (amt: number) => { - // Allow p to be negative so zoom-out (< 1.0) goes below the baseline - const p = Math.min(1, (amt - 1) / 3.5); - return 90 - 85 * p; + const minAmt = 1.0; + const maxAmt = 4.5; + const minY = 90; + const maxY = 5; + const clampedAmt = Math.min(maxAmt, Math.max(minAmt, amt)); + return minY - ((clampedAmt - minAmt) / (maxAmt - minAmt)) * (minY - maxY); }; const startY = () => getY(prevAmt()); @@ -832,13 +856,13 @@ export function ZoomCurveTrack() { const endY = () => getY(nextAmt()); const W = () => Math.max(1, width()); - // The video rendering engine uses exactly 1.0 second for the zoom transition - const rampDurationSecs = 1.0; const rampPixels = () => rampDurationSecs / secsPerPixel(); const toPct = (px: number) => (px / W()) * 100; const rampUpPct = () => isInstant() ? 0 : toPct(Math.min(rampPixels(), W() / 2)); const rampDownPct = () => isInstant() ? 0 : toPct(rampPixels()); + + const actualRampDownPct = () => isInstant() ? 0 : toPct(Math.min(rampDurationSecs, gapAfterSecs()) / secsPerPixel()); const dGray = () => { let parts = []; @@ -846,13 +870,14 @@ export function ZoomCurveTrack() { // 1. Gap before if (i() === 0) { parts.push(`M -100000 ${getY(1.0)} L 0 ${getY(1.0)}`); - } else { + } else if (gapBeforeSecs() >= rampDurationSecs) { const prevSeg = prev(); - const prevEndOffset = (prevSeg.end - segment.start) / secsPerPixel(); - const prevRampDownW = prevSeg.instantAnimation ? 0 : rampPixels(); - const gapStartX = isContiguousWithPrev() ? 0 : prevEndOffset + prevRampDownW; - if (gapStartX < 0) { - parts.push(`M ${toPct(gapStartX)} ${getY(1.0)} L 0 ${getY(1.0)}`); + if (prevSeg) { + const prevEndOffset = (prevSeg.end - segment.start) / secsPerPixel(); + const gapStartX = prevSeg.instantAnimation ? prevEndOffset : prevEndOffset + rampPixels(); + if (gapStartX < 0) { + parts.push(`M ${toPct(gapStartX)} ${getY(1.0)} L 0 ${getY(1.0)}`); + } } } @@ -883,7 +908,7 @@ export function ZoomCurveTrack() { if (isInstant()) { parts.push(`M 100 ${currY()} L 100 ${endY()}`); } else { - parts.push(`M 100 ${currY()} C ${100 + rampDownPct() / 2} ${currY()}, ${100 + rampDownPct() / 2} ${endY()}, ${100 + rampDownPct()} ${endY()}`); + parts.push(`M 100 ${currY()} C ${100 + actualRampDownPct() / 2} ${currY()}, ${100 + actualRampDownPct() / 2} ${endY()}, ${100 + actualRampDownPct()} ${endY()}`); } } From b38d084c57b7decaaaf03e8f07c2cfe35c30c4d1 Mon Sep 17 00:00:00 2001 From: yihuan Date: Mon, 8 Jun 2026 00:38:41 +0800 Subject: [PATCH 28/30] fix(ui): restore track row hover scope for delete button, constrain icon overlay --- apps/desktop/src/routes/editor/Timeline/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 24f90b1e904..65841224aa6 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -1039,17 +1039,19 @@ function TrackRow(props: { }) { return (
-
+
@@ -1068,7 +1070,7 @@ function TrackRow(props: {