Skip to content

Commit d8fac77

Browse files
committed
add callback store and integration points to read/write it
1 parent 3775e2d commit d8fac77

File tree

2 files changed

+80
-28
lines changed

2 files changed

+80
-28
lines changed

internal/animate/state.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface InternalKeyframe {
1616
timestamp: number;
1717
timingFunction: TimingFunc;
1818
initialPoints: Point[];
19+
transitionSourceFrameIndex: number | null;
1920
}
2021

2122
export interface RenderCache {
@@ -26,7 +27,7 @@ export interface RenderCache {
2627
}
2728

2829
export interface RenderInput {
29-
keyframes: InternalKeyframe[];
30+
currentFrames: InternalKeyframe[];
3031
timestamp: number;
3132
cache: RenderCache;
3233
}
@@ -42,7 +43,7 @@ export interface TransitionInput extends RenderInput {
4243
}
4344

4445
export interface TransitionOutput {
45-
frames: InternalKeyframe[];
46+
newFrames: InternalKeyframe[];
4647
cache: RenderCache;
4748
}
4849

@@ -61,25 +62,25 @@ export const removeStaleFrames = (
6162
};
6263

6364
export const renderFramesAt = (input: RenderInput): RenderOutput => {
64-
const {cache, keyframes} = input;
65+
const {cache, currentFrames} = input;
6566

66-
if (keyframes.length === 0) {
67+
if (currentFrames.length === 0) {
6768
return {cache, lastFrameId: null, points: []};
6869
}
6970

7071
// Animation freezes at the final shape if there are no more keyframes.
71-
if (keyframes.length === 1) {
72-
const first = keyframes[0];
72+
if (currentFrames.length === 1) {
73+
const first = currentFrames[0];
7374
return {cache, lastFrameId: first.id, points: first.initialPoints};
7475
}
7576

7677
// Find the start/end keyframes according to the timestamp.
77-
let startKeyframe = keyframes[0];
78-
let endKeyframe = keyframes[1];
79-
for (let i = 2; i < keyframes.length; i++) {
78+
let startKeyframe = currentFrames[0];
79+
let endKeyframe = currentFrames[1];
80+
for (let i = 2; i < currentFrames.length; i++) {
8081
if (endKeyframe.timestamp < input.timestamp) break;
81-
startKeyframe = keyframes[i - 1];
82-
endKeyframe = keyframes[i];
82+
startKeyframe = currentFrames[i - 1];
83+
endKeyframe = currentFrames[i];
8384
}
8485

8586
// Use and cache prepared points for current interpolation.
@@ -108,15 +109,22 @@ export const renderFramesAt = (input: RenderInput): RenderOutput => {
108109
cache,
109110
lastFrameId: startKeyframe.id,
110111
points: interpolateBetween(adjustedProgress, preparedStartPoints, preparedEndPoints),
111-
}
112+
};
112113
};
113114

115+
// TODO render cache cleaner.
116+
114117
// TODO generate internal frames. Delayed frames can just copy the previous one.
115118
// TODO store current blob when interrupts happen to use as source.
116119
// TODO don't remove any frames.
117120
export const transitionFrames = (input: TransitionInput): TransitionOutput => {
118121
const {cache, timestamp, newFrames} = input;
119122

123+
// Wipe animation when given no keyframes.
124+
if (input.newFrames.length === 0) {
125+
return {cache: input.cache, newFrames: []};
126+
}
127+
120128
// Add current state as initial frame.
121129
const currentState = renderFramesAt(input);
122130
let internalFrames: InternalKeyframe[] = [
@@ -125,6 +133,7 @@ export const transitionFrames = (input: TransitionInput): TransitionOutput => {
125133
initialPoints: currentState.points,
126134
timestamp: timestamp,
127135
timingFunction: (p) => p,
136+
transitionSourceFrameIndex: null,
128137
},
129138
];
130139

@@ -135,5 +144,5 @@ export const transitionFrames = (input: TransitionInput): TransitionOutput => {
135144
}
136145
}
137146

138-
return {cache, frames: internalFrames};
147+
return {cache, newFrames: internalFrames};
139148
};

public/animate.ts

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ import {
99
transitionFrames,
1010
Keyframe,
1111
removeStaleFrames,
12+
RenderCache,
1213
} from "../internal/animate/state";
1314

1415
// TODO copy keyframes as soon as possible to make sure they aren't modified afterwards.
1516
// TODO make sure callbacks don't fill up the stack.
1617
// TODO defend against "bad" keyframes like negative timing.
1718
// TODO keyframe callbacks
1819

19-
export interface CanvasKeyframe extends Keyframe {
20+
interface CallbackKeyframe extends Keyframe {
21+
callback?: () => void;
22+
}
23+
24+
export interface CanvasKeyframe extends CallbackKeyframe {
2025
canvasOptions?: CanvasOptions;
2126
}
2227

@@ -27,32 +32,70 @@ export interface CanvasAnimation {
2732
transition(...keyframes: CanvasKeyframe[]): void;
2833
}
2934

35+
interface CallbackStore {
36+
[frameId: string]: () => void;
37+
}
38+
39+
const removeExpiredFrameCallbacks = (
40+
oldStore: CallbackStore,
41+
frames: InternalKeyframe[],
42+
): CallbackStore => {
43+
const newStore: CallbackStore = {};
44+
for (const frame of frames) {
45+
newStore[frame.id] = oldStore[frame.id];
46+
}
47+
return newStore;
48+
};
49+
3050
export const canvasPath = (): CanvasAnimation => {
3151
let internalFrames: InternalKeyframe[] = [];
32-
33-
const genBlob = (keyframe: CanvasKeyframe): Point[] =>
34-
mapPoints(genFromOptions(keyframe.blobOptions), ({curr}) => {
35-
curr.x += keyframe?.canvasOptions?.offsetX || 0;
36-
curr.y += keyframe?.canvasOptions?.offsetY || 0;
37-
return curr;
38-
});
52+
let renderCache: RenderCache = {};
53+
let callbackStore: CallbackStore = {};
3954

4055
const renderFrame: CanvasAnimation["renderFrame"] = () => {
4156
const renderTime = Date.now();
4257
internalFrames = removeStaleFrames(internalFrames, renderTime);
43-
return renderPath2D(renderFramesAt(internalFrames, renderTime));
58+
const renderOutput = renderFramesAt({
59+
cache: renderCache,
60+
timestamp: renderTime,
61+
currentFrames: internalFrames,
62+
});
63+
renderCache = renderOutput.cache;
64+
if (renderOutput.lastFrameId && callbackStore[renderOutput.lastFrameId]) {
65+
callbackStore[renderOutput.lastFrameId]();
66+
delete callbackStore[renderOutput.lastFrameId];
67+
}
68+
return renderPath2D(renderOutput.points);
4469
};
4570

71+
const genBlob = (keyframe: CanvasKeyframe): Point[] => {
72+
return mapPoints(genFromOptions(keyframe.blobOptions), ({curr}) => {
73+
curr.x += keyframe?.canvasOptions?.offsetX || 0;
74+
curr.y += keyframe?.canvasOptions?.offsetY || 0;
75+
return curr;
76+
});
77+
}
78+
4679
const transition: CanvasAnimation["transition"] = (...keyframes) => {
4780
const transitionTime = Date.now();
81+
const transitionOutput = transitionFrames({
82+
cache: renderCache,
83+
timestamp: transitionTime,
84+
currentFrames: internalFrames,
85+
newFrames: keyframes,
86+
});
87+
renderCache = transitionOutput.cache;
88+
internalFrames = transitionOutput.newFrames;
4889

49-
// Immediately wipe animation when given no keyframes.
50-
if (keyframes.length === 0) {
51-
internalFrames = [];
52-
return;
53-
}
90+
// Remove callbacks that are no longer associated with a known frame.
91+
callbackStore = removeExpiredFrameCallbacks(callbackStore, internalFrames);
5492

55-
internalFrames = transitionFrames(internalFrames, keyframes, transitionTime);
93+
// Populate the callback using returned frame ids.
94+
for (const newFrame of internalFrames) {
95+
if (newFrame.transitionSourceFrameIndex === null) continue;
96+
const {callback} = keyframes[newFrame.transitionSourceFrameIndex];
97+
if (callback) callbackStore[newFrame.id] = callback;
98+
}
5699
};
57100

58101
return {renderFrame, transition};

0 commit comments

Comments
 (0)