summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-02-02 18:11:12 +0100
committerMaze Winther <mazewinther@gmail.com>2026-02-02 18:11:12 +0100
commitc3f0d640ef366fe4d94ff995ee8fe5ef282b9c1e (patch)
treeb1f6907de1250698f19b00791cf50f27ad69f48a /apps
parent22ea6b89256328458bd8d67f6e5a53d9a3623a90 (diff)
downloadopencut-c3f0d640ef366fe4d94ff995ee8fe5ef282b9c1e.tar.gz
feat: exponential zoom
Diffstat (limited to 'apps')
-rw-r--r--apps/web/src/components/editor/timeline/timeline-toolbar.tsx31
-rw-r--r--apps/web/src/lib/timeline/zoom-utils.ts33
2 files changed, 47 insertions, 17 deletions
diff --git a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
index 822bb438..33cf83ae 100644
--- a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
@@ -16,6 +16,7 @@ import {
import { Slider } from "@/components/ui/slider";
import { formatTimeCode } from "@/lib/time";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { sliderToZoom, zoomToSlider } from "@/lib/timeline/zoom-utils";
import { EditableTimecode } from "@/components/editable-timecode";
import { ScenesView } from "../scenes-view";
import { type TAction, invokeAction } from "@/lib/actions";
@@ -50,13 +51,11 @@ export function TimelineToolbar({
setZoomLevel: ({ zoom }: { zoom: number }) => void;
}) {
const handleZoom = ({ direction }: { direction: "in" | "out" }) => {
+ const zoomFactor = 1.25;
const newZoomLevel =
direction === "in"
- ? Math.min(
- TIMELINE_CONSTANTS.ZOOM_MAX,
- zoomLevel + TIMELINE_CONSTANTS.ZOOM_STEP,
- )
- : Math.max(minZoom, zoomLevel - TIMELINE_CONSTANTS.ZOOM_STEP);
+ ? Math.min(TIMELINE_CONSTANTS.ZOOM_MAX, zoomLevel * zoomFactor)
+ : Math.max(minZoom, zoomLevel / zoomFactor);
setZoomLevel({ zoom: newZoomLevel });
};
@@ -127,17 +126,13 @@ function ToolbarLeftSection() {
<ToolbarButton
icon={<HugeiconsIcon icon={ScissorIcon} />}
tooltip="Split element"
- onClick={({ event }) =>
- handleAction({ action: "split", event })
- }
+ onClick={({ event }) => handleAction({ action: "split", event })}
/>
<ToolbarButton
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
tooltip="Split left"
- onClick={({ event }) =>
- handleAction({ action: "split-left", event })
- }
+ onClick={({ event }) => handleAction({ action: "split-left", event })}
/>
<ToolbarButton
@@ -305,12 +300,14 @@ function ToolbarRightSection({
<HugeiconsIcon icon={SearchAddIcon} />
</Button>
<Slider
- className="w-24"
- value={[zoomLevel]}
- onValueChange={(values) => onZoomChange(values[0])}
- min={minZoom}
- max={TIMELINE_CONSTANTS.ZOOM_MAX}
- step={TIMELINE_CONSTANTS.ZOOM_STEP}
+ className="w-28"
+ value={[zoomToSlider({ zoomLevel, minZoom })]}
+ onValueChange={(values) =>
+ onZoomChange(sliderToZoom({ sliderPosition: values[0], minZoom }))
+ }
+ min={0}
+ max={1}
+ step={0.005}
/>
<Button
variant="text"
diff --git a/apps/web/src/lib/timeline/zoom-utils.ts b/apps/web/src/lib/timeline/zoom-utils.ts
index 49f7fcb6..c4a64ed1 100644
--- a/apps/web/src/lib/timeline/zoom-utils.ts
+++ b/apps/web/src/lib/timeline/zoom-utils.ts
@@ -51,3 +51,36 @@ export function getZoomPercent({
}): number {
return (zoomLevel - minZoom) / (TIMELINE_CONSTANTS.ZOOM_MAX - minZoom);
}
+
+/**
+ * Convert linear slider position (0-1) to exponential zoom level.
+ * At low slider values, zoom changes are small. At high values, changes are large.
+ */
+export function sliderToZoom({
+ sliderPosition,
+ minZoom,
+ maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX,
+}: {
+ sliderPosition: number;
+ minZoom: number;
+ maxZoom?: number;
+}): number {
+ const clampedPosition = Math.max(0, Math.min(1, sliderPosition));
+ return minZoom * (maxZoom / minZoom) ** clampedPosition;
+}
+
+/**
+ * Convert exponential zoom level to linear slider position (0-1).
+ */
+export function zoomToSlider({
+ zoomLevel,
+ minZoom,
+ maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX,
+}: {
+ zoomLevel: number;
+ minZoom: number;
+ maxZoom?: number;
+}): number {
+ const clampedZoom = Math.max(minZoom, Math.min(maxZoom, zoomLevel));
+ return Math.log(clampedZoom / minZoom) / Math.log(maxZoom / minZoom);
+}