feat: exponential zoom

This commit is contained in:
Maze Winther 2026-02-02 18:11:12 +01:00
parent 22ea6b8925
commit c3f0d640ef
2 changed files with 47 additions and 17 deletions

View File

@ -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"

View File

@ -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);
}