mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: restructure files to their domains, new preview overlay system, and dep graph
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
import type {
|
||||
AnimationBindingInstance,
|
||||
AnimationChannel,
|
||||
AnimationPath,
|
||||
ElementAnimations,
|
||||
ElementKeyframe,
|
||||
} from "@/animation/types";
|
||||
import {
|
||||
type AnimationComponentValue,
|
||||
composeAnimationValue,
|
||||
} from "./binding-values";
|
||||
import {
|
||||
getChannelValueAtTime,
|
||||
getScalarSegmentInterpolation,
|
||||
} from "./interpolation";
|
||||
import { isAnimationPath } from "./target-resolver";
|
||||
|
||||
function getBindingFallbackValue({
|
||||
channel,
|
||||
}: {
|
||||
channel: ElementAnimations["channels"][string];
|
||||
}) {
|
||||
if (!channel || channel.keys.length === 0) {
|
||||
return channel?.kind === "discrete" ? false : 0;
|
||||
}
|
||||
|
||||
return channel.keys[0].value;
|
||||
}
|
||||
|
||||
interface BindingKeyframeMatch {
|
||||
componentIndex: number;
|
||||
channel: AnimationChannel;
|
||||
keyframe: AnimationChannel["keys"][number];
|
||||
}
|
||||
|
||||
function getBindingKeyframeMatches({
|
||||
animations,
|
||||
binding,
|
||||
}: {
|
||||
animations: ElementAnimations;
|
||||
binding: AnimationBindingInstance;
|
||||
}): BindingKeyframeMatch[] {
|
||||
return binding.components.flatMap((component, componentIndex) => {
|
||||
const channel = animations.channels[component.channelId];
|
||||
if (!channel || channel.keys.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return channel.keys.map((keyframe) => ({
|
||||
componentIndex,
|
||||
channel,
|
||||
keyframe,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function getUniqueBindingKeyframeMatches({
|
||||
animations,
|
||||
binding,
|
||||
}: {
|
||||
animations: ElementAnimations;
|
||||
binding: AnimationBindingInstance;
|
||||
}): BindingKeyframeMatch[] {
|
||||
const sortedMatches = getBindingKeyframeMatches({
|
||||
animations,
|
||||
binding,
|
||||
}).sort(
|
||||
(leftMatch, rightMatch) =>
|
||||
leftMatch.keyframe.time - rightMatch.keyframe.time ||
|
||||
leftMatch.componentIndex - rightMatch.componentIndex,
|
||||
);
|
||||
const uniqueMatches: BindingKeyframeMatch[] = [];
|
||||
|
||||
for (const match of sortedMatches) {
|
||||
const previousMatch = uniqueMatches[uniqueMatches.length - 1];
|
||||
if (
|
||||
!previousMatch ||
|
||||
previousMatch.keyframe.time !== match.keyframe.time
|
||||
) {
|
||||
uniqueMatches.push(match);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
previousMatch.componentIndex !== 0 &&
|
||||
match.componentIndex === 0
|
||||
) {
|
||||
uniqueMatches[uniqueMatches.length - 1] = match;
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueMatches;
|
||||
}
|
||||
|
||||
function getPreferredBindingKeyframeMatch({
|
||||
matches,
|
||||
}: {
|
||||
matches: BindingKeyframeMatch[];
|
||||
}): BindingKeyframeMatch | null {
|
||||
return (
|
||||
matches.find((match) => match.componentIndex === 0) ??
|
||||
matches[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function getComposedBindingValueAtTime({
|
||||
animations,
|
||||
binding,
|
||||
time,
|
||||
}: {
|
||||
animations: ElementAnimations;
|
||||
binding: AnimationBindingInstance;
|
||||
time: number;
|
||||
}) {
|
||||
const componentValues = Object.fromEntries(
|
||||
binding.components.map((component) => {
|
||||
const channel = animations.channels[component.channelId];
|
||||
return [
|
||||
component.key,
|
||||
getChannelValueAtTime({
|
||||
channel,
|
||||
time,
|
||||
fallbackValue: getBindingFallbackValue({ channel }),
|
||||
}),
|
||||
];
|
||||
}),
|
||||
) as Record<string, AnimationComponentValue | undefined>;
|
||||
|
||||
return composeAnimationValue({
|
||||
binding,
|
||||
componentValues,
|
||||
});
|
||||
}
|
||||
|
||||
function getKeyframeInterpolation({
|
||||
channel,
|
||||
keyframe,
|
||||
}: {
|
||||
channel: AnimationChannel;
|
||||
keyframe: AnimationChannel["keys"][number];
|
||||
}) {
|
||||
return channel.kind === "scalar" && "segmentToNext" in keyframe
|
||||
? getScalarSegmentInterpolation({ segment: keyframe.segmentToNext })
|
||||
: "hold";
|
||||
}
|
||||
|
||||
function toElementKeyframe({
|
||||
animations,
|
||||
binding,
|
||||
propertyPath,
|
||||
keyframeMatch,
|
||||
}: {
|
||||
animations: ElementAnimations;
|
||||
binding: AnimationBindingInstance;
|
||||
propertyPath: AnimationPath;
|
||||
keyframeMatch: BindingKeyframeMatch;
|
||||
}): ElementKeyframe | null {
|
||||
const value = getComposedBindingValueAtTime({
|
||||
animations,
|
||||
binding,
|
||||
time: keyframeMatch.keyframe.time,
|
||||
});
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
propertyPath,
|
||||
id: keyframeMatch.keyframe.id,
|
||||
time: keyframeMatch.keyframe.time,
|
||||
value,
|
||||
interpolation: getKeyframeInterpolation({
|
||||
channel: keyframeMatch.channel,
|
||||
keyframe: keyframeMatch.keyframe,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function getElementKeyframes({
|
||||
animations,
|
||||
}: {
|
||||
animations: ElementAnimations | undefined;
|
||||
}): ElementKeyframe[] {
|
||||
if (!animations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(animations.bindings).flatMap(
|
||||
([propertyPath, binding]) => {
|
||||
if (!binding || !isAnimationPath(propertyPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getUniqueBindingKeyframeMatches({
|
||||
animations,
|
||||
binding,
|
||||
}).flatMap((keyframeMatch) => {
|
||||
const keyframe = toElementKeyframe({
|
||||
animations,
|
||||
binding,
|
||||
propertyPath,
|
||||
keyframeMatch,
|
||||
});
|
||||
if (!keyframe) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [keyframe];
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function hasKeyframesForPath({
|
||||
animations,
|
||||
propertyPath,
|
||||
}: {
|
||||
animations: ElementAnimations | undefined;
|
||||
propertyPath: AnimationPath;
|
||||
}): boolean {
|
||||
const binding = animations?.bindings[propertyPath];
|
||||
if (!binding) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return binding.components.some((component) =>
|
||||
Boolean(animations?.channels[component.channelId]?.keys.length),
|
||||
);
|
||||
}
|
||||
|
||||
export function getKeyframeAtTime({
|
||||
animations,
|
||||
propertyPath,
|
||||
time,
|
||||
}: {
|
||||
animations: ElementAnimations | undefined;
|
||||
propertyPath: AnimationPath;
|
||||
time: number;
|
||||
}): ElementKeyframe | null {
|
||||
const binding = animations?.bindings[propertyPath];
|
||||
if (!binding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyframeMatch = getPreferredBindingKeyframeMatch({
|
||||
matches: getBindingKeyframeMatches({
|
||||
animations,
|
||||
binding,
|
||||
}).filter(({ keyframe }) => keyframe.time === time),
|
||||
});
|
||||
if (!keyframeMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toElementKeyframe({
|
||||
animations,
|
||||
binding,
|
||||
propertyPath,
|
||||
keyframeMatch,
|
||||
});
|
||||
}
|
||||
|
||||
export function getKeyframeById({
|
||||
animations,
|
||||
propertyPath,
|
||||
keyframeId,
|
||||
}: {
|
||||
animations: ElementAnimations | undefined;
|
||||
propertyPath: AnimationPath;
|
||||
keyframeId: string;
|
||||
}): ElementKeyframe | null {
|
||||
const binding = animations?.bindings[propertyPath];
|
||||
if (!binding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyframeMatch = getPreferredBindingKeyframeMatch({
|
||||
matches: getBindingKeyframeMatches({
|
||||
animations,
|
||||
binding,
|
||||
}).filter(({ keyframe }) => keyframe.id === keyframeId),
|
||||
});
|
||||
if (!keyframeMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toElementKeyframe({
|
||||
animations,
|
||||
binding,
|
||||
propertyPath,
|
||||
keyframeMatch,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user