Skip to content

Commit 56adb13

Browse files
committed
Optimize
1 parent b2088b9 commit 56adb13

File tree

10 files changed

+229
-108
lines changed

10 files changed

+229
-108
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
"lint": "eslint src",
2222
"test": "jest --coverage"
2323
},
24-
"dependencies": {},
24+
"dependencies": {
25+
"lodash": "^4.17.4"
26+
},
2527
"devDependencies": {
2628
"babel-cli": "^6.18.0",
2729
"babel-eslint": "^7.1.1",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getAnimatableProps } from '../props'
1+
import { getAnimatableProps } from '../parse'
22

33
describe('getAnimatableProps', () => {
44
test('omits keys that do not exist in both startProps and endProps', () => {

src/__tests__/util.test.js

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/index.js

Lines changed: 93 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { isArray, mapKeys, mapValues, pickBy } from 'lodash'
12
import stepper from './stepper'
2-
import { mapValues } from './util'
3-
import { getAnimatableProps } from './props'
3+
import { combine } from './values'
4+
import { getAnimatableProps } from './parse'
45

6+
// spring presets. selected combinations of stiffness/damping.
57
const presets = {
68
noWobble: { stiffness: 170, damping: 26 },
79
gentle: { stiffness: 120, damping: 14 },
@@ -11,15 +13,51 @@ const presets = {
1113

1214
// default spring options.
1315
// damping and precision reflect the values of the `wobbly` preset,
14-
// precision defaults to 3 which should be a good tradeoff between
16+
// precision defaults to 2 which should be a good tradeoff between
1517
// animation detail and resulting filesize.
1618
const defaultOptions = {
1719
stiffness: 180,
1820
damping: 12,
19-
precision: 3,
21+
precision: 2,
22+
}
23+
24+
export const appendUnits = (object, units) =>
25+
mapValues(object, (value, key) => {
26+
return isArray(value)
27+
? value.map((value, i) => `${value}${units[key][i]}`)
28+
: `${value}${units[key]}`
29+
})
30+
31+
// @todo comment
32+
const buildInterpolation = (stiffness, damping) => {
33+
return (start, end) => {
34+
const interpolated = []
35+
const interpolateArray = isArray(start)
36+
let value = interpolateArray ? start[0] : start
37+
let velocity = 0
38+
39+
for (let i = 1; i < 100; i += 1) {
40+
if (interpolateArray) {
41+
let something
42+
for (let j = 0; j < start.length; j += 1) {
43+
something = [];
44+
[ value, velocity ] = stepper(0.01, value, velocity, end[j], stiffness, damping)
45+
something.push(value)
46+
}
47+
interpolated.push(something)
48+
} else {
49+
[ value, velocity ] = stepper(0.01, value, velocity, end, stiffness, damping)
50+
interpolated.push(value)
51+
}
52+
}
53+
54+
return [].concat(start, interpolated, end)
55+
}
2056
}
2157

2258
export const spring = (startProps, endProps, options = {}) => {
59+
let result = {}
60+
2361
// define stiffness, damping and precision based on default options
2462
// and options given in arguments.
2563
const { stiffness, damping, precision } = Object.assign(
@@ -29,51 +67,65 @@ export const spring = (startProps, endProps, options = {}) => {
2967
presets[options.preset] || {}
3068
)
3169

32-
const animatableProps = getAnimatableProps(startProps, endProps)
33-
const startValues = mapValues(animatableProps, ({ start }) => start)
34-
const endValues = mapValues(animatableProps, ({ end }) => end)
70+
// build an interpolation function based on the given stiffness and
71+
// damping values
72+
const interpolate = buildInterpolation(stiffness, damping)
3573

36-
const addUnits = (object) =>
37-
mapValues(object, (v, k) => `${v}${animatableProps[k].unit}`)
74+
// @todo comment!
75+
const data = getAnimatableProps(startProps, endProps)
76+
data.forEach(({ prop, unit, start, end }) => {
77+
interpolate(start, end).forEach((interpolated, i) => {
78+
// round to desired precision (except when interpolating pixels)
79+
const value = Number(interpolated.toFixed(unit === 'px' ? 0 : precision))
80+
// when the value is 0, there's no need to add a unit.
81+
const valueWithUnit = value === 0 ? value : `${value}${unit}`
3882

39-
const keyframes = {
40-
'0%': addUnits(startValues),
41-
'100%': addUnits(endValues),
42-
}
43-
44-
Object.keys(startValues).forEach((key) => {
45-
let velocity = 0
46-
let value = startValues[key]
47-
const end = endValues[key]
83+
if (result[i] === undefined) {
84+
result[i] = { [prop]: valueWithUnit }
85+
} else {
86+
result[i][prop] = result[i][prop] === undefined
87+
? valueWithUnit
88+
: [].concat(result[i][prop], valueWithUnit)
89+
}
90+
})
91+
})
4892

49-
for (let i = 1; i < 100; i += 1) {
50-
[ value, velocity ] = stepper(0.01, value, velocity, end, stiffness, damping)
51-
const percent = `${i}%`
52-
keyframes[percent] = Object.assign(
53-
keyframes[percent] || {},
54-
{ [key]: `${Number(value.toFixed(precision))}${animatableProps[key].unit}` }
55-
)
93+
// iterate over the result object, combining values and identifying
94+
// equal frames to be able to eliminate duplicates in a later step
95+
let prevFrame
96+
const obsoleteFrames = []
97+
Object.keys(result).forEach((i) => {
98+
const currentFrame = JSON.stringify(result[i])
99+
result[i] = mapValues(result[i], (value, key) => combine(key, value))
100+
if (prevFrame === currentFrame) {
101+
obsoleteFrames.push(i - 1)
56102
}
103+
prevFrame = currentFrame
57104
})
58105

59-
return keyframes
106+
// remove obsolute frames to reduce size and add % to keys
107+
// @todo might chain this - not using chain.
108+
// @see https://medium.com/making-internets/why-using-chain-is-a-mistake-9bc1f80d51ba
109+
result = mapKeys(
110+
pickBy(result, (value, key) => obsoleteFrames.indexOf(Number(key)) < 0),
111+
(value, key) => `${key}%`
112+
)
113+
114+
console.log(result)
115+
return result
60116
}
61117

62-
// console.log(spring({
63-
// left: '10px',
64-
// right: '20em',
65-
// foo: 'bar',
66-
// opacity: 0,
67-
// rotate: '5deg'
68-
// }, {
69-
// left: '20px',
70-
// right: '30em',
71-
// baz: true,
72-
// opacity: 1,
73-
// rotate: '10deg'
74-
// }, {
75-
// preset: 'noWobble'
76-
// }))
118+
spring({
119+
left: '10px',
120+
right: '20px',
121+
padding: '0 0 10px 10rem',
122+
}, {
123+
left: '20px',
124+
right: 0,
125+
padding: '10em 10em 0 20rem',
126+
}, {
127+
preset: 'noWobble',
128+
})
77129

78130
export { default as toString } from './to-string'
79131
export default spring

src/parse.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { split } from './values'
2+
import { isArray, map } from 'lodash'
3+
4+
// this splits css numbers from units.
5+
// according to the css spec, a number can either be an integer or it can be
6+
// zero or more digits followed by a dot followed by one or more digits.
7+
// assuming the unit can be any sequence of lowercase letters (including none)
8+
const numberUnitSplit = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)$/
9+
10+
// WAT
11+
const parseUnit = (startProp, endProp) => {
12+
const startMatches = startProp.toString().match(numberUnitSplit)
13+
const endMatches = endProp.toString().match(numberUnitSplit)
14+
15+
// when start and end match css number with optional unit
16+
if (startMatches && endMatches) {
17+
const startUnit = startMatches[2]
18+
const endUnit = endMatches[2]
19+
20+
// when start unit is the same as end unit or one of them is unitless
21+
if (startUnit === endUnit || !startUnit || !endUnit) {
22+
return {
23+
unit: startUnit || endUnit,
24+
start: Number(startMatches[1]),
25+
end: Number(endMatches[1]),
26+
}
27+
}
28+
}
29+
}
30+
31+
// returns an object that lists the unit, start and end values of the
32+
// animatable properties based on the given arguments.
33+
// to be animatable, a property has to be present on both `startProps` and
34+
// `endProps` with a numeric value and same unit for both or unitless for one
35+
// of them which will then take the unit of the other.
36+
export const getAnimatableProps = (startProps, endProps) => {
37+
let result = []
38+
39+
// @todo check if props are listed in animatable properties!
40+
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties
41+
// @see https://github.com/gilmoreorless/css-animated-properties
42+
for (let prop in startProps) {
43+
if (prop in endProps) {
44+
const startProp = split(prop, startProps[prop])
45+
const endProp = split(prop, endProps[prop])
46+
47+
if (isArray(startProp) && isArray(endProp) && startProp.length === endProp.length) {
48+
// array of values
49+
const temp = []
50+
for (let key in startProp) {
51+
if ({}.hasOwnProperty.call(startProp, key)) {
52+
const parsed = parseUnit(startProp[key], endProp[key])
53+
54+
if (parsed) {
55+
const { unit, start, end } = parsed
56+
temp.push({ prop, unit, start, end })
57+
}
58+
}
59+
}
60+
if (temp.length === startProp.length) {
61+
result = result.concat(temp)
62+
}
63+
} else {
64+
// probably single values
65+
const parsed = parseUnit(startProp, endProp)
66+
67+
if (parsed) {
68+
const { unit, start, end } = parsed
69+
result.push({ prop, unit, start, end })
70+
}
71+
}
72+
}
73+
}
74+
75+
return result
76+
}

src/props.js

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/stepper.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const reusedTuple = [ 0, 0 ]
44

55
// eslint-disable-next-line max-params
6-
export default (secondPerFrame, x, v, destX, k, b, precision = 0.01) => {
6+
const stepper = (secondPerFrame, x, v, destX, k, b, precision = 0.01) => {
77
// Spring stiffness, in kg / s^2
88

99
// for animations, destX is really spring length (spring at rest). initial
@@ -21,6 +21,8 @@ export default (secondPerFrame, x, v, destX, k, b, precision = 0.01) => {
2121
const newV = v + (a * secondPerFrame)
2222
const newX = x + (newV * secondPerFrame)
2323

24+
stepper.count += 1
25+
2426
if (Math.abs(newV) < precision && Math.abs(newX - destX) < precision) {
2527
reusedTuple[0] = destX
2628
reusedTuple[1] = 0
@@ -31,3 +33,6 @@ export default (secondPerFrame, x, v, destX, k, b, precision = 0.01) => {
3133
reusedTuple[1] = newV
3234
return reusedTuple
3335
}
36+
37+
stepper.count = 0
38+
export default stepper

src/util.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)