From c5c58ae0b0f59bb43169c250c39179cfc75907d7 Mon Sep 17 00:00:00 2001 From: Eric Wiener Date: Wed, 11 Jun 2025 09:36:58 -0400 Subject: [PATCH 1/5] WIP to add speed editing - video is sped up but the timeline doesnt change --- .../src/routes/editor/ConfigSidebar.tsx | 131 ++++++++++++++---- .../src/routes/editor/Timeline/ClipTrack.tsx | 42 +++--- packages/ui-solid/src/auto-imports.d.ts | 1 + rust-toolchain.toml | 4 + 4 files changed, 129 insertions(+), 49 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index 08906742b..c7265192c 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -263,7 +263,7 @@ export function ConfigSidebar() { class={cx( "flex justify-center relative border-transparent border z-10 items-center rounded-md size-9 transition will-change-transform", state.selectedTab !== item.id && - "group-hover:border-gray-300 group-disabled:border-none" + "group-hover:border-gray-300 group-disabled:border-none" )} > @@ -940,17 +940,13 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { ref={setBackgroundRef} class="flex overflow-x-auto overscroll-contain relative z-40 flex-row gap-2 items-center mb-3 text-xs hide-scroll" style={{ - "-webkit-mask-image": `linear-gradient(to right, transparent, black ${ - scrollX() > 0 ? "24px" : "0" - }, black calc(100% - ${ - reachedEndOfScroll() ? "0px" : "24px" - }), transparent)`, - - "mask-image": `linear-gradient(to right, transparent, black ${ - scrollX() > 0 ? "24px" : "0" - }, black calc(100% - ${ - reachedEndOfScroll() ? "0px" : "24px" - }), transparent);`, + "-webkit-mask-image": `linear-gradient(to right, transparent, black ${scrollX() > 0 ? "24px" : "0" + }, black calc(100% - ${reachedEndOfScroll() ? "0px" : "24px" + }), transparent)`, + + "mask-image": `linear-gradient(to right, transparent, black ${scrollX() > 0 ? "24px" : "0" + }, black calc(100% - ${reachedEndOfScroll() ? "0px" : "24px" + }), transparent);`, }} > @@ -977,10 +973,10 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { value={ project.background.source.type === "wallpaper" ? wallpapers()?.find((w) => - ( - project.background.source as { path?: string } - ).path?.includes(w.id) - )?.url ?? undefined + ( + project.background.source as { path?: string } + ).path?.includes(w.id) + )?.url ?? undefined : undefined } onChange={(photoUrl) => { @@ -1271,7 +1267,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { const rawNewAngle = Math.round( start + - (downEvent.clientY - moveEvent.clientY) + (downEvent.clientY - moveEvent.clientY) ) % max; const newAngle = moveEvent.shiftKey ? rawNewAngle @@ -1281,7 +1277,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { !moveEvent.shiftKey && hapticsEnabled() && project.background.source.type === - "gradient" && + "gradient" && project.background.source.angle !== newAngle ) { commands.performHapticFeedback( @@ -1494,8 +1490,8 @@ function CameraConfig(props: { scrollRef: HTMLDivElement }) { item.x === "left" ? "left-2" : item.x === "right" - ? "right-2" - : "left-1/2 transform -translate-x-1/2", + ? "right-2" + : "left-1/2 transform -translate-x-1/2", item.y === "top" ? "top-2" : "bottom-2" )} onClick={() => setProject("camera", "position", item)} @@ -1752,8 +1748,7 @@ function ZoomSegmentConfig(props: { createEffect(() => { video.src = convertFileSrc( // TODO: this shouldn't be so hardcoded - `${ - editorInstance.path + `${editorInstance.path }/content/segments/segment-${segmentIndex()}/display.mp4` ); }); @@ -1850,7 +1845,7 @@ function ZoomSegmentConfig(props: { x: Math.max( Math.min( (moveEvent.clientX - bounds.left) / - bounds.width, + bounds.width, 1 ), 0 @@ -1858,7 +1853,7 @@ function ZoomSegmentConfig(props: { y: Math.max( Math.min( (moveEvent.clientY - bounds.top) / - bounds.height, + bounds.height, 1 ), 0 @@ -1873,12 +1868,10 @@ function ZoomSegmentConfig(props: {
@@ -1945,6 +1938,84 @@ function ClipSegmentConfig(props: { Delete
+ }> +
+ + setProject( + "timeline", + "segments", + props.segmentIndex, + "timescale", + v[0] + ) + } + minValue={0.25} + maxValue={4} + step={0.25} + formatTooltip={(v) => `${v}x`} + /> +
+ + + + +
+
+
} /> diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index 2f3ea8128..6b4f9ba5b 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -79,7 +79,7 @@ export function ClipTrack( const segmentRecording = (s = i()) => editorInstance.recordings.segments[ - segments()[s].recordingSegment ?? 0 + segments()[s].recordingSegment ?? 0 ]; const marker = useSectionMarker(() => ({ @@ -112,9 +112,8 @@ export function ClipTrack(
@@ -277,7 +276,7 @@ export function ClipTrack( segmentI === i() ? acc : acc + - (segment.end - segment.start) / segment.timescale, + (segment.end - segment.start) / segment.timescale, 0 ); @@ -290,7 +289,7 @@ export function ClipTrack( const prevSegmentIsSameClip = prevSegment?.recordingSegment !== undefined ? prevSegment.recordingSegment === - segment.recordingSegment + segment.recordingSegment : false; function update(event: MouseEvent) { @@ -344,6 +343,12 @@ export function ClipTrack( {" "} {(segment.end - segment.start).toFixed(1)}s
+ +
+ + {segment.timescale}x +
+
); @@ -367,7 +372,7 @@ export function ClipTrack( segmentI === i() ? acc : acc + - (segment.end - segment.start) / segment.timescale, + (segment.end - segment.start) / segment.timescale, 0 ); @@ -375,7 +380,7 @@ export function ClipTrack( const nextSegmentIsSameClip = nextSegment?.recordingSegment !== undefined ? nextSegment.recordingSegment === - segment.recordingSegment + segment.recordingSegment : false; function update(event: MouseEvent) { @@ -482,9 +487,8 @@ function Markings(props: { segment: TimelineSegment; prevDuration: number }) { {(marking) => (
@@ -528,10 +532,10 @@ function useSectionMarker( } ): Accessor< | ({ type: "dual" } & ( - | { left: SectionMarker; right: null } - | { left: null; right: SectionMarker } - | { left: SectionMarker; right: SectionMarker } - )) + | { left: SectionMarker; right: null } + | { left: null; right: SectionMarker } + | { left: SectionMarker; right: SectionMarker } + )) | { type: "single"; value: SectionMarker } | null > { @@ -544,10 +548,10 @@ function useSectionMarker( return segments[0].start === 0 ? null : { - type: "dual", - right: { type: "time", time: segments[0].start }, - left: null, - }; + type: "dual", + right: { type: "time", time: segments[0].start }, + left: null, + }; } if (i === segments.length - 1 && position === "right") { diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index 09ff520c5..8556b768c 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -82,6 +82,7 @@ declare global { const IconLucideDatabase: typeof import('~icons/lucide/database.jsx')['default'] const IconLucideEdit: typeof import('~icons/lucide/edit.jsx')['default'] const IconLucideEye: typeof import('~icons/lucide/eye.jsx')['default'] + const IconLucideFastForward: typeof import('~icons/lucide/fast-forward.jsx')['default'] const IconLucideFolder: typeof import('~icons/lucide/folder.jsx')['default'] const IconLucideGift: typeof import('~icons/lucide/gift.jsx')['default'] const IconLucideHardDrive: typeof import('~icons/lucide/hard-drive.jsx')['default'] diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..509da7f23 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly" +components = ["rustfmt", "clippy"] +profile = "minimal" From 390394c2b4d7615afd79e5f847fcd607df51c238 Mon Sep 17 00:00:00 2001 From: Eric Wiener Date: Wed, 11 Jun 2025 09:36:58 -0400 Subject: [PATCH 2/5] Time scale used for seg duration --- apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index 6b4f9ba5b..329ad7df0 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -71,7 +71,7 @@ export function ClipTrack( const relativeSegment = mergeProps(segment, () => ({ start: prevDuration(), - end: segment.end - segment.start + prevDuration(), + end: prevDuration() + (segment.end - segment.start) / segment.timescale, })); const segmentX = useSegmentTranslateX(() => relativeSegment); From fae66c02a389bd744b77b65e33c9937a6a126e2d Mon Sep 17 00:00:00 2001 From: Eric Wiener Date: Wed, 11 Jun 2025 09:36:58 -0400 Subject: [PATCH 3/5] Add custom speed up --- .../src/routes/editor/ConfigSidebar.tsx | 95 +++++++++++++++---- apps/desktop/src/routes/editor/ui.tsx | 4 +- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index c7265192c..187056c7e 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -1940,23 +1940,78 @@ function ClipSegmentConfig(props: {
}>
- - setProject( - "timeline", - "segments", - props.segmentIndex, - "timescale", - v[0] - ) - } - minValue={0.25} - maxValue={4} - step={0.25} - formatTooltip={(v) => `${v}x`} - /> +
+ + setProject( + "timeline", + "segments", + props.segmentIndex, + "timescale", + v[0] + ) + } + minValue={0.1} + maxValue={10} + step={0.01} + formatTooltip={(v) => `${v.toFixed(2)}x`} + class="flex-1" + /> +
+ { + const value = parseFloat(e.currentTarget.value); + if (!isNaN(value) && value > 0 && value <= 10) { + setProject( + "timeline", + "segments", + props.segmentIndex, + "timescale", + value + ); + } + }} + onBlur={(e) => { + const value = parseFloat(e.currentTarget.value); + if (isNaN(value) || value <= 0) { + e.currentTarget.value = props.segment.timescale.toFixed(2); + } else if (value > 10) { + setProject( + "timeline", + "segments", + props.segmentIndex, + "timescale", + 10 + ); + e.currentTarget.value = "10.00"; + } + }} + class="w-16 px-2 py-1 text-xs text-center border rounded-md bg-gray-2 text-gray-12" + min="0.01" + max="10" + step="0.01" + /> + x +
+
+
diff --git a/apps/desktop/src/routes/editor/ui.tsx b/apps/desktop/src/routes/editor/ui.tsx index e2fdc31d1..3d4cf979a 100644 --- a/apps/desktop/src/routes/editor/ui.tsx +++ b/apps/desktop/src/routes/editor/ui.tsx @@ -123,8 +123,8 @@ export function Slider( ? typeof props.formatTooltip === "string" ? `${props.value[0].toFixed(1)}${props.formatTooltip}` : props.formatTooltip - ? props.formatTooltip(props.value[0]) - : props.value[0].toFixed(1) + ? props.formatTooltip(props.value[0]) + : props.value[0].toFixed(1) : undefined } > From 77372d762da096ca3f62270f08adfaa467c285a3 Mon Sep 17 00:00:00 2001 From: Eric Wiener Date: Wed, 11 Jun 2025 09:36:58 -0400 Subject: [PATCH 4/5] Improve text input handling --- .../src/routes/editor/ConfigSidebar.tsx | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index 187056c7e..37d64b25d 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -1900,6 +1900,14 @@ function ClipSegmentConfig(props: { }) { const { setProject, setEditorState, project } = useEditorContext(); + // Local state for the input field + const [inputValue, setInputValue] = createSignal(props.segment.timescale.toString()); + + // Update input value when timescale changes externally (e.g., from slider or buttons) + createEffect(() => { + setInputValue(props.segment.timescale.toString()); + }); + return ( <>
@@ -1961,23 +1969,30 @@ function ClipSegmentConfig(props: {
{ - const value = parseFloat(e.currentTarget.value); - if (!isNaN(value) && value > 0 && value <= 10) { + const value = e.currentTarget.value; + setInputValue(value); + + const numValue = parseFloat(value); + if (!isNaN(numValue) && numValue > 0 && numValue <= 10) { setProject( "timeline", "segments", props.segmentIndex, "timescale", - value + numValue ); } }} + onFocus={(e) => { + // Select all text on focus for easy replacement + e.currentTarget.select(); + }} onBlur={(e) => { const value = parseFloat(e.currentTarget.value); if (isNaN(value) || value <= 0) { - e.currentTarget.value = props.segment.timescale.toFixed(2); + setInputValue(props.segment.timescale.toFixed(2)); } else if (value > 10) { setProject( "timeline", @@ -1986,7 +2001,10 @@ function ClipSegmentConfig(props: { "timescale", 10 ); - e.currentTarget.value = "10.00"; + setInputValue("10.00"); + } else { + // Format to 2 decimal places on blur + setInputValue(value.toFixed(2)); } }} class="w-16 px-2 py-1 text-xs text-center border rounded-md bg-gray-2 text-gray-12" From 4cb04cb3de4b04b2d3d267527b0b4c1b64284a26 Mon Sep 17 00:00:00 2001 From: Eric Wiener Date: Wed, 11 Jun 2025 09:38:31 -0400 Subject: [PATCH 5/5] Undo unintentional changes --- apps/desktop/src/routes/editor/ui.tsx | 4 ++-- rust-toolchain.toml | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 rust-toolchain.toml diff --git a/apps/desktop/src/routes/editor/ui.tsx b/apps/desktop/src/routes/editor/ui.tsx index 3d4cf979a..e2fdc31d1 100644 --- a/apps/desktop/src/routes/editor/ui.tsx +++ b/apps/desktop/src/routes/editor/ui.tsx @@ -123,8 +123,8 @@ export function Slider( ? typeof props.formatTooltip === "string" ? `${props.value[0].toFixed(1)}${props.formatTooltip}` : props.formatTooltip - ? props.formatTooltip(props.value[0]) - : props.value[0].toFixed(1) + ? props.formatTooltip(props.value[0]) + : props.value[0].toFixed(1) : undefined } > diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index 509da7f23..000000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,4 +0,0 @@ -[toolchain] -channel = "nightly" -components = ["rustfmt", "clippy"] -profile = "minimal"