diff --git a/packages/transition/CHANGELOG.md b/packages/transition/CHANGELOG.md new file mode 100644 index 0000000..6133f76 --- /dev/null +++ b/packages/transition/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- First release of the Mechanic transition helpers diff --git a/packages/transition/README.md b/packages/transition/README.md new file mode 100644 index 0000000..1b79dde --- /dev/null +++ b/packages/transition/README.md @@ -0,0 +1,35 @@ +# @mechanic-design/transition + +Helper functions to make animation easier. + +## Usage + +```js + +import { transition } from "@mechanic-design/transition"; + +export const handler = ({ inputs, frame, done, drawLoop }) => { + const xPos = transition({ + from: 0, + to: 100, + // These can be frames or seconds, depending on what + // you pass as the tick in the next step + duration: 10, + delay: 1, + }); + + drawLoop({ frameCount, timestamp }) => { + ctx.fillRect( + // If you pass the timestamp here, duration and delay + // will be treated as seconds. + // + // If you passed frameCount, duration and delay would + // be considered as frame offsets. + xPos(timestamp), + 0, + 100, + 100, + ); + }); +}; +``` diff --git a/packages/transition/package.json b/packages/transition/package.json new file mode 100644 index 0000000..9699ec8 --- /dev/null +++ b/packages/transition/package.json @@ -0,0 +1,48 @@ +{ + "name": "@mechanic-design/transition", + "version": "2.0.0-beta.10", + "description": "A collection of utilities to help with transitions for animations", + "license": "MIT", + "homepage": "https://github.com/designsystemsinternational/mechanic#readme", + "repository": { + "type": "git", + "url": "https://github.com/designsystemsinternational/mechanic", + "directory": "packages/transition" + }, + "bugs": { + "url": "https://github.com/designsystemsinternational/mechanic/issues", + "email": "i@designsystems.international" + }, + "author": { + "name": "Rune Skjoldborg Madsen", + "email": "i@designsystems.international", + "url": "https://designsystems.international" + }, + "contributors": [ + { + "name": "Fernando Florenzano Hernández", + "email": "i@designsystems.international", + "url": "https://designsystems.international" + }, + { + "name": "Martin Bravo", + "email": "i@designsystems.international", + "url": "https://designsystems.international" + }, + { + "name": "Lucas Dino Nolte", + "email": "lucas@designsystems.international", + "url": "https://designsystems.international" + } + ], + "type": "module", + "exports": { + ".": "./src/index.js" + }, + "dependencies": { + "@mechanic-design/core": "^2.0.0-beta.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } +} diff --git a/packages/transition/src/easings.js b/packages/transition/src/easings.js new file mode 100644 index 0000000..e43756a --- /dev/null +++ b/packages/transition/src/easings.js @@ -0,0 +1,234 @@ +const easeOutBounce = t => { + const scaledTime = t / 1; + + if (scaledTime < 1 / 2.75) { + return 7.5625 * scaledTime * scaledTime; + } else if (scaledTime < 2 / 2.75) { + const scaledTime2 = scaledTime - 1.5 / 2.75; + return 7.5625 * scaledTime2 * scaledTime2 + 0.75; + } else if (scaledTime < 2.5 / 2.75) { + const scaledTime2 = scaledTime - 2.25 / 2.75; + return 7.5625 * scaledTime2 * scaledTime2 + 0.9375; + } else { + const scaledTime2 = scaledTime - 2.625 / 2.75; + return 7.5625 * scaledTime2 * scaledTime2 + 0.984375; + } +}; + +const easeInBounce = t => { + return 1 - easeOutBounce(1 - t); +}; + +/** + * A pre-configured collection of the most commonly used + * easing curves. + * + * Ported from https://gist.github.com/gre/1650294 + * and https://github.com/AndrewRayCode/easing-utils/blob/master/src/easing.js + */ +export const EasingFunctions = { + // no easing, no acceleration + linear: t => t, + + // accelerating from zero velocity + easeInQuad: t => t * t, + + // decelerating to zero velocity + easeOutQuad: t => t * (2 - t), + + // acceleration until halfway, then deceleration + easeInOutQuad: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), + + // accelerating from zero velocity + easeInCubic: t => t * t * t, + + // decelerating to zero velocity + easeOutCubic: t => --t * t * t + 1, + + // acceleration until halfway, then deceleration + easeInOutCubic: t => + t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1, + + // accelerating from zero velocity + easeInQuart: t => t * t * t * t, + + // decelerating to zero velocity + easeOutQuart: t => 1 - --t * t * t * t, + + // acceleration until halfway, then deceleration + easeInOutQuart: t => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t), + + // accelerating from zero velocity + easeInQuint: t => t * t * t * t * t, + + // decelerating to zero velocity + easeOutQuint: t => 1 + --t * t * t * t * t, + + // acceleration until halfway, then deceleration + easeInOutQuint: t => + t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t, + + // Accelerate exponentially until finish + easeInExpo: t => { + if (t === 0) { + return 0; + } + + return Math.pow(2, 10 * (t - 1)); + }, + + // Initial exponential acceleration slowing to stop + easeOutExpo: t => { + if (t === 1) { + return 1; + } + + return -Math.pow(2, -10 * t) + 1; + }, + + // Exponential acceleration and deceleration + easeInOutExpo: t => { + if (t === 0 || t === 1) { + return t; + } + + const scaledTime = t * 2; + const scaledTime1 = scaledTime - 1; + + if (scaledTime < 1) { + return 0.5 * Math.pow(2, 10 * scaledTime1); + } + + return 0.5 * (-Math.pow(2, -10 * scaledTime1) + 2); + }, + + // Increasing velocity until stop + easeInCirc: t => { + const scaledTime = t / 1; + return -1 * (Math.sqrt(1 - scaledTime * t) - 1); + }, + + // Start fast, decreasing velocity until stop + easeOutCirc: t => { + const t1 = t - 1; + return Math.sqrt(1 - t1 * t1); + }, + + // Fast increase in velocity, fast decrease in velocity + easeInOutCirc: t => { + const scaledTime = t * 2; + const scaledTime1 = scaledTime - 2; + + if (scaledTime < 1) { + return -0.5 * (Math.sqrt(1 - scaledTime * scaledTime) - 1); + } + + return 0.5 * (Math.sqrt(1 - scaledTime1 * scaledTime1) + 1); + }, + + // Slow movement backwards then fast snap to finish + easeInBack: (t, magnitude = 1.70158) => { + return t * t * ((magnitude + 1) * t - magnitude); + }, + + // Fast snap to backwards point then slow resolve to finish + easeOutBack: (t, magnitude = 1.70158) => { + const scaledTime = t / 1 - 1; + + return ( + scaledTime * scaledTime * ((magnitude + 1) * scaledTime + magnitude) + 1 + ); + }, + + // Slow movement backwards, fast snap to past finish, slow resolve to finish + easeInOutBack: (t, magnitude = 1.70158) => { + const scaledTime = t * 2; + const scaledTime2 = scaledTime - 2; + + const s = magnitude * 1.525; + + if (scaledTime < 1) { + return 0.5 * scaledTime * scaledTime * ((s + 1) * scaledTime - s); + } + + return 0.5 * (scaledTime2 * scaledTime2 * ((s + 1) * scaledTime2 + s) + 2); + }, + + // Bounces slowly then quickly to finish + easeInElastic: (t, magnitude = 0.7) => { + if (t === 0 || t === 1) { + return t; + } + + const scaledTime = t / 1; + const scaledTime1 = scaledTime - 1; + + const p = 1 - magnitude; + const s = (p / (2 * Math.PI)) * Math.asin(1); + + return -( + Math.pow(2, 10 * scaledTime1) * + Math.sin(((scaledTime1 - s) * (2 * Math.PI)) / p) + ); + }, + + // Fast acceleration, bounces to zero + easeOutElastic: (t, magnitude = 0.7) => { + if (t === 0 || t === 1) { + return t; + } + + const p = 1 - magnitude; + const scaledTime = t * 2; + + const s = (p / (2 * Math.PI)) * Math.asin(1); + return ( + Math.pow(2, -10 * scaledTime) * + Math.sin(((scaledTime - s) * (2 * Math.PI)) / p) + + 1 + ); + }, + + // Slow start and end, two bounces sandwich a fast motion + easeInOutElastic: (t, magnitude = 0.65) => { + if (t === 0 || t === 1) { + return t; + } + + const p = 1 - magnitude; + const scaledTime = t * 2; + const scaledTime1 = scaledTime - 1; + + const s = (p / (2 * Math.PI)) * Math.asin(1); + + if (scaledTime < 1) { + return ( + -0.5 * + (Math.pow(2, 10 * scaledTime1) * + Math.sin(((scaledTime1 - s) * (2 * Math.PI)) / p)) + ); + } + + return ( + Math.pow(2, -10 * scaledTime1) * + Math.sin(((scaledTime1 - s) * (2 * Math.PI)) / p) * + 0.5 + + 1 + ); + }, + + // Bounce to completion + easeOutBounce, + + // Bounce increasing in velocity until completion + easeInBounce, + + // Bounce in and bounce out + easeInOutBounce: t => { + if (t < 0.5) { + return easeInBounce(t * 2) * 0.5; + } + + return easeOutBounce(t * 2 - 1) * 0.5 + 0.5; + } +}; diff --git a/packages/transition/src/index.js b/packages/transition/src/index.js new file mode 100644 index 0000000..852bbff --- /dev/null +++ b/packages/transition/src/index.js @@ -0,0 +1,54 @@ +import { EasingFunctions } from "./easings.js"; + +const defaultOptions = { + from: 0, + to: 0, + duration: 0, + delay: 0, + easing: "linear" +}; + +/** + * Turns the user specified easing into a function that can + * be executed by the transition utility. + * + * Easing can either be a string referencing one of the + * pre-built easing functions or a custom function. + * + * @params{string|function} easing + */ +const resolveEasing = easing => { + // If the user specified a function for the easing, + // we’re using that + if (typeof easing === "function") return easing; + + // If not, we check if we can return a pre-built + // easing function + if (EasingFunctions[easing]) return EasingFunctions[easing]; + + // Lastly we need to throw + throw new Error( + `Unexpected easing (${easing}) passed to transition function. Easing should either be a custom function or one of the pre-built values: ${Object.keys( + EasingFunctions + ).join(", ")}.` + ); +}; + +export const transition = (_options = {}) => { + const options = Object.assign({}, defaultOptions, _options); + const easing = resolveEasing(options.easing); + const { from, to, duration, delay } = options; + const change = to - from; + + return tick => { + const currentTime = Math.max(0, tick - delay); + + if (currentTime >= duration) return to; + + const t = currentTime / duration; + + return change * easing(t) + from; + }; +}; + +export const namedEasings = Object.keys(EasingFunctions);