designsystemsinternational / mechanic

Mechanic is a framework to build assets built on web code.
https://mechanic.design
MIT License
253 stars 11 forks source link

FPS Throttling #125

Closed lucasdinonolte closed 2 years ago

lucasdinonolte commented 2 years ago

@bravomartin @runemadsen @fdoflorenzano leaving this here to start a discussion.

When working on credits for a movie we noticed the need to export at a different framerate than 60. We solved this for now, by still exporting at 60 and then let ffmpeg do the conversion afterwards.

However there is the possibility to add a frameRate to the webm-writer. Which is currently hardcoded at 60 (see #124 ).

What this PR does so far is two things:

Let's discuss if this the right approach and if we even want something like this in mechanic, or if we just live with the ffmpeg conversion solution if you want a different framerate.

fdoflorenzano commented 2 years ago

Hey @lnolte! Thanks for looking into this! I'm not super familiar with video creation so this is huge help!

I guess one thing I wonder by looking at everything is how would a design function use this different pieces. Can you share an example of such a design function? Just as a code block here is fine.

lucasdinonolte commented 2 years ago

Sure. Here is a simple example using the React renderer

import React, { useEffect, useRef } from "react";

import { useDrawLoop } from "@mechanic-design/engine-react";

export const handler = ({ inputs, mechanic }) => {
  const { width, height, duration } = inputs;
  const { frame, done, frameRate } = mechanic;

  const maxFrames = duration * frameRate;

  const isPlaying = useRef(true);

  // User is responsible of setting up everything
  const frameCount = useDrawLoop(isPlaying.current, frameRate);

  // User is also responsible of tearing everything down
  useEffect(() => {
    if (frameCount <= maxFrames) {
      frame();
    } else if (isPlaying.current) {
      isPlaying.current = false;
      done();
    }
  }, [frameCount]);

  return (
    <svg width={width} height={height}>
      <rect x="0" y="0" width={width} height={height} fill="white" />
      <text x={width / 2} y={height / 2}>
        {frameCount}
      </text>
    </svg>
  );
};

export const inputs = {
  width: {
    type: "number",
    default: 640,
  },
  height: {
    type: "number",
    default: 480,
  },
  duration: {
    type: "number",
    default: 6,
  },
};

export const settings = {
  engine: require("@mechanic-design/engine-react"),
  animated: true,
  frameRate: 24,
};

So my initial idea was to keep things simple and just provide helpers for users to get the desired behavior. For the react renderer it's the useDrawLoop hook. For the other renderers it's the drawLoop utils as shown below:

import { drawLoop } from "@mechanic-design/core";

export const handler = ({ inputs, mechanic }) => {
  const { width, height, duration } = inputs;
  const maxFrames = duration * mechanic.frameRate;

  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext("2d");

  drawLoop(({ frameCount, stop }) => {
    console.log(frameCount, mechanic);
    ctx.save();
    ctx.clearRect(0, 0, width, height);
    ctx.fillStyle = "white";
    ctx.fillRect(0, 0, width, height);
    ctx.fillStyle = "red";
    ctx.font = "48px serif";
    ctx.fillText(frameCount, width / 2, height / 2);
    ctx.restore();

    if (frameCount <= maxFrames) {
      mechanic.frame(canvas);
    } else {
      stop();
      mechanic.done(canvas);
    }
  }, mechanic.frameRate);
};

export const inputs = {
  width: {
    type: "number",
    default: 300,
    min: 100,
  },
  height: {
    type: "number",
    default: 300,
    min: 100,
  },
  duration: {
    type: "number",
    default: 2,
  },
};

export const settings = {
  engine: require("@mechanic-design/engine-canvas"),
  animated: true,
  frameRate: 40,
};

This really is just a first draft. We could also think about integrating these helper functions tighter with the mechanic instance – because the instance would know the frameRate… But I think it depends on how "smart" we want mechanic to become.

Let me know if this makes sense. We could also see if we can schedule a quick call about this in the coming days 😊

fdoflorenzano commented 2 years ago

Thanks for these examples! It's all making sense. I think it's a good angle, I can't think of why not doing it like this.

I would want to test this examples before merging, and right now I have a little bit of hell with other pending branch. Is this urgent to get to main and release it?

In the meantime, if it all works, I would consider adapting some of the create-mechanic video examples to use this utilities too. Maybe if you have time you can check that out sometime while I test your base examples.

And sorry for taking this long to respond. This is great.

lucasdinonolte commented 2 years ago

@fdoflorenzano thanks, I'll adapt the create-mechanic examples. I also think there is no hurry in merging this. It's just something I wanted to explore while working on movie credits using mechanic.

Also yesterday @bravomartin had some cool ideas about the possibilities of a frame-based (rather than time-based) animation-approach in Mechanic. So I'd like him to have a look at this as well, to see if this could be a starting point for his ideas.

bravomartin commented 2 years ago

Love this! I would use the push to also add the ability to override the quality setting of the export?

lucasdinonolte commented 2 years ago

Closing this in favor of #152