├── .gitignore
├── LICENSE
├── README.md
├── docs
└── static
│ ├── ae-expression-engine.jpg
│ └── header-img.svg
├── jest.config.js
├── package-lock.json
├── package.json
├── prettier.config.js
├── rollup.config.js
└── src
├── index.test.ts
├── index.ts
└── interpolators.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # Dependency directories
9 | node_modules/
10 |
11 | # Logs
12 | logs
13 | *.log
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 |
18 | # Optional npm cache directory
19 | .npm
20 |
21 | # Optional eslint cache
22 | .eslintcache
23 |
24 | # dotenv environment variables file
25 | .env
26 |
27 | # Coverage directory used by tools like istanbul
28 | coverage/
29 |
30 | # IDEs and editors
31 | /.idea
32 | .project
33 | .classpath
34 | .c9/
35 | *.launch
36 | .settings/
37 | *.sublime-workspace
38 |
39 | # IDE - VSCode
40 | .vscode/*
41 | !.vscode/settings.json
42 | !.vscode/tasks.json
43 | !.vscode/launch.json
44 | !.vscode/extensions.json
45 |
46 | # misc
47 | /.sass-cache
48 | /connect.lock
49 | /coverage
50 | /libpeerconnection.log
51 | npm-debug.log
52 | testem.log
53 | /typings
54 | yarn-error.log
55 |
56 | # e2e
57 | /e2e/*.js
58 | /e2e/*.map
59 |
60 | # System Files
61 | .DS_Store
62 | Thumbs.db
63 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tim Haywood
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🔑 aeFunctions
2 |
3 | **Keyframe animation in After Effects Expressions**
4 |
5 | ---
6 |
7 | ✨ View more details on our website: **[motiondeveloper.com/tools/eKeys](https://www.motiondeveloper.com/tools/ekeys)**
8 |
9 | ---
10 |
11 | - Animate dynamically with expressions
12 | - Full control over easing
13 | - Simple and keyframe-like API
14 |
15 | ---
16 |
17 | 🏗 This project was created with [create-expression-lib](https://github.com/motiondeveloper/create-expression-lib) - our utility for creating and managing After Effects `.jsx` libraries.
18 |
19 | ---
20 |
21 | ## Setup
22 |
23 | 1. Download the latest version from the [releases](https://github.com/motiondeveloper/ekeys/releases) page.
24 | 2. Import it into After Effects
25 |
26 | ## Expression
27 |
28 | Usage:
29 |
30 | ```js
31 | const { animate } = footage('eKeys.jsx').sourceData;
32 | animate([
33 | {
34 | keyTime: 0,
35 | keyValue: [0, 0],
36 | easeOut: 90,
37 | }, {
38 | keyTime: 3,
39 | keyValue: [960, 540],
40 | easeIn: 80,
41 | }
42 | ]);
43 | ```
44 |
45 | ## Development
46 |
47 | 1. **Clone project locally**
48 |
49 | ```sh
50 | git clone https://github.com/motiondeveloper/eKeys.git
51 | cd aeFunctions
52 | ```
53 |
54 | 2. **Start Rollup**
55 |
56 | Start Rollup in watch mode to automatically refresh your code as you make changes, by running:
57 |
58 | ```sh
59 | npm run watch
60 | ```
61 |
62 | _You can run also run a once off build:_ `npm run build`
63 |
64 | 3. **Edit the `src` files**
65 |
66 | _The `index.ts` contains an example expression setup._
67 |
68 | Any values exported from this file will be included in your library, for example:
69 |
70 | ```js
71 | export { someValue };
72 | ```
73 |
74 | 4. **Import the `dist` file into After Effects**
75 |
76 | Use the compiled output file as you would any other `.jsx` library. Any changes to the `src` files will be live updated, and After Effects will update the result of your expression.
77 |
--------------------------------------------------------------------------------
/docs/static/ae-expression-engine.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motiondeveloper/eKeys/7e839a808f1022d0366093d4192676945de57369/docs/static/ae-expression-engine.jpg
--------------------------------------------------------------------------------
/docs/static/header-img.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
484 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ekeys",
3 | "version": "4.4.1",
4 | "description": "Animation engine for After Effects expressions",
5 | "private": true,
6 | "main": "dist/eKeys.jsx",
7 | "scripts": {
8 | "test": "jest --watch",
9 | "tsc": "tsc",
10 | "build": "rollup -c",
11 | "watch": "rollup -cw",
12 | "release": "npm run build && gh release create $npm_package_version $npm_package_main"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/motiondeveloper/eKeys.git"
17 | },
18 | "keywords": [
19 | "after",
20 | "effects",
21 | "expressions",
22 | "easing"
23 | ],
24 | "author": "Tim Haywood",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/motiondeveloper/eKeys/issues"
28 | },
29 | "homepage": "https://github.com/motiondeveloper/eKeys#readme",
30 | "devDependencies": {
31 | "@rollup/plugin-replace": "^2.3.3",
32 | "@rollup/plugin-typescript": "^5.0.2",
33 | "@types/jest": "^26.0.13",
34 | "jest": "^26.4.2",
35 | "prettier": "^1.19.1",
36 | "rollup": "^2.27.0",
37 | "rollup-plugin-ae-jsx": "^2.0.0",
38 | "ts-jest": "^26.3.0",
39 | "tslib": "^2.0.1",
40 | "typescript": "^3.9.7"
41 | },
42 | "dependencies": {
43 | "expression-globals-typescript": "^3.0.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'es5',
3 | singleQuote: true
4 | }
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 | import replace from '@rollup/plugin-replace';
3 | import afterEffectJsx from 'rollup-plugin-ae-jsx';
4 | import pkg from './package.json';
5 |
6 | export default {
7 | input: 'src/index.ts',
8 | output: {
9 | file: pkg.main,
10 | format: 'es',
11 | },
12 | external: Object.keys(pkg.dependencies),
13 | plugins: [
14 | replace({
15 | _npmVersion: pkg.version,
16 | }),
17 | typescript({
18 | module: 'esnext',
19 | target: 'esnext',
20 | noImplicitAny: true,
21 | moduleResolution: 'node',
22 | strict: true,
23 | }),
24 | afterEffectJsx(),
25 | ],
26 | };
27 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { Layer } from 'expression-globals-typescript';
2 | import { animate, interpolators } from './index';
3 |
4 | // This let's Jest: ReferenceError: Cannot access before initialization
5 | // https://stackoverflow.com/questions/61157392/jest-mock-aws-sdk-referenceerror-cannot-access-before-initialization
6 | jest.mock('expression-globals-typescript', () => {
7 | const mLayer = { getItem: jest.fn().mockReturnThis(), promise: jest.fn() };
8 | return { Layer: jest.fn(() => mLayer) };
9 | });
10 |
11 | test('animates between default keys', () => {
12 | expect(
13 | animate(
14 | [
15 | { keyTime: 0, keyValue: 0 },
16 | { keyTime: 1, keyValue: 1 },
17 | ],
18 | { inputTime: 0.5 }
19 | )
20 | ).toBe(0.5);
21 | });
22 |
23 | test('animates between eased keys', () => {
24 | expect(
25 | animate(
26 | [
27 | { keyTime: 0, keyValue: 0, easeOut: 100 },
28 | { keyTime: 1, keyValue: 1, easeIn: 100 },
29 | ],
30 | { inputTime: 0.5 }
31 | )
32 | ).toBe(0.5);
33 | });
34 |
35 | test('animates between custom velocity keys', () => {
36 | expect(
37 | animate(
38 | [
39 | { keyTime: 0, keyValue: 0, velocityOut: 20 },
40 | { keyTime: 1, keyValue: 1, velocityIn: 20 },
41 | ],
42 | { inputTime: 0.5 }
43 | )
44 | ).toBe(0.5);
45 | });
46 |
47 | test('re-orders keys by time', () => {
48 | expect(
49 | animate(
50 | [
51 | { keyTime: 2, keyValue: 1 },
52 | { keyTime: 3, keyValue: 0 },
53 | ],
54 | { inputTime: 2.5 }
55 | )
56 | ).toBe(0.5);
57 | });
58 |
59 | test('animates between arrays', () => {
60 | expect(
61 | animate(
62 | [
63 | { keyTime: 0, keyValue: [0, 0, 0] },
64 | { keyTime: 1, keyValue: [1, 1, 1] },
65 | ],
66 | { inputTime: 0.5 }
67 | )
68 | ).toEqual([0.5, 0.5, 0.5]);
69 | });
70 |
71 | test('animates with custom linear interpolator', () => {
72 | expect(
73 | animate(
74 | [
75 | { keyTime: 0, keyValue: 0 },
76 | { keyTime: 1, keyValue: 1 },
77 | ],
78 | { inputTime: 0.5, interpolator: x => x }
79 | )
80 | ).toBe(0.5);
81 | });
82 |
83 | test('animates with provided linear interpolator', () => {
84 | expect(
85 | animate(
86 | [
87 | { keyTime: 0, keyValue: 0 },
88 | { keyTime: 1, keyValue: 1 },
89 | ],
90 | { inputTime: 0.5, interpolator: interpolators.linear }
91 | )
92 | ).toBe(0.5);
93 | });
94 |
95 | test('animates with provided easeInOutQuad interpolator', () => {
96 | expect(
97 | animate(
98 | [
99 | { keyTime: 0, keyValue: 0 },
100 | { keyTime: 1, keyValue: 1 },
101 | ],
102 | { inputTime: 0.25, interpolator: interpolators.easeInOutQuad }
103 | )
104 | ).toBe(0.125);
105 | });
106 |
107 | test('animates with provided elastic interpolator', () => {
108 | expect(
109 | animate(
110 | [
111 | { keyTime: 0, keyValue: 0 },
112 | { keyTime: 2, keyValue: 1 },
113 | ],
114 | { inputTime: 2, interpolator: interpolators.easeInElastic }
115 | )
116 | ).toBe(1);
117 | });
118 |
119 | test('animates with custom quad interpolator', () => {
120 | expect(
121 | animate(
122 | [
123 | { keyTime: 0, keyValue: 0 },
124 | { keyTime: 1, keyValue: 1 },
125 | ],
126 | {
127 | inputTime: 0.3,
128 | interpolator: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
129 | }
130 | )
131 | ).toBe(0.18);
132 | });
133 |
134 | test('errors if keys are different dimensions', () => {
135 | expect(() =>
136 | animate(
137 | [
138 | { keyTime: 0, keyValue: 0 },
139 | { keyTime: 1, keyValue: [1, 1, 1] },
140 | ],
141 | { inputTime: 0.5 }
142 | )
143 | ).toThrowError(
144 | 'Keyframe 0 and 1 values must be of the same type. Received number and array'
145 | );
146 | });
147 |
148 | test('errors if a key value is missing', () => {
149 | expect(() =>
150 | // @ts-ignore
151 | animate([{ keyTime: 0 }, { keyTime: 1, keyValue: 1 }], { inputTime: 0.5 })
152 | ).toThrowError('keyValue is required in keyframe 0');
153 | });
154 |
155 | test('errors if a key time is missing', () => {
156 | expect(() =>
157 | // @ts-ignore
158 | animate([{ keyValue: 0 }, { keyTime: 1, keyValue: 1 }], { inputTime: 0.5 })
159 | ).toThrowError('keyValue is required in keyframe 0');
160 | });
161 |
162 | test('errors if a key time is a string', () => {
163 | expect(() =>
164 | animate(
165 | [
166 | // @ts-ignore
167 | { keyValue: 0, keyTime: '0' },
168 | { keyTime: 1, keyValue: 1 },
169 | ],
170 | { inputTime: 0.5 }
171 | )
172 | ).toThrowError('Keyframe 0 time must be of type number. Received string');
173 | });
174 |
175 | test('errors if a key value is a string', () => {
176 | expect(() =>
177 | animate(
178 | [
179 | // @ts-ignore
180 | { keyValue: '0', keyTime: 0 },
181 | { keyTime: 1, keyValue: 1 },
182 | ],
183 | { inputTime: 0.5 }
184 | )
185 | ).toThrowError(
186 | 'Keyframe 0 value must be of type number,array. Received string'
187 | );
188 | });
189 |
190 | test('errors if easeIn is a string', () => {
191 | expect(() =>
192 | animate(
193 | [
194 | // @ts-ignore
195 | { keyValue: 0, keyTime: 0, easeIn: '0' },
196 | { keyTime: 1, keyValue: 1 },
197 | ],
198 | { inputTime: 0.5 }
199 | )
200 | ).toThrowError('Keyframe 0 easeIn must be of type number. Received string');
201 | });
202 |
203 | test('errors if easeOut is a string', () => {
204 | expect(() =>
205 | animate(
206 | [
207 | // @ts-ignore
208 | { keyValue: 0, keyTime: 0, easeOut: '0' },
209 | { keyTime: 1, keyValue: 1 },
210 | ],
211 | { inputTime: 0.5 }
212 | )
213 | ).toThrowError('Keyframe 0 easeOut must be of type number. Received string');
214 | });
215 |
216 | test('errors if velocityIn is a string', () => {
217 | expect(() =>
218 | animate(
219 | [
220 | // @ts-ignore
221 | { keyValue: 0, keyTime: 0, velocityIn: '0' },
222 | { keyTime: 1, keyValue: 1 },
223 | ],
224 | { inputTime: 0.5 }
225 | )
226 | ).toThrowError(
227 | 'Keyframe 0 velocityIn must be of type number. Received string'
228 | );
229 | });
230 |
231 | test('errors if velocityOut is a string', () => {
232 | expect(() =>
233 | animate(
234 | [
235 | // @ts-ignore
236 | { keyValue: 0, keyTime: 0, velocityOut: '0' },
237 | { keyTime: 1, keyValue: 1 },
238 | ],
239 | { inputTime: 0.5 }
240 | )
241 | ).toThrowError(
242 | 'Keyframe 0 velocityOut must be of type number. Received string'
243 | );
244 | });
245 |
246 | test('errors on unexpected keyframe property', () => {
247 | expect(() =>
248 | animate(
249 | [
250 | { keyTime: 0, keyValue: 0 },
251 | // @ts-ignore
252 | { keyTime: 1, keyValue: 1, ease: 0 },
253 | ],
254 | { inputTime: 0.5 }
255 | )
256 | ).toThrowError('Unexpected property on keyframe 1: ease');
257 | });
258 |
259 | test('errors if values are arrays of different lengths', () => {
260 | expect(() =>
261 | // @ts-ignore
262 | animate(
263 | [
264 | { keyTime: 0, keyValue: [0, 0] },
265 | { keyTime: 1, keyValue: [1, 1, 1] },
266 | ],
267 | { inputTime: 0.5 }
268 | )
269 | ).toThrowError(
270 | 'Keyframe 0 and 1 values must be of the same dimension. Received 2 and 3'
271 | );
272 | });
273 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Layer, Vector } from 'expression-globals-typescript';
2 | import { Interpolator, interpolators } from './interpolators';
3 |
4 | const thisLayer = new Layer();
5 |
6 | type KeyVal = number | Vector;
7 |
8 | interface InputKey {
9 | keyTime: number;
10 | keyValue: KeyVal;
11 | easeIn?: number;
12 | easeOut?: number;
13 | velocityIn?: number;
14 | velocityOut?: number;
15 | }
16 |
17 | interface EKey extends InputKey {
18 | easeIn: number;
19 | easeOut: number;
20 | velocityIn: number;
21 | velocityOut: number;
22 | }
23 |
24 | interface AnimateOptions {
25 | inputTime: number;
26 | interpolator?: Interpolator;
27 | }
28 |
29 | // The function that's called from After Effects
30 | // as eKeys.animate()
31 | function animate(
32 | inputKeyframes: InputKey[],
33 | options: AnimateOptions = { inputTime: thisLayer.time }
34 | ) {
35 | const { inputTime = thisLayer.time, interpolator } = options;
36 | // Validate function inputs
37 | checkTypes([
38 | ['.animate() input keyframes', inputKeyframes, 'array'],
39 | ['.animate() input time', inputTime, 'number'],
40 | ]);
41 | // Validate and sort the given keys
42 | const validKeys: EKey[] = inputKeyframes
43 | .map((key, index) => validateKeyframe(key, index))
44 | .sort((a, b) => a.keyTime - b.keyTime);
45 |
46 | return animateBetweenKeys(validKeys, inputTime);
47 |
48 | // Returns the final animated value
49 | // This is the function that's returned
50 | function animateBetweenKeys(keys: EKey[], time: number) {
51 | const lastKey: EKey = keys[keys.length - 1];
52 | const firstKey: EKey = keys[0];
53 |
54 | // If outside of all keys, return closest
55 | // key value, skip animation
56 | if (time <= firstKey.keyTime) {
57 | return firstKey.keyValue;
58 | }
59 | if (time >= lastKey.keyTime) {
60 | return lastKey.keyValue;
61 | }
62 |
63 | const curKeyNum: number = getCurrentKeyNum(keys, time);
64 | const curKey: EKey = keys[curKeyNum];
65 | const nextKey: EKey = keys[curKeyNum + 1];
66 |
67 | // Check to see if no animation is
68 | // required between current keys
69 | if (curKey.keyValue === nextKey.keyValue) {
70 | return curKey.keyValue;
71 | }
72 |
73 | // If we're on the keyframe,
74 | // return its value
75 | if (time === curKey.keyTime) {
76 | return curKey.keyValue;
77 | }
78 |
79 | // Incrementing time value that
80 | // starts from the current keyTime
81 | const movedTime: number = Math.max(time - curKey.keyTime, 0);
82 |
83 | // The total duration of the animation
84 | const animationLength: number = nextKey.keyTime - curKey.keyTime;
85 |
86 | // Animation progress amount between 0 and 1
87 | const linearProgress: number = Math.min(1, movedTime / animationLength);
88 |
89 | const easedProgress: number =
90 | interpolator !== undefined
91 | ? // If they've passed in a custom interpolator, use that
92 | // and opt out of bezier generation
93 | interpolator(linearProgress, curKey.easeOut, nextKey.easeIn)
94 | : // Otherwise generate a bezier function and pass in
95 | // the progress amount
96 | bezier(
97 | curKey.easeOut / 100,
98 | curKey.velocityOut / 100,
99 | 1 - nextKey.easeIn / 100,
100 | 1 - nextKey.velocityIn / 100
101 | )(linearProgress);
102 |
103 | // Animate between values according to
104 | // whether they are arrays
105 | if (Array.isArray(curKey.keyValue) && Array.isArray(nextKey.keyValue)) {
106 | return animateArrayFromProgress(
107 | curKey.keyValue,
108 | nextKey.keyValue,
109 | easedProgress
110 | );
111 | }
112 | if (
113 | typeof curKey.keyValue === 'number' &&
114 | typeof nextKey.keyValue === 'number'
115 | ) {
116 | return animateValueFromProgress(
117 | curKey.keyValue,
118 | nextKey.keyValue,
119 | easedProgress
120 | );
121 | }
122 |
123 | // If the keys aren't both arrays
124 | // or numbers, return an error
125 | throw Error(
126 | `Keyframe ${curKeyNum} and ${curKeyNum +
127 | 1} values must be of the same type. Received ${getType(
128 | curKey.keyValue
129 | )} and ${getType(nextKey.keyValue)}`
130 | );
131 |
132 | // Set current key to most recent keyframe
133 | function getCurrentKeyNum(keys: EKey[], time: number): number {
134 | const lastKeyNum = keys.length - 1;
135 | let curKeyNum = 0;
136 | while (curKeyNum < lastKeyNum && time >= keys[curKeyNum + 1].keyTime) {
137 | curKeyNum++;
138 | }
139 | return curKeyNum;
140 | }
141 |
142 | // Performs animation on each element of array individually
143 | function animateArrayFromProgress(
144 | startArray: Vector,
145 | endArray: Vector,
146 | progressAmount: number
147 | ): Vector {
148 | if (startArray.length !== endArray.length) {
149 | // Check that the key values are the same dimension
150 | throw new TypeError(
151 | `Keyframe ${curKeyNum} and ${curKeyNum +
152 | 1} values must be of the same dimension. Received ${
153 | startArray.length
154 | } and ${endArray.length}`
155 | );
156 | } else {
157 | // Arrays are the same length
158 | // TypeScript doesn't know if the Array elements exist in a .map()
159 | // so we need to provide a fallback
160 | // Array Subtraction
161 | const arrayDelta: Vector = endArray.map((dimension, index) => {
162 | const curKeyDim = dimension as number;
163 | const nextKeyDim = startArray[index] as number;
164 | return (curKeyDim - nextKeyDim) as number;
165 | }) as Vector;
166 | // Multiply difference by progress
167 | const deltaProgressed = arrayDelta.map(
168 | item => (item as number) * progressAmount
169 | );
170 | // Add to current key and return
171 | return startArray.map(
172 | (item, index) => ((item as number) + deltaProgressed[index]) as number
173 | ) as Vector;
174 | }
175 | }
176 |
177 | // Animate between values according to progress
178 | function animateValueFromProgress(
179 | startVal: number,
180 | endVal: number,
181 | progressAmount: number
182 | ): number {
183 | const valueDelta = endVal - startVal;
184 | return startVal + valueDelta * progressAmount;
185 | }
186 |
187 | // Creates bezier curve and returns function
188 | // to calculate eased value
189 | function bezier(mX1: number, mY1: number, mX2: number, mY2: number) {
190 | /**
191 | * https://github.com/gre/bezier-easing
192 | * BezierEasing - use bezier curve for transition easing function
193 | * by Gaëtan Renaudeau 2014 - 2015 – MIT License
194 | */
195 |
196 | // These values are established by empiricism with tests (tradeoff: performance VS precision)
197 | const NEWTON_ITERATIONS = 4;
198 | const NEWTON_MIN_SLOPE = 0.001;
199 | const SUBDIVISION_PRECISION = 0.0000001;
200 | const SUBDIVISION_MAX_ITERATIONS = 10;
201 |
202 | const kSplineTableSize = 11;
203 | const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
204 |
205 | const float32ArraySupported = typeof Float32Array === 'function';
206 |
207 | const A = (aA1: number, aA2: number) => 1.0 - 3.0 * aA2 + 3.0 * aA1;
208 | const B = (aA1: number, aA2: number) => 3.0 * aA2 - 6.0 * aA1;
209 | const C = (aA1: number) => 3.0 * aA1;
210 |
211 | // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
212 | const calcBezier = (aT: number, aA1: number, aA2: number) =>
213 | ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
214 |
215 | // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
216 | const getSlope = (aT: number, aA1: number, aA2: number) =>
217 | 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
218 |
219 | const binarySubdivide = (
220 | aX: number,
221 | aA: number,
222 | aB: number,
223 | mX1: number,
224 | mX2: number
225 | ) => {
226 | let currentX;
227 | let currentT;
228 | let i = 0;
229 | do {
230 | currentT = aA + (aB - aA) / 2.0;
231 | currentX = calcBezier(currentT, mX1, mX2) - aX;
232 | if (currentX > 0.0) {
233 | aB = currentT;
234 | } else {
235 | aA = currentT;
236 | }
237 | } while (
238 | Math.abs(currentX) > SUBDIVISION_PRECISION &&
239 | ++i < SUBDIVISION_MAX_ITERATIONS
240 | );
241 | return currentT;
242 | };
243 |
244 | const newtonRaphsonIterate = (
245 | aX: number,
246 | aGuessT: number,
247 | mX1: number,
248 | mX2: number
249 | ) => {
250 | for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
251 | const currentSlope = getSlope(aGuessT, mX1, mX2);
252 | if (currentSlope === 0.0) {
253 | return aGuessT;
254 | }
255 | const currentX = calcBezier(aGuessT, mX1, mX2) - aX;
256 | aGuessT -= currentX / currentSlope;
257 | }
258 | return aGuessT;
259 | };
260 |
261 | const LinearEasing = (x: number) => x;
262 |
263 | if (!(mX1 >= 0 && mX1 <= 1 && mX2 >= 0 && mX2 <= 1)) {
264 | throw new Error('bezier x values must be in [0, 1] range');
265 | }
266 |
267 | if (mX1 === mY1 && mX2 === mY2) {
268 | return LinearEasing;
269 | }
270 |
271 | // Precompute samples table
272 | const sampleValues = float32ArraySupported
273 | ? new Float32Array(kSplineTableSize)
274 | : new Array(kSplineTableSize);
275 | for (let i = 0; i < kSplineTableSize; ++i) {
276 | sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2);
277 | }
278 |
279 | const getTForX = (aX: number) => {
280 | let intervalStart = 0.0;
281 | let currentSample = 1;
282 | const lastSample = kSplineTableSize - 1;
283 |
284 | for (
285 | ;
286 | currentSample !== lastSample && sampleValues[currentSample] <= aX;
287 | ++currentSample
288 | ) {
289 | intervalStart += kSampleStepSize;
290 | }
291 | --currentSample;
292 |
293 | // Interpolate to provide an initial guess for t
294 | const dist =
295 | (aX - sampleValues[currentSample]) /
296 | (sampleValues[currentSample + 1] - sampleValues[currentSample]);
297 | const guessForT = intervalStart + dist * kSampleStepSize;
298 | const initialSlope = getSlope(guessForT, mX1, mX2);
299 |
300 | if (initialSlope >= NEWTON_MIN_SLOPE) {
301 | return newtonRaphsonIterate(aX, guessForT, mX1, mX2);
302 | }
303 | if (initialSlope === 0.0) {
304 | return guessForT;
305 | }
306 | return binarySubdivide(
307 | aX,
308 | intervalStart,
309 | intervalStart + kSampleStepSize,
310 | mX1,
311 | mX2
312 | );
313 | };
314 |
315 | const bezierEasing = (x: number) => {
316 | if (x === 0) {
317 | return 0;
318 | }
319 | if (x === 1) {
320 | return 1;
321 | }
322 | return calcBezier(getTForX(x), mY1, mY2);
323 | };
324 |
325 | return bezierEasing;
326 | }
327 | }
328 |
329 | // Make sure that a given keyframe is valid
330 | // Sets defaults and checks for errors
331 | function validateKeyframe(key: InputKey, index: number): EKey {
332 | // Set keyframe defaults
333 | const {
334 | keyTime,
335 | keyValue,
336 | easeIn = 33,
337 | easeOut = 33,
338 | velocityIn = 0,
339 | velocityOut = 0,
340 | ...nonValidKeyProps
341 | } = key;
342 | const invalidPropNames = Object.getOwnPropertyNames(nonValidKeyProps);
343 | if (invalidPropNames.length !== 0) {
344 | const triedTime = invalidPropNames.indexOf('time') > -1;
345 | const triedValue = invalidPropNames.indexOf('value') > -1;
346 | // Extra parameters were added to a keyframe
347 | throw new Error(
348 | `Unexpected property on keyframe ${index}: ${invalidPropNames.join(
349 | ', '
350 | )}${triedTime ? `. Did you mean "keyTime"?` : ``}${
351 | triedValue ? `. Did you mean "keyValue"?` : ``
352 | }`
353 | );
354 | }
355 | // Alert the user if an eKey is missing
356 | // the required arguments
357 | if (keyTime == null) {
358 | requiredArgumentError('keyValue', `keyframe ${index}`);
359 | }
360 | if (keyValue == null) {
361 | requiredArgumentError('keyValue', `keyframe ${index}`);
362 | }
363 |
364 | // Check data types of keyframe parameters
365 | checkTypes([
366 | [`Keyframe ${index} time`, keyTime, `number`],
367 | [`Keyframe ${index} value`, keyValue, [`number`, `array`]],
368 | [`Keyframe ${index} easeIn`, easeIn, `number`],
369 | [`Keyframe ${index} easeOut`, easeOut, `number`],
370 | [`Keyframe ${index} velocityIn`, velocityIn, `number`],
371 | [`Keyframe ${index} velocityOut`, velocityOut, `number`],
372 | ]);
373 |
374 | // Return validated keyframe
375 | const validKey: EKey = {
376 | keyTime,
377 | keyValue,
378 | easeIn,
379 | easeOut,
380 | velocityIn,
381 | velocityOut,
382 | };
383 |
384 | return validKey;
385 | }
386 |
387 | // Loops through an array of the format
388 | // [name, variable, 'expectedType']
389 | // and checks if each variable is of the expected type and
390 | // returns a TypeError if it's not
391 | type checking = [string, any, string | string[]];
392 | type checkingArray = checking[];
393 | function checkTypes(checkingArray: checkingArray) {
394 | checkingArray.map(checking => {
395 | const name: string = checking[0];
396 | const argumentType: string = getType(checking[1]);
397 | const expectedType: string | string[] = checking[2];
398 | if (!isValidType(argumentType, expectedType)) {
399 | typeErrorMessage(name, expectedType, argumentType);
400 | }
401 | });
402 | }
403 |
404 | // More reliable version of standard js typeof
405 | function getType(value: any) {
406 | return Object.prototype.toString
407 | .call(value)
408 | .replace(/^\[object |\]$/g, '')
409 | .toLowerCase();
410 | }
411 |
412 | // Error message template for an incorrect type
413 | function typeErrorMessage(
414 | variableName: string,
415 | expectedType: string | string[],
416 | receivedType: string
417 | ) {
418 | throw new TypeError(
419 | `${variableName} must be of type ${expectedType}. Received ${receivedType}`
420 | );
421 | }
422 |
423 | // Error message template for missing required argument
424 | function requiredArgumentError(variableName: string, functionName: string) {
425 | throw new Error(`${variableName} is required in ${functionName}`);
426 | }
427 |
428 | // Checks if a variable type matches the given expected type
429 | // expected type can be array of types
430 | function isValidType(argumentType: string, expectedType: string | string[]) {
431 | if (getType(expectedType) === 'string') {
432 | return argumentType === expectedType;
433 | }
434 | // Could also check using getType()
435 | // but then typescript would complain
436 | if (Array.isArray(expectedType)) {
437 | return expectedType.filter(type => argumentType === type).length > 0;
438 | }
439 | return typeErrorMessage(
440 | 'The expected type',
441 | 'string or array',
442 | getType(expectedType)
443 | );
444 | }
445 | }
446 |
447 | const version: string = '_npmVersion';
448 |
449 | export { animate, version, interpolators };
450 |
--------------------------------------------------------------------------------
/src/interpolators.ts:
--------------------------------------------------------------------------------
1 | export type Interpolator = (
2 | progress: number,
3 | easeOut?: number,
4 | easeIn?: number
5 | ) => number;
6 |
7 | export const interpolators: Record = {
8 | // no easing, no acceleration
9 | linear: t => t,
10 | // accelerating from zero velocity
11 | easeInQuad: t => t * t,
12 | // decelerating to zero velocity
13 | easeOutQuad: t => t * (2 - t),
14 | // acceleration until halfway, then deceleration
15 | easeInOutQuad: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
16 | // accelerating from zero velocity
17 | easeInCubic: t => t * t * t,
18 | // decelerating to zero velocity
19 | easeOutCubic: t => --t * t * t + 1,
20 | // acceleration until halfway, then deceleration
21 | easeInOutCubic: t =>
22 | t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
23 | // accelerating from zero velocity
24 | easeInQuart: t => t * t * t * t,
25 | // decelerating to zero velocity
26 | easeOutQuart: t => 1 - --t * t * t * t,
27 | // acceleration until halfway, then deceleration
28 | easeInOutQuart: t => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t),
29 | // accelerating from zero velocity
30 | easeInQuint: t => t * t * t * t * t,
31 | // decelerating to zero velocity
32 | easeOutQuint: t => 1 + --t * t * t * t * t,
33 | // acceleration until halfway, then deceleration
34 | easeInOutQuint: t =>
35 | t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t,
36 | easeInElastic: t => (0.04 - 0.04 / t) * Math.sin(25 * t) + 1,
37 | easeOutElastic: t => ((0.04 * t) / --t) * Math.sin(25 * t),
38 | easeInOutElastic: t =>
39 | (t -= 0.5) < 0
40 | ? (0.02 + 0.01 / t) * Math.sin(50 * t)
41 | : (0.02 - 0.01 / t) * Math.sin(50 * t) + 1,
42 | };
43 |
--------------------------------------------------------------------------------