Skip to content

Commit

Permalink
add animation value parser
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Oct 19, 2020
1 parent 50fa810 commit f35e7c6
Show file tree
Hide file tree
Showing 3 changed files with 305 additions and 0 deletions.
228 changes: 228 additions & 0 deletions __tests__/animationParser.test.js
@@ -0,0 +1,228 @@
import parse from '../src/util/animationParser'
import { produce } from './util/produce'

describe('Tailwind Defaults', () => {
it.each([
[
'spin 1s linear infinite',
{ name: 'spin', duration: '1s', timingFunction: 'linear', iterationCount: 'infinite' },
],
[
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
{
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
],
[
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
{
name: 'pulse',
duration: '2s',
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
iterationCount: 'infinite',
},
],
['bounce 1s infinite', { name: 'bounce', duration: '1s', iterationCount: 'infinite' }],
])('should be possible to parse: "%s"', (input, expected) => {
expect(parse(input)).toEqual(expected)
})
})

describe('MDN Examples', () => {
it.each([
[
'3s ease-in 1s 2 reverse both paused slidein',
{
delay: '1s',
direction: 'reverse',
duration: '3s',
fillMode: 'both',
iterationCount: '2',
name: 'slidein',
playState: 'paused',
timingFunction: 'ease-in',
},
],
[
'slidein 3s linear 1s',
{ delay: '1s', duration: '3s', name: 'slidein', timingFunction: 'linear' },
],
['slidein 3s', { duration: '3s', name: 'slidein' }],
])('should be possible to parse: "%s"', (input, expected) => {
expect(parse(input)).toEqual(expected)
})
})

describe('duration & delay', () => {
it.each([
// Positive seconds (integer)
['spin 1s 1s linear', { duration: '1s', delay: '1s' }],
['spin 2s 1s linear', { duration: '2s', delay: '1s' }],
['spin 1s 2s linear', { duration: '1s', delay: '2s' }],

// Negative seconds (integer)
['spin -1s -1s linear', { duration: '-1s', delay: '-1s' }],
['spin -2s -1s linear', { duration: '-2s', delay: '-1s' }],
['spin -1s -2s linear', { duration: '-1s', delay: '-2s' }],

// Positive seconds (float)
['spin 1.321s 1.321s linear', { duration: '1.321s', delay: '1.321s' }],
['spin 2.321s 1.321s linear', { duration: '2.321s', delay: '1.321s' }],
['spin 1.321s 2.321s linear', { duration: '1.321s', delay: '2.321s' }],

// Negative seconds (float)
['spin -1.321s -1.321s linear', { duration: '-1.321s', delay: '-1.321s' }],
['spin -2.321s -1.321s linear', { duration: '-2.321s', delay: '-1.321s' }],
['spin -1.321s -2.321s linear', { duration: '-1.321s', delay: '-2.321s' }],

// Positive milliseconds (integer)
['spin 100ms 100ms linear', { duration: '100ms', delay: '100ms' }],
['spin 200ms 100ms linear', { duration: '200ms', delay: '100ms' }],
['spin 100ms 200ms linear', { duration: '100ms', delay: '200ms' }],

// Negative milliseconds (integer)
['spin -100ms -100ms linear', { duration: '-100ms', delay: '-100ms' }],
['spin -200ms -100ms linear', { duration: '-200ms', delay: '-100ms' }],
['spin -100ms -200ms linear', { duration: '-100ms', delay: '-200ms' }],

// Positive milliseconds (float)
['spin 100.321ms 100.321ms linear', { duration: '100.321ms', delay: '100.321ms' }],
['spin 200.321ms 100.321ms linear', { duration: '200.321ms', delay: '100.321ms' }],
['spin 100.321ms 200.321ms linear', { duration: '100.321ms', delay: '200.321ms' }],

// Negative milliseconds (float)
['spin -100.321ms -100.321ms linear', { duration: '-100.321ms', delay: '-100.321ms' }],
['spin -200.321ms -100.321ms linear', { duration: '-200.321ms', delay: '-100.321ms' }],
['spin -100.321ms -200.321ms linear', { duration: '-100.321ms', delay: '-200.321ms' }],
])('should be possible to parse "%s" into %o', (input, { duration, delay }) => {
const parsed = parse(input)
expect(parsed.duration).toEqual(duration)
expect(parsed.delay).toEqual(delay)
})
})

describe('iteration count', () => {
it.each([
// Number
['1 spin 200s 100s linear', '1'],
['spin 2 200s 100s linear', '2'],
['spin 200s 3 100s linear', '3'],
['spin 200s 100s 4 linear', '4'],
['spin 200s 100s linear 5', '5'],

// Infinite
['infinite spin 200s 100s linear', 'infinite'],
['spin infinite 200s 100s linear', 'infinite'],
['spin 200s infinite 100s linear', 'infinite'],
['spin 200s 100s infinite linear', 'infinite'],
['spin 200s 100s linear infinite', 'infinite'],
])(
'should be possible to parse "%s" with an iteraction count of "%s"',
(input, iterationCount) => {
expect(parse(input).iterationCount).toEqual(iterationCount)
}
)
})

describe('iteration count', () => {
it.each([
// Number
['1 spin 200s 100s linear', '1'],
['spin 2 200s 100s linear', '2'],
['spin 200s 3 100s linear', '3'],
['spin 200s 100s 4 linear', '4'],
['spin 200s 100s linear 5', '5'],
['100 spin 200s 100s linear', '100'],
['spin 200 200s 100s linear', '200'],
['spin 200s 300 100s linear', '300'],
['spin 200s 100s 400 linear', '400'],
['spin 200s 100s linear 500', '500'],

// Infinite
['infinite spin 200s 100s linear', 'infinite'],
['spin infinite 200s 100s linear', 'infinite'],
['spin 200s infinite 100s linear', 'infinite'],
['spin 200s 100s infinite linear', 'infinite'],
['spin 200s 100s linear infinite', 'infinite'],
])(
'should be possible to parse "%s" with an iteraction count of "%s"',
(input, iterationCount) => {
expect(parse(input).iterationCount).toEqual(iterationCount)
}
)
})

describe('multiple animations', () => {
it('should be possible to parse multiple applications at once', () => {
const input = [
'spin 1s linear infinite',
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
].join(',')

const parsed = parse(input)
expect(parsed).toHaveLength(3)
expect(parsed).toEqual([
{ name: 'spin', duration: '1s', timingFunction: 'linear', iterationCount: 'infinite' },
{
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
{
name: 'pulse',
duration: '2s',
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
iterationCount: 'infinite',
},
])
})
})

describe('randomized crazy big examples', () => {
function reOrder(input, offset = 0) {
return [...input.slice(offset), ...input.slice(0, offset)]
}

it.each(
produce((choose) => {
const direction = choose('normal', 'reverse', 'alternate', 'alternate-reverse')
const playState = choose('running', 'paused')
const fillMode = choose('none', 'forwards', 'backwards', 'both')
const iterationCount = choose('infinite', '1', '100')
const timingFunction = choose(
'linear',
'ease',
'ease-in',
'ease-out',
'ease-in-out',
'cubic-bezier(0, 0, 0.2, 1)',
'steps(4, end)'
)
const name = choose('animation-name-a', 'animation-name-b')
const inputArgs = [direction, playState, fillMode, iterationCount, timingFunction, name]
const orderOffset = choose(...Array(inputArgs.length).keys())

return [
// Input
reOrder(inputArgs, orderOffset).join(' '),

// Output
{
direction,
playState,
fillMode,
iterationCount,
timingFunction,
name,
},
]
})
)('should be possible to parse "%s"', (input, output) => {
expect(parse(input)).toEqual(output)
})
})
42 changes: 42 additions & 0 deletions __tests__/util/produce.js
@@ -0,0 +1,42 @@
// Full credit goes to: https://github.com/purplestone/exhaust
// However, it is modified so that it is a bit more modern
export function produce(blueprint) {
let groups = []

// Call the blueprint once so that we can collect all the possible values we
// spit out in the callback function.
blueprint((...args) => {
if (args.length <= 0) throw new Error('Blueprint callback must have at least a single value')
groups.push(args)
})

// Calculate how many combinations there are
let iterations = groups.reduce((total, current) => total * current.length, 1)

// Calculate all the combinations possible
let zippedGroups = []
let currentIteration = iterations
groups.forEach((a) => {
let n = a.length
currentIteration = currentIteration / n
let iS = -1
let aS = []

for (let i = 0; i < iterations; i++) {
if (!(i % currentIteration)) iS++
aS.push(a[iS % n])
}
zippedGroups.push(aS)
})

// Transpose the matrix, so that we can get the correct rows/columns structure
// again.
zippedGroups = zippedGroups[0].map((_, i) => zippedGroups.map((o) => o[i]))

// Call the blueprint again, but now give the inner function a single value
// every time so that we can build up the final result with single values.
return zippedGroups.map((group) => {
let i = 0
return blueprint(() => group[i++])
})
}
35 changes: 35 additions & 0 deletions src/util/animationParser.js
@@ -0,0 +1,35 @@
const directions = new Set(['normal', 'reverse', 'alternate', 'alternate-reverse'])
const playStates = new Set(['running', 'paused'])
const fillModes = new Set(['none', 'forwards', 'backwards', 'both'])
const iterationCount = new Set(['infinite'])
const timingFns = new Set(['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'])
const timingFnsWithArgs = ['cubic-bezier', 'steps']
const commaSeparator = /\,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
const spaceSeparator = /\ (?![^(]*\))/g // Similar to the one above, but with spaces instead.

export default function parse(input) {
const animations = input.split(commaSeparator)
const result = animations.map((animation) => {
const parts = animation.split(spaceSeparator)

const result = {}

for (let part of parts) {
if (directions.has(part)) result.direction = part
else if (playStates.has(part)) result.playState = part
else if (fillModes.has(part)) result.fillMode = part
else if (iterationCount.has(part)) result.iterationCount = part
else if (timingFns.has(part)) result.timingFunction = part
else if (timingFnsWithArgs.some((fn) => part.startsWith(`${fn}(`)))
result.timingFunction = part
else if (/^(-?[\d.]+m?s)$/.test(part))
result[result.duration === undefined ? 'duration' : 'delay'] = part
else if (/^(\d+)$/.test(part)) result.iterationCount = part
else result.name = part
}

return result
})

return animations.length > 1 ? result : result[0]
}

0 comments on commit f35e7c6

Please sign in to comment.