Skip to content

Commit f35e7c6

Browse files
committed
add animation value parser
1 parent 50fa810 commit f35e7c6

File tree

3 files changed

+305
-0
lines changed

3 files changed

+305
-0
lines changed

__tests__/animationParser.test.js

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import parse from '../src/util/animationParser'
2+
import { produce } from './util/produce'
3+
4+
describe('Tailwind Defaults', () => {
5+
it.each([
6+
[
7+
'spin 1s linear infinite',
8+
{ name: 'spin', duration: '1s', timingFunction: 'linear', iterationCount: 'infinite' },
9+
],
10+
[
11+
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
12+
{
13+
name: 'ping',
14+
duration: '1s',
15+
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
16+
iterationCount: 'infinite',
17+
},
18+
],
19+
[
20+
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
21+
{
22+
name: 'pulse',
23+
duration: '2s',
24+
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
25+
iterationCount: 'infinite',
26+
},
27+
],
28+
['bounce 1s infinite', { name: 'bounce', duration: '1s', iterationCount: 'infinite' }],
29+
])('should be possible to parse: "%s"', (input, expected) => {
30+
expect(parse(input)).toEqual(expected)
31+
})
32+
})
33+
34+
describe('MDN Examples', () => {
35+
it.each([
36+
[
37+
'3s ease-in 1s 2 reverse both paused slidein',
38+
{
39+
delay: '1s',
40+
direction: 'reverse',
41+
duration: '3s',
42+
fillMode: 'both',
43+
iterationCount: '2',
44+
name: 'slidein',
45+
playState: 'paused',
46+
timingFunction: 'ease-in',
47+
},
48+
],
49+
[
50+
'slidein 3s linear 1s',
51+
{ delay: '1s', duration: '3s', name: 'slidein', timingFunction: 'linear' },
52+
],
53+
['slidein 3s', { duration: '3s', name: 'slidein' }],
54+
])('should be possible to parse: "%s"', (input, expected) => {
55+
expect(parse(input)).toEqual(expected)
56+
})
57+
})
58+
59+
describe('duration & delay', () => {
60+
it.each([
61+
// Positive seconds (integer)
62+
['spin 1s 1s linear', { duration: '1s', delay: '1s' }],
63+
['spin 2s 1s linear', { duration: '2s', delay: '1s' }],
64+
['spin 1s 2s linear', { duration: '1s', delay: '2s' }],
65+
66+
// Negative seconds (integer)
67+
['spin -1s -1s linear', { duration: '-1s', delay: '-1s' }],
68+
['spin -2s -1s linear', { duration: '-2s', delay: '-1s' }],
69+
['spin -1s -2s linear', { duration: '-1s', delay: '-2s' }],
70+
71+
// Positive seconds (float)
72+
['spin 1.321s 1.321s linear', { duration: '1.321s', delay: '1.321s' }],
73+
['spin 2.321s 1.321s linear', { duration: '2.321s', delay: '1.321s' }],
74+
['spin 1.321s 2.321s linear', { duration: '1.321s', delay: '2.321s' }],
75+
76+
// Negative seconds (float)
77+
['spin -1.321s -1.321s linear', { duration: '-1.321s', delay: '-1.321s' }],
78+
['spin -2.321s -1.321s linear', { duration: '-2.321s', delay: '-1.321s' }],
79+
['spin -1.321s -2.321s linear', { duration: '-1.321s', delay: '-2.321s' }],
80+
81+
// Positive milliseconds (integer)
82+
['spin 100ms 100ms linear', { duration: '100ms', delay: '100ms' }],
83+
['spin 200ms 100ms linear', { duration: '200ms', delay: '100ms' }],
84+
['spin 100ms 200ms linear', { duration: '100ms', delay: '200ms' }],
85+
86+
// Negative milliseconds (integer)
87+
['spin -100ms -100ms linear', { duration: '-100ms', delay: '-100ms' }],
88+
['spin -200ms -100ms linear', { duration: '-200ms', delay: '-100ms' }],
89+
['spin -100ms -200ms linear', { duration: '-100ms', delay: '-200ms' }],
90+
91+
// Positive milliseconds (float)
92+
['spin 100.321ms 100.321ms linear', { duration: '100.321ms', delay: '100.321ms' }],
93+
['spin 200.321ms 100.321ms linear', { duration: '200.321ms', delay: '100.321ms' }],
94+
['spin 100.321ms 200.321ms linear', { duration: '100.321ms', delay: '200.321ms' }],
95+
96+
// Negative milliseconds (float)
97+
['spin -100.321ms -100.321ms linear', { duration: '-100.321ms', delay: '-100.321ms' }],
98+
['spin -200.321ms -100.321ms linear', { duration: '-200.321ms', delay: '-100.321ms' }],
99+
['spin -100.321ms -200.321ms linear', { duration: '-100.321ms', delay: '-200.321ms' }],
100+
])('should be possible to parse "%s" into %o', (input, { duration, delay }) => {
101+
const parsed = parse(input)
102+
expect(parsed.duration).toEqual(duration)
103+
expect(parsed.delay).toEqual(delay)
104+
})
105+
})
106+
107+
describe('iteration count', () => {
108+
it.each([
109+
// Number
110+
['1 spin 200s 100s linear', '1'],
111+
['spin 2 200s 100s linear', '2'],
112+
['spin 200s 3 100s linear', '3'],
113+
['spin 200s 100s 4 linear', '4'],
114+
['spin 200s 100s linear 5', '5'],
115+
116+
// Infinite
117+
['infinite spin 200s 100s linear', 'infinite'],
118+
['spin infinite 200s 100s linear', 'infinite'],
119+
['spin 200s infinite 100s linear', 'infinite'],
120+
['spin 200s 100s infinite linear', 'infinite'],
121+
['spin 200s 100s linear infinite', 'infinite'],
122+
])(
123+
'should be possible to parse "%s" with an iteraction count of "%s"',
124+
(input, iterationCount) => {
125+
expect(parse(input).iterationCount).toEqual(iterationCount)
126+
}
127+
)
128+
})
129+
130+
describe('iteration count', () => {
131+
it.each([
132+
// Number
133+
['1 spin 200s 100s linear', '1'],
134+
['spin 2 200s 100s linear', '2'],
135+
['spin 200s 3 100s linear', '3'],
136+
['spin 200s 100s 4 linear', '4'],
137+
['spin 200s 100s linear 5', '5'],
138+
['100 spin 200s 100s linear', '100'],
139+
['spin 200 200s 100s linear', '200'],
140+
['spin 200s 300 100s linear', '300'],
141+
['spin 200s 100s 400 linear', '400'],
142+
['spin 200s 100s linear 500', '500'],
143+
144+
// Infinite
145+
['infinite spin 200s 100s linear', 'infinite'],
146+
['spin infinite 200s 100s linear', 'infinite'],
147+
['spin 200s infinite 100s linear', 'infinite'],
148+
['spin 200s 100s infinite linear', 'infinite'],
149+
['spin 200s 100s linear infinite', 'infinite'],
150+
])(
151+
'should be possible to parse "%s" with an iteraction count of "%s"',
152+
(input, iterationCount) => {
153+
expect(parse(input).iterationCount).toEqual(iterationCount)
154+
}
155+
)
156+
})
157+
158+
describe('multiple animations', () => {
159+
it('should be possible to parse multiple applications at once', () => {
160+
const input = [
161+
'spin 1s linear infinite',
162+
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
163+
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
164+
].join(',')
165+
166+
const parsed = parse(input)
167+
expect(parsed).toHaveLength(3)
168+
expect(parsed).toEqual([
169+
{ name: 'spin', duration: '1s', timingFunction: 'linear', iterationCount: 'infinite' },
170+
{
171+
name: 'ping',
172+
duration: '1s',
173+
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
174+
iterationCount: 'infinite',
175+
},
176+
{
177+
name: 'pulse',
178+
duration: '2s',
179+
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
180+
iterationCount: 'infinite',
181+
},
182+
])
183+
})
184+
})
185+
186+
describe('randomized crazy big examples', () => {
187+
function reOrder(input, offset = 0) {
188+
return [...input.slice(offset), ...input.slice(0, offset)]
189+
}
190+
191+
it.each(
192+
produce((choose) => {
193+
const direction = choose('normal', 'reverse', 'alternate', 'alternate-reverse')
194+
const playState = choose('running', 'paused')
195+
const fillMode = choose('none', 'forwards', 'backwards', 'both')
196+
const iterationCount = choose('infinite', '1', '100')
197+
const timingFunction = choose(
198+
'linear',
199+
'ease',
200+
'ease-in',
201+
'ease-out',
202+
'ease-in-out',
203+
'cubic-bezier(0, 0, 0.2, 1)',
204+
'steps(4, end)'
205+
)
206+
const name = choose('animation-name-a', 'animation-name-b')
207+
const inputArgs = [direction, playState, fillMode, iterationCount, timingFunction, name]
208+
const orderOffset = choose(...Array(inputArgs.length).keys())
209+
210+
return [
211+
// Input
212+
reOrder(inputArgs, orderOffset).join(' '),
213+
214+
// Output
215+
{
216+
direction,
217+
playState,
218+
fillMode,
219+
iterationCount,
220+
timingFunction,
221+
name,
222+
},
223+
]
224+
})
225+
)('should be possible to parse "%s"', (input, output) => {
226+
expect(parse(input)).toEqual(output)
227+
})
228+
})

