295 lines
5.9 KiB
TypeScript
295 lines
5.9 KiB
TypeScript
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 "./path";
|
|
|
|
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,
|
|
});
|
|
}
|