Skip to content

Commit 338e2c2

Browse files
committed
add checks for all provided options + move stateful keyframe callback logic to internal
1 parent 40c0b98 commit 338e2c2

File tree

8 files changed

+293
-236
lines changed

8 files changed

+293
-236
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 2.1.0
2+
3+
- Add animation API.
4+
- More type checks.
5+
16
# 2.0.1
27

38
- Fix typo in code example of README

index.animated.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
66
<title>blobs</title>
77
<script src="./v2/animate/index.js"></script>
8+
<!-- TODO preview -->
89
</head>
910
<body>
1011
<style>

internal/animate/frames.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import {TimingFunc, timingFunctions} from "./timing";
2+
import {Point} from "../types";
3+
import {prepare} from "./prepare";
4+
import {interpolateBetween} from "./interpolate";
5+
6+
export interface Keyframe {
7+
delay?: number;
8+
duration: number;
9+
timingFunction?: keyof typeof timingFunctions;
10+
}
11+
12+
export interface InternalKeyframe {
13+
id: string;
14+
timestamp: number;
15+
timingFunction: TimingFunc;
16+
initialPoints: Point[];
17+
transitionSourceFrameIndex: number;
18+
isSynthetic: boolean;
19+
}
20+
21+
export interface RenderCache {
22+
[frameId: string]: {
23+
preparedEndPoints?: Point[];
24+
preparedStartPoints?: Point[];
25+
};
26+
}
27+
28+
export interface RenderInput {
29+
currentFrames: InternalKeyframe[];
30+
timestamp: number;
31+
renderCache: RenderCache;
32+
}
33+
34+
export interface RenderOutput {
35+
points: Point[];
36+
lastFrameId: string | null;
37+
renderCache: RenderCache;
38+
}
39+
40+
export interface TransitionInput<T extends Keyframe> extends RenderInput {
41+
newFrames: T[];
42+
shapeGenerator: (keyframe: T) => Point[];
43+
}
44+
45+
export interface TransitionOutput {
46+
newFrames: InternalKeyframe[];
47+
}
48+
49+
const genId = (): string => {
50+
return String(Math.random()).substr(2);
51+
};
52+
53+
export const renderFramesAt = (input: RenderInput): RenderOutput => {
54+
const {renderCache, currentFrames} = input;
55+
56+
if (currentFrames.length === 0) {
57+
return {renderCache, lastFrameId: null, points: []};
58+
}
59+
60+
// Animation freezes at the final shape if there are no more keyframes.
61+
if (currentFrames.length === 1) {
62+
const first = currentFrames[0];
63+
return {renderCache, lastFrameId: first.id, points: first.initialPoints};
64+
}
65+
66+
// Find the start/end keyframes according to the timestamp.
67+
let startKeyframe = currentFrames[0];
68+
let endKeyframe = currentFrames[1];
69+
for (let i = 2; i < currentFrames.length; i++) {
70+
if (endKeyframe.timestamp > input.timestamp) break;
71+
startKeyframe = currentFrames[i - 1];
72+
endKeyframe = currentFrames[i];
73+
}
74+
75+
// Use and cache prepared points for current interpolation.
76+
let preparedStartPoints: Point[] | undefined =
77+
renderCache[startKeyframe.id]?.preparedStartPoints;
78+
let preparedEndPoints: Point[] | undefined = renderCache[endKeyframe.id]?.preparedEndPoints;
79+
if (!preparedStartPoints || !preparedEndPoints) {
80+
[preparedStartPoints, preparedEndPoints] = prepare(
81+
startKeyframe.initialPoints,
82+
endKeyframe.initialPoints,
83+
{rawAngles: false, divideRatio: 1},
84+
);
85+
86+
renderCache[startKeyframe.id] = renderCache[startKeyframe.id] || {};
87+
renderCache[startKeyframe.id].preparedStartPoints = preparedStartPoints;
88+
89+
renderCache[endKeyframe.id] = renderCache[endKeyframe.id] || {};
90+
renderCache[endKeyframe.id].preparedEndPoints = preparedEndPoints;
91+
}
92+
93+
// Calculate progress between frames as a fraction.
94+
const progress =
95+
(input.timestamp - startKeyframe.timestamp) /
96+
(endKeyframe.timestamp - startKeyframe.timestamp);
97+
98+
// Keep progress withing expected range (ex. division by 0).
99+
const clampedProgress = Math.max(0, Math.min(1, progress));
100+
101+
// Apply timing function of end frame.
102+
const adjustedProgress = endKeyframe.timingFunction(clampedProgress);
103+
104+
return {
105+
renderCache,
106+
lastFrameId: clampedProgress === 1 ? endKeyframe.id : startKeyframe.id,
107+
points: interpolateBetween(adjustedProgress, preparedStartPoints, preparedEndPoints),
108+
};
109+
};
110+
111+
export const transitionFrames = <T extends Keyframe>(
112+
input: TransitionInput<T>,
113+
): TransitionOutput => {
114+
// Erase all old frames.
115+
const newInternalFrames: InternalKeyframe[] = [];
116+
117+
// Reset animation when given no keyframes.
118+
if (input.newFrames.length === 0) {
119+
return {newFrames: newInternalFrames};
120+
}
121+
122+
// Add current state as initial frame.
123+
const currentState = renderFramesAt(input);
124+
if (currentState.lastFrameId === null) {
125+
// If there is currently no shape being rendered, use a point in the
126+
// center of the next frame as the initial point.
127+
const firstShape = input.shapeGenerator(input.newFrames[0]);
128+
let firstShapeCenterPoint: Point = {
129+
x: 0,
130+
y: 0,
131+
handleIn: {angle: 0, length: 0},
132+
handleOut: {angle: 0, length: 0},
133+
};
134+
for (const point of firstShape) {
135+
firstShapeCenterPoint.x += point.x / firstShape.length;
136+
firstShapeCenterPoint.y += point.y / firstShape.length;
137+
}
138+
currentState.points = [firstShapeCenterPoint, firstShapeCenterPoint, firstShapeCenterPoint];
139+
}
140+
newInternalFrames.push({
141+
id: genId(),
142+
initialPoints: currentState.points,
143+
timestamp: input.timestamp,
144+
timingFunction: timingFunctions.linear,
145+
transitionSourceFrameIndex: -1,
146+
isSynthetic: true,
147+
});
148+
149+
// Generate and add new frames.
150+
let totalOffset = 0;
151+
for (let i = 0; i < input.newFrames.length; i++) {
152+
const keyframe = input.newFrames[i];
153+
154+
// Copy previous frame when current one has a delay.
155+
if (keyframe.delay) {
156+
totalOffset += keyframe.delay;
157+
const prevFrame = newInternalFrames[newInternalFrames.length - 1];
158+
newInternalFrames.push({
159+
id: genId(),
160+
initialPoints: prevFrame.initialPoints,
161+
timestamp: input.timestamp + totalOffset,
162+
timingFunction: timingFunctions.linear,
163+
transitionSourceFrameIndex: i - 1,
164+
isSynthetic: true,
165+
});
166+
}
167+
168+
totalOffset += keyframe.duration;
169+
newInternalFrames.push({
170+
id: genId(),
171+
initialPoints: input.shapeGenerator(keyframe),
172+
timestamp: input.timestamp + totalOffset,
173+
timingFunction: timingFunctions[keyframe.timingFunction || "linear"],
174+
transitionSourceFrameIndex: i,
175+
isSynthetic: false,
176+
});
177+
}
178+
179+
return {newFrames: newInternalFrames};
180+
};

0 commit comments

Comments
 (0)