├── .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 | alt text 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 | --------------------------------------------------------------------------------