summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/animation/resolve.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-02-27 16:33:57 +0100
committerMaze Winther <mazewinther@gmail.com>2026-02-27 16:33:57 +0100
commit49426f19cd8eaed7cf6c47999b66fee7ee637a56 (patch)
tree1b37451529cfcb36e6cae79494c61b9c616077df /apps/web/src/lib/animation/resolve.ts
parent9b94f89def4f96836c4f42a23a73e97fe647a43c (diff)
downloadopencut-49426f19cd8eaed7cf6c47999b66fee7ee637a56.tar.gz
feat: implement keyframe animation system
Add keyframe support for transform, opacity, and volume properties. Includes animation engine (interpolation, mutations, resolvers), timeline markers with selection/snapping, properties panel toggles, keyframe-aware renderer, and full undo/redo command support. Also refactor element command constructors to object params, extract timeline pixel math to pixel-utils.ts, and update cursor rules.
Diffstat (limited to 'apps/web/src/lib/animation/resolve.ts')
-rw-r--r--apps/web/src/lib/animation/resolve.ts111
1 files changed, 111 insertions, 0 deletions
diff --git a/apps/web/src/lib/animation/resolve.ts b/apps/web/src/lib/animation/resolve.ts
new file mode 100644
index 00000000..58f292b1
--- /dev/null
+++ b/apps/web/src/lib/animation/resolve.ts
@@ -0,0 +1,111 @@
+import type { ElementAnimations } from "@/types/animation";
+import type { Transform } from "@/types/timeline";
+import { getNumberChannelValueAtTime } from "./interpolation";
+import { getNumberChannelForPath } from "./number-channel";
+
+export function getElementLocalTime({
+ timelineTime,
+ elementStartTime,
+ elementDuration,
+}: {
+ timelineTime: number;
+ elementStartTime: number;
+ elementDuration: number;
+}): number {
+ const localTime = timelineTime - elementStartTime;
+ if (localTime <= 0) {
+ return 0;
+ }
+
+ if (localTime >= elementDuration) {
+ return elementDuration;
+ }
+
+ return localTime;
+}
+
+export function resolveTransformAtTime({
+ baseTransform,
+ animations,
+ localTime,
+}: {
+ baseTransform: Transform;
+ animations: ElementAnimations | undefined;
+ localTime: number;
+}): Transform {
+ const safeLocalTime = Math.max(0, localTime);
+ return {
+ position: {
+ x: getNumberChannelValueAtTime({
+ channel: getNumberChannelForPath({
+ animations,
+ propertyPath: "transform.position.x",
+ }),
+ time: safeLocalTime,
+ fallbackValue: baseTransform.position.x,
+ }),
+ y: getNumberChannelValueAtTime({
+ channel: getNumberChannelForPath({
+ animations,
+ propertyPath: "transform.position.y",
+ }),
+ time: safeLocalTime,
+ fallbackValue: baseTransform.position.y,
+ }),
+ },
+ scale: getNumberChannelValueAtTime({
+ channel: getNumberChannelForPath({
+ animations,
+ propertyPath: "transform.scale",
+ }),
+ time: safeLocalTime,
+ fallbackValue: baseTransform.scale,
+ }),
+ rotate: getNumberChannelValueAtTime({
+ channel: getNumberChannelForPath({
+ animations,
+ propertyPath: "transform.rotate",
+ }),
+ time: safeLocalTime,
+ fallbackValue: baseTransform.rotate,
+ }),
+ };
+}
+
+export function resolveOpacityAtTime({
+ baseOpacity,
+ animations,
+ localTime,
+}: {
+ baseOpacity: number;
+ animations: ElementAnimations | undefined;
+ localTime: number;
+}): number {
+ return getNumberChannelValueAtTime({
+ channel: getNumberChannelForPath({
+ animations,
+ propertyPath: "opacity",
+ }),
+ time: Math.max(0, localTime),
+ fallbackValue: baseOpacity,
+ });
+}
+
+export function resolveVolumeAtTime({
+ baseVolume,
+ animations,
+ localTime,
+}: {
+ baseVolume: number;
+ animations: ElementAnimations | undefined;
+ localTime: number;
+}): number {
+ return getNumberChannelValueAtTime({
+ channel: getNumberChannelForPath({
+ animations,
+ propertyPath: "volume",
+ }),
+ time: Math.max(0, localTime),
+ fallbackValue: baseVolume,
+ });
+}