summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/zoom-utils.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/timeline/zoom-utils.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/timeline/zoom-utils.ts')
-rw-r--r--apps/web/src/timeline/zoom-utils.ts90
1 files changed, 90 insertions, 0 deletions
diff --git a/apps/web/src/timeline/zoom-utils.ts b/apps/web/src/timeline/zoom-utils.ts
new file mode 100644
index 00000000..39090d92
--- /dev/null
+++ b/apps/web/src/timeline/zoom-utils.ts
@@ -0,0 +1,90 @@
+import {
+ BASE_TIMELINE_PIXELS_PER_SECOND,
+ TIMELINE_ZOOM_MAX,
+} from "@/timeline/scale";
+import { TICKS_PER_SECOND } from "@/wasm";
+
+const PADDING_MAX_RATIO = 0.75;
+const PADDING_MIN_RATIO = 0.15;
+const PADDING_MIN_AT_ZOOM_PERCENT = 0.2;
+
+export function getTimelineZoomMin({
+ duration,
+ containerWidth,
+}: {
+ duration: number;
+ containerWidth: number | null | undefined;
+}): number {
+ const safeDurationSeconds = Math.max(duration / TICKS_PER_SECOND, 1);
+ const safeContainerWidth = containerWidth ?? 1000;
+ const contentRatioAtMinZoom = 1 - PADDING_MAX_RATIO;
+ const availableWidth = safeContainerWidth * contentRatioAtMinZoom;
+ const zoomToFit =
+ availableWidth / (safeDurationSeconds * BASE_TIMELINE_PIXELS_PER_SECOND);
+
+ return Math.min(TIMELINE_ZOOM_MAX, zoomToFit);
+}
+
+export function getTimelinePaddingPx({
+ containerWidth,
+ zoomLevel,
+ minZoom,
+}: {
+ containerWidth: number;
+ zoomLevel: number;
+ minZoom: number;
+}): number {
+ const zoomPercent = getZoomPercent({ zoomLevel, minZoom });
+ const paddingTransitionPercent = Math.min(
+ zoomPercent / PADDING_MIN_AT_ZOOM_PERCENT,
+ 1,
+ );
+ const paddingRatio =
+ PADDING_MAX_RATIO -
+ (PADDING_MAX_RATIO - PADDING_MIN_RATIO) * paddingTransitionPercent;
+
+ return containerWidth * paddingRatio;
+}
+
+export function getZoomPercent({
+ zoomLevel,
+ minZoom,
+}: {
+ zoomLevel: number;
+ minZoom: number;
+}): number {
+ return (zoomLevel - minZoom) / (TIMELINE_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_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_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);
+}