__tests__/util/produce.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Full credit goes to: https://github.com/purplestone/exhaust
2+
// However, it is modified so that it is a bit more modern
3+
export function produce(blueprint) {
4+
let groups = []
5+
6+
// Call the blueprint once so that we can collect all the possible values we
7+
// spit out in the callback function.
8+
blueprint((...args) => {
9+
if (args.length <= 0) throw new Error('Blueprint callback must have at least a single value')
10+
groups.push(args)
11+
})
12+
13+
// Calculate how many combinations there are
14+
let iterations = groups.reduce((total, current) => total * current.length, 1)
15+
16+
// Calculate all the combinations possible
17+
let zippedGroups = []
18+
let currentIteration = iterations
19+
groups.forEach((a) => {
20+
let n = a.length
21+
currentIteration = currentIteration / n
22+
let iS = -1
23+
let aS = []
24+
25+
for (let i = 0; i < iterations; i++) {
26+
if (!(i % currentIteration)) iS++
27+
aS.push(a[iS % n])
28+
}
29+
zippedGroups.push(aS)
30+
})
31+
32+
// Transpose the matrix, so that we can get the correct rows/columns structure
33+
// again.
34+
zippedGroups = zippedGroups[0].map((_, i) => zippedGroups.map((o) => o[i]))
35+
36+
// Call the blueprint again, but now give the inner function a single value
37+
// every time so that we can build up the final result with single values.
38+
return zippedGroups.map((group) => {
39+
let i = 0
40+
return blueprint(() => group[i++])
41+
})
42+
}

