mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: exponential zoom
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { formatTimeCode } from "@/lib/time";
|
import { formatTimeCode } from "@/lib/time";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
|
import { sliderToZoom, zoomToSlider } from "@/lib/timeline/zoom-utils";
|
||||||
import { EditableTimecode } from "@/components/editable-timecode";
|
import { EditableTimecode } from "@/components/editable-timecode";
|
||||||
import { ScenesView } from "../scenes-view";
|
import { ScenesView } from "../scenes-view";
|
||||||
import { type TAction, invokeAction } from "@/lib/actions";
|
import { type TAction, invokeAction } from "@/lib/actions";
|
||||||
@@ -50,13 +51,11 @@ export function TimelineToolbar({
|
|||||||
setZoomLevel: ({ zoom }: { zoom: number }) => void;
|
setZoomLevel: ({ zoom }: { zoom: number }) => void;
|
||||||
}) {
|
}) {
|
||||||
const handleZoom = ({ direction }: { direction: "in" | "out" }) => {
|
const handleZoom = ({ direction }: { direction: "in" | "out" }) => {
|
||||||
|
const zoomFactor = 1.25;
|
||||||
const newZoomLevel =
|
const newZoomLevel =
|
||||||
direction === "in"
|
direction === "in"
|
||||||
? Math.min(
|
? Math.min(TIMELINE_CONSTANTS.ZOOM_MAX, zoomLevel * zoomFactor)
|
||||||
TIMELINE_CONSTANTS.ZOOM_MAX,
|
: Math.max(minZoom, zoomLevel / zoomFactor);
|
||||||
zoomLevel + TIMELINE_CONSTANTS.ZOOM_STEP,
|
|
||||||
)
|
|
||||||
: Math.max(minZoom, zoomLevel - TIMELINE_CONSTANTS.ZOOM_STEP);
|
|
||||||
setZoomLevel({ zoom: newZoomLevel });
|
setZoomLevel({ zoom: newZoomLevel });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,17 +126,13 @@ function ToolbarLeftSection() {
|
|||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
||||||
tooltip="Split element"
|
tooltip="Split element"
|
||||||
onClick={({ event }) =>
|
onClick={({ event }) => handleAction({ action: "split", event })}
|
||||||
handleAction({ action: "split", event })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
|
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
|
||||||
tooltip="Split left"
|
tooltip="Split left"
|
||||||
onClick={({ event }) =>
|
onClick={({ event }) => handleAction({ action: "split-left", event })}
|
||||||
handleAction({ action: "split-left", event })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
@@ -305,12 +300,14 @@ function ToolbarRightSection({
|
|||||||
<HugeiconsIcon icon={SearchAddIcon} />
|
<HugeiconsIcon icon={SearchAddIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
<Slider
|
<Slider
|
||||||
className="w-24"
|
className="w-28"
|
||||||
value={[zoomLevel]}
|
value={[zoomToSlider({ zoomLevel, minZoom })]}
|
||||||
onValueChange={(values) => onZoomChange(values[0])}
|
onValueChange={(values) =>
|
||||||
min={minZoom}
|
onZoomChange(sliderToZoom({ sliderPosition: values[0], minZoom }))
|
||||||
max={TIMELINE_CONSTANTS.ZOOM_MAX}
|
}
|
||||||
step={TIMELINE_CONSTANTS.ZOOM_STEP}
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.005}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
|
|||||||
@@ -51,3 +51,36 @@ export function getZoomPercent({
|
|||||||
}): number {
|
}): number {
|
||||||
return (zoomLevel - minZoom) / (TIMELINE_CONSTANTS.ZOOM_MAX - minZoom);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user