├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── src
├── __snapshots__
│ └── index.test.ts.snap
├── index.test.ts
├── index.ts
└── types.d.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | lib
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "semi": false,
5 | "jsxBracketSameLine": true
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | #### Generate css keyframes in css-in-js based on a spring algorithm, with emotion: https://github.com/emotion-js/emotion.
4 |
5 | Spring transform properties: `transformX`, `transformY`, `scale3d`, as `x`, `y`, and `scale`, as well as `opacity`.
6 |
7 | - The default export is wrapped in emotion's `keyframes`, however you can also export `{ spring }` which returns an array you can join and use with other css-in-js solutions. (I think...)
8 |
9 | Note: for scale, be sure to use a higher precision, like 4.
10 |
11 | ### Example
12 |
13 | This example is done for a react app, but can easily work without react with `emotion`
14 |
15 | ```
16 | import spring from 'spring-keyframes'
17 | import styled from 'react-emotion'
18 |
19 | const options = {
20 | stiffness: 80,
21 | damping: 50,
22 | precision: 4,
23 | unit: 'px',
24 | }
25 |
26 | const Component = styled.div`
27 | animation: ${spring({
28 | from: {
29 | opacity: 0,
30 | x: 0,
31 | scale: 0
32 | },
33 | to: {
34 | opacity: 1,
35 | x: 100,
36 | scale: 1
37 | }
38 | }, options)} 300ms;
39 | animation-fill-mode: both;
40 | `
41 | ```
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spring-keyframes",
3 | "description": "Use javascript to generate css keyframe animations based on spring physics with emotion",
4 | "version": "2.0.1",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "files": [
8 | "lib"
9 | ],
10 | "author": "hemlok",
11 | "keywords": [
12 | "emotion",
13 | "css-in-js",
14 | "css-animations",
15 | "spring",
16 | "react-motion"
17 | ],
18 | "scripts": {
19 | "test": "NODE_ENV=test jest --watch",
20 | "build": "tsc"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/hemlok/spring-keyframes"
25 | },
26 | "license": "MIT",
27 | "dependencies": {
28 | "springer": "0.0.1"
29 | },
30 | "devDependencies": {
31 | "@emotion/core": "^10.0.16",
32 | "@types/jest": "24.0.17",
33 | "@types/react": "16.9.2",
34 | "jest": "24.9.0",
35 | "prettier": "1.18.2",
36 | "react": "16.9.0",
37 | "react-dom": "16.9.0",
38 | "ts-jest": "24.0.2",
39 | "typescript": "3.5.3"
40 | },
41 | "peerDependencies": {
42 | "@emotion/core": "~10"
43 | },
44 | "jest": {
45 | "roots": [
46 | "/src"
47 | ],
48 | "transform": {
49 | "^.+\\.tsx?$": "ts-jest"
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/__snapshots__/index.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`returns a keyframe array, with duplicate values removed 1`] = `
4 | Array [
5 | "0% {opacity: 0.01;transform: translateX(0.08px) scale3d(0.01, 0.01, 1);}",
6 | "1% {opacity: 0.02;transform: translateX(1.88px) scale3d(0.02, 0.02, 1);}",
7 | "2% {opacity: 0.06;transform: translateX(5.7px) scale3d(0.06, 0.06, 1);}",
8 | "3% {opacity: 0.12;transform: translateX(11.09px) scale3d(0.12, 0.12, 1);}",
9 | "4% {opacity: 0.18;transform: translateX(17.63px) scale3d(0.18, 0.18, 1);}",
10 | "5% {opacity: 0.24;transform: translateX(23.71px) scale3d(0.24, 0.24, 1);}",
11 | "6% {opacity: 0.32;transform: translateX(31.47px) scale3d(0.32, 0.32, 1);}",
12 | "7% {opacity: 0.4;transform: translateX(39.49px) scale3d(0.4, 0.4, 1);}",
13 | "8% {opacity: 0.48;transform: translateX(47.52px) scale3d(0.48, 0.48, 1);}",
14 | "9% {opacity: 0.56;transform: translateX(55.38px) scale3d(0.56, 0.56, 1);}",
15 | "10% {opacity: 0.62;transform: translateX(61.68px) scale3d(0.62, 0.62, 1);}",
16 | "11% {opacity: 0.69;transform: translateX(68.84px) scale3d(0.69, 0.69, 1);}",
17 | "12% {opacity: 0.76;transform: translateX(75.47px) scale3d(0.76, 0.76, 1);}",
18 | "13% {opacity: 0.82;transform: translateX(81.5px) scale3d(0.82, 0.82, 1);}",
19 | "14% {opacity: 0.87;transform: translateX(86.91px) scale3d(0.87, 0.87, 1);}",
20 | "15% {opacity: 0.91;transform: translateX(90.92px) scale3d(0.91, 0.91, 1);}",
21 | "16% {opacity: 0.96;transform: translateX(95.14px) scale3d(0.96, 0.96, 1);}",
22 | "17% {opacity: 0.99;transform: translateX(98.73px) scale3d(0.99, 0.99, 1);}",
23 | "18% {opacity: 1.02;transform: translateX(101.71px) scale3d(1.02, 1.02, 1);}",
24 | "19% {opacity: 1.05;transform: translateX(104.12px) scale3d(1.05, 1.05, 1);}",
25 | "20% {opacity: 1.06;transform: translateX(105.73px) scale3d(1.06, 1.06, 1);}",
26 | "21% {opacity: 1.08;transform: translateX(107.22px) scale3d(1.08, 1.08, 1);}",
27 | "22% {opacity: 1.09;transform: translateX(108.27px) scale3d(1.09, 1.09, 1);}",
28 | "23% {opacity: 1.09;transform: translateX(108.94px) scale3d(1.09, 1.09, 1);}",
29 | "24% {opacity: 1.1;transform: translateX(109.27px) scale3d(1.1, 1.1, 1);}",
30 | "25% {opacity: 1.1;transform: translateX(109.34px) scale3d(1.1, 1.1, 1);}",
31 | "26% {opacity: 1.1;transform: translateX(109.2px) scale3d(1.1, 1.1, 1);}",
32 | "27% {opacity: 1.09;transform: translateX(108.87px) scale3d(1.09, 1.09, 1);}",
33 | "28% {opacity: 1.09;transform: translateX(108.38px) scale3d(1.09, 1.09, 1);}",
34 | "29% {opacity: 1.08;transform: translateX(107.78px) scale3d(1.08, 1.08, 1);}",
35 | "30% {opacity: 1.08;transform: translateX(107.22px) scale3d(1.08, 1.08, 1);}",
36 | "31% {opacity: 1.07;transform: translateX(106.5px) scale3d(1.07, 1.07, 1);}",
37 | "32% {opacity: 1.06;transform: translateX(105.76px) scale3d(1.06, 1.06, 1);}",
38 | "33% {opacity: 1.06;transform: translateX(105.01px) scale3d(1.06, 1.06, 1);}",
39 | "34% {opacity: 1.05;transform: translateX(104.27px) scale3d(1.05, 1.05, 1);}",
40 | "35% {opacity: 1.04;transform: translateX(103.68px) scale3d(1.04, 1.04, 1);}",
41 | "36% {opacity: 1.03;transform: translateX(103px) scale3d(1.03, 1.03, 1);}",
42 | "37% {opacity: 1.03;transform: translateX(102.38px) scale3d(1.03, 1.03, 1);}",
43 | "38% {opacity: 1.02;transform: translateX(101.81px) scale3d(1.02, 1.02, 1);}",
44 | "39% {opacity: 1.02;transform: translateX(101.29px) scale3d(1.02, 1.02, 1);}",
45 | "40% {opacity: 1.01;transform: translateX(100.91px) scale3d(1.01, 1.01, 1);}",
46 | "41% {opacity: 1.01;transform: translateX(100.51px) scale3d(1.01, 1.01, 1);}",
47 | "42% {opacity: 1.01;transform: translateX(100.17px) scale3d(1.01, 1.01, 1);}",
48 | "43% {opacity: 1;transform: translateX(99.88px) scale3d(1, 1, 1);}",
49 | "44% {opacity: 1;transform: translateX(99.65px) scale3d(1, 1, 1);}",
50 | "45% {opacity: 1;transform: translateX(99.49px) scale3d(1, 1, 1);}",
51 | "46% {opacity: 1;transform: translateX(99.35px) scale3d(1, 1, 1);}",
52 | "47% {opacity: 1;transform: translateX(99.24px) scale3d(1, 1, 1);}",
53 | "48% {opacity: 1;transform: translateX(99.18px) scale3d(1, 1, 1);}",
54 | "49% {opacity: 1;transform: translateX(99.14px) scale3d(1, 1, 1);}",
55 | "50% {opacity: 1;transform: translateX(99.13px) scale3d(1, 1, 1);}",
56 | "51% {opacity: 1;transform: translateX(99.14px) scale3d(1, 1, 1);}",
57 | "52% {opacity: 1;transform: translateX(99.17px) scale3d(1, 1, 1);}",
58 | "53% {opacity: 1;transform: translateX(99.22px) scale3d(1, 1, 1);}",
59 | "54% {opacity: 1;transform: translateX(99.27px) scale3d(1, 1, 1);}",
60 | "55% {opacity: 1;transform: translateX(99.32px) scale3d(1, 1, 1);}",
61 | "56% {opacity: 1;transform: translateX(99.39px) scale3d(1, 1, 1);}",
62 | "57% {opacity: 1;transform: translateX(99.46px) scale3d(1, 1, 1);}",
63 | "58% {opacity: 1;transform: translateX(99.53px) scale3d(1, 1, 1);}",
64 | "59% {opacity: 1;transform: translateX(99.6px) scale3d(1, 1, 1);}",
65 | "60% {opacity: 1;transform: translateX(99.65px) scale3d(1, 1, 1);}",
66 | "61% {opacity: 1;transform: translateX(99.72px) scale3d(1, 1, 1);}",
67 | "62% {opacity: 1;transform: translateX(99.78px) scale3d(1, 1, 1);}",
68 | "63% {opacity: 1;transform: translateX(99.83px) scale3d(1, 1, 1);}",
69 | "64% {opacity: 1;transform: translateX(99.88px) scale3d(1, 1, 1);}",
70 | "65% {opacity: 1;transform: translateX(99.91px) scale3d(1, 1, 1);}",
71 | "66% {opacity: 1;transform: translateX(99.95px) scale3d(1, 1, 1);}",
72 | "67% {opacity: 1;transform: translateX(99.99px) scale3d(1, 1, 1);}",
73 | "68% {opacity: 1.01;transform: translateX(100.01px) scale3d(1.01, 1.01, 1);}",
74 | "69% {opacity: 1.01;transform: translateX(100.04px) scale3d(1.01, 1.01, 1);}",
75 | "70% {opacity: 1.01;transform: translateX(100.05px) scale3d(1.01, 1.01, 1);}",
76 | "71% {opacity: 1.01;transform: translateX(100.06px) scale3d(1.01, 1.01, 1);}",
77 | "72% {opacity: 1.01;transform: translateX(100.07px) scale3d(1.01, 1.01, 1);}",
78 | "73% {opacity: 1.01;transform: translateX(100.08px) scale3d(1.01, 1.01, 1);}",
79 | "74% {opacity: 1.01;transform: translateX(100.09px) scale3d(1.01, 1.01, 1);}",
80 | "77% {opacity: 1.01;transform: translateX(100.08px) scale3d(1.01, 1.01, 1);}",
81 | "79% {opacity: 1.01;transform: translateX(100.07px) scale3d(1.01, 1.01, 1);}",
82 | "81% {opacity: 1.01;transform: translateX(100.06px) scale3d(1.01, 1.01, 1);}",
83 | "83% {opacity: 1.01;transform: translateX(100.05px) scale3d(1.01, 1.01, 1);}",
84 | "84% {opacity: 1.01;transform: translateX(100.04px) scale3d(1.01, 1.01, 1);}",
85 | "86% {opacity: 1.01;transform: translateX(100.03px) scale3d(1.01, 1.01, 1);}",
86 | "88% {opacity: 1.01;transform: translateX(100.02px) scale3d(1.01, 1.01, 1);}",
87 | "90% {opacity: 1.01;transform: translateX(100.01px) scale3d(1.01, 1.01, 1);}",
88 | "93% {opacity: 1;transform: translateX(100px) scale3d(1, 1, 1);}",
89 | "100% {opacity: 1;transform: translateX(100px) scale3d(1, 1, 1);}",
90 | ]
91 | `;
92 |
93 | exports[`returns a keyframe array, with the desired precision 1`] = `
94 | Array [
95 | "0% {opacity: 0.0008;}",
96 | "1% {opacity: 0.0188;}",
97 | "2% {opacity: 0.057;}",
98 | "3% {opacity: 0.1109;}",
99 | "4% {opacity: 0.1763;}",
100 | "5% {opacity: 0.2371;}",
101 | "6% {opacity: 0.3147;}",
102 | "7% {opacity: 0.3949;}",
103 | "8% {opacity: 0.4752;}",
104 | "9% {opacity: 0.5538;}",
105 | "10% {opacity: 0.6168;}",
106 | "11% {opacity: 0.6884;}",
107 | "12% {opacity: 0.7547;}",
108 | "13% {opacity: 0.815;}",
109 | "14% {opacity: 0.8691;}",
110 | "15% {opacity: 0.9092;}",
111 | "16% {opacity: 0.9514;}",
112 | "17% {opacity: 0.9873;}",
113 | "18% {opacity: 1.0171;}",
114 | "19% {opacity: 1.0412;}",
115 | "20% {opacity: 1.0573;}",
116 | "21% {opacity: 1.0722;}",
117 | "22% {opacity: 1.0827;}",
118 | "23% {opacity: 1.0894;}",
119 | "24% {opacity: 1.0927;}",
120 | "25% {opacity: 1.0934;}",
121 | "26% {opacity: 1.092;}",
122 | "27% {opacity: 1.0887;}",
123 | "28% {opacity: 1.0838;}",
124 | "29% {opacity: 1.0778;}",
125 | "30% {opacity: 1.0722;}",
126 | "31% {opacity: 1.065;}",
127 | "32% {opacity: 1.0576;}",
128 | "33% {opacity: 1.0501;}",
129 | "34% {opacity: 1.0427;}",
130 | "35% {opacity: 1.0368;}",
131 | "36% {opacity: 1.03;}",
132 | "37% {opacity: 1.0238;}",
133 | "38% {opacity: 1.0181;}",
134 | "39% {opacity: 1.0129;}",
135 | "40% {opacity: 1.0091;}",
136 | "41% {opacity: 1.0051;}",
137 | "42% {opacity: 1.0017;}",
138 | "43% {opacity: 0.9988;}",
139 | "44% {opacity: 0.9965;}",
140 | "45% {opacity: 0.9949;}",
141 | "46% {opacity: 0.9935;}",
142 | "47% {opacity: 0.9924;}",
143 | "48% {opacity: 0.9918;}",
144 | "49% {opacity: 0.9914;}",
145 | "50% {opacity: 0.9913;}",
146 | "51% {opacity: 0.9914;}",
147 | "52% {opacity: 0.9917;}",
148 | "53% {opacity: 0.9922;}",
149 | "54% {opacity: 0.9927;}",
150 | "55% {opacity: 0.9932;}",
151 | "56% {opacity: 0.9939;}",
152 | "57% {opacity: 0.9946;}",
153 | "58% {opacity: 0.9953;}",
154 | "59% {opacity: 0.996;}",
155 | "60% {opacity: 0.9965;}",
156 | "61% {opacity: 0.9972;}",
157 | "62% {opacity: 0.9978;}",
158 | "63% {opacity: 0.9983;}",
159 | "64% {opacity: 0.9988;}",
160 | "65% {opacity: 0.9991;}",
161 | "66% {opacity: 0.9995;}",
162 | "67% {opacity: 0.9999;}",
163 | "68% {opacity: 1.0001;}",
164 | "69% {opacity: 1.0004;}",
165 | "70% {opacity: 1.0005;}",
166 | "71% {opacity: 1.0006;}",
167 | "72% {opacity: 1.0007;}",
168 | "73% {opacity: 1.0008;}",
169 | "74% {opacity: 1.0009;}",
170 | "77% {opacity: 1.0008;}",
171 | "79% {opacity: 1.0007;}",
172 | "81% {opacity: 1.0006;}",
173 | "83% {opacity: 1.0005;}",
174 | "84% {opacity: 1.0004;}",
175 | "86% {opacity: 1.0003;}",
176 | "88% {opacity: 1.0002;}",
177 | "90% {opacity: 1.0001;}",
178 | "93% {opacity: 1;}",
179 | "100% {opacity: 1;}",
180 | ]
181 | `;
182 |
183 | exports[`returns a keyframe array, with the desired precision 2`] = `
184 | Array [
185 | "0% {opacity: 1;}",
186 | "1% {opacity: 1;}",
187 | "18% {opacity: 2;}",
188 | "43% {opacity: 1;}",
189 | "68% {opacity: 2;}",
190 | "93% {opacity: 1;}",
191 | "100% {opacity: 1;}",
192 | ]
193 | `;
194 |
195 | exports[`returns a keyframe array, with the passed unit 1`] = `
196 | Array [
197 | "0% {opacity: 0.01;transform: translateX(0.08%);}",
198 | "1% {opacity: 0.02;transform: translateX(1.88%);}",
199 | "2% {opacity: 0.06;transform: translateX(5.7%);}",
200 | "3% {opacity: 0.12;transform: translateX(11.09%);}",
201 | "4% {opacity: 0.18;transform: translateX(17.63%);}",
202 | "5% {opacity: 0.24;transform: translateX(23.71%);}",
203 | "6% {opacity: 0.32;transform: translateX(31.47%);}",
204 | "7% {opacity: 0.4;transform: translateX(39.49%);}",
205 | "8% {opacity: 0.48;transform: translateX(47.52%);}",
206 | "9% {opacity: 0.56;transform: translateX(55.38%);}",
207 | "10% {opacity: 0.62;transform: translateX(61.68%);}",
208 | "11% {opacity: 0.69;transform: translateX(68.84%);}",
209 | "12% {opacity: 0.76;transform: translateX(75.47%);}",
210 | "13% {opacity: 0.82;transform: translateX(81.5%);}",
211 | "14% {opacity: 0.87;transform: translateX(86.91%);}",
212 | "15% {opacity: 0.91;transform: translateX(90.92%);}",
213 | "16% {opacity: 0.96;transform: translateX(95.14%);}",
214 | "17% {opacity: 0.99;transform: translateX(98.73%);}",
215 | "18% {opacity: 1.02;transform: translateX(101.71%);}",
216 | "19% {opacity: 1.05;transform: translateX(104.12%);}",
217 | "20% {opacity: 1.06;transform: translateX(105.73%);}",
218 | "21% {opacity: 1.08;transform: translateX(107.22%);}",
219 | "22% {opacity: 1.09;transform: translateX(108.27%);}",
220 | "23% {opacity: 1.09;transform: translateX(108.94%);}",
221 | "24% {opacity: 1.1;transform: translateX(109.27%);}",
222 | "25% {opacity: 1.1;transform: translateX(109.34%);}",
223 | "26% {opacity: 1.1;transform: translateX(109.2%);}",
224 | "27% {opacity: 1.09;transform: translateX(108.87%);}",
225 | "28% {opacity: 1.09;transform: translateX(108.38%);}",
226 | "29% {opacity: 1.08;transform: translateX(107.78%);}",
227 | "30% {opacity: 1.08;transform: translateX(107.22%);}",
228 | "31% {opacity: 1.07;transform: translateX(106.5%);}",
229 | "32% {opacity: 1.06;transform: translateX(105.76%);}",
230 | "33% {opacity: 1.06;transform: translateX(105.01%);}",
231 | "34% {opacity: 1.05;transform: translateX(104.27%);}",
232 | "35% {opacity: 1.04;transform: translateX(103.68%);}",
233 | "36% {opacity: 1.03;transform: translateX(103%);}",
234 | "37% {opacity: 1.03;transform: translateX(102.38%);}",
235 | "38% {opacity: 1.02;transform: translateX(101.81%);}",
236 | "39% {opacity: 1.02;transform: translateX(101.29%);}",
237 | "40% {opacity: 1.01;transform: translateX(100.91%);}",
238 | "41% {opacity: 1.01;transform: translateX(100.51%);}",
239 | "42% {opacity: 1.01;transform: translateX(100.17%);}",
240 | "43% {opacity: 1;transform: translateX(99.88%);}",
241 | "44% {opacity: 1;transform: translateX(99.65%);}",
242 | "45% {opacity: 1;transform: translateX(99.49%);}",
243 | "46% {opacity: 1;transform: translateX(99.35%);}",
244 | "47% {opacity: 1;transform: translateX(99.24%);}",
245 | "48% {opacity: 1;transform: translateX(99.18%);}",
246 | "49% {opacity: 1;transform: translateX(99.14%);}",
247 | "50% {opacity: 1;transform: translateX(99.13%);}",
248 | "51% {opacity: 1;transform: translateX(99.14%);}",
249 | "52% {opacity: 1;transform: translateX(99.17%);}",
250 | "53% {opacity: 1;transform: translateX(99.22%);}",
251 | "54% {opacity: 1;transform: translateX(99.27%);}",
252 | "55% {opacity: 1;transform: translateX(99.32%);}",
253 | "56% {opacity: 1;transform: translateX(99.39%);}",
254 | "57% {opacity: 1;transform: translateX(99.46%);}",
255 | "58% {opacity: 1;transform: translateX(99.53%);}",
256 | "59% {opacity: 1;transform: translateX(99.6%);}",
257 | "60% {opacity: 1;transform: translateX(99.65%);}",
258 | "61% {opacity: 1;transform: translateX(99.72%);}",
259 | "62% {opacity: 1;transform: translateX(99.78%);}",
260 | "63% {opacity: 1;transform: translateX(99.83%);}",
261 | "64% {opacity: 1;transform: translateX(99.88%);}",
262 | "65% {opacity: 1;transform: translateX(99.91%);}",
263 | "66% {opacity: 1;transform: translateX(99.95%);}",
264 | "67% {opacity: 1;transform: translateX(99.99%);}",
265 | "68% {opacity: 1.01;transform: translateX(100.01%);}",
266 | "69% {opacity: 1.01;transform: translateX(100.04%);}",
267 | "70% {opacity: 1.01;transform: translateX(100.05%);}",
268 | "71% {opacity: 1.01;transform: translateX(100.06%);}",
269 | "72% {opacity: 1.01;transform: translateX(100.07%);}",
270 | "73% {opacity: 1.01;transform: translateX(100.08%);}",
271 | "74% {opacity: 1.01;transform: translateX(100.09%);}",
272 | "77% {opacity: 1.01;transform: translateX(100.08%);}",
273 | "79% {opacity: 1.01;transform: translateX(100.07%);}",
274 | "81% {opacity: 1.01;transform: translateX(100.06%);}",
275 | "83% {opacity: 1.01;transform: translateX(100.05%);}",
276 | "84% {opacity: 1.01;transform: translateX(100.04%);}",
277 | "86% {opacity: 1.01;transform: translateX(100.03%);}",
278 | "88% {opacity: 1.01;transform: translateX(100.02%);}",
279 | "90% {opacity: 1.01;transform: translateX(100.01%);}",
280 | "93% {opacity: 1;transform: translateX(100%);}",
281 | "100% {opacity: 1;transform: translateX(100%);}",
282 | ]
283 | `;
284 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { spring } from './'
2 |
3 | it('returns a keyframe array, with duplicate values removed', () => {
4 | const animation = spring({
5 | from: {
6 | opacity: 0,
7 | x: 0,
8 | scale: 0,
9 | },
10 | to: {
11 | opacity: 1,
12 | x: 100,
13 | scale: 1,
14 | },
15 | })
16 |
17 | expect(animation).toMatchSnapshot()
18 | })
19 |
20 | it('returns a keyframe array, with the passed unit', () => {
21 | const animation = spring(
22 | {
23 | from: {
24 | opacity: 0,
25 | x: 0,
26 | },
27 | to: {
28 | opacity: 1,
29 | x: 100,
30 | },
31 | },
32 | { unit: '%' }
33 | )
34 |
35 | expect(animation).toMatchSnapshot()
36 | })
37 |
38 | it('returns a keyframe array, with the desired precision', () => {
39 | const highPrecisionAnimation = spring(
40 | {
41 | from: {
42 | opacity: 0,
43 | },
44 | to: {
45 | opacity: 1,
46 | },
47 | },
48 | { precision: 4 }
49 | )
50 |
51 | expect(highPrecisionAnimation).toMatchSnapshot()
52 |
53 | const lowPrecisionAnimation = spring(
54 | {
55 | from: {
56 | opacity: 0,
57 | },
58 | to: {
59 | opacity: 1,
60 | },
61 | },
62 | { precision: 0 }
63 | )
64 |
65 | expect(lowPrecisionAnimation).toMatchSnapshot()
66 | })
67 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import springer from 'springer'
2 | import { keyframes } from '@emotion/core'
3 |
4 | const defaults = {
5 | stiffness: 50,
6 | damping: 80,
7 | precision: 2,
8 | unit: 'px',
9 | }
10 |
11 | const transformMap = ['x', 'y', 'scale']
12 |
13 | function roundToPrecision(num: number, precision = 2) {
14 | const decimalPoints = Math.pow(10, precision)
15 |
16 | return Math.ceil(num * decimalPoints) / decimalPoints
17 | }
18 |
19 | const calcPropTweenVal = (
20 | prop: keyof Css,
21 | frame: number,
22 | from: Partial,
23 | to: Partial,
24 | { precision }: Pick
25 | ) => (spring: (number: any) => number) => {
26 | const value =
27 | (from[prop] || 0) +
28 | ((to[prop] || 0) - (from[prop] || 0)) * spring(frame / 100)
29 |
30 | return roundToPrecision(value, precision)
31 | }
32 |
33 | const createCalcPropTweenVal = (
34 | from: Partial,
35 | to: Partial,
36 | options: Omit
37 | ) => (prop: keyof Css, frame: number) => {
38 | const spring = springer(options.damping / 100, options.stiffness / 100)
39 | return calcPropTweenVal(prop, frame, from, to, options)(spring)
40 | }
41 |
42 | const splitTransform = (prop: keyof Css, value: number, transformList = []) =>
43 | transformMap.includes(prop)
44 | ? { transform: [...transformList, [prop, value]] }
45 | : { [prop]: value }
46 |
47 | const reduceFrame = (
48 | tween: Record,
49 | property: keyof Css,
50 | value: number
51 | ) => ({ ...tween, ...splitTransform(property, value, tween.transform) })
52 |
53 | function mapTransformPropToCss(
54 | prop: keyof Css,
55 | sprungValue: number,
56 | unit = 'px'
57 | ) {
58 | switch (prop) {
59 | case 'y':
60 | return `translateY(${sprungValue}${unit})`
61 | case 'x':
62 | return `translateX(${sprungValue}${unit})`
63 | case 'scale':
64 | return `scale3d(${sprungValue}, ${sprungValue}, 1)`
65 | default:
66 | return `${prop}(${sprungValue})`
67 | }
68 | }
69 |
70 | const mapTransformProps = (
71 | sprungFrameTuples: [keyof Css, number][],
72 | unit: string
73 | ) =>
74 | sprungFrameTuples.reduce(
75 | (transform, [prop, spring]) =>
76 | `${transform} ${mapTransformPropToCss(prop, spring, unit)}`,
77 | 'transform:'
78 | )
79 |
80 | const mapPropTypes = (
81 | prop: string,
82 | spring: [keyof Css, number][],
83 | unit: string
84 | ) =>
85 | prop === 'transform'
86 | ? `${mapTransformProps(spring, unit)};`
87 | : `${prop}: ${spring};`
88 |
89 | const mapToCss = (spring: any, unit: string) =>
90 | Object.keys(spring).reduce(
91 | (animation, prop) =>
92 | `${animation}${mapPropTypes(prop, spring[prop], unit)}`,
93 | ''
94 | )
95 |
96 | export function spring({ from, to }: Props, options?: Partial) {
97 | const { stiffness, damping, precision, unit } = {
98 | ...defaults,
99 | ...options,
100 | }
101 |
102 | const calcTween = createCalcPropTweenVal(from, to, {
103 | stiffness,
104 | damping,
105 | precision,
106 | })
107 |
108 | const frames = new Array(101).fill('')
109 |
110 | return frames
111 | .map((_, frame: number) => [
112 | Object.keys(from).reduce(
113 | //@ts-ignore
114 | (tween, prop: keyof Css) =>
115 | reduceFrame(tween, prop, calcTween(prop, frame)),
116 | {}
117 | ),
118 | frame,
119 | ])
120 | .map(([sprungValues, frame]) => [`${frame}%`, mapToCss(sprungValues, unit)])
121 | .filter(([frame, spring], i, frames) => {
122 | const lastIndex = i - 1 > 0 ? i - 1 : 0
123 | return lastIndex > 0 && frame !== '100%'
124 | ? frames[lastIndex][1] !== spring
125 | : true
126 | })
127 | .map(([frame, spring]) => `${frame} {${spring}}`)
128 | }
129 |
130 | export interface Props {
131 | from: Partial
132 | to: Partial
133 | }
134 |
135 | interface Css {
136 | x: number
137 | y: number
138 | scale: number
139 | opacity: number
140 | }
141 |
142 | interface Options {
143 | stiffness: number
144 | damping: number
145 | precision: number
146 | unit: string
147 | }
148 |
149 | export default function({ from, to }: Props, options?: Partial) {
150 | return keyframes(spring({ from, to }, options).join(''))
151 | }
152 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'springer'
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": true /* Enable incremental compilation */,
4 | "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
6 | "lib": [
7 | "es2015",
8 | "es2017",
9 | "esnext",
10 | "dom"
11 | ] /* Specify library files to be included in the compilation. */,
12 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
13 | "declaration": true /* Generates corresponding '.d.ts' file. */,
14 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
15 | "sourceMap": true /* Generates corresponding '.map' file. */,
16 | "outDir": "./lib" /* Redirect output structure to the directory. */,
17 | "removeComments": true /* Do not emit comments to output. */,
18 | "strict": true /* Enable all strict type-checking options. */,
19 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
20 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
21 |
22 | "noUnusedLocals": true /* Report errors on unused locals. */,
23 | "noUnusedParameters": true /* Report errors on unused parameters. */,
24 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
25 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
26 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
27 | },
28 | "exclude": ["src/*.test.ts", "lib"],
29 | "include": ["src"]
30 | }
31 |
--------------------------------------------------------------------------------