src/util/animationParser.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const directions = new Set(['normal', 'reverse', 'alternate', 'alternate-reverse'])
2+
const playStates = new Set(['running', 'paused'])
3+
const fillModes = new Set(['none', 'forwards', 'backwards', 'both'])
4+
const iterationCount = new Set(['infinite'])
5+
const timingFns = new Set(['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'])
6+
const timingFnsWithArgs = ['cubic-bezier', 'steps']
7+
const commaSeparator = /\,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
8+
const spaceSeparator = /\ (?![^(]*\))/g // Similar to the one above, but with spaces instead.
9+
10+
export default function parse(input) {
11+
const animations = input.split(commaSeparator)
12+
const result = animations.map((animation) => {
13+
const parts = animation.split(spaceSeparator)
14+
15+
const result = {}
16+
17+
for (let part of parts) {
18+
if (directions.has(part)) result.direction = part
19+
else if (playStates.has(part)) result.playState = part
20+
else if (fillModes.has(part)) result.fillMode = part
21+
else if (iterationCount.has(part)) result.iterationCount = part
22+
else if (timingFns.has(part)) result.timingFunction = part
23+
else if (timingFnsWithArgs.some((fn) => part.startsWith(`${fn}(`)))
24+
result.timingFunction = part
25+
else if (/^(-?[\d.]+m?s)$/.test(part))
26+
result[result.duration === undefined ? 'duration' : 'delay'] = part
27+
else if (/^(\d+)$/.test(part)) result.iterationCount = part
28+
else result.name = part
29+
}
30+
31+
return result
32+
})
33+
34+
return animations.length > 1 ? result : result[0]
35+
}

0 commit comments

Comments
 (0)