Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FPS Throttling #125

Closed
wants to merge 10 commits into from
5 changes: 5 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Add frameRate and videoQuality options to settings
- Add drawLoop utility function that allows previewing an animation at a specified frameRate

## 2.0.0-beta.6 - 2022-05-08

### Added
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { Mechanic } from "./mechanic.js";
export { drawLoop } from "./mechanic-utils.js";
56 changes: 55 additions & 1 deletion packages/core/src/mechanic-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,59 @@ const getTimeStamp = () => {
return `${year}-${month}-${day}-${hour}-${minute}`;
};

/**
* A throttled version of requestAnimationFrame to support preview
* at a certain framerate.
*
* Greatly inspired by how p5 handles its throttled drawloop.
*
* @param drawingFunction
* @param frameRate
*/
const drawLoop = (drawingFunction, frameRate = 60) => {
const fpsInterval = 1000 / frameRate;
const epsilon = 5;

let lastFrameTime = 0;
let frameCount = 0;
let raf;
let isPlaying = true;

const stop = () => {
isPlaying = false;
window.cancelAnimationFrame(raf);
};

const draw = () => {
const now = window.performance.now();
const timeSinceLast = now - lastFrameTime;

// The Epsilon is taken from p5's solution. Also copying their
// comment on why they do it here.
//
// From p5 source:
// only draw if we really need to; don't overextend the browser.
// draw if we're within 5ms of when our next frame should paint
// (this will prevent us from giving up opportunities to draw
// again when it's really about time for us to do so). fixes an
// issue where the frameRate is too low if our refresh loop isn't
// in sync with the browser. note that we have to draw once even
// if looping is off, so we bypass the time delay if that
// is the case.
if (isPlaying && timeSinceLast >= fpsInterval - epsilon) {
drawingFunction.call(null, { frameCount, stop });
frameCount++;
lastFrameTime = now;
}

if (isPlaying) {
raf = window.requestAnimationFrame(draw);
}
};

draw();
};

export {
isSVG,
isCanvas,
Expand All @@ -172,5 +225,6 @@ export {
htmlToCanvas,
extractSvgSize,
dataUrlToCanvas,
getTimeStamp
getTimeStamp,
drawLoop
};
33 changes: 30 additions & 3 deletions packages/core/src/mechanic.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,37 @@ export class Mechanic {
}
}

this.settings = settings;
this.settings = {
frameRate: settings.frameRate || 60,
videoQuality: settings.videoQuality || 0.95,
...settings
};

this.values = values;
this.functionState = lastRun?.functionState ?? {};
this.exportType = exportType;

// Validate the settings
this.validateSettings();
}

/**
* Helper function to validate settings
* Throws an exception if invalid settings
* are applied.
*/
validateSettings() {
// Validates that quality is within 0–0.99
//
// 1 isn't a working value for the webm-writer we use, as it doesn't support
// losless export. The webm-writer clamps the quality value, so passing 1 to
// it would technically work. But this way is more transparent towards the
// users. We currently do not support losless video export.
if (this.settings.videoQuality < 0 || this.settings.videoQuality > 0.99) {
throw new MechanicError(
`The given videoQuality setting (${this.settings.videoQuality}) does not match the expected range (0–0.99)`
);
}
}

/**
Expand Down Expand Up @@ -112,8 +139,8 @@ export class Mechanic {
this.exportInit = true;
this.serializer = new XMLSerializer();
this.videoWriter = new WebMWriter({
quality: 0.95,
frameRate: 60
quality: this.settings.videoQuality,
frameRate: this.settings.frameRate
});
}

Expand Down
3 changes: 3 additions & 0 deletions packages/create-mechanic/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added
- Added function templates for framebased animations

## 2.0.0-beta.3 - 2022-03-24

### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"@mechanic-design/engine-canvas": "^2.0.0-beta.4"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { drawLoop } from "@mechanic-design/core";

export const handler = async ({ inputs, mechanic }) => {
const { width, height, text, color1, color2, radiusPercentage, turns } =
inputs;

const center = [width / 2, height / 2];
const radius = ((height / 2) * radiusPercentage) / 100;
let angle = 0;

const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;

const ctx = canvas.getContext("2d");

drawLoop(({ frameCount, stop }) => {
ctx.fillStyle = "#F4F4F4";
ctx.fillRect(0, 0, width, height);

ctx.fillStyle = color1;
ctx.beginPath();
ctx.arc(
center[0],
center[1],
radius,
Math.PI + angle,
2 * Math.PI + angle,
false
);
ctx.fill();

ctx.fillStyle = color2;
ctx.beginPath();
ctx.arc(center[0], center[1], radius, 0 + angle, Math.PI + angle, false);
ctx.fill();

ctx.fillStyle = "#000000";
ctx.font = `${height / 10}px sans-serif`;
ctx.textAlign = "center";
ctx.strokeText(text, width / 2, height - height / 20);
ctx.fillText(text, width / 2, height - height / 20);

if (angle < turns * 2 * Math.PI) {
mechanic.frame(canvas);
angle = frameCount;
} else {
stop();
mechanic.done(canvas);
}
}, mechanic.frameRate);
};

export const inputs = {
width: {
type: "number",
default: 400,
},
height: {
type: "number",
default: 300,
},
text: {
type: "text",
default: "mechanic",
},
color1: {
type: "color",
model: "hex",
default: "#E94225",
},
color2: {
type: "color",
model: "hex",
default: "#002EBB",
},
radiusPercentage: {
type: "number",
default: 40,
min: 0,
max: 100,
slider: true,
},
turns: {
type: "number",
default: 3,
},
};

export const presets = {
medium: {
width: 800,
height: 600,
},
large: {
width: 1600,
height: 1200,
},
};

export const settings = {
engine: require("@mechanic-design/engine-canvas"),
animated: true,
frameRate: 60,
};
15 changes: 15 additions & 0 deletions packages/create-mechanic/function-templates/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ const options = [
type: "Canvas",
dir: "canvas-video",
},
{
name: "Vanilla JS Video (Framebased)",
type: "Canvas",
dir: "canvas-video-framebased",
},
{
name: "Vanilla JS Image",
type: "SVG",
Expand All @@ -19,6 +24,11 @@ const options = [
type: "SVG",
dir: "svg-video",
},
{
name: "Vanilla JS Video (Framebased)",
type: "Canvas",
dir: "svg-video-framebased",
},
{
name: "p5.js Image",
type: "Canvas",
Expand All @@ -39,6 +49,11 @@ const options = [
type: "SVG",
dir: "react-video",
},
{
name: "React Video (Framebased)",
type: "SVG",
dir: "react-video-framebased",
},
];

module.exports = options.reduce((acc, cur) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/create-mechanic/function-templates/p5-video/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const handler = ({ inputs, mechanic, sketch }) => {

sketch.setup = () => {
sketch.createCanvas(width, height);
sketch.frameRate(mechanic.frameRate);
};

sketch.draw = () => {
Expand Down Expand Up @@ -89,4 +90,5 @@ export const presets = {
export const settings = {
engine: require("@mechanic-design/engine-p5"),
animated: true,
frameRate: 60,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"@mechanic-design/engine-react": "^2.0.0-beta.4"
}
}
Loading