diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/animation/keyframes.ts | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz | |
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/animation/keyframes.ts')
| -rw-r--r-- | apps/web/src/lib/animation/keyframes.ts | 1338 |
1 files changed, 0 insertions, 1338 deletions
diff --git a/apps/web/src/lib/animation/keyframes.ts b/apps/web/src/lib/animation/keyframes.ts deleted file mode 100644 index a5efb6ee..00000000 --- a/apps/web/src/lib/animation/keyframes.ts +++ /dev/null @@ -1,1338 +0,0 @@ -import type { - AnimationBindingInstance, - AnimationBindingKind, - AnimationChannel, - AnimationInterpolation, - AnimationPath, - AnimationPropertyPath, - AnimationValue, - DiscreteAnimationChannel, - DiscreteAnimationKey, - ElementAnimations, - ScalarAnimationChannel, - ScalarAnimationKey, - ScalarCurveKeyframePatch, - ScalarSegmentType, -} from "@/lib/animation/types"; -import { generateUUID } from "@/utils/id"; -import { - cloneAnimationBinding, - createAnimationBinding, - decomposeAnimationValue, -} from "./binding-values"; -import { - getBezierPoint, - getDefaultLeftHandle, - getDefaultRightHandle, - solveBezierProgressForTime, -} from "./bezier"; -import { - getChannelValueAtTime, - getScalarSegmentInterpolation, - normalizeChannel, -} from "./interpolation"; -import { - coerceAnimationValueForProperty, - getAnimationPropertyDefinition, -} from "./property-registry"; - -function isNearlySameTime({ - leftTime, - rightTime, -}: { - leftTime: number; - rightTime: number; -}): boolean { - return leftTime === rightTime; -} - -function hasChannelKeys({ - channel, -}: { - channel: AnimationChannel | undefined; -}): boolean { - return Boolean(channel && channel.keys.length > 0); -} - -function toAnimation({ - animations, -}: { - animations: ElementAnimations; -}): ElementAnimations | undefined { - const nextBindings = Object.fromEntries( - Object.entries(animations.bindings).filter(([, binding]) => binding), - ); - const nextChannels = Object.fromEntries( - Object.entries(animations.channels).filter(([, channel]) => - hasChannelKeys({ channel }), - ), - ); - if (Object.keys(nextBindings).length === 0 || Object.keys(nextChannels).length === 0) { - return undefined; - } - - return { - bindings: nextBindings, - channels: nextChannels, - }; -} - -function cloneAnimationsState({ - animations, -}: { - animations: ElementAnimations | undefined; -}): ElementAnimations { - return { - bindings: { ...(animations?.bindings ?? {}) }, - channels: { ...(animations?.channels ?? {}) }, - }; -} - -function getBindingChannelKind({ - kind, -}: { - kind: AnimationBindingKind; -}): AnimationChannel["kind"] { - return kind === "discrete" ? "discrete" : "scalar"; -} - -function getPrimaryComponent({ - binding, -}: { - binding: AnimationBindingInstance; -}) { - return binding.components[0] ?? null; -} - -function getPrimaryChannelId({ - binding, -}: { - binding: AnimationBindingInstance; -}) { - return getPrimaryComponent({ binding })?.channelId ?? null; -} - -function getScalarSegmentType({ - interpolation, -}: { - interpolation: AnimationInterpolation; -}): ScalarSegmentType { - if (interpolation === "hold") { - return "step"; - } - return interpolation === "bezier" ? "bezier" : "linear"; -} - -function getInterpolationForBinding({ - kind, - interpolation, -}: { - kind: AnimationBindingKind; - interpolation: AnimationInterpolation | undefined; -}): AnimationInterpolation { - if (kind === "discrete") { - return "hold"; - } - - if ( - interpolation === "linear" || - interpolation === "hold" || - interpolation === "bezier" - ) { - return interpolation; - } - - return "linear"; -} - -function createEmptyChannelForBindingKind({ - kind, -}: { - kind: AnimationBindingKind; -}): AnimationChannel { - if (kind === "discrete") { - return { - kind: "discrete", - keys: [], - } satisfies DiscreteAnimationChannel; - } - - return { - kind: "scalar", - keys: [], - } satisfies ScalarAnimationChannel; -} - -function createScalarKey({ - id, - time, - value, - interpolation, - previousKey, -}: { - id: string; - time: number; - value: number; - interpolation?: AnimationInterpolation; - previousKey?: ScalarAnimationKey; -}): ScalarAnimationKey { - return { - id, - time, - value, - leftHandle: previousKey?.leftHandle, - rightHandle: previousKey?.rightHandle, - segmentToNext: - previousKey?.segmentToNext ?? - getScalarSegmentType({ interpolation: interpolation ?? "linear" }), - tangentMode: previousKey?.tangentMode ?? "flat", - }; -} - -function createDiscreteKey({ - id, - time, - value, -}: { - id: string; - time: number; - value: string | boolean; -}): DiscreteAnimationKey { - return { - id, - time, - value, - }; -} - -function getBinding({ - animations, - propertyPath, -}: { - animations: ElementAnimations | undefined; - propertyPath: AnimationPath; -}): AnimationBindingInstance | undefined { - return animations?.bindings[propertyPath]; -} - -function getChannelById({ - animations, - channelId, -}: { - animations: ElementAnimations | undefined; - channelId: string; -}): AnimationChannel | undefined { - return animations?.channels[channelId]; -} - -function getBindingComponent({ - binding, - componentKey, -}: { - binding: AnimationBindingInstance; - componentKey: string; -}) { - return binding.components.find((component) => component.key === componentKey) ?? null; -} - -function getTargetKeyMetadata({ - channel, - time, - keyframeId, -}: { - channel: AnimationChannel | undefined; - time: number; - keyframeId?: string; -}) { - const normalizedChannel = - channel != null ? normalizeChannel({ channel }) : undefined; - const keys = normalizedChannel?.keys ?? []; - if (keyframeId) { - const keyById = keys.find((key) => key.id === keyframeId); - if (keyById) { - return { - id: keyById.id, - time, - }; - } - } - - const keyAtTime = keys.find((key) => - isNearlySameTime({ leftTime: key.time, rightTime: time }), - ); - if (keyAtTime) { - return { - id: keyAtTime.id, - time: keyAtTime.time, - }; - } - - return { - id: keyframeId ?? generateUUID(), - time, - }; -} - -function upsertDiscreteChannelKey({ - channel, - time, - value, - keyframeId, -}: { - channel: DiscreteAnimationChannel | undefined; - time: number; - value: string | boolean; - keyframeId?: string; -}): DiscreteAnimationChannel { - const normalizedChannel = normalizeChannel({ - channel: channel ?? { kind: "discrete", keys: [] }, - }); - const keys = [...normalizedChannel.keys]; - if (keyframeId) { - const existingIndex = keys.findIndex((key) => key.id === keyframeId); - if (existingIndex >= 0) { - keys[existingIndex] = createDiscreteKey({ - id: keys[existingIndex].id, - time, - value, - }); - return normalizeChannel({ - channel: { kind: "discrete", keys }, - }); - } - } - - const existingAtTimeIndex = keys.findIndex((key) => - isNearlySameTime({ leftTime: key.time, rightTime: time }), - ); - if (existingAtTimeIndex >= 0) { - keys[existingAtTimeIndex] = createDiscreteKey({ - id: keys[existingAtTimeIndex].id, - time: keys[existingAtTimeIndex].time, - value, - }); - return normalizeChannel({ - channel: { kind: "discrete", keys }, - }); - } - - keys.push( - createDiscreteKey({ - id: keyframeId ?? generateUUID(), - time, - value, - }), - ); - return normalizeChannel({ - channel: { kind: "discrete", keys }, - }); -} - -function upsertScalarChannelKey({ - channel, - time, - value, - interpolation, - defaultInterpolation, - keyframeId, -}: { - channel: ScalarAnimationChannel | undefined; - time: number; - value: number; - interpolation?: AnimationInterpolation; - defaultInterpolation?: AnimationInterpolation; - keyframeId?: string; -}): ScalarAnimationChannel { - const normalizedChannel = normalizeChannel({ - channel: channel ?? { kind: "scalar", keys: [] }, - }); - const keys = [...normalizedChannel.keys]; - if (keyframeId) { - const existingIndex = keys.findIndex((key) => key.id === keyframeId); - if (existingIndex >= 0) { - keys[existingIndex] = createScalarKey({ - id: keys[existingIndex].id, - time, - value, - interpolation, - previousKey: - interpolation != null - ? { - ...keys[existingIndex], - segmentToNext: getScalarSegmentType({ interpolation }), - } - : keys[existingIndex], - }); - return normalizeChannel({ - channel: { - kind: "scalar", - keys, - extrapolation: normalizedChannel.extrapolation, - }, - }); - } - } - - const existingAtTimeIndex = keys.findIndex((key) => - isNearlySameTime({ leftTime: key.time, rightTime: time }), - ); - if (existingAtTimeIndex >= 0) { - keys[existingAtTimeIndex] = createScalarKey({ - id: keys[existingAtTimeIndex].id, - time: keys[existingAtTimeIndex].time, - value, - interpolation, - previousKey: - interpolation != null - ? { - ...keys[existingAtTimeIndex], - segmentToNext: getScalarSegmentType({ interpolation }), - } - : keys[existingAtTimeIndex], - }); - return normalizeChannel({ - channel: { - kind: "scalar", - keys, - extrapolation: normalizedChannel.extrapolation, - }, - }); - } - - keys.push( - createScalarKey({ - id: keyframeId ?? generateUUID(), - time, - value, - interpolation: interpolation ?? defaultInterpolation, - }), - ); - return normalizeChannel({ - channel: { - kind: "scalar", - keys, - extrapolation: normalizedChannel.extrapolation, - }, - }); -} - -export function getChannel({ - animations, - propertyPath, -}: { - animations: ElementAnimations | undefined; - propertyPath: AnimationPath; -}): AnimationChannel | undefined { - const binding = getBinding({ animations, propertyPath }); - const primaryChannelId = - binding != null ? getPrimaryChannelId({ binding }) : null; - return primaryChannelId ? animations?.channels[primaryChannelId] : undefined; -} - -export function upsertPathKeyframe({ - animations, - propertyPath, - time, - value, - interpolation, - keyframeId, - kind, - defaultInterpolation, - coerceValue, -}: { - animations: ElementAnimations | undefined; - propertyPath: AnimationPath; - time: number; - value: AnimationValue; - interpolation?: AnimationInterpolation; - keyframeId?: string; - kind: AnimationBindingKind; - defaultInterpolation: AnimationInterpolation; - coerceValue: ({ value }: { value: AnimationValue }) => AnimationValue | null; -}): ElementAnimations | undefined { - const coercedValue = coerceValue({ value }); - if (coercedValue === null) { - return animations; - } - - const nextAnimations = cloneAnimationsState({ animations }); - const existingBinding = getBinding({ - animations, - propertyPath, - }); - const binding = - existingBinding && existingBinding.kind === kind - ? cloneAnimationBinding({ binding: existingBinding }) - : createAnimationBinding({ path: propertyPath, kind }); - const primaryChannel = getChannel({ - animations, - propertyPath, - }); - const targetKey = getTargetKeyMetadata({ - channel: primaryChannel, - time, - keyframeId, - }); - const componentValues = decomposeAnimationValue({ - kind, - value: coercedValue, - }); - if (!componentValues) { - return animations; - } - - const explicitInterpolation = - interpolation != null - ? getInterpolationForBinding({ kind, interpolation }) - : undefined; - const validatedDefaultInterpolation = getInterpolationForBinding({ - kind, - interpolation: defaultInterpolation, - }); - nextAnimations.bindings[propertyPath] = binding; - for (const component of binding.components) { - const nextValue = componentValues[component.key]; - if (nextValue == null) { - continue; - } - - const currentChannel = getChannelById({ - animations, - channelId: component.channelId, - }); - const targetChannel = - currentChannel?.kind === getBindingChannelKind({ kind }) - ? currentChannel - : createEmptyChannelForBindingKind({ kind }); - nextAnimations.channels[component.channelId] = - targetChannel.kind === "discrete" - ? upsertDiscreteChannelKey({ - channel: targetChannel, - time: targetKey.time, - value: nextValue as string | boolean, - keyframeId: targetKey.id, - }) - : upsertScalarChannelKey({ - channel: targetChannel, - time: targetKey.time, - value: nextValue as number, - interpolation: explicitInterpolation, - defaultInterpolation: validatedDefaultInterpolation, - keyframeId: targetKey.id, - }); - } - - return toAnimation({ - animations: nextAnimations, - }); -} - -export function upsertElementKeyframe({ - animations, - propertyPath, - time, - value, - interpolation, - keyframeId, -}: { - animations: ElementAnimations | undefined; - propertyPath: AnimationPropertyPath; - time: number; - value: AnimationValue; - interpolation?: AnimationInterpolation; - keyframeId?: string; -}): ElementAnimations | undefined { - const coercedValue = coerceAnimationValueForProperty({ - propertyPath, - value, - }); - if (coercedValue === null) { - return animations; - } - - const propertyDefinition = getAnimationPropertyDefinition({ propertyPath }); - return upsertPathKeyframe({ - animations, - propertyPath, - time, - value: coercedValue, - interpolation, - keyframeId, - kind: propertyDefinition.kind, - defaultInterpolation: propertyDefinition.defaultInterpolation, - coerceValue: ({ value: nextValue }) => - coerceAnimationValueForProperty({ - propertyPath, - value: nextValue, - }), - }); -} - -export function upsertKeyframe({ - channel, - time, - value, - interpolation, - keyframeId, -}: { - channel: AnimationChannel | undefined; - time: number; - value: AnimationValue; - interpolation?: AnimationInterpolation; - keyframeId?: string; -}): AnimationChannel | undefined { - if (!channel) { - return undefined; - } - - if (channel.kind === "discrete") { - if (typeof value !== "string" && typeof value !== "boolean") { - return channel; - } - - return upsertDiscreteChannelKey({ - channel, - time, - value, - keyframeId, - }); - } - - if (typeof value !== "number") { - return channel; - } - - return upsertScalarChannelKey({ - channel, - time, - value, - interpolation, - keyframeId, - }); -} - -export function removeKeyframe({ - channel, - keyframeId, -}: { - channel: AnimationChannel | undefined; - keyframeId: string; -}): AnimationChannel | undefined { - if (!channel) { - return undefined; - } - - const nextKeys = channel.keys.filter((keyframe) => keyframe.id !== keyframeId); - if (nextKeys.length === 0) { - return undefined; - } - - return normalizeChannel({ - channel: { - ...channel, - keys: nextKeys, - } as AnimationChannel, - }); -} - -export function retimeKeyframe({ - channel, - keyframeId, - time, -}: { - channel: AnimationChannel | undefined; - keyframeId: string; - time: number; -}): AnimationChannel | undefined { - if (!channel) { - return undefined; - } - - const keyframeByIdIndex = channel.keys.findIndex( - (keyframe) => keyframe.id === keyframeId, - ); - if (keyframeByIdIndex < 0) { - return channel; - } - - const nextKeys = [...channel.keys]; - nextKeys[keyframeByIdIndex] = { - ...nextKeys[keyframeByIdIndex], - time, - }; - - return normalizeChannel({ - channel: { - ...channel, - keys: nextKeys, - } as AnimationChannel, - }); -} - -export function setChannel({ - animations, - propertyPath, - channel, -}: { - animations: ElementAnimations | undefined; - propertyPath: AnimationPath; - channel: AnimationChannel | undefined; -}): ElementAnimations | undefined { - const binding = getBinding({ animations, propertyPath }); - if (!binding) { - return animations; - } - - if (binding.components.length !== 1) { - throw new Error( - `setChannel only supports single-component bindings. Received "${propertyPath}" with ${binding.components.length} components.`, - ); - } - - const primaryComponent = getPrimaryComponent({ binding }); - if (!primaryComponent) { - return animations; - } - - return setBindingComponentChannel({ - animations, - propertyPath, - componentKey: primaryComponent.key, - channel, - }); -} - -export function setBindingComponentChannel({ - animations, - propertyPath, - componentKey, - channel, -}: { - animations: ElementAnimations | undefined; - propertyPath: AnimationPath; - componentKey: string; - channel: AnimationChannel | undefined; -}): ElementAnimations | undefined { - const binding = getBinding({ animations, propertyPath }); - if (!binding) { - return animations; - } - - const component = getBindingComponent({ - binding, - componentKey, - }); - if (!component) { - return animations; - } - - const nextAnimations = cloneAnimationsState({ animations }); - if (!channel || !hasChannelKeys({ channel })) { - delete nextAnimations.channels[component.channelId]; - const hasRemainingKeys = binding.components.some((candidate) => - hasChannelKeys({ - channel: nextAnimations.channels[candidate.channelId], - }), - ); - if (!hasRemainingKeys) { - delete nextAnimations.bindings[propertyPath]; - } - return toAnimation({ - animations: nextAnimations, - }); - } - - nextAnimations.channels[component.channelId] = normalizeChannel({ - channel, - }); - return toAnimation({ - animations: nextAnimations, - }); -} - -export function updateScalarKeyframeCurve({ - animations, - propertyPath, - componentKey, - keyframeId, - patch, -}: { - animations: ElementAnimations | undefined; - propertyPath: AnimationPath; - componentKey: string; - keyframeId: string; - patch: ScalarCurveKeyframePatch; -}): ElementAnimations | undefined { - const binding = getBinding({ animations, propertyPath }); - if (!binding) { - return animations; - } - - const component = getBindingComponent({ - binding, - componentKey, - }); - if (!component) { - return animations; - } - - const channel = getChannelById({ - animations, - channelId: component.channelId, - }); - if (channel?.kind !== "scalar") { - return animations; - } - - const keyframeIndex = channel.keys.findIndex((keyframe) => keyframe.id === keyframeId); - if (keyframeIndex < 0) { - return animations; - } - - const nextKeys = [...channel.keys]; - const currentKey = nextKeys[keyframeIndex]; - nextKeys[keyframeIndex] = { - ...currentKey, - leftHandle: - patch.leftHandle === undefined - ? currentKey.leftHandle - : patch.leftHandle ?? undefined, - rightHandle: - patch.rightHandle === undefined - ? currentKey.rightHandle - : patch.rightHandle ?? undefined, - segmentToNext: patch.segmentToNext ?? currentKey.segmentToNext, - tangentMode: patch.tangentMode ?? currentKey.tangentMode, - }; - - return setBindingComponentChannel({ - animations, - propertyPath, - componentKey, - channel: { - kind: "scalar", - keys: nextKeys, - extrapolation: channel.extrapolation, - }, - }); -} - -export function cloneAnimations({ - animations, - shouldRegenerateKeyframeIds = false, -}: { - animations: ElementAnimations | undefined; - shouldRegenerateKeyframeIds?: boolean; -}): ElementAnimations | undefined { - if (!animations) { - return undefined; - } - - const nextAnimations = cloneAnimationsState({ animations }); - nextAnimations.bindings = Object.fromEntries( - Object.entries(animations.bindings).map(([path, binding]) => [ - path, - binding ? cloneAnimationBinding({ binding }) : binding, - ]), - ); - nextAnimations.channels = {}; - - for (const binding of Object.values(nextAnimations.bindings)) { - if (!binding) { - continue; - } - - const primaryChannel = getChannelById({ - animations, - channelId: getPrimaryChannelId({ binding }) ?? "", - }); - const keyIdMap = new Map<string, string>(); - if (primaryChannel) { - for (const key of primaryChannel.keys) { - keyIdMap.set( - key.id, - shouldRegenerateKeyframeIds ? generateUUID() : key.id, - ); - } - } - - for (const component of binding.components) { - const currentChannel = getChannelById({ - animations, - channelId: component.channelId, - }); - if (!currentChannel) { - continue; - } - - nextAnimations.channels[component.channelId] = normalizeChannel({ - channel: { - ...currentChannel, - keys: currentChannel.keys.map((key) => ({ - ...key, - id: keyIdMap.get(key.id) ?? key.id, - })), - } as AnimationChannel, - }); - } - } - - return toAnimation({ - animations: nextAnimations, - }); -} - -export function clampAnimationsToDuration({ - animations, - duration, -}: { - animations: ElementAnimations | undefined; - duration: number; -}): ElementAnimations | undefined { - if (!animations || duration <= 0) { - return undefined; - } - - return splitAnimationsAtTime({ - animations, - splitTime: duration, - shouldIncludeSplitBoundary: true, - }).leftAnimations; -} - -function lerpPoint({ - left, - right, - progress, -}: { - left: { x: number; y: number }; - right: { x: number; y: number }; - progress: number; -}) { - return { - x: left.x + (right.x - left.x) * progress, - y: left.y + (right.y - left.y) * progress, - }; -} - -function splitDiscreteChannelAtTime({ - channel, - splitTime, - leftBoundaryId, - rightBoundaryId, - shouldIncludeSplitBoundary, -}: { - channel: DiscreteAnimationChannel | undefined; - splitTime: number; - leftBoundaryId: string; - rightBoundaryId: string; - shouldIncludeSplitBoundary: boolean; -}) { - if (!channel || channel.keys.length === 0) { - return { - leftChannel: undefined, - rightChannel: undefined, - }; - } - - const normalizedChannel = normalizeChannel({ channel }); - let leftKeys = normalizedChannel.keys.filter((key) => key.time <= splitTime); - let rightKeys = normalizedChannel.keys - .filter((key) => key.time >= splitTime) - .map((key) => ({ ...key, time: key.time - splitTime })); - - if (shouldIncludeSplitBoundary) { - const hasBoundaryOnLeft = leftKeys.some((key) => - isNearlySameTime({ leftTime: key.time, rightTime: splitTime }), - ); - const hasBoundaryOnRight = rightKeys.some((key) => - isNearlySameTime({ leftTime: key.time, rightTime: 0 }), - ); - const boundaryValue = getChannelValueAtTime({ - channel: normalizedChannel, - time: splitTime, - fallbackValue: normalizedChannel.keys[0].value, - }); - if (!hasBoundaryOnLeft) { - leftKeys = [ - ...leftKeys, - createDiscreteKey({ - id: leftBoundaryId, - time: splitTime, - value: boundaryValue as string | boolean, - }), - ]; - } - if (!hasBoundaryOnRight) { - rightKeys = [ - createDiscreteKey({ - id: rightBoundaryId, - time: 0, - value: boundaryValue as string | boolean, - }), - ...rightKeys, - ]; - } - } - - return { - leftChannel: leftKeys.length - ? normalizeChannel({ channel: { kind: "discrete", keys: leftKeys } }) - : undefined, - rightChannel: rightKeys.length - ? normalizeChannel({ channel: { kind: "discrete", keys: rightKeys } }) - : undefined, - }; -} - -function splitScalarChannelAtTime({ - channel, - splitTime, - leftBoundaryId, - rightBoundaryId, - shouldIncludeSplitBoundary, -}: { - channel: ScalarAnimationChannel | undefined; - splitTime: number; - leftBoundaryId: string; - rightBoundaryId: string; - shouldIncludeSplitBoundary: boolean; -}) { - if (!channel || channel.keys.length === 0) { - return { - leftChannel: undefined, - rightChannel: undefined, - }; - } - - const normalizedChannel = normalizeChannel({ channel }); - let leftKeys = normalizedChannel.keys.filter((key) => key.time <= splitTime); - let rightKeys = normalizedChannel.keys - .filter((key) => key.time >= splitTime) - .map((key) => ({ ...key, time: key.time - splitTime })); - - const hasBoundaryOnLeft = leftKeys.some((key) => - isNearlySameTime({ leftTime: key.time, rightTime: splitTime }), - ); - const hasBoundaryOnRight = rightKeys.some((key) => - isNearlySameTime({ leftTime: key.time, rightTime: 0 }), - ); - if (!shouldIncludeSplitBoundary || (hasBoundaryOnLeft && hasBoundaryOnRight)) { - return { - leftChannel: leftKeys.length - ? normalizeChannel({ - channel: { - kind: "scalar", - keys: leftKeys, - extrapolation: normalizedChannel.extrapolation, - }, - }) - : undefined, - rightChannel: rightKeys.length - ? normalizeChannel({ - channel: { - kind: "scalar", - keys: rightKeys, - extrapolation: normalizedChannel.extrapolation, - }, - }) - : undefined, - }; - } - - for (let keyIndex = 0; keyIndex < normalizedChannel.keys.length - 1; keyIndex++) { - const leftKey = normalizedChannel.keys[keyIndex]; - const rightKey = normalizedChannel.keys[keyIndex + 1]; - if ( - !( - splitTime > leftKey.time && - splitTime < rightKey.time - ) - ) { - continue; - } - - const boundaryValue = getChannelValueAtTime({ - channel: normalizedChannel, - time: splitTime, - fallbackValue: leftKey.value, - }) as number; - - if (leftKey.segmentToNext === "bezier") { - const rightHandle = - leftKey.rightHandle ?? getDefaultRightHandle({ leftKey, rightKey }); - const leftHandle = - rightKey.leftHandle ?? getDefaultLeftHandle({ leftKey, rightKey }); - const progress = solveBezierProgressForTime({ - time: splitTime, - leftKey, - rightKey, - }); - const p0 = { x: leftKey.time, y: leftKey.value }; - const p1 = { - x: leftKey.time + rightHandle.dt, - y: leftKey.value + rightHandle.dv, - }; - const p2 = { - x: rightKey.time + leftHandle.dt, - y: rightKey.value + leftHandle.dv, - }; - const p3 = { x: rightKey.time, y: rightKey.value }; - const q0 = lerpPoint({ left: p0, right: p1, progress }); - const q1 = lerpPoint({ left: p1, right: p2, progress }); - const q2 = lerpPoint({ left: p2, right: p3, progress }); - const r0 = lerpPoint({ left: q0, right: q1, progress }); - const r1 = lerpPoint({ left: q1, right: q2, progress }); - const splitPoint = lerpPoint({ left: r0, right: r1, progress }); - leftKeys = [ - ...normalizedChannel.keys.filter((key) => key.time < splitTime), - { - ...leftKey, - rightHandle: { - dt: q0.x - p0.x, - dv: q0.y - p0.y, - }, - }, - { - id: leftBoundaryId, - time: splitTime, - value: boundaryValue, - leftHandle: { - dt: r0.x - splitPoint.x, - dv: r0.y - splitPoint.y, - }, - segmentToNext: leftKey.segmentToNext, - tangentMode: leftKey.tangentMode, - }, - ]; - rightKeys = [ - { - id: rightBoundaryId, - time: 0, - value: boundaryValue, - rightHandle: { - dt: r1.x - splitPoint.x, - dv: r1.y - splitPoint.y, - }, - segmentToNext: "bezier", - tangentMode: leftKey.tangentMode, - }, - { - ...rightKey, - time: rightKey.time - splitTime, - leftHandle: { - dt: q2.x - p3.x, - dv: q2.y - p3.y, - }, - }, - ...normalizedChannel.keys - .filter((key) => key.time > rightKey.time) - .map((key) => ({ - ...key, - time: key.time - splitTime, - })), - ]; - } else { - leftKeys = [ - ...leftKeys, - createScalarKey({ - id: leftBoundaryId, - time: splitTime, - value: boundaryValue, - interpolation: "linear", - }), - ]; - rightKeys = [ - createScalarKey({ - id: rightBoundaryId, - time: 0, - value: boundaryValue, - interpolation: getScalarSegmentInterpolation({ - segment: leftKey.segmentToNext, - }), - }), - ...rightKeys, - ]; - } - - return { - leftChannel: normalizeChannel({ - channel: { - kind: "scalar", - keys: leftKeys, - extrapolation: normalizedChannel.extrapolation, - }, - }), - rightChannel: normalizeChannel({ - channel: { - kind: "scalar", - keys: rightKeys, - extrapolation: normalizedChannel.extrapolation, - }, - }), - }; - } - - return { - leftChannel: leftKeys.length - ? normalizeChannel({ - channel: { - kind: "scalar", - keys: leftKeys, - extrapolation: normalizedChannel.extrapolation, - }, - }) - : undefined, - rightChannel: rightKeys.length - ? normalizeChannel({ - channel: { - kind: "scalar", - keys: rightKeys, - extrapolation: normalizedChannel.extrapolation, - }, - }) - : undefined, - }; -} - -export function splitAnimationsAtTime({ - animations, - splitTime, - shouldIncludeSplitBoundary = true, -}: { - animations: ElementAnimations | undefined; - splitTime: number; - shouldIncludeSplitBoundary?: boolean; -}): { - leftAnimations: ElementAnimations | undefined; - rightAnimations: ElementAnimations | undefined; -} { - if (!animations) { - return { leftAnimations: undefined, rightAnimations: undefined }; - } - - const leftAnimations = cloneAnimationsState({ animations: undefined }); - const rightAnimations = cloneAnimationsState({ animations: undefined }); - - for (const [propertyPath, binding] of Object.entries(animations.bindings)) { - if (!binding) { - continue; - } - - const leftBinding = cloneAnimationBinding({ binding }); - const rightBinding = cloneAnimationBinding({ binding }); - const leftBoundaryId = generateUUID(); - const rightBoundaryId = generateUUID(); - let hasLeftKeys = false; - let hasRightKeys = false; - - for (const component of binding.components) { - const channel = getChannelById({ - animations, - channelId: component.channelId, - }); - const splitResult = - channel?.kind === "discrete" - ? splitDiscreteChannelAtTime({ - channel, - splitTime, - leftBoundaryId, - rightBoundaryId, - shouldIncludeSplitBoundary, - }) - : splitScalarChannelAtTime({ - channel: channel as ScalarAnimationChannel | undefined, - splitTime, - leftBoundaryId, - rightBoundaryId, - shouldIncludeSplitBoundary, - }); - if (splitResult.leftChannel) { - leftAnimations.channels[component.channelId] = splitResult.leftChannel; - hasLeftKeys = true; - } - if (splitResult.rightChannel) { - rightAnimations.channels[component.channelId] = splitResult.rightChannel; - hasRightKeys = true; - } - } - - if (hasLeftKeys) { - leftAnimations.bindings[propertyPath] = leftBinding; - } - if (hasRightKeys) { - rightAnimations.bindings[propertyPath] = rightBinding; - } - } - - return { - leftAnimations: toAnimation({ animations: leftAnimations }), - rightAnimations: toAnimation({ animations: rightAnimations }), - }; -} - -export function removeElementKeyframe({ - animations, - propertyPath, - keyframeId, -}: { - animations: ElementAnimations | undefined; - propertyPath: AnimationPath; - keyframeId: string; -}): ElementAnimations | undefined { - const binding = getBinding({ animations, propertyPath }); - if (!binding) { - return animations; - } - - const nextAnimations = cloneAnimationsState({ animations }); - for (const component of binding.components) { - nextAnimations.channels[component.channelId] = removeKeyframe({ - channel: nextAnimations.channels[component.channelId], - keyframeId, - }); - } - const hasRemainingKeys = binding.components.some((component) => - hasChannelKeys({ - channel: nextAnimations.channels[component.channelId], - }), - ); - if (!hasRemainingKeys) { - delete nextAnimations.bindings[propertyPath]; - for (const component of binding.components) { - delete nextAnimations.channels[component.channelId]; - } - } - return toAnimation({ - animations: nextAnimations, - }); -} - -export function retimeElementKeyframe({ - animations, - propertyPath, - keyframeId, - time, -}: { - animations: ElementAnimations | undefined; - propertyPath: AnimationPath; - keyframeId: string; - time: number; -}): ElementAnimations | undefined { - const binding = getBinding({ animations, propertyPath }); - if (!binding) { - return animations; - } - - const nextAnimations = cloneAnimationsState({ animations }); - for (const component of binding.components) { - nextAnimations.channels[component.channelId] = retimeKeyframe({ - channel: nextAnimations.channels[component.channelId], - keyframeId, - time, - }); - } - return toAnimation({ - animations: nextAnimations, - }); -} |
