From 4e160980d30f9c05388b86d09bf3344488e39377 Mon Sep 17 00:00:00 2001 From: Rune Madsen Date: Tue, 27 Sep 2022 15:37:15 +0200 Subject: [PATCH 1/5] adding decision document around new animation api --- decisions/01-new-animation-api.md | 208 ++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 decisions/01-new-animation-api.md diff --git a/decisions/01-new-animation-api.md b/decisions/01-new-animation-api.md new file mode 100644 index 00000000..942d171f --- /dev/null +++ b/decisions/01-new-animation-api.md @@ -0,0 +1,208 @@ +# Title + +## Status + +Proposed + +## Context + +Mechanic currently has support for animations. When setting `animation: true` in a design function, it receives the `frame()` and `done` callbacks that can be used to record individual frames into a movie file and finish the movie file all together. However, the framework doesn't come with any built-in tooling for running a draw loop inside your design function, so each template and example ships with its own helper code to facilitate this. For `engine-react`, this is a `useDrawLoop` hook that will re-render the React component 60 frames per second until the `done` callback is called. For the `engine-canvas`, we set up a `drawFrame` function that gets called with `requestAnimationFrame`. + +For the `2.0` release, we want to improve this setup to create a standardized animation API that cleans up the animated design functions by moving some logic into the Mechanic core, while also giving users the ability to write their own animation code if they choose so. Choosing a standardized animation API is a bit tricky with Mechanic, since it integrates with different kinds of frameworks and web technologies, and each of these frameworks have their own ways of doing things. + +- **p5.js** has its own animation api with the `setup` and `draw` functions. So `engine-p5` will most likely just want to use the `frame` and `done` callbacks as it currently does. Furthermore, this frame-based animation style encourages the use of global variables to hold the cumulated state of the animation. +- **React** does not ship with any animation API, so users can either just re-render their component once per frame (which is a bit slow) or use something like `react-spring` to perform animations in a more event-driven fashion (which is faster because of the `react-spring` `animated` components that bypass the react rendering tree). Furthermore, React components are most of the time pure functions, which is a benefit for the timeline functionality mentioned below. +- `engine-svg` and `engine-canvas` have no best practices since these are just slim wrappers around the HTML5 elements. + +One important aspect of this new animation API is that we want the ability to show a timeline scrubber in the Mechanic UI to give users ofan easy way to preview a specific frame of their design function output. This is much easier for frameworks that encourage functional design functions such as React and harder for frameworks such as p5.js. This timeline functionality is not described in this decision, but will build upon any decisions made in this proposal. + +## Decision + +We propose a new animation API where design functions default to a frame-based approach. If a design function is animated, it will automatically get called continously in a pace determined by the `frameRate` setting, until it calls the `done` function to stop the animation. This means that design functions are expected to be pure functions and not rely on global state to render the given frame. This in turn makes it possible for the Mechanic UI to provide a timeline that will update the design function with the given `frameCount` and result in a preview for that specific frame. Note that this new animation API does not impose any rules about the duration of the animation. This is always controlled by calling `done` at some point during the animation lifecycle. + +## Implementation Details + +The new animation API comes with a few changes to the settings: + +- The `animated` setting is renamed to `mode` (default: `static`) and the options are `static` (for static images), `animation` (for the new animation API) and `customAnimation` (for a user to implement their own animation code using the `frame` and `done` callbacks). +- `frameRate` (default: `60`) is a number that can be used to change the number of frames per second that the design function is called in `animation` mode. + +Since the new animation API encourages pure functions, the engines now ship with a `memo` function that can be used to cache things across function calls. Engine-specific helpers such as a `useCanvas` function will also be rewritten to create the canvas element on the first call and return it on following calls. + +The argument syntax is also changing slightly by placing the `frame` and `done` functions as root properties of the design function argument object. The `frame` function will only get passed to the design function in `customAnimation` mode. + +Here's a look at what this new animation API will look like for each engine. + +### `engine-canvas` + +```js +export const handler = async ({ inputs, frameCount, done, useCanvas }) => { + const canvas = useCanvas(inputs.width, inputs.height); + if (frameCount >= 100) { + done(canvas); + } + // drawing code + return canvas; +}; + +export const settings = { + engine: require('@mechanic-design/engine-canvas'), + mode: 'animation' +}; +``` + +```js +export const handler = async ({ inputs, frame, done, useCanvas }) => { + const canvas = useCanvas(inputs.width, inputs.height); + let frameCount = 0; + const myDrawLoop = () => { + frameCount++; + // drawing code + if (frameCount >= 100) { + done(canvas); + } else { + frame(canvas); + window.requestAnimationFrame(myDrawLoop); + } + }; + myDrawLoop(); +}; + +export const settings = { + engine: require('@mechanic-design/engine-canvas'), + mode: 'customAnimation' +}; +``` + +### `engine-svg` + +```js +export const handler = async ({ inputs, frameCount, done }) => { + // drawing code + if (frameCount >= 100) { + done(svgString); + } + return svgString; +}; + +export const settings = { + engine: require('@mechanic-design/engine-svg'), + mode: 'animation' +}; +``` + +```js +export const handler = async ({ inputs, frame, done }) => { + let frameCount = 0; + const myDrawLoop = () => { + frameCount++; + // drawing code + if (frameCount >= 100) { + done(svgString); + } else { + frame(svgString); + window.requestAnimationFrame(myDrawLoop); + } + }; + myDrawLoop(); +}; + +export const settings = { + engine: require('@mechanic-design/engine-svg'), + mode: 'customAnimation' +}; +``` + +### `engine-react` + +```js +export const handler = async ({ inputs, done, frameCount }) => { + if (frameCount >= 100) { + done(); + } + return
; +}; + +export const settings = { + engine: require('@mechanic-design/engine-react'), + mode: 'animation' +}; +``` + +The example above re-renders the component for every frame. If a user wants to use something like `react-spring` to speed up animation while keeping the frame-based approach, it could be done like this: + +```js +export const handler = async ({ inputs, done, frameCount }) => { + const spring = useSpring({ frameCount }); + + if (frameCount >= 100) { + done(); + } + return ; +}; + +const MyMemoedComponent = React.memo(({ spring }) => { + return ; +}); + +export const settings = { + engine: require('@mechanic-design/engine-react'), + mode: 'animation' +}; +``` + +```js +export const handler = async ({ inputs, frame, done }) => { + const frameCount = myOwnDrawLoopHook(); + useEffect(() => { + if (frameCount < 100) { + frame(); + } else { + done(); + } + }, [frameCount]); + return
; +}; + +export const settings = { + engine: require('@mechanic-design/engine-react'), + mode: 'customAnimation' +}; +``` + +### `engine-p5` + +For `engine-p5`, `mode` can only be set to `static` or `customAnimation`. Or perhaps we default both `animation` and `customAnimation` to the code below? + +```js +export const handler = async ({ inputs, sketch, frame, done }) => { + sketch.setup = () => { + // do stuff + }; + sketch.draw = () => { + // drawing code + if (sketch.frameCount < 100) { + frame(); + } else { + done(); + } + }; +}; + +export const settings = { + engine: require('@mechanic-design/engine-p5'), + mode: 'animation' +}; +``` + +## Consequences + +### Positive Consequences + +The benefits for most users is that they will get an animation API that gets out of their way. The templates and examples for animated design functions will be a lot slimmer, and it will provide us with an API that we can build nice things on top of. + +### Negative Consequences + +The main disadvantage I can see is: + +- Users who are not comfortable writing pure functions are now defaulted into this, and if they opt out, they need to write their own animation code. From ccde6f9271109452299c2318edecf9f97e9248c6 Mon Sep 17 00:00:00 2001 From: Rune Skjoldborg Madsen Date: Wed, 28 Sep 2022 09:56:05 +0200 Subject: [PATCH 2/5] Update decisions/01-new-animation-api.md Co-authored-by: Lucas Nolte --- decisions/01-new-animation-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decisions/01-new-animation-api.md b/decisions/01-new-animation-api.md index 942d171f..5e570243 100644 --- a/decisions/01-new-animation-api.md +++ b/decisions/01-new-animation-api.md @@ -14,7 +14,7 @@ For the `2.0` release, we want to improve this setup to create a standardized an - **React** does not ship with any animation API, so users can either just re-render their component once per frame (which is a bit slow) or use something like `react-spring` to perform animations in a more event-driven fashion (which is faster because of the `react-spring` `animated` components that bypass the react rendering tree). Furthermore, React components are most of the time pure functions, which is a benefit for the timeline functionality mentioned below. - `engine-svg` and `engine-canvas` have no best practices since these are just slim wrappers around the HTML5 elements. -One important aspect of this new animation API is that we want the ability to show a timeline scrubber in the Mechanic UI to give users ofan easy way to preview a specific frame of their design function output. This is much easier for frameworks that encourage functional design functions such as React and harder for frameworks such as p5.js. This timeline functionality is not described in this decision, but will build upon any decisions made in this proposal. +One important aspect of this new animation API is that we want the ability to show a timeline scrubber in the Mechanic UI to give users an easy way to preview a specific frame of their design function output. This is much easier for frameworks that encourage functional design functions such as React and harder for frameworks such as p5.js. This timeline functionality is not described in this decision, but will build upon any decisions made in this proposal. ## Decision From 880617cdc753838fbbcff622336875573532fec4 Mon Sep 17 00:00:00 2001 From: Lucas Nolte Date: Fri, 7 Oct 2022 11:11:06 +0200 Subject: [PATCH 3/5] update decision log with experiences of implementing this --- decisions/01-new-animation-api.md | 172 ++++++++++++------------------ 1 file changed, 69 insertions(+), 103 deletions(-) diff --git a/decisions/01-new-animation-api.md b/decisions/01-new-animation-api.md index 5e570243..21e5c8bc 100644 --- a/decisions/01-new-animation-api.md +++ b/decisions/01-new-animation-api.md @@ -6,173 +6,139 @@ Proposed ## Context -Mechanic currently has support for animations. When setting `animation: true` in a design function, it receives the `frame()` and `done` callbacks that can be used to record individual frames into a movie file and finish the movie file all together. However, the framework doesn't come with any built-in tooling for running a draw loop inside your design function, so each template and example ships with its own helper code to facilitate this. For `engine-react`, this is a `useDrawLoop` hook that will re-render the React component 60 frames per second until the `done` callback is called. For the `engine-canvas`, we set up a `drawFrame` function that gets called with `requestAnimationFrame`. +Mechanic currently has support for animations. When setting `animation: true` in a design function, it receives the `frame()` and `done()` callbacks that can be used to record individual frames into a movie file and finish the movie file all together. However, the framework doesn't come with any built-in tooling for running a draw loop inside your design function, so each template and example ships with its own helper code to facilitate this. For `engine-react`, this is a `useDrawLoop` hook that will re-render the React component 60 frames per second until the `done` callback is called. For the `engine-canvas`, we set up a `drawFrame` function that gets called with `requestAnimationFrame`. -For the `2.0` release, we want to improve this setup to create a standardized animation API that cleans up the animated design functions by moving some logic into the Mechanic core, while also giving users the ability to write their own animation code if they choose so. Choosing a standardized animation API is a bit tricky with Mechanic, since it integrates with different kinds of frameworks and web technologies, and each of these frameworks have their own ways of doing things. +For the `2.0` release, we want to improve this setup to create a standardized animation API that cleans up the animated design functions by moving some logic into the Mechanic core, while still giving users the flexibility to write animation with whatever mental model feels comfortable for them. Choosing a standardized animation API is a bit tricky with Mechanic, since it integrates with different kinds of frameworks and web technologies, and each of these frameworks have their own ways of doing things. - **p5.js** has its own animation api with the `setup` and `draw` functions. So `engine-p5` will most likely just want to use the `frame` and `done` callbacks as it currently does. Furthermore, this frame-based animation style encourages the use of global variables to hold the cumulated state of the animation. -- **React** does not ship with any animation API, so users can either just re-render their component once per frame (which is a bit slow) or use something like `react-spring` to perform animations in a more event-driven fashion (which is faster because of the `react-spring` `animated` components that bypass the react rendering tree). Furthermore, React components are most of the time pure functions, which is a benefit for the timeline functionality mentioned below. +- **React** does not ship with any animation API, so users can either just re-render their component once per frame (which is a bit slow) or use something like `react-spring` to perform animations in a more event-driven fashion (which is faster because of the `react-spring` `animated` components that bypass the react rendering tree; the same could manually be done by manipulating the DOM directly inside `useEffect`). Furthermore, React components are most of the time pure functions, which is a benefit for the timeline functionality mentioned below. - `engine-svg` and `engine-canvas` have no best practices since these are just slim wrappers around the HTML5 elements. -One important aspect of this new animation API is that we want the ability to show a timeline scrubber in the Mechanic UI to give users an easy way to preview a specific frame of their design function output. This is much easier for frameworks that encourage functional design functions such as React and harder for frameworks such as p5.js. This timeline functionality is not described in this decision, but will build upon any decisions made in this proposal. +One important aspect of this new animation API is that we want the ability to show a timeline scrubber in the Mechanic UI to give users an easy way to preview a specific frame of their design function output. This can only be done for pure functions, where each frame is a function of the current frame number. This timeline functionality is not described in this decision, but will build upon any decisions made in this proposal. + +We explored a new animation API where design functions default to a frame-based +approach and the drawLoop was hidden from the user inside mechanic core. This +approach would call the design function over and over again in a pace determined +by the `frameRate` setting, making it a pure +function of the current framecount. This approach would make implementing a +timeline in the UI very simple, as the frameCount can be passed to the design +function directly. + +However this approach comes with drawbacks. Treating the entire design function +as a pure function is very opioniated and removes a lot of flexibilty from the +way mechanic can currently be used or at least makes things like loading +fonts/images or generating random numbers more verbose, because they need to be +persisted across function calls (as a pure function is stateless). + +See [#152](https://github.com/designsystemsinternational/mechanic/pull/152) for +a full discussion and demo implementation of this approach. ## Decision -We propose a new animation API where design functions default to a frame-based approach. If a design function is animated, it will automatically get called continously in a pace determined by the `frameRate` setting, until it calls the `done` function to stop the animation. This means that design functions are expected to be pure functions and not rely on global state to render the given frame. This in turn makes it possible for the Mechanic UI to provide a timeline that will update the design function with the given `frameCount` and result in a preview for that specific frame. Note that this new animation API does not impose any rules about the duration of the animation. This is always controlled by calling `done` at some point during the animation lifecycle. +We propose a new animation API that provides users with a simple and unified +drawLoop. If the drawLoop is used it will automatically respect the pace +determined in the `frameRate` setting and call the callback given to the +drawloop until `done` is called to stop the animation. + +The callback inside the drawLoop receives the current frame number as its only +argument. Its implementation is up to the user. A pure function is encouraged +but not enforced. + +In this approach the timeline could be an opt-in feature that +can be enabled in the settings. If a timeline value is given mechanic-core could +bypass the drawloop and just call the frame callback once with the frame number +the user wants to preview. As the drawLoop does not enforce a pure function, a +warning and good documentation should be added that a pure drawing function is +needed for the timeline to properly work. This would make the timeline more of a +power-user feature. + +This approach does not impose any rules about the duration (or exit condition) +of an animation. It is still up to the user to call `done` at some point in the +animation lifecycle to finalize the animation. ## Implementation Details The new animation API comes with a few changes to the settings: -- The `animated` setting is renamed to `mode` (default: `static`) and the options are `static` (for static images), `animation` (for the new animation API) and `customAnimation` (for a user to implement their own animation code using the `frame` and `done` callbacks). +- The `animated` setting is removed. Instead mechanic-core can figure out if a function is animated by checking if the provided drawLoop was called or not. - `frameRate` (default: `60`) is a number that can be used to change the number of frames per second that the design function is called in `animation` mode. -Since the new animation API encourages pure functions, the engines now ship with a `memo` function that can be used to cache things across function calls. Engine-specific helpers such as a `useCanvas` function will also be rewritten to create the canvas element on the first call and return it on following calls. - -The argument syntax is also changing slightly by placing the `frame` and `done` functions as root properties of the design function argument object. The `frame` function will only get passed to the design function in `customAnimation` mode. +The argument syntax is also changing slightly by placing the `frame`, `done` and the new `drawLoop` functions as root properties of the design function argument object. Here's a look at what this new animation API will look like for each engine. ### `engine-canvas` ```js -export const handler = async ({ inputs, frameCount, done, useCanvas }) => { +export const handler = async ({ inputs, frame, drawLoop, done, useCanvas }) => { const canvas = useCanvas(inputs.width, inputs.height); - if (frameCount >= 100) { - done(canvas); - } - // drawing code - return canvas; -}; -export const settings = { - engine: require('@mechanic-design/engine-canvas'), - mode: 'animation' -}; -``` + const font = await doSomeHeavyFontLoading(); -```js -export const handler = async ({ inputs, frame, done, useCanvas }) => { - const canvas = useCanvas(inputs.width, inputs.height); - let frameCount = 0; - const myDrawLoop = () => { - frameCount++; - // drawing code + drawLoop((frameCount) => { + // Drawing code if (frameCount >= 100) { done(canvas); } else { frame(canvas); - window.requestAnimationFrame(myDrawLoop); } - }; - myDrawLoop(); + }); }; export const settings = { engine: require('@mechanic-design/engine-canvas'), - mode: 'customAnimation' + frameRate: 24, }; ``` ### `engine-svg` ```js -export const handler = async ({ inputs, frameCount, done }) => { - // drawing code - if (frameCount >= 100) { - done(svgString); - } - return svgString; -}; - -export const settings = { - engine: require('@mechanic-design/engine-svg'), - mode: 'animation' -}; -``` +export const handler = async ({ inputs, frame, done, drawLoop }) => { + let someGlobalState = 0; -```js -export const handler = async ({ inputs, frame, done }) => { - let frameCount = 0; - const myDrawLoop = () => { - frameCount++; + // This example shows an "impure" function passed to the drawLoop + drawLoop((_) => { // drawing code - if (frameCount >= 100) { + someGlobalState += 10; + if (someGlobalState >= 100) { done(svgString); } else { frame(svgString); - window.requestAnimationFrame(myDrawLoop); } - }; - myDrawLoop(); + }); }; export const settings = { engine: require('@mechanic-design/engine-svg'), - mode: 'customAnimation' }; ``` ### `engine-react` ```js -export const handler = async ({ inputs, done, frameCount }) => { - if (frameCount >= 100) { - done(); - } - return
; -}; - -export const settings = { - engine: require('@mechanic-design/engine-react'), - mode: 'animation' -}; -``` - -The example above re-renders the component for every frame. If a user wants to use something like `react-spring` to speed up animation while keeping the frame-based approach, it could be done like this: - -```js -export const handler = async ({ inputs, done, frameCount }) => { - const spring = useSpring({ frameCount }); - - if (frameCount >= 100) { - done(); - } - return ; -}; - -const MyMemoedComponent = React.memo(({ spring }) => { - return ; -}); - -export const settings = { - engine: require('@mechanic-design/engine-react'), - mode: 'animation' -}; -``` - -```js -export const handler = async ({ inputs, frame, done }) => { - const frameCount = myOwnDrawLoopHook(); - useEffect(() => { - if (frameCount < 100) { - frame(); - } else { +export const handler = async ({ inputs, frame, done, useDrawLoop }) => { + useDrawLoop((frameCount) => { + // Potentially doing state updates here + if (frameCount >= 100) { done(); + } else { + frame(); } - }, [frameCount]); + }); return
; }; export const settings = { engine: require('@mechanic-design/engine-react'), - mode: 'customAnimation' }; ``` ### `engine-p5` -For `engine-p5`, `mode` can only be set to `static` or `customAnimation`. Or perhaps we default both `animation` and `customAnimation` to the code below? +For `engine-p5` now drawLoop is provided, as `p5` comes with its own drawLoop. +Here we only make sure to pass any value specified for the `frameRate` to p5 +before rendering the sketch. ```js export const handler = async ({ inputs, sketch, frame, done }) => { @@ -191,7 +157,7 @@ export const handler = async ({ inputs, sketch, frame, done }) => { export const settings = { engine: require('@mechanic-design/engine-p5'), - mode: 'animation' + frameRate: 30, }; ``` @@ -199,10 +165,10 @@ export const settings = { ### Positive Consequences -The benefits for most users is that they will get an animation API that gets out of their way. The templates and examples for animated design functions will be a lot slimmer, and it will provide us with an API that we can build nice things on top of. +The benefits for most users is that they will get an animation API that gets out of their way while still providing flexibility. The templates and examples for animated design functions will be a lot slimmer because there is no custom boilerplate code left in the functions. And it will provide us with an API that we can build nice things on top of. ### Negative Consequences The main disadvantage I can see is: -- Users who are not comfortable writing pure functions are now defaulted into this, and if they opt out, they need to write their own animation code. +- As we are not enforcing a pure functional approach the timeline might be harder to implement or yield weird results when a drawing function is not a pure function From f2f92550ad53101dc4ac22528f94493406c32d6e Mon Sep 17 00:00:00 2001 From: Rune Madsen Date: Mon, 10 Oct 2022 10:10:05 +0200 Subject: [PATCH 4/5] update docs --- decisions/01-new-animation-api.md | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/decisions/01-new-animation-api.md b/decisions/01-new-animation-api.md index 21e5c8bc..7c8c4b99 100644 --- a/decisions/01-new-animation-api.md +++ b/decisions/01-new-animation-api.md @@ -10,21 +10,16 @@ Mechanic currently has support for animations. When setting `animation: true` in For the `2.0` release, we want to improve this setup to create a standardized animation API that cleans up the animated design functions by moving some logic into the Mechanic core, while still giving users the flexibility to write animation with whatever mental model feels comfortable for them. Choosing a standardized animation API is a bit tricky with Mechanic, since it integrates with different kinds of frameworks and web technologies, and each of these frameworks have their own ways of doing things. -- **p5.js** has its own animation api with the `setup` and `draw` functions. So `engine-p5` will most likely just want to use the `frame` and `done` callbacks as it currently does. Furthermore, this frame-based animation style encourages the use of global variables to hold the cumulated state of the animation. +- **p5.js** has its own animation api with the `setup` and `draw` functions. So `engine-p5` will most likely just want to use the `frame` and `done` callbacks as it currently does. Furthermore, this frame-based animation style encourages the use of global variables to hold the cumulated state of the animation, which makes it harder to implement the timeline functionality described below. - **React** does not ship with any animation API, so users can either just re-render their component once per frame (which is a bit slow) or use something like `react-spring` to perform animations in a more event-driven fashion (which is faster because of the `react-spring` `animated` components that bypass the react rendering tree; the same could manually be done by manipulating the DOM directly inside `useEffect`). Furthermore, React components are most of the time pure functions, which is a benefit for the timeline functionality mentioned below. -- `engine-svg` and `engine-canvas` have no best practices since these are just slim wrappers around the HTML5 elements. +- `engine-svg` and `engine-canvas` have no best practices since these are just slim wrappers around the native HTML5 elements. One important aspect of this new animation API is that we want the ability to show a timeline scrubber in the Mechanic UI to give users an easy way to preview a specific frame of their design function output. This can only be done for pure functions, where each frame is a function of the current frame number. This timeline functionality is not described in this decision, but will build upon any decisions made in this proposal. We explored a new animation API where design functions default to a frame-based -approach and the drawLoop was hidden from the user inside mechanic core. This +approach and the draw loop was hidden from the user inside mechanic core. This approach would call the design function over and over again in a pace determined -by the `frameRate` setting, making it a pure -function of the current framecount. This approach would make implementing a -timeline in the UI very simple, as the frameCount can be passed to the design -function directly. - -However this approach comes with drawbacks. Treating the entire design function +by the `frameRate` setting, making it a pure function of the current framecount. This approach would make implementing a timeline in the UI very simple, as the frameCount can be passed to the design function directly. However this approach comes with drawbacks. Treating the entire design function as a pure function is very opioniated and removes a lot of flexibilty from the way mechanic can currently be used or at least makes things like loading fonts/images or generating random numbers more verbose, because they need to be @@ -42,7 +37,7 @@ drawloop until `done` is called to stop the animation. The callback inside the drawLoop receives the current frame number as its only argument. Its implementation is up to the user. A pure function is encouraged -but not enforced. +but not enforced. In this approach the timeline could be an opt-in feature that can be enabled in the settings. If a timeline value is given mechanic-core could @@ -87,7 +82,7 @@ export const handler = async ({ inputs, frame, drawLoop, done, useCanvas }) => { export const settings = { engine: require('@mechanic-design/engine-canvas'), - frameRate: 24, + frameRate: 24 }; ``` @@ -110,7 +105,7 @@ export const handler = async ({ inputs, frame, done, drawLoop }) => { }; export const settings = { - engine: require('@mechanic-design/engine-svg'), + engine: require('@mechanic-design/engine-svg') }; ``` @@ -130,7 +125,7 @@ export const handler = async ({ inputs, frame, done, useDrawLoop }) => { }; export const settings = { - engine: require('@mechanic-design/engine-react'), + engine: require('@mechanic-design/engine-react') }; ``` @@ -157,7 +152,7 @@ export const handler = async ({ inputs, sketch, frame, done }) => { export const settings = { engine: require('@mechanic-design/engine-p5'), - frameRate: 30, + frameRate: 30 }; ``` From de8afea0d5a75120f9fd183e3c7e4e45c631ba59 Mon Sep 17 00:00:00 2001 From: Rune Madsen Date: Mon, 10 Oct 2022 10:53:47 +0200 Subject: [PATCH 5/5] update decision doc --- .prettierrc | 1 + decisions/01-new-animation-api.md | 87 +++++++++++++------------------ 2 files changed, 36 insertions(+), 52 deletions(-) diff --git a/.prettierrc b/.prettierrc index 64286652..91751c60 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,6 +6,7 @@ "arrowParens": "avoid", "jsxBracketSameLine": true, "trailingComma": "none", + "proveWrap": "always", "overrides": [ { "files": ["*.css", "*.scss"], diff --git a/decisions/01-new-animation-api.md b/decisions/01-new-animation-api.md index 7c8c4b99..f02e9612 100644 --- a/decisions/01-new-animation-api.md +++ b/decisions/01-new-animation-api.md @@ -16,72 +16,51 @@ For the `2.0` release, we want to improve this setup to create a standardized an One important aspect of this new animation API is that we want the ability to show a timeline scrubber in the Mechanic UI to give users an easy way to preview a specific frame of their design function output. This can only be done for pure functions, where each frame is a function of the current frame number. This timeline functionality is not described in this decision, but will build upon any decisions made in this proposal. -We explored a new animation API where design functions default to a frame-based -approach and the draw loop was hidden from the user inside mechanic core. This -approach would call the design function over and over again in a pace determined -by the `frameRate` setting, making it a pure function of the current framecount. This approach would make implementing a timeline in the UI very simple, as the frameCount can be passed to the design function directly. However this approach comes with drawbacks. Treating the entire design function -as a pure function is very opioniated and removes a lot of flexibilty from the -way mechanic can currently be used or at least makes things like loading -fonts/images or generating random numbers more verbose, because they need to be -persisted across function calls (as a pure function is stateless). - -See [#152](https://github.com/designsystemsinternational/mechanic/pull/152) for -a full discussion and demo implementation of this approach. +We explored a new animation API where design functions default to a frame-based approach and the draw loop was hidden from the user inside mechanic core. This approach would call the design function over and over again in a pace determined by the `frameRate` setting, making it a pure function of the current frame count. This approach would make implementing a timeline in the UI very simple, as the frameCount can be passed to the design function directly. However this approach comes with drawbacks. Treating the entire design function as a pure function is very opioniated and removes a lot of flexibilty from the way mechanic can currently be used or at least makes things like loading fonts/images or generating random numbers more verbose, because they need to be persisted across function calls (as a pure function is stateless). + +See [#152](https://github.com/designsystemsinternational/mechanic/pull/152) for a full discussion and demo implementation of this approach. ## Decision -We propose a new animation API that provides users with a simple and unified -drawLoop. If the drawLoop is used it will automatically respect the pace -determined in the `frameRate` setting and call the callback given to the -drawloop until `done` is called to stop the animation. - -The callback inside the drawLoop receives the current frame number as its only -argument. Its implementation is up to the user. A pure function is encouraged -but not enforced. - -In this approach the timeline could be an opt-in feature that -can be enabled in the settings. If a timeline value is given mechanic-core could -bypass the drawloop and just call the frame callback once with the frame number -the user wants to preview. As the drawLoop does not enforce a pure function, a -warning and good documentation should be added that a pure drawing function is -needed for the timeline to properly work. This would make the timeline more of a +We propose a new animation API that provides users with a simple and unified drawLoop. If the drawLoop is used it will automatically respect the pace determined in the `frameRate` setting and call the callback given to the draw loop until `done` is called to stop the animation. + +The callback inside the draw loop receives the current frame number as its only argument. Its implementation is up to the user. A pure function is encouraged but not enforced. + +In this approach the timeline could be an opt-in feature that can be enabled in the settings. If a timeline value is given mechanic-core could bypass the draw loop and just call the frame callback once with the frame number the user wants to preview. As the draw loop does not enforce a pure function, a warning and good documentation should be added that a pure drawing function is needed for the timeline to properly work. This would make the timeline more of a power-user feature. -This approach does not impose any rules about the duration (or exit condition) -of an animation. It is still up to the user to call `done` at some point in the -animation lifecycle to finalize the animation. +This approach does not impose any rules about the duration (or exit condition) of an animation. It is still up to the user to call `done` at some point in the animation lifecycle to finalize the animation. ## Implementation Details -The new animation API comes with a few changes to the settings: +The new animation API comes with a new setting: -- The `animated` setting is removed. Instead mechanic-core can figure out if a function is animated by checking if the provided drawLoop was called or not. - `frameRate` (default: `60`) is a number that can be used to change the number of frames per second that the design function is called in `animation` mode. -The argument syntax is also changing slightly by placing the `frame`, `done` and the new `drawLoop` functions as root properties of the design function argument object. +The argument syntax is also changing slightly by placing the `frame`, `done` and the new `drawLoop` functions as root properties of the design function argument object. We are also removing the need for passing the element into these callbacks except for `engine-svg` where it is needed. Here's a look at what this new animation API will look like for each engine. ### `engine-canvas` ```js -export const handler = async ({ inputs, frame, drawLoop, done, useCanvas }) => { - const canvas = useCanvas(inputs.width, inputs.height); +import engine from "@mechanic-design/engine-canvas"; +export const handler = async ({ inputs, frame, done, drawLoop, getCanvas }) => { + const canvas = getCanvas(inputs.width, inputs.height); const font = await doSomeHeavyFontLoading(); - - drawLoop((frameCount) => { + drawLoop(frameCount => { // Drawing code if (frameCount >= 100) { - done(canvas); + done(); } else { - frame(canvas); + frame(); } }); }; export const settings = { - engine: require('@mechanic-design/engine-canvas'), + engine, frameRate: 24 }; ``` @@ -89,11 +68,14 @@ export const settings = { ### `engine-svg` ```js +import engine from "@mechanic-design/engine-svg"; + export const handler = async ({ inputs, frame, done, drawLoop }) => { let someGlobalState = 0; // This example shows an "impure" function passed to the drawLoop - drawLoop((_) => { + // and will not work with the timeline functionality. + drawLoop(_ => { // drawing code someGlobalState += 10; if (someGlobalState >= 100) { @@ -105,35 +87,38 @@ export const handler = async ({ inputs, frame, done, drawLoop }) => { }; export const settings = { - engine: require('@mechanic-design/engine-svg') + engine }; ``` ### `engine-react` ```js -export const handler = async ({ inputs, frame, done, useDrawLoop }) => { - useDrawLoop((frameCount) => { +import engine, { useDrawLoop } from "@mechanic-design/engine-react"; + +export const handler = async ({ inputs, frame, done, drawLoop }) => { + const frameCount = useDrawLoop(drawLoop); + + useEffect(() => { // Potentially doing state updates here if (frameCount >= 100) { done(); } else { frame(); } - }); + }, [frameCount]); + return
; }; export const settings = { - engine: require('@mechanic-design/engine-react') + engine }; ``` ### `engine-p5` -For `engine-p5` now drawLoop is provided, as `p5` comes with its own drawLoop. -Here we only make sure to pass any value specified for the `frameRate` to p5 -before rendering the sketch. +For `engine-p5`, no `drawLoop` is provided as `p5` comes with its own draw loop. Here we only make sure to pass any value specified for the `frameRate` to p5 before rendering the sketch. ```js export const handler = async ({ inputs, sketch, frame, done }) => { @@ -151,7 +136,7 @@ export const handler = async ({ inputs, sketch, frame, done }) => { }; export const settings = { - engine: require('@mechanic-design/engine-p5'), + engine: require("@mechanic-design/engine-p5"), frameRate: 30 }; ``` @@ -164,6 +149,4 @@ The benefits for most users is that they will get an animation API that gets out ### Negative Consequences -The main disadvantage I can see is: - -- As we are not enforcing a pure functional approach the timeline might be harder to implement or yield weird results when a drawing function is not a pure function +The main disadvantage is that since we are are not enforcing a pure functional approach, the timeline might be harder to implement or yield weird results when a drawing function is not a pure function