summaryrefslogtreecommitdiff
path: root/apps/web/src/animation/curve-bridge.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/animation/curve-bridge.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/animation/curve-bridge.ts')
-rw-r--r--apps/web/src/animation/curve-bridge.ts95
1 files changed, 95 insertions, 0 deletions
diff --git a/apps/web/src/animation/curve-bridge.ts b/apps/web/src/animation/curve-bridge.ts
new file mode 100644
index 00000000..af411dd1
--- /dev/null
+++ b/apps/web/src/animation/curve-bridge.ts
@@ -0,0 +1,95 @@
+import {
+ getDefaultLeftHandle,
+ getDefaultRightHandle,
+} from "@/animation/bezier";
+import type {
+ CurveHandle,
+ NormalizedCubicBezier,
+ ScalarAnimationKey,
+} from "@/animation/types";
+
+const VALUE_EPSILON = 1e-6;
+
+function clamp01({ value }: { value: number }): number {
+ return Math.max(0, Math.min(1, value));
+}
+
+export function getNormalizedCubicBezierForScalarSegment({
+ leftKey,
+ rightKey,
+ referenceSpanValue,
+}: {
+ leftKey: ScalarAnimationKey;
+ rightKey: ScalarAnimationKey;
+ /** Fallback Y-axis scale used when the segment is flat (spanValue ≈ 0). */
+ referenceSpanValue?: number;
+}): NormalizedCubicBezier | null {
+ const spanTime = rightKey.time - leftKey.time;
+ const spanValue = rightKey.value - leftKey.value;
+ const effectiveSpanValue =
+ Math.abs(spanValue) > VALUE_EPSILON
+ ? spanValue
+ : referenceSpanValue !== undefined && Math.abs(referenceSpanValue) > VALUE_EPSILON
+ ? referenceSpanValue
+ : null;
+
+ if (spanTime === 0 || effectiveSpanValue === null) {
+ return null;
+ }
+
+ const rightHandle =
+ leftKey.rightHandle ?? getDefaultRightHandle({ leftKey, rightKey });
+ const leftHandle =
+ rightKey.leftHandle ?? getDefaultLeftHandle({ leftKey, rightKey });
+
+ return [
+ clamp01({ value: rightHandle.dt / spanTime }),
+ rightHandle.dv / effectiveSpanValue,
+ clamp01({ value: 1 + leftHandle.dt / spanTime }),
+ 1 + leftHandle.dv / effectiveSpanValue,
+ ];
+}
+
+export function getCurveHandlesForNormalizedCubicBezier({
+ leftKey,
+ rightKey,
+ cubicBezier,
+ referenceSpanValue,
+}: {
+ leftKey: ScalarAnimationKey;
+ rightKey: ScalarAnimationKey;
+ cubicBezier: NormalizedCubicBezier;
+ /** Fallback Y-axis scale used when the segment is flat (spanValue ≈ 0). */
+ referenceSpanValue?: number;
+}): {
+ rightHandle: CurveHandle;
+ leftHandle: CurveHandle;
+} | null {
+ const spanTime = rightKey.time - leftKey.time;
+ const spanValue = rightKey.value - leftKey.value;
+ const effectiveSpanValue =
+ Math.abs(spanValue) > VALUE_EPSILON
+ ? spanValue
+ : referenceSpanValue !== undefined && Math.abs(referenceSpanValue) > VALUE_EPSILON
+ ? referenceSpanValue
+ : null;
+
+ if (spanTime === 0 || effectiveSpanValue === null) {
+ return null;
+ }
+
+ const [rawX1, y1, rawX2, y2] = cubicBezier;
+ const x1 = clamp01({ value: rawX1 });
+ const x2 = clamp01({ value: rawX2 });
+
+ return {
+ rightHandle: {
+ dt: spanTime * x1,
+ dv: effectiveSpanValue * y1,
+ },
+ leftHandle: {
+ dt: spanTime * (x2 - 1),
+ dv: effectiveSpanValue * (y2 - 1),
+ },
+ };
+}