framer-motion
Version:
A simple and powerful JavaScript animation library
289 lines (278 loc) • 9.26 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var motionUtils = require('motion-utils');
const stepsOrder = [
"read", // Read
"resolveKeyframes", // Write/Read/Write/Read
"update", // Compute
"preRender", // Compute
"render", // Write
"postRender", // Compute
];
const statsBuffer = {
value: null,
addProjectionMetrics: null,
};
function createRenderStep(runNextFrame, stepName) {
/**
* We create and reuse two queues, one to queue jobs for the current frame
* and one for the next. We reuse to avoid triggering GC after x frames.
*/
let thisFrame = new Set();
let nextFrame = new Set();
/**
* Track whether we're currently processing jobs in this step. This way
* we can decide whether to schedule new jobs for this frame or next.
*/
let isProcessing = false;
let flushNextFrame = false;
/**
* A set of processes which were marked keepAlive when scheduled.
*/
const toKeepAlive = new WeakSet();
let latestFrameData = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
};
let numCalls = 0;
function triggerCallback(callback) {
if (toKeepAlive.has(callback)) {
step.schedule(callback);
runNextFrame();
}
numCalls++;
callback(latestFrameData);
}
const step = {
/**
* Schedule a process to run on the next frame.
*/
schedule: (callback, keepAlive = false, immediate = false) => {
const addToCurrentFrame = immediate && isProcessing;
const queue = addToCurrentFrame ? thisFrame : nextFrame;
if (keepAlive)
toKeepAlive.add(callback);
if (!queue.has(callback))
queue.add(callback);
return callback;
},
/**
* Cancel the provided callback from running on the next frame.
*/
cancel: (callback) => {
nextFrame.delete(callback);
toKeepAlive.delete(callback);
},
/**
* Execute all schedule callbacks.
*/
process: (frameData) => {
latestFrameData = frameData;
/**
* If we're already processing we've probably been triggered by a flushSync
* inside an existing process. Instead of executing, mark flushNextFrame
* as true and ensure we flush the following frame at the end of this one.
*/
if (isProcessing) {
flushNextFrame = true;
return;
}
isProcessing = true;
[thisFrame, nextFrame] = [nextFrame, thisFrame];
// Execute this frame
thisFrame.forEach(triggerCallback);
/**
* If we're recording stats then
*/
if (stepName && statsBuffer.value) {
statsBuffer.value.frameloop[stepName].push(numCalls);
}
numCalls = 0;
// Clear the frame so no callbacks remain. This is to avoid
// memory leaks should this render step not run for a while.
thisFrame.clear();
isProcessing = false;
if (flushNextFrame) {
flushNextFrame = false;
step.process(frameData);
}
},
};
return step;
}
const maxElapsed = 40;
function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
let runNextFrame = false;
let useDefaultElapsed = true;
const state = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
};
const flagRunNextFrame = () => (runNextFrame = true);
const steps = stepsOrder.reduce((acc, key) => {
acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
return acc;
}, {});
const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
const processBatch = () => {
const timestamp = performance.now();
runNextFrame = false;
{
state.delta = useDefaultElapsed
? 1000 / 60
: Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
}
state.timestamp = timestamp;
state.isProcessing = true;
// Unrolled render loop for better per-frame performance
read.process(state);
resolveKeyframes.process(state);
update.process(state);
preRender.process(state);
render.process(state);
postRender.process(state);
state.isProcessing = false;
if (runNextFrame && allowKeepAlive) {
useDefaultElapsed = false;
scheduleNextBatch(processBatch);
}
};
const wake = () => {
runNextFrame = true;
useDefaultElapsed = true;
if (!state.isProcessing) {
scheduleNextBatch(processBatch);
}
};
const schedule = stepsOrder.reduce((acc, key) => {
const step = steps[key];
acc[key] = (process, keepAlive = false, immediate = false) => {
if (!runNextFrame)
wake();
return step.schedule(process, keepAlive, immediate);
};
return acc;
}, {});
const cancel = (process) => {
for (let i = 0; i < stepsOrder.length; i++) {
steps[stepsOrder[i]].cancel(process);
}
};
return { schedule, cancel, state, steps };
}
const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : motionUtils.noop, true);
const activeAnimations = {
layout: 0,
mainThread: 0,
waapi: 0,
};
function record() {
const { value } = statsBuffer;
if (value === null) {
cancelFrame(record);
return;
}
value.frameloop.rate.push(frameData.delta);
value.animations.mainThread.push(activeAnimations.mainThread);
value.animations.waapi.push(activeAnimations.waapi);
value.animations.layout.push(activeAnimations.layout);
}
function mean(values) {
return values.reduce((acc, value) => acc + value, 0) / values.length;
}
function summarise(values, calcAverage = mean) {
if (values.length === 0) {
return {
min: 0,
max: 0,
avg: 0,
};
}
return {
min: Math.min(...values),
max: Math.max(...values),
avg: calcAverage(values),
};
}
const msToFps = (ms) => Math.round(1000 / ms);
function clearStatsBuffer() {
statsBuffer.value = null;
statsBuffer.addProjectionMetrics = null;
}
function reportStats() {
const { value } = statsBuffer;
if (!value) {
throw new Error("Stats are not being measured");
}
clearStatsBuffer();
cancelFrame(record);
const summary = {
frameloop: {
rate: summarise(value.frameloop.rate),
read: summarise(value.frameloop.read),
resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
update: summarise(value.frameloop.update),
preRender: summarise(value.frameloop.preRender),
render: summarise(value.frameloop.render),
postRender: summarise(value.frameloop.postRender),
},
animations: {
mainThread: summarise(value.animations.mainThread),
waapi: summarise(value.animations.waapi),
layout: summarise(value.animations.layout),
},
layoutProjection: {
nodes: summarise(value.layoutProjection.nodes),
calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
},
};
/**
* Convert the rate to FPS
*/
const { rate } = summary.frameloop;
rate.min = msToFps(rate.min);
rate.max = msToFps(rate.max);
rate.avg = msToFps(rate.avg);
[rate.min, rate.max] = [rate.max, rate.min];
return summary;
}
function recordStats() {
if (statsBuffer.value) {
clearStatsBuffer();
throw new Error("Stats are already being measured");
}
const newStatsBuffer = statsBuffer;
newStatsBuffer.value = {
frameloop: {
rate: [],
read: [],
resolveKeyframes: [],
update: [],
preRender: [],
render: [],
postRender: [],
},
animations: {
mainThread: [],
waapi: [],
layout: [],
},
layoutProjection: {
nodes: [],
calculatedTargetDeltas: [],
calculatedProjections: [],
},
};
newStatsBuffer.addProjectionMetrics = (metrics) => {
const { layoutProjection } = newStatsBuffer.value;
layoutProjection.nodes.push(metrics.nodes);
layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
};
frame.postRender(record, true);
return reportStats;
}
exports.recordStats = recordStats;
;