From 73133a38539e6a7385b3b4e5f0b41f48beba313d Mon Sep 17 00:00:00 2001 From: Christopher Wallis Date: Thu, 13 Jul 2017 00:02:08 -0400 Subject: [PATCH] formatting, structuring, plugin system --- .editorconfig | 3 + package.json | 4 +- src/core/animator.ts | 64 --- src/core/effects.ts | 125 +++++ src/core/index.ts | 4 +- src/core/keyframes.ts | 106 ---- src/core/plugins.ts | 6 + src/core/split-text.ts | 166 +++--- src/core/timeline.ts | 729 +++++++++++++------------ src/core/timeloop.ts | 118 ++-- src/handlers/index.ts | 2 - src/handlers/transforms.ts | 100 ---- src/handlers/units.ts | 72 --- src/helpers/animate.ts | 22 - src/helpers/index.ts | 2 - src/helpers/sequence.ts | 10 - src/main.ts | 45 +- src/plugins/waapi-plugin.ts | 195 +++++++ src/transformers/fix-offsets.ts | 42 -- src/transformers/index.ts | 2 +- src/transformers/infer-offsets.ts | 102 ++-- src/transformers/props-to-keyframes.ts | 50 +- src/transformers/resolve-property.ts | 56 +- src/types.ts | 20 +- src/utils/get-targets.ts | 60 +- src/utils/lists.ts | 89 +-- src/utils/math.ts | 8 +- src/utils/objects.ts | 72 +-- src/utils/random.ts | 19 +- src/utils/resources.ts | 35 +- src/utils/strings.ts | 11 +- src/utils/type.ts | 26 +- src/utils/units.ts | 52 +- src/utils/utils.ts | 22 +- tests/features/basic.ts | 2 +- tests/features/staggering.ts | 2 +- tests/features/timeline.add.ts | 2 +- tests/features/timeline.to.ts | 2 +- tests/features/transforms.ts | 2 +- tests/utils/type.ts | 8 +- tsconfig.json | 3 +- 41 files changed, 1225 insertions(+), 1235 deletions(-) create mode 100644 src/core/effects.ts delete mode 100644 src/core/keyframes.ts create mode 100644 src/core/plugins.ts delete mode 100644 src/handlers/index.ts delete mode 100644 src/handlers/transforms.ts delete mode 100644 src/handlers/units.ts delete mode 100644 src/helpers/animate.ts delete mode 100644 src/helpers/index.ts delete mode 100644 src/helpers/sequence.ts create mode 100644 src/plugins/waapi-plugin.ts delete mode 100644 src/transformers/fix-offsets.ts diff --git a/.editorconfig b/.editorconfig index a8fa9822..2b61a6a8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,10 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true +spaces_around_operators = true insert_final_newline = true +quote_type = single +indent_brace_style = K&R [*.md] trim_trailing_whitespace = true \ No newline at end of file diff --git a/package.json b/package.json index 0b4daf0c..57159f8f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "karma-spec-reporter": "0.0.31", "karma-typescript": "^3.0.4", "mocha": "^3.4.2", - "prettier": "^1.4.4", + "prettier": "^1.5.2", "rollup": "^0.42.0", "rollup-plugin-node-resolve": "^3.0.0", "rollup-plugin-typescript": "^0.8.1", @@ -52,7 +52,7 @@ "compress": "npm run compress:umd", "compress:umd": "uglifyjs --c --lift-vars --m --screw-ie8 --o dist/just-animate-core.min.js dist/just-animate-core.js", "clean": "node_modules/.bin/del-cli -f dist lib lib.es2015 types", - "format": "tsfmt -r src/**/*.ts", + "format": "prettier --single-quote --no-semi --list-different --write \"src/**/*.ts\"", "preversion": "npm run rebuild", "postversion": "git push --follow-tags && npm publish", "rebuild": "npm run clean && npm run build && npm run compress", diff --git a/src/core/animator.ts b/src/core/animator.ts index b642e263..e69de29b 100644 --- a/src/core/animator.ts +++ b/src/core/animator.ts @@ -1,64 +0,0 @@ -import * as types from '../types'; -import { RUNNING, CANCEL, PAUSE, FINISH, SEEK, UPDATE, inRange, minMax, _, lazy } from '../utils'; - -const framePadding = 0 - -export interface IAnimationController { - (type: string, time: number, playbackRate: number): void -} - -export function createWebAnimation({ keyframes, from, to, target }: types.Effect): IAnimationController { - const getAnimator = lazy(() => { - const a = (target as any).animate(keyframes, { - duration: to - from, - fill: 'both' - }) - a.pause() - return a - }) - - return (type: string, time: number, playbackRate: number) => { - const animator = getAnimator(); - - if (animator.playbackRate !== playbackRate) { - // set playbackRate direction/speed - animator.playbackRate = playbackRate - } - - if (type === CANCEL) { - animator.cancel() - return - } - if (type === FINISH) { - // without pause() WAAPI appears to play the animation again on seek - animator.finish() - animator.pause() - return - } - - if (type === PAUSE) { - animator.pause() - } - - const isForwards = (playbackRate || 0) >= 0 - const duration = to - from - const currentTime = (time !== _ ? time : isForwards ? 0 : duration) - from - if (type === PAUSE || type === SEEK) { - // sync if paused or seeking - animator.currentTime = minMax(currentTime, 0, duration) - } - - if (type === UPDATE && animator.playState !== RUNNING) { - const sign = isForwards ? 1 : -1 - const isActive = inRange(currentTime + (framePadding * sign), 0, duration) - - if (isActive) { - // sync time - animator.currentTime = minMax(currentTime, 0, duration) - - // start if ticking and animator is not running - animator.play() - } - } - } -} diff --git a/src/core/effects.ts b/src/core/effects.ts new file mode 100644 index 00000000..fad6c905 --- /dev/null +++ b/src/core/effects.ts @@ -0,0 +1,125 @@ +import * as types from '../types' +import { isFunction, convertToMs, isDefined, indexOf } from '../utils' +import { resolve } from '../transformers' +import { getPlugins } from './plugins' + +export function toEffects( + targets: types.TargetConfiguration[] +): types.Effect[] { + const result: types.Effect[] = [] + + for (var i = 0, ilen = targets.length; i < ilen; i++) { + const targetConfig = targets[i] + const { from, to, duration, keyframes, target } = targetConfig + + // construct property animation options + var effects: types.PropertyEffects = {} + for (var j = 0, jlen = keyframes.length; j < jlen; j++) { + const p = keyframes[j] + const propName = p.prop + const offset = (p.time - from) / (duration || 1) + const value = isFunction(p.value) + ? (p.value as Function)(target, p.index) + : p.value as string | number + + // get or create property + const effect = effects[propName] || (effects[propName] = []) + effect.push({ offset, value }) + } + + // process handlers + var plugins = getPlugins() + for (var q = 0, qlen = plugins.length; q < qlen; q++) { + var plugin = plugins[q] + if (plugin.transform) { + plugin.transform(targetConfig, effects) + } + } + + for (var propName in effects) { + var effect = effects[propName] + if (!effect) { + continue + } + + // remap the keyframes field to remove multiple values + var effect2 = { + target, + from, + to, + keyframes: effect.map(({ offset, value }) => ({ + offset, + [propName]: value + })) + } + + // add to list of Effects + result.push(effect2) + } + } + return result +} + +export function addKeyframes( + target: types.TargetConfiguration, + index: number, + options: types.AnimationOptions +) { + const { css } = options + const staggerMs = convertToMs( + resolve(options.stagger, target, index, true) || 0 + ) as number + const delayMs = convertToMs( + resolve(options.delay, target, index) || 0 + ) as number + const endDelayMs = convertToMs( + resolve(options.endDelay, target, index) || 0 + ) as number + + // todo: incorporate WAAPI delay/endDelay + const from = staggerMs + delayMs + options.from + const to = staggerMs + delayMs + options.to + endDelayMs + const duration = to - from + + for (var i = 0, ilen = css.length; i < ilen; i++) { + var keyframe = css[i] + var time = Math.floor(duration * keyframe.offset + from) + addKeyframe(target, time, index, keyframe) + } +} + +function addKeyframe( + target: types.TargetConfiguration, + time: number, + index: number, + keyframe: types.KeyframeOptions +) { + var { keyframes } = target + for (var name in keyframe) { + if (name === 'offset') { + continue + } + + var value = keyframe[name] as string | number + if (!isDefined(value)) { + continue + } + + var indexOfFrame = indexOf( + keyframes, + k => k.prop === name && k.time === time + ) + if (indexOfFrame !== -1) { + keyframes[indexOfFrame].value = value + continue + } + + keyframes.push({ + index, + prop: name, + time, + order: 0, // fixme + value + }) + } +} diff --git a/src/core/index.ts b/src/core/index.ts index 30bd881f..159e0484 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,5 +1,5 @@ -export * from './animator' -export * from './keyframes' +export * from './effects' +export * from './plugins' export * from './split-text' export * from './timeline' export * from './timeloop' diff --git a/src/core/keyframes.ts b/src/core/keyframes.ts deleted file mode 100644 index 28ea9430..00000000 --- a/src/core/keyframes.ts +++ /dev/null @@ -1,106 +0,0 @@ -import * as types from '../types' -import { isFunction, convertToMs, isDefined, indexOf } from '../utils'; -import { resolve } from '../transformers'; -import { unitHandler, transformHandler } from '../handlers'; - -const propertyHandlers: types.PropertyHandler[] = [unitHandler, transformHandler] - -export function toEffects(targets: types.TargetConfiguration[]): types.Effect[] { - const result: types.Effect[] = [] - - for (var i = 0, ilen = targets.length; i < ilen; i++) { - const targetConfig = targets[i] - const { from, to, duration, keyframes, target } = targetConfig - - // construct property animation options - var effects: types.PropertyEffects = {} - for (var j = 0, jlen = keyframes.length; j < jlen; j++) { - const p = keyframes[j] - const propName = p.prop - const offset = (p.time - from) / (duration || 1) - const value = isFunction(p.value) ? (p.value as Function)(target, p.index) : p.value as string | number - - // get or create property - const effect = effects[propName] || (effects[propName] = []) - effect.push({ offset, value }) - } - - // process handlers - for (var q = 0, qlen = propertyHandlers.length; q < qlen; q++) { - propertyHandlers[q](targetConfig, effects) - } - - for (var propName in effects) { - var effect = effects[propName] - if (!effect) { - continue - } - - // remap the keyframes field to remove multiple values - var effect2 = { - target, - from, - to, - keyframes: effect.map(({ offset, value }) => ({ - offset, - [propName]: value - })) - }; - - // add to list of Effects - result.push(effect2) - } - } - return result -} - -export function addKeyframes(target: types.TargetConfiguration, index: number, options: types.AnimationOptions) { - const { css } = options - const staggerMs = convertToMs(resolve(options.stagger, target, index, true) || 0) as number - const delayMs = convertToMs(resolve(options.delay, target, index) || 0) as number - const endDelayMs = convertToMs(resolve(options.endDelay, target, index) || 0) as number - - // todo: incorporate WAAPI delay/endDelay - const from = staggerMs + delayMs + options.from - const to = staggerMs + delayMs + options.to + endDelayMs; - const duration = to - from - - for (var i = 0, ilen = css.length; i < ilen; i++) { - var keyframe = css[i] - var time = Math.floor((duration * keyframe.offset) + from) - addKeyframe( - target, - time, - index, - keyframe - ) - } -} - -function addKeyframe(target: types.TargetConfiguration, time: number, index: number, keyframe: types.KeyframeOptions) { - var { keyframes } = target - for (var name in keyframe) { - if (name === 'offset') { - continue - } - - var value = keyframe[name] as string | number - if (!isDefined(value)) { - continue; - } - - var indexOfFrame = indexOf(keyframes, k => k.prop === name && k.time === time) - if (indexOfFrame !== -1) { - keyframes[indexOfFrame].value = value - continue; - } - - keyframes.push({ - index, - prop: name, - time, - order: 0, // fixme - value - }) - } -} diff --git a/src/core/plugins.ts b/src/core/plugins.ts new file mode 100644 index 00000000..447c7ed2 --- /dev/null +++ b/src/core/plugins.ts @@ -0,0 +1,6 @@ +import * as types from '../types' + +const plugins: types.Plugin[] = [] + +export const getPlugins = () => plugins +export const addPlugin = (plugin: types.Plugin) => plugins.push(plugin) diff --git a/src/core/split-text.ts b/src/core/split-text.ts index fc00a5c9..6da70850 100644 --- a/src/core/split-text.ts +++ b/src/core/split-text.ts @@ -1,10 +1,13 @@ import { AnimationTarget, SplitTextResult } from '../types' import { getTargets, toArray } from '../utils' - + function newElement() { - const el = document.createElement('div'); - el.setAttribute('style', 'display:inline-block;position:relative;text-align:start'); - return el; + const el = document.createElement('div') + el.setAttribute( + 'style', + 'display:inline-block;position:relative;text-align:start' + ) + return el } /** @@ -13,85 +16,84 @@ function newElement() { * list of characters and numbers */ export const splitText = (target: AnimationTarget): SplitTextResult => { - - // output parameters - const characters: HTMLElement[] = [] - const words: HTMLElement[] = [] - - // acquiring targets ;) - const elements = getTargets(target) as HTMLElement[] - - // get paragraphs, words, and characters for each element - for (let i = 0, ilen = elements.length; i < ilen; i++) { - const element = elements[i] - - // if we have already split this element, check if it was already split - if (element.getAttribute('ja-split-text')) { - const ws = toArray(element.querySelectorAll('[ja-word]')) - const cs = toArray(element.querySelectorAll('[ja-character]')) - - // if split already return query result - if (ws.length || cs.length) { - // apply found split elements - words.push.apply(words, ws) - characters.push.apply(characters, cs) - continue - } - // otherwise split it! - } - - // remove tabs, spaces, and newlines - const contents = element.textContent!.replace(/[\r\n\s\t]+/ig, ' ').trim() - - // clear element - element.innerHTML = '' - - // mark element as already being split - element.setAttribute('ja-split', '') - - // split on spaces - const ws = contents.split(/[\s]+/ig) - - // handle each word - for (let j = 0, jlen = ws.length; j < jlen; j++) { - const w = ws[j] - // create new div for word/run" - const word = newElement() - - // mark element as a word - word.setAttribute('ja-word', w) - // add to the result - words.push(word) - - // if not the first word, add a space - if (j > 0) { - const space = newElement() - space.innerHTML = ' ' - space.setAttribute('ja-space', '') - element.appendChild(space) - } - - // add to the paragraph - element.appendChild(word) - - for (let k = 0, klen = w.length; k < klen; k++) { - const c = w[k] - // create new div for character" - const char = newElement() - char.textContent = c - - // mark element as a character - char.setAttribute('ja-character', c) - // add to the result - characters.push(char) - // append to the word - word.appendChild(char) - } - } + // output parameters + const characters: HTMLElement[] = [] + const words: HTMLElement[] = [] + + // acquiring targets ;) + const elements = getTargets(target) as HTMLElement[] + + // get paragraphs, words, and characters for each element + for (let i = 0, ilen = elements.length; i < ilen; i++) { + const element = elements[i] + + // if we have already split this element, check if it was already split + if (element.getAttribute('ja-split-text')) { + const ws = toArray(element.querySelectorAll('[ja-word]')) + const cs = toArray(element.querySelectorAll('[ja-character]')) + + // if split already return query result + if (ws.length || cs.length) { + // apply found split elements + words.push.apply(words, ws) + characters.push.apply(characters, cs) + continue + } + // otherwise split it! } - return { - characters: characters, - words: words + // remove tabs, spaces, and newlines + const contents = element.textContent!.replace(/[\r\n\s\t]+/gi, ' ').trim() + + // clear element + element.innerHTML = '' + + // mark element as already being split + element.setAttribute('ja-split', '') + + // split on spaces + const ws = contents.split(/[\s]+/gi) + + // handle each word + for (let j = 0, jlen = ws.length; j < jlen; j++) { + const w = ws[j] + // create new div for word/run" + const word = newElement() + + // mark element as a word + word.setAttribute('ja-word', w) + // add to the result + words.push(word) + + // if not the first word, add a space + if (j > 0) { + const space = newElement() + space.innerHTML = ' ' + space.setAttribute('ja-space', '') + element.appendChild(space) + } + + // add to the paragraph + element.appendChild(word) + + for (let k = 0, klen = w.length; k < klen; k++) { + const c = w[k] + // create new div for character" + const char = newElement() + char.textContent = c + + // mark element as a character + char.setAttribute('ja-character', c) + // add to the result + characters.push(char) + // append to the word + word.appendChild(char) + } } + } + + return { + characters: characters, + words: words + } } diff --git a/src/core/timeline.ts b/src/core/timeline.ts index e21c6950..5ec22ee8 100644 --- a/src/core/timeline.ts +++ b/src/core/timeline.ts @@ -1,413 +1,428 @@ +import * as types from '../types' + +import { toEffects, addKeyframes } from './effects' import { - AddAnimationOptions, - direction, - AnimationOptions, - playbackState, - BaseAnimationOptions, - Effect, - KeyframeOptions, - PropertyKeyframe, - PropertyOptions, - TargetConfiguration, - ToAnimationOptions -} from '../types' - -import { toEffects, addKeyframes } from './keyframes' -import { convertToMs, getTargets, inRange, isDefined, isArray, sortBy, head, SEEK, UPDATE } from '../utils' + convertToMs, + getTargets, + inRange, + isDefined, + isArrayLike, + sortBy, + head, + SEEK, + UPDATE +} from '../utils' import { _, CANCEL, FINISH, PAUSE, PLAY } from '../utils/resources' -import { createWebAnimation, timeloop, IAnimationController } from '.' -import { inferOffsets } from '../transformers/infer-offsets'; +import { timeloop, getPlugins } from '.' +import { inferOffsets } from '../transformers/infer-offsets' import { propsToKeyframes } from '../transformers/props-to-keyframes' -const propKeyframeSort = sortBy('time') +const propKeyframeSort = sortBy('time') export class Timeline { - public duration: number - public playbackRate: number - public playState: number - - private targets: TargetConfiguration[] - private _animators: IAnimationController[] - private _iteration: number - private _time: number - private _dir: number - private _listeners: { [key: string]: { (time: number): void }[] } - private _iterations: number - private _isReady: boolean - - public get currentTime() { - return this._time - } - public set currentTime(time: number) { - this.seek(time) + public duration: number + public playbackRate: number + public playState: number + + private targets: types.TargetConfiguration[] + private _animators: types.AnimationController[] + private _iteration: number + private _time: number + private _dir: number + private _listeners: { [key: string]: { (time: number): void }[] } + private _iterations: number + private _isReady: boolean + + public get currentTime() { + return this._time + } + public set currentTime(time: number) { + this.seek(time) + } + + constructor() { + const self = this + self.duration = 0 + self._time = _ + self.playbackRate = 1 + self.playState = types.playbackState.idle + self._animators = [] + self.targets = [] + self._isReady = false + self._iteration = _ + self._dir = types.direction.normal + self._iterations = _ + self._listeners = {} + } + + public add(opts: types.AddAnimationOptions) { + const self = this + const hasTo = isDefined(opts.to) + const hasFrom = isDefined(opts.from) + const hasDuration = isDefined(opts.duration) + + // pretty exaustive rules for importing times + let from: number, to: number + if (hasFrom && hasTo) { + from = convertToMs(opts.from) + to = convertToMs(opts.to) + } else if (hasFrom && hasDuration) { + from = convertToMs(opts.from) + to = from + convertToMs(opts.duration) + } else if (hasTo && hasDuration) { + to = convertToMs(opts.to) + from = to - convertToMs(opts.duration) + } else if (hasTo && !hasDuration) { + from = self.duration + to = from + convertToMs(opts.to) + } else if (hasDuration) { + from = self.duration + to = from + convertToMs(opts.duration) + } else { + throw new Error('Please provide to/from/duration') } - constructor() { - const self = this - self.duration = 0 - self._time = _ - self.playbackRate = 1 - self.playState = playbackState.idle - self._animators = [] - self.targets = [] - self._isReady = false - self._iteration = _ - self._dir = direction.normal - self._iterations = _ - self._listeners = {} + // ensure from/to is not negative + from = Math.max(from, 0) + to = Math.max(to, 0) + + self.fromTo(from, to, opts) + return self + } + + public fromTo( + from: number | string, + to: number | string, + options: types.BaseAnimationOptions + ) { + const self = this + + if (isArrayLike(options.css)) { + // fill in missing offsets + inferOffsets(options.css as types.KeyframeOptions[]) + } else { + // convert properties to offsets + options.css = propsToKeyframes(options.css as types.PropertyOptions) } - public add(opts: AddAnimationOptions) { - const self = this - const hasTo = isDefined(opts.to) - const hasFrom = isDefined(opts.from) - const hasDuration = isDefined(opts.duration) - - // pretty exaustive rules for importing times - let from: number, to: number; - if (hasFrom && hasTo) { - from = convertToMs(opts.from) - to = convertToMs(opts.to) - } else if (hasFrom && hasDuration) { - from = convertToMs(opts.from) - to = from + convertToMs(opts.duration) - } else if (hasTo && hasDuration) { - to = convertToMs(opts.to) - from = to - convertToMs(opts.duration) - } else if (hasTo && !hasDuration) { - from = self.duration - to = from + convertToMs(opts.to) - } else if (hasDuration) { - from = self.duration - to = from + convertToMs(opts.duration) - } else { - throw new Error('Please provide to/from/duration') + // ensure to/from are in milliseconds (as numbers) + const options2 = options as types.AnimationOptions + options2.from = convertToMs(from) + options2.to = convertToMs(to) + options2.duration = options2.to - options2.from + + // add all targets as property keyframes + const targets = getTargets(options.targets) + for (let i = 0, ilen = targets.length; i < ilen; i++) { + var target = targets[i] + var targetConfig = head(self.targets, t2 => t2.target === target) + + if (!targetConfig) { + targetConfig = { + from: options2.from, + to: options2.to, + duration: options2.to - options2.from, + target, + keyframes: [], + propOrder: {} } + self.targets.push(targetConfig) + } - // ensure from/to is not negative - from = Math.max(from, 0) - to = Math.max(to, 0) - - self.fromTo(from, to, opts) - return self + addKeyframes(targetConfig, i, options2) } - public fromTo(from: number | string, to: number | string, options: BaseAnimationOptions) { - const self = this + // sort property keyframes + self._sortPropKeyframes() - if (isArray(options.css)) { - // fill in missing offsets - inferOffsets(options.css as KeyframeOptions[]) - } else { - // convert properties to offsets - options.css = propsToKeyframes(options.css as PropertyOptions) - } + // recalculate property keyframe times and total duration + self._calcTimes() - // ensure to/from are in milliseconds (as numbers) - const options2 = options as AnimationOptions - options2.from = convertToMs(from) - options2.to = convertToMs(to) - options2.duration = options2.to - options2.from - - // add all targets as property keyframes - const targets = getTargets(options.targets) - for (let i = 0, ilen = targets.length; i < ilen; i++) { - var target = targets[i] - var targetConfig = head(self.targets, t2 => t2.target === target) - - if (!targetConfig) { - targetConfig = { - from: options2.from, - to: options2.to, - duration: options2.to - options2.from, - target, - keyframes: [], - propOrder: {} - } - self.targets.push(targetConfig) - } - - addKeyframes(targetConfig, i, options2) - } + return self + } - // sort property keyframes - self._sortPropKeyframes() + public to(toTime: string | number, opts: types.ToAnimationOptions) { + const self = this + const to = convertToMs(toTime) - // recalculate property keyframe times and total duration - self._calcTimes() + let fromTime: number + if (isDefined(opts.from)) { + fromTime = convertToMs(opts.from) + } else if (isDefined(opts.duration)) { + fromTime = to - convertToMs(opts.duration) + } else { + fromTime = self.duration + } - return self + return self.fromTo(Math.max(fromTime, 0), to, opts) + } + + public cancel() { + const self = this + timeloop.off(self._tick) + self._time = 0 + self._iteration = _ + self.playState = types.playbackState.idle + for (let i = 0, ilen = self._animators.length; i < ilen; i++) { + self._animators[i](CANCEL, 0, self.playbackRate) } + self._animators = [] + self._trigger(CANCEL) + self._teardown() + return self + } + + public finish() { + const self = this + self._setup() + timeloop.off(self._tick) + self._time = _ + self._iteration = _ + self.playState = types.playbackState.finished + for (let i = 0, ilen = self._animators.length; i < ilen; i++) { + self._animators[i](FINISH, _, self.playbackRate) + } + self._trigger(FINISH) + return self + } - public to(toTime: string | number, opts: ToAnimationOptions) { - const self = this - const to = convertToMs(toTime) - - let fromTime: number - if (isDefined(opts.from)) { - fromTime = convertToMs(opts.from) - } else if (isDefined(opts.duration)) { - fromTime = to - convertToMs(opts.duration) - } else { - fromTime = self.duration - } + public on(eventName: string, listener: () => void) { + const self = this + const { _listeners } = self - return self.fromTo(Math.max(fromTime, 0), to, opts) + const listeners = _listeners[eventName] || (_listeners[eventName] = []) + if (listeners.indexOf(listener) === -1) { + listeners.push(listener) } - public cancel() { - const self = this - timeloop.off(self._tick) - self._time = 0 - self._iteration = _ - self.playState = playbackState.idle - for (let i = 0, ilen = self._animators.length; i < ilen; i++) { - self._animators[i](CANCEL, 0, self.playbackRate) - } - self._animators = [] - self._trigger(CANCEL) - self._teardown() - return self + return self + } + public off(eventName: string, listener: () => void) { + const self = this + const listeners = self._listeners[eventName] + if (listeners) { + const indexOfListener = listeners.indexOf(listener) + if (indexOfListener !== -1) { + listeners.splice(indexOfListener, 1) + } } - - public finish() { - const self = this - self._setup() - timeloop.off(self._tick) - self._time = _ - self._iteration = _ - self.playState = playbackState.finished - for (let i = 0, ilen = self._animators.length; i < ilen; i++) { - self._animators[i](FINISH, _, self.playbackRate) - } - self._trigger(FINISH) - return self + return self + } + + public pause() { + const self = this + const { currentTime, playbackRate } = self + timeloop.off(self._tick) + self._setup() + self.playState = types.playbackState.paused + for (let i = 0, ilen = self._animators.length; i < ilen; i++) { + self._animators[i](PAUSE, currentTime, playbackRate) + } + self._trigger(PAUSE) + return self + } + + public play(iterations = 1, dir = types.direction.normal) { + const self = this + self._setup() + self._iterations = iterations + self._dir = dir + + if ( + self.playState === types.playbackState.paused || + (self.playState !== types.playbackState.running && + self.playState !== types.playbackState.pending) + ) { + self.playState = types.playbackState.pending } - public on(eventName: string, listener: () => void) { - const self = this - const { _listeners } = self + timeloop.on(self._tick) + self._trigger(PLAY) + return self + } - const listeners = _listeners[eventName] || (_listeners[eventName] = []) - if (listeners.indexOf(listener) === -1) { - listeners.push(listener) - } + public reverse() { + const self = this + self.playbackRate = (self.playbackRate || 0) * -1 - return self + if (self.playState === types.playbackState.running) { + // if currently running, pause the animation and replay from that position + self.pause().play() } - public off(eventName: string, listener: () => void) { - const self = this - const listeners = self._listeners[eventName] - if (listeners) { - const indexOfListener = listeners.indexOf(listener) - if (indexOfListener !== -1) { - listeners.splice(indexOfListener, 1) - } - } - return self + return self + } + + public seek(time: number | string) { + const self = this + const timeMs = convertToMs(time) + self._time = timeMs + for (let i = 0, ilen = self._animators.length; i < ilen; i++) { + self._animators[i](SEEK, timeMs, self.playbackRate) } + } - public pause() { - const self = this - const { currentTime, playbackRate } = self - timeloop.off(self._tick) - self._setup() - self.playState = playbackState.paused - for (let i = 0, ilen = self._animators.length; i < ilen; i++) { - self._animators[i](PAUSE, currentTime, playbackRate) - } - self._trigger(PAUSE) - return self - } + public getEffects(): types.Effect[] { + return toEffects(this.targets) + } - public play(iterations = 1, dir = direction.normal) { - const self = this - self._setup() - self._iterations = iterations - self._dir = dir + private _calcTimes() { + const self = this + let timelineTo = 0 - if (self.playState === playbackState.paused - || self.playState !== playbackState.running - && self.playState !== playbackState.pending) { - self.playState = playbackState.pending - } + for (let i = 0, ilen = self.targets.length; i < ilen; i++) { + const target = self.targets[i] + const { keyframes } = target - timeloop.on(self._tick) - self._trigger(PLAY) - return self - } + var targetFrom = undefined + var targetTo = undefined - public reverse() { - const self = this - self.playbackRate = (self.playbackRate || 0) * -1 - - if (self.playState === playbackState.running) { - // if currently running, pause the animation and replay from that position - self.pause().play() + for (let j = 0, jlen = keyframes.length; j < jlen; j++) { + const keyframe = keyframes[j] + if (keyframe.time < targetFrom || targetFrom === undefined) { + targetFrom = keyframe.time } - return self - } - - public seek(time: number | string) { - const self = this - const timeMs = convertToMs(time) - self._time = timeMs - for (let i = 0, ilen = self._animators.length; i < ilen; i++) { - self._animators[i](SEEK, timeMs, self.playbackRate) + if (keyframe.time > targetTo || targetTo === undefined) { + targetTo = keyframe.time + if (keyframe.time > timelineTo) { + timelineTo = keyframe.time + } } - } + } - public getEffects(): Effect[] { - return toEffects(this.targets) + target.to = targetTo + target.from = targetFrom + target.duration = targetTo - targetFrom } - - private _calcTimes() { - const self = this - let timelineTo = 0 - - for (let i = 0, ilen = self.targets.length; i < ilen; i++) { - const target = self.targets[i] - const { keyframes } = target - - var targetFrom = undefined - var targetTo = undefined - - for (let j = 0, jlen = keyframes.length; j < jlen; j++) { - const keyframe = keyframes[j] - if (keyframe.time < targetFrom || targetFrom === undefined) { - targetFrom = keyframe.time - } - if (keyframe.time > targetTo || targetTo === undefined) { - targetTo = keyframe.time - if (keyframe.time > timelineTo) { - timelineTo = keyframe.time - } - } - } - - target.to = targetTo - target.from = targetFrom - target.duration = targetTo - targetFrom - } - self.duration = timelineTo + self.duration = timelineTo + } + + private _setup(): void { + const self = this + if (!self._isReady) { + const effects = toEffects(self.targets) + const plugins = getPlugins() + const animations: types.AnimationController[] = [] + for (var i = 0, ilen = plugins.length; i < ilen; i++) { + plugins[i].animate(effects, animations) + } + + self._animators = animations + self._isReady = true } + } - private _setup(): void { - const self = this - if (!self._isReady) { - self._animators = toEffects(self.targets).map(createWebAnimation) - self._isReady = true - } + private _sortPropKeyframes() { + const self = this + const { targets } = self + for (let i = 0, ilen = targets.length; i < ilen; i++) { + targets[i].keyframes.sort(propKeyframeSort) } - - private _sortPropKeyframes() { - const self = this - const { targets } = self - for (let i = 0, ilen = targets.length; i < ilen; i++) { - targets[i].keyframes.sort(propKeyframeSort) - } + } + + private _teardown(): void { + const self = this + self._animators = [] + self._isReady = false + } + + private _trigger = (eventName: string) => { + const self = this + const { _time } = self + const listeners = self._listeners[eventName as string] + if (listeners) { + for (const listener of listeners) { + listener(_time) + } } + return self + } - private _teardown(): void { - const self = this - self._animators = [] - self._isReady = false - } + private _tick = (delta: number) => { + const self = this + const playState = self.playState - private _trigger = (eventName: string) => { - const self = this - const { _time } = self - const listeners = self._listeners[eventName as string] - if (listeners) { - for (const listener of listeners) { - listener(_time) - } - } - return self + // canceled + if (playState === types.playbackState.idle) { + self.cancel() + return + } + // finished + if (playState === types.playbackState.finished) { + self.finish() + return + } + // paused + if (playState === types.playbackState.paused) { + self.pause() + return + } + // running/pending + + // calculate running range + const duration = self.duration + const iterations = self._iterations + const playbackRate = self.playbackRate + const isReversed = playbackRate < 0 + + let time = self._time + let iteration = self._iteration || 0 + + if (self.playState === types.playbackState.pending) { + // reset position properties if necessary + if ( + time === _ || + (isReversed && time > duration) || + (!isReversed && time < 0) + ) { + // if at finish, reset to start time + time = isReversed ? duration : 0 + } + if (iteration === iterations) { + // if at finish reset iterations to 0 + iteration = 0 + } + self.playState = types.playbackState.running } - private _tick = (delta: number) => { - const self = this - const playState = self.playState + time += delta * playbackRate - // canceled - if (playState === playbackState.idle) { - self.cancel() - return - } - // finished - if (playState === playbackState.finished) { - self.finish() - return - } - // paused - if (playState === playbackState.paused) { - self.pause() - return - } - // running/pending - - // calculate running range - const duration = self.duration - const iterations = self._iterations - const playbackRate = self.playbackRate - const isReversed = playbackRate < 0 - - let time = self._time - let iteration = self._iteration || 0 - - if (self.playState === playbackState.pending) { - // reset position properties if necessary - if (time === _ || (isReversed && time > duration) || (!isReversed && time < 0)) { - // if at finish, reset to start time - time = isReversed ? duration : 0 - } - if (iteration === iterations) { - // if at finish reset iterations to 0 - iteration = 0 - } - self.playState = playbackState.running - } - - time += delta * playbackRate - - // check if timeline has finished - let hasEnded = false - if (!inRange(time, 0, duration)) { - self._iteration = ++iteration - time = isReversed ? 0 : duration - hasEnded = true - } + // check if timeline has finished + let hasEnded = false + if (!inRange(time, 0, duration)) { + self._iteration = ++iteration + time = isReversed ? 0 : duration + hasEnded = true + } - // call update - self._iteration = iteration - self._time = time - self._trigger(UPDATE) + // call update + self._iteration = iteration + self._time = time + self._trigger(UPDATE) - // call tick for all animations - for (let i = 0, ilen = self._animators.length; i < ilen; i++) { - self._animators[i](UPDATE, time, playbackRate) - } + // call tick for all animations + for (let i = 0, ilen = self._animators.length; i < ilen; i++) { + self._animators[i](UPDATE, time, playbackRate) + } - if (!hasEnded) { - // if not ended, return early - return - } + if (!hasEnded) { + // if not ended, return early + return + } - if (iterations === iteration) { - // end the cycle - self.finish() - return - } + if (iterations === iteration) { + // end the cycle + self.finish() + return + } - if (self._dir === direction.alternate) { - // change direction - self.playbackRate = (self.playbackRate || 0) * -1 - } - - // if not the last iteration, reset the clock and call tick again - time = self.playbackRate < 0 ? duration : 0 - self._time = time - self._tick(0) + if (self._dir === types.direction.alternate) { + // change direction + self.playbackRate = (self.playbackRate || 0) * -1 } + + // if not the last iteration, reset the clock and call tick again + time = self.playbackRate < 0 ? duration : 0 + self._time = time + self._tick(0) + } } diff --git a/src/core/timeloop.ts b/src/core/timeloop.ts index 1a7c4b6a..593ebd54 100644 --- a/src/core/timeloop.ts +++ b/src/core/timeloop.ts @@ -11,84 +11,84 @@ let lastTime: number = _ type TimeLoopCallback = (delta: number, elapsed: number) => any const updateOffs = (): void => { - for (let i = 0, ilen = offs.length; i < ilen; i++) { - const indexOfSub = active.indexOf(offs[i]) - if (indexOfSub !== -1) { - active.splice(indexOfSub, 1) - elapses.splice(indexOfSub, 1) - } + for (let i = 0, ilen = offs.length; i < ilen; i++) { + const indexOfSub = active.indexOf(offs[i]) + if (indexOfSub !== -1) { + active.splice(indexOfSub, 1) + elapses.splice(indexOfSub, 1) } + } } const updateOns = (): void => { - for (let i = 0, ilen = ons.length; i < ilen; i++) { - const fn = ons[i] - if (active.indexOf(fn) === -1) { - active.push(fn) - elapses.push(0) - } + for (let i = 0, ilen = ons.length; i < ilen; i++) { + const fn = ons[i] + if (active.indexOf(fn) === -1) { + active.push(fn) + elapses.push(0) } + } } const update = (): void => { - updateOffs() - updateOns() + updateOffs() + updateOns() - const len = active.length + const len = active.length - lastTime = lastTime || now() - const thisTime = now() - const delta = thisTime - lastTime + lastTime = lastTime || now() + const thisTime = now() + const delta = thisTime - lastTime - // if not is subscribed, kill the cycle - if (!len) { - // end recursion - isActive = _ - lastTime = _ - return - } + // if not is subscribed, kill the cycle + if (!len) { + // end recursion + isActive = _ + lastTime = _ + return + } - // ensure running and requestAnimationFrame is called - isActive = true - lastTime = thisTime - raf(self, update) + // ensure running and requestAnimationFrame is called + isActive = true + lastTime = thisTime + raf(self, update) - for (let i = 0; i < len; i++) { - // update delta and save result - const existingElapsed = elapses[i] - const updatedElapsed = existingElapsed + delta - elapses[i] = updatedElapsed + for (let i = 0; i < len; i++) { + // update delta and save result + const existingElapsed = elapses[i] + const updatedElapsed = existingElapsed + delta + elapses[i] = updatedElapsed - // call sub with updated delta - active[i](delta, updatedElapsed) - } + // call sub with updated delta + active[i](delta, updatedElapsed) + } } const on = (fn: TimeLoopCallback) => { - const offIndex = offs.indexOf(fn) - if (offIndex !== -1) { - offs.splice(offIndex, 1) - } - if (ons.indexOf(fn) === -1) { - ons.push(fn) - } - if (!isActive) { - isActive = true - raf(self, update) - } + const offIndex = offs.indexOf(fn) + if (offIndex !== -1) { + offs.splice(offIndex, 1) + } + if (ons.indexOf(fn) === -1) { + ons.push(fn) + } + if (!isActive) { + isActive = true + raf(self, update) + } } const off = (fn: TimeLoopCallback) => { - const onIndex = ons.indexOf(fn) - if (onIndex !== -1) { - ons.splice(onIndex, 1) - } - if (offs.indexOf(fn) === -1) { - offs.push(fn) - } - if (!isActive) { - isActive = true - raf(self, update) - } + const onIndex = ons.indexOf(fn) + if (onIndex !== -1) { + ons.splice(onIndex, 1) + } + if (offs.indexOf(fn) === -1) { + offs.push(fn) + } + if (!isActive) { + isActive = true + raf(self, update) + } } export const timeloop = { on, off } diff --git a/src/handlers/index.ts b/src/handlers/index.ts deleted file mode 100644 index ebfb2678..00000000 --- a/src/handlers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './transforms' -export * from './units' diff --git a/src/handlers/transforms.ts b/src/handlers/transforms.ts deleted file mode 100644 index 0205e0e4..00000000 --- a/src/handlers/transforms.ts +++ /dev/null @@ -1,100 +0,0 @@ -import * as types from '../types' -import { TRANSFORM, isNumber, includes, _, indexOf } from '../utils'; - -const TOLERANCE = 0.0001 - -const perspective = 'perspective' -const rotate = 'rotate' -const scale = 'scale' -const translate = 'translate' -const px = 'px' -const deg = 'deg' - -const X = 'X' -const Y = 'Y' -const Z = 'Z' - -const angles = [ - rotate + X, - rotate + Y, - rotate + Z, - rotate -] - -const scales = [ - scale + X, - scale + Y, - scale + Z, - scale -] - -const lengths = [ - perspective, - 'x', - 'y', - 'z' -] - -const transforms = angles.concat(scales, lengths) - -const aliases = { - x: translate + X, - y: translate + Y, - z: translate + Z -} - -export const transformHandler = (_target: types.TargetConfiguration, effects: types.PropertyEffects) => { - const transformEffects: { - offset: number - value: string - }[] = [] - - for (var name in effects) { - if (name !== TRANSFORM && !includes(transforms, name)) { - continue - } - - // pull transform prop and remove it from the effects - const effectOptions = effects[name] - effects[name] = _ - - // append default unit if property requires one - for (var i = 0, ilen = effectOptions.length; i < ilen; i++) { - var k = effectOptions[i] - // use an episilon so same frames aren't missed due to floating point issues - var index = indexOf(transformEffects, t => Math.abs(t.offset - k.offset) < TOLERANCE) - - // create or find an existing effect at the offset - const effect = index !== -1 - ? transformEffects[index] - : { - offset: k.offset, - value: '' - } - - if (index === -1) { - transformEffects.push(effect) - } - - if (name === TRANSFORM) { - // take transform at face value, override all other values - effect.value = k.value + '' - } else { - // convert each found values to transform functions and to list of effects - let value = k.value - if (includes(lengths, name) && isNumber(value)) { - value += px - } else if (includes(angles, name) && isNumber(value)) { - value += deg - } - effect.value += `${(aliases[name] || name)}(${value})` - } - - } - } - - if (transformEffects.length) { - // apply transform effects to keyframes - effects[TRANSFORM] = transformEffects - } -} diff --git a/src/handlers/units.ts b/src/handlers/units.ts deleted file mode 100644 index 1d2c3f26..00000000 --- a/src/handlers/units.ts +++ /dev/null @@ -1,72 +0,0 @@ -import * as types from '../types' -import { includes, isNumber } from '../utils' - -// animatable length properties -export const lengthProps = [ - 'backgroundSize', - 'border', - 'borderBottom', - 'borderBottomLeftRadius', - 'borderBottomRightRadius', - 'borderBottomWidth', - 'borderLeft', - 'borderLeftWidth', - 'borderRadius', - 'borderRight', - 'borderRightWidth', - 'borderTop', - 'borderTopLeftRadius', - 'borderTopRightRadius', - 'borderTopWidth', - 'borderWidth', - 'bottom', - 'columnGap', - 'columnRuleWidth', - 'columnWidth', - 'columns', - 'flexBasis', - 'font', - 'fontSize', - 'gridColumnGap', - 'gridGap', - 'gridRowGap', - 'height', - 'left', - 'letterSpacing', - 'lineHeight', - 'margin', - 'marginBottom', - 'marginLeft', - 'marginRight', - 'marginTop', - 'maskSize', - 'maxHeight', - 'maxWidth', - 'minHeight', - 'minWidth', - 'outline', - 'outlineOffset', - 'outlineWidth', - 'padding', - 'paddingBottom', - 'paddingLeft', - 'paddingRight', - 'paddingTop', - 'perspective', - 'right', - 'shapeMargin', - 'tabSize', - 'top', - 'width', - 'wordSpacing' -] - -export const unitHandler = (_target: types.TargetConfiguration, effects: types.PropertyEffects) => { - for (const propName in effects) { - const keyframes = effects[propName] - const value = keyframes[propName] - if (includes(lengthProps, propName) && isNumber(value)) { - keyframes[propName] = value + 'px' - } - } -} diff --git a/src/helpers/animate.ts b/src/helpers/animate.ts deleted file mode 100644 index 52c75cd4..00000000 --- a/src/helpers/animate.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { AddAnimationOptions } from '../types' -import { listify, isDefined } from '../utils' -import { Timeline } from '../core' - -/** - * Returns a new timeline of animation(s) using the options provided - * @param options Animtion options or an array of options - */ -export const animate = (options?: AddAnimationOptions | AddAnimationOptions[]) => { - const timeline = new Timeline() - if (options) { - var optionList = listify(options) - for (var i = 0, ilen = optionList.length; i < ilen; i++) { - var opt = optionList[i] - if (!isDefined(opt.from)) { - opt.from = 0 - } - timeline.add(opt) - } - } - return timeline -} diff --git a/src/helpers/index.ts b/src/helpers/index.ts deleted file mode 100644 index d23b7442..00000000 --- a/src/helpers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './animate' -export * from './sequence' diff --git a/src/helpers/sequence.ts b/src/helpers/sequence.ts deleted file mode 100644 index 0b2e7911..00000000 --- a/src/helpers/sequence.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AddAnimationOptions } from '../types' -import { Timeline } from '../core' - -export const sequence = (seqOptions: AddAnimationOptions[]) => { - const timeline = new Timeline() - for (let i = 0, ilen = seqOptions.length; i < ilen; i++) { - timeline.add(seqOptions[i]) - } - return timeline -} diff --git a/src/main.ts b/src/main.ts index ab5d375d..b7c95eec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,45 @@ -export * from './helpers' +import { listify, isDefined } from './utils' +import { addPlugin, Timeline } from './core' +import { waapiPlugin } from './plugins/waapi-plugin' +import * as types from './types' + +// configure plugins +addPlugin(waapiPlugin) + +/** + * Returns a new timeline of animation(s) using the options provided + * @param options Animtion options or an array of options + */ +export const animate = ( + options?: types.AddAnimationOptions | types.AddAnimationOptions[] +) => { + const timeline = new Timeline() + if (options) { + var optionList = listify(options) + for (var i = 0, ilen = optionList.length; i < ilen; i++) { + var opt = optionList[i] + if (!isDefined(opt.from)) { + opt.from = 0 + } + timeline.add(opt) + } + } + return timeline +} + +/** + * Returns a new sequence of animations in a timeline + * @param seqOptions an array of animations options + */ +export const sequence = (seqOptions: types.AddAnimationOptions[]) => { + const timeline = new Timeline() + for (let i = 0, ilen = seqOptions.length; i < ilen; i++) { + timeline.add(seqOptions[i]) + } + return timeline +} + +// export utils and types export { random, shuffle } from './utils' export { splitText } from './core' - -import * as types from './types' export { types } diff --git a/src/plugins/waapi-plugin.ts b/src/plugins/waapi-plugin.ts new file mode 100644 index 00000000..352e8d52 --- /dev/null +++ b/src/plugins/waapi-plugin.ts @@ -0,0 +1,195 @@ +import * as types from '../types' +import { + _, + isDOM, + isNumber, + includes, + indexOf, + inRange, + lazy, + minMax, + RUNNING, + CANCEL, + PAUSE, + FINISH, + SEEK, + TRANSFORM, + UPDATE +} from '../utils' + +const TOLERANCE = 0.0001 +const PADDING = 0 +const PERSPECTIVE = 'perspective' +const ROTATE = 'rotate' +const SCALE = 'scale' +const TRANSLATE = 'translate' +const PX = 'px' +const DEG = 'deg' +const X = 'X' +const Y = 'Y' +const Z = 'Z' + +// animatable length properties +const lengthProps = (`backgroundSize,border,borderBottom,borderBottomLeftRadius,borderBottomRightRadius,borderBottomWidth,borderLeft,borderLeftWidth,borderRadius,borderRight,borderRightWidth` + + `,borderTop,borderTopLeftRadius,borderTopRightRadius,borderTopWidth,borderWidth,bottom,columnGap,columnRuleWidth,columnWidth,columns,flexBasis,font,fontSize,gridColumnGap,gridGap,gridRowGap` + + `,height,left,letterSpacing,lineHeight,margin,marginBottom,marginLeft,marginRight,marginTop,maskSize,maxHeight,maxWidth,minHeight,minWidth,outline,outlineOffset,outlineWidth,padding,` + + `paddingBottom,paddingLeft,paddingRight,paddingTop,perspective,right,shapeMargin,tabSize,top,width,wordSpacing`).split( + ',' +) + +const transformAngles = [ROTATE + X, ROTATE + Y, ROTATE + Z, ROTATE] +const transformScales = [SCALE + X, SCALE + Y, SCALE + Z, SCALE] +const transformLengths = [PERSPECTIVE, 'x', 'y', 'z'] +const transforms = transformAngles.concat(transformScales, transformLengths) + +const aliases = { + x: TRANSLATE + X, + y: TRANSLATE + Y, + z: TRANSLATE + Z +} + +function fixUnits(effects: types.PropertyEffects) { + // suffix lengths + for (const propName in effects) { + const keyframes = effects[propName] + const value = keyframes[propName] + if (includes(lengthProps, propName) && isNumber(value)) { + keyframes[propName] = value + 'px' + } + } +} + +function handleTransforms(effects: types.PropertyEffects) { + // handle transforms + const transformEffects: { + offset: number + value: string + }[] = [] + + for (var name in effects) { + if (name !== TRANSFORM && !includes(transforms, name)) { + continue + } + + // pull transform prop and remove it from the effects + const effectOptions = effects[name] + effects[name] = _ + + // append default unit if property requires one + for (var i = 0, ilen = effectOptions.length; i < ilen; i++) { + var k = effectOptions[i] + // use an episilon so same frames aren't missed due to floating point issues + var index = indexOf( + transformEffects, + t => Math.abs(t.offset - k.offset) < TOLERANCE + ) + + // create or find an existing effect at the offset + const effect = + index !== -1 + ? transformEffects[index] + : { + offset: k.offset, + value: '' + } + + if (index === -1) { + transformEffects.push(effect) + } + + if (name === TRANSFORM) { + // take transform at face value, override all other values + effect.value = k.value + '' + } else { + // convert each found values to transform functions and to list of effects + let value = k.value + if (includes(transformLengths, name) && isNumber(value)) { + value += PX + } else if (includes(transformAngles, name) && isNumber(value)) { + value += DEG + } + effect.value += `${aliases[name] || name}(${value})` + } + } + } + + if (transformEffects.length) { + // apply transform effects to keyframes + effects[TRANSFORM] = transformEffects + } +} + +function animateEffect(effect: types.Effect): types.AnimationController { + const { keyframes, from, to, target } = effect + const getAnimator = lazy(() => { + const a = (target as any).animate(keyframes, { + duration: to - from, + fill: 'both' + }) + a.pause() + return a + }) + + return (type: string, time: number, playbackRate: number) => { + const animator = getAnimator() + + if (animator.playbackRate !== playbackRate) { + // set playbackRate direction/speed + animator.playbackRate = playbackRate + } + + if (type === CANCEL) { + animator.cancel() + return + } + if (type === FINISH) { + // without pause() WAAPI appears to play the animation again on seek + animator.finish() + animator.pause() + return + } + + if (type === PAUSE) { + animator.pause() + } + + const isForwards = (playbackRate || 0) >= 0 + const duration = to - from + const currentTime = (time !== _ ? time : isForwards ? 0 : duration) - from + if (type === PAUSE || type === SEEK) { + // sync if paused or seeking + animator.currentTime = minMax(currentTime, 0, duration) + } + + if (type === UPDATE && animator.playState !== RUNNING) { + const sign = isForwards ? 1 : -1 + const isActive = inRange(currentTime + PADDING * sign, 0, duration) + + if (isActive) { + // sync time + animator.currentTime = minMax(currentTime, 0, duration) + + // start if ticking and animator is not running + animator.play() + } + } + } +} + +export const waapiPlugin: types.Plugin = { + animate(effects: types.Effect[], animations: types.AnimationController[]) { + for (var i = 0, ilen = effects.length; i < ilen; i++) { + var effect = effects[i] + if (isDOM(effect.target)) { + animations.push(animateEffect(effect)) + } + } + }, + transform( + _target: types.TargetConfiguration, + effects: types.PropertyEffects + ) { + fixUnits(effects) + handleTransforms(effects) + } +} diff --git a/src/transformers/fix-offsets.ts b/src/transformers/fix-offsets.ts deleted file mode 100644 index 29e93097..00000000 --- a/src/transformers/fix-offsets.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Keyframe } from '../types' -import { _, tail, head } from '../utils' - -const fixOffset0 = (keyframes: Keyframe[]) => { - // find offset 0 or first with no offset - let first: Keyframe = - head(keyframes, (k: Keyframe) => k.offset === 0) - || head(keyframes, (k: Keyframe) => k.offset === _) - - // if no offset 0, insert an empty one - if (first === _) { - first = {} - keyframes.splice(0, 0, first) - } - - // set the first frame to offset 0 - if (first.offset !== 0) { - first.offset = 0 - } -} - -const fixOffset1 = (keyframes: Keyframe[]) => { - // find offset 1 or the last with no offset - let last: Keyframe = - tail(keyframes, (k: Keyframe) => k.offset === 1) - || tail(keyframes, (k: Keyframe) => k.offset === _) - - // if no offset 1, insert an empty one - if (last === _) { - last = {} - keyframes.push(last) - } - // if last offset not 1, set it explicitly - if (last.offset !== 1) { - last.offset = 1 - } -} - -export const fixOffsets = (keyframes: Keyframe[]) => { - fixOffset0(keyframes) - fixOffset1(keyframes) -} diff --git a/src/transformers/index.ts b/src/transformers/index.ts index 0f6d61a7..65b2b3a8 100644 --- a/src/transformers/index.ts +++ b/src/transformers/index.ts @@ -1,3 +1,3 @@ -export * from './fix-offsets' +export * from './infer-offsets' export * from './props-to-keyframes' export * from './resolve-property' diff --git a/src/transformers/infer-offsets.ts b/src/transformers/infer-offsets.ts index 8b73de5e..d01dc550 100644 --- a/src/transformers/infer-offsets.ts +++ b/src/transformers/infer-offsets.ts @@ -1,57 +1,57 @@ - -import { KeyframeOptions } from '../types'; -import { head, tail } from '../utils/lists'; -import { isDefined } from '../utils/type'; +import { KeyframeOptions } from '../types' +import { head, tail } from '../utils/lists' +import { isDefined } from '../utils/type' export function inferOffsets(keyframes: KeyframeOptions[]) { - if (!keyframes.length) { - return; - } - - // search for offset 0 or assume it is the first one in the list - const first = head(keyframes, k => k.offset === 0) || keyframes[0] - if (!isDefined(first.offset)) { - // if no offset is set on first keyframe, it is assumed to be 0 - first.offset = 0 - } - - // search for offset 1 or assume it is the last one in the list - const last = tail(keyframes, k => k.offset === 1) || keyframes[keyframes.length - 1] - if (keyframes.length > 1 && !isDefined(last.offset)) { - // if no offset is set on last keyframe, it is assumed to be 1 - last.offset = 1 + if (!keyframes.length) { + return + } + + // search for offset 0 or assume it is the first one in the list + const first = head(keyframes, k => k.offset === 0) || keyframes[0] + if (!isDefined(first.offset)) { + // if no offset is set on first keyframe, it is assumed to be 0 + first.offset = 0 + } + + // search for offset 1 or assume it is the last one in the list + const last = + tail(keyframes, k => k.offset === 1) || keyframes[keyframes.length - 1] + if (keyframes.length > 1 && !isDefined(last.offset)) { + // if no offset is set on last keyframe, it is assumed to be 1 + last.offset = 1 + } + + // fill in the rest of the offsets + for (let i = 1, ilen = keyframes.length; i < ilen; i++) { + const target = keyframes[i] + if (isDefined(target.offset)) { + // skip entries that have an offset + continue } - // fill in the rest of the offsets - for (let i = 1, ilen = keyframes.length; i < ilen; i++) { - const target = keyframes[i] - if (isDefined(target.offset)) { - // skip entries that have an offset - continue - } - - // search for the next offset with a value - for (let j = i + 1; j < ilen; j++) { - // pass if offset is not set - const endTime = keyframes[j].offset - if (!isDefined(endTime)) { - continue - } - - // calculate timing/position info - const startTime = keyframes[i - 1].offset - const timeDelta = endTime - startTime - const deltaLength = j - i + 1 - - // set the values of all keyframes between i and j (exclusive) - for (let k = 1; k < deltaLength; k++) { - // set to percentage of change over time delta + starting time - keyframes[k - 1 + i].offset = ((k / j) * timeDelta) + startTime - } - - // move i past this keyframe since all frames between should be processed - i = j - break - } + // search for the next offset with a value + for (let j = i + 1; j < ilen; j++) { + // pass if offset is not set + const endTime = keyframes[j].offset + if (!isDefined(endTime)) { + continue + } + + // calculate timing/position info + const startTime = keyframes[i - 1].offset + const timeDelta = endTime - startTime + const deltaLength = j - i + 1 + + // set the values of all keyframes between i and j (exclusive) + for (let k = 1; k < deltaLength; k++) { + // set to percentage of change over time delta + starting time + keyframes[k - 1 + i].offset = k / j * timeDelta + startTime + } + + // move i past this keyframe since all frames between should be processed + i = j + break } + } } diff --git a/src/transformers/props-to-keyframes.ts b/src/transformers/props-to-keyframes.ts index 47765573..7e378fdf 100644 --- a/src/transformers/props-to-keyframes.ts +++ b/src/transformers/props-to-keyframes.ts @@ -1,29 +1,33 @@ -import { KeyframeOptions, PropertyOptions, KeyframeValueResolver } from '../types' -import { isArray } from '../utils' +import { + KeyframeOptions, + PropertyOptions, + KeyframeValueResolver +} from '../types' +import { isArrayLike } from '../utils' -export function propsToKeyframes (css: PropertyOptions): KeyframeOptions[] { - // create a map to capture each keyframe by offset - const keyframes: KeyframeOptions[] = [] +export function propsToKeyframes(css: PropertyOptions): KeyframeOptions[] { + // create a map to capture each keyframe by offset + const keyframes: KeyframeOptions[] = [] - // iterate over each property split it into keyframes - for (let prop in css) { - if (!css.hasOwnProperty(prop)) { - continue - } + // iterate over each property split it into keyframes + for (let prop in css) { + if (!css.hasOwnProperty(prop)) { + continue + } - // resolve value (changes function into discrete value or array) - const val = css[prop] + // resolve value (changes function into discrete value or array) + const val = css[prop] - if (isArray(val)) { - // if the value is an array, split up the offset automatically - const valAsArray = val as KeyframeValueResolver[] - for (let i = 0, ilen = valAsArray.length; i < ilen; i++) { - const offset = i === 0 ? 0 : i === ilen - 1 ? 1 : i / (ilen - 1.0) - keyframes.push({ offset, [prop]: val[i] }) - } - } else { - keyframes.push({ offset: 1, [prop]: val as KeyframeValueResolver }) - } + if (isArrayLike(val)) { + // if the value is an array, split up the offset automatically + const valAsArray = val as KeyframeValueResolver[] + for (let i = 0, ilen = valAsArray.length; i < ilen; i++) { + const offset = i === 0 ? 0 : i === ilen - 1 ? 1 : i / (ilen - 1.0) + keyframes.push({ offset, [prop]: val[i] }) + } + } else { + keyframes.push({ offset: 1, [prop]: val as KeyframeValueResolver }) } - return keyframes + } + return keyframes } diff --git a/src/transformers/resolve-property.ts b/src/transformers/resolve-property.ts index dab00dd6..7b17bb19 100644 --- a/src/transformers/resolve-property.ts +++ b/src/transformers/resolve-property.ts @@ -1,29 +1,41 @@ -import { AnimationTarget, KeyframeValueResolver, KeyframeFunction } from '../types'; -import { isDefined, isFunction, isNumber, unitExpression } from '../utils'; +import { + AnimationTarget, + KeyframeValueResolver, + KeyframeFunction +} from '../types' +import { isDefined, isFunction, isNumber, unitExpression } from '../utils' /** * Resolves the property/value of an animation */ -export const resolve = (input: KeyframeValueResolver, target: AnimationTarget, index?: number, isStep = false): any => { - let output = isFunction(input) - ? resolve((input as KeyframeFunction)(target, index), target, index) - : input as T1 | number | string +export const resolve = ( + input: KeyframeValueResolver, + target: AnimationTarget, + index?: number, + isStep = false +): any => { + let output = isFunction(input) + ? resolve((input as KeyframeFunction)(target, index), target, index) + : input as T1 | number | string - if (isDefined(output) && !isNumber(output)) { - const match = unitExpression.exec(output as any as string) as RegExpExecArray - if (match) { - const stepTypeString = match[1] - const startString = match[2] - const unitTypeString = match[3] - const num = startString ? parseFloat(startString) : 0 - const sign = stepTypeString === '-=' ? -1 : 1 - const step = isStep || stepTypeString && isDefined(index) ? (index || 0) + 1 : 1 - const val = sign * num * step - return unitTypeString ? val + unitTypeString : val - } + if (isDefined(output) && !isNumber(output)) { + const match = unitExpression.exec( + (output as any) as string + ) as RegExpExecArray + if (match) { + const stepTypeString = match[1] + const startString = match[2] + const unitTypeString = match[3] + const num = startString ? parseFloat(startString) : 0 + const sign = stepTypeString === '-=' ? -1 : 1 + const step = + isStep || (stepTypeString && isDefined(index)) ? (index || 0) + 1 : 1 + const val = sign * num * step + return unitTypeString ? val + unitTypeString : val } - if (isNumber(output) && isStep) { - return (output as number) * (index + 1) - } - return output + } + if (isNumber(output) && isStep) { + return (output as number) * (index + 1) + } + return output } diff --git a/src/types.ts b/src/types.ts index 34c63fc1..858244e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,3 @@ - export interface KeyframeOptions { offset?: number [val: string]: KeyframeValueResolver @@ -26,7 +25,7 @@ export const direction = { normal: 0, alternate: 1 } - + export type AnimationDomTarget = Node | NodeList | string export type AnimationTarget = string | {} export type KeyframeValue = string | number @@ -49,22 +48,27 @@ export interface SplitTextResult { characters: HTMLElement[] } -export interface PropertyHandler { - (target: TargetConfiguration, effects: PropertyEffects): void +export interface AnimationController { + (type: string, time: number, playbackRate: number): void +} + +export interface Plugin { + transform(target: TargetConfiguration, effects: PropertyEffects): void + animate(options: Effect[], animations: AnimationController[]): void } export interface PropertyKeyframe { time: number prop: string index: number - order: number; + order: number value: KeyframeValueResolver } export interface EffectOptions { - offset: number - value: string | number - } + offset: number + value: string | number +} export interface PropertyEffects { [name: string]: EffectOptions[] } diff --git a/src/utils/get-targets.ts b/src/utils/get-targets.ts index c415d7d4..1c61e342 100644 --- a/src/utils/get-targets.ts +++ b/src/utils/get-targets.ts @@ -1,41 +1,41 @@ import { AnimationTarget } from '../types' import { toArray } from './lists' -import { isArray, isElement, isFunction, isObject, isString } from './type' +import { isArrayLike, isDOM, isFunction, isObject, isString } from './type' /** * Recursively resolves the element source from dom, selector, jquery, array, and function sources * * @param {ElementSource} source from which to locate elements * @returns {Element[]} array of elements found */ -export function getTargets (target: AnimationTarget): (HTMLElement | {})[] { - if (isString(target)) { - // if query selector, search for elements - return toArray(document.querySelectorAll(target as string)) - } - if (isElement(target)) { - // if a single element, wrap in array - return [target] - } - if (isFunction(target)) { - // if function, call it and call this function - const provider = target as { (): AnimationTarget; } - const result = provider() - return getTargets(result) - } - if (isArray(target)) { - // if array or jQuery object, flatten to an array - const elements: HTMLElement[] = [] - for (let i = 0, ilen = (target as any[]).length; i < ilen; i++) { - // recursively call this function in case of nested elements - elements.push.apply(elements, getTargets(target[i])) - } - return elements - } - if (isObject(target)) { - // if it is an actual object at this point, handle it - return [target] as {}[] +export function getTargets(target: AnimationTarget): (HTMLElement | {})[] { + if (isString(target)) { + // if query selector, search for elements + return toArray(document.querySelectorAll(target as string)) + } + if (isDOM(target)) { + // if a single element, wrap in array + return [target] + } + if (isFunction(target)) { + // if function, call it and call this function + const provider = target as { (): AnimationTarget } + const result = provider() + return getTargets(result) + } + if (isArrayLike(target)) { + // if array or jQuery object, flatten to an array + const elements: HTMLElement[] = [] + for (let i = 0, ilen = (target as any[]).length; i < ilen; i++) { + // recursively call this function in case of nested elements + elements.push.apply(elements, getTargets(target[i])) } + return elements + } + if (isObject(target)) { + // if it is an actual object at this point, handle it + return [target] as {}[] + } - // otherwise return empty - return [] + // otherwise return empty + return [] } diff --git a/src/utils/lists.ts b/src/utils/lists.ts index 9162c37b..62aef4a2 100644 --- a/src/utils/lists.ts +++ b/src/utils/lists.ts @@ -1,70 +1,76 @@ -import { isArray } from './type' +import { isArrayLike } from './type' import { _ } from '.' const slice = Array.prototype.slice export interface IList { - [key: number]: T - length: number + [key: number]: T + length: number } -export const includes = (items: T[], item: T) => - items.indexOf(item) !== -1 +export const includes = (items: T[], item: T) => items.indexOf(item) !== -1 /** * Returns the first object in the list or undefined */ -export const head = (indexed: IList, predicate?: { (t: T): boolean; }): T => { - if (!indexed || indexed.length < 1) { - return _ - } - if (predicate === _) { - return indexed[0] - } - for (const item of indexed as T[]) { - if (predicate(item)) { - return item - } - } +export const head = ( + indexed: IList, + predicate?: { (t: T): boolean } +): T => { + if (!indexed || indexed.length < 1) { return _ + } + if (predicate === _) { + return indexed[0] + } + for (const item of indexed as T[]) { + if (predicate(item)) { + return item + } + } + return _ } /** * Returns the last object in the list or undefined */ -export const tail = (indexed: IList, predicate?: { (t: T): boolean; }): T => { - if (!indexed || indexed.length < 1) { - return _ - } - if (predicate === _) { - return indexed[indexed.length - 1] - } - for (const item of indexed as T[]) { - if (predicate(item)) { - return item - } - } +export const tail = ( + indexed: IList, + predicate?: { (t: T): boolean } +): T => { + if (!indexed || indexed.length < 1) { return _ + } + if (predicate === _) { + return indexed[indexed.length - 1] + } + for (const item of indexed as T[]) { + if (predicate(item)) { + return item + } + } + return _ } /** * Returns the index of the first matching item or -1 */ export const indexOf = (items: T[], predicate: { (t: T): boolean }) => { - for (let i = 0, ilen = items.length; i < ilen; i++) { - const item = items[i] - if (predicate(item)) { - return i - } + for (let i = 0, ilen = items.length; i < ilen; i++) { + const item = items[i] + if (predicate(item)) { + return i } - return -1 + } + return -1 } export function sortBy(fieldName: keyof T) { - return (a: T, b: T) => { - const a1 = a[fieldName], b1 = b[fieldName] - return a1 < b1 ? -1 : a1 > b1 ? 1 : 0 - } + return (a: T, b: T) => { + const a1 = a[fieldName], + b1 = b[fieldName] + return a1 < b1 ? -1 : a1 > b1 ? 1 : 0 + } } /** @@ -76,7 +82,8 @@ export function sortBy(fieldName: keyof T) { * @param {T[]} list to convert * @returns {T[]} array clone of list */ -export const toArray = (indexed: IList, index?: number): T[] => slice.call(indexed, index || 0) +export const toArray = (indexed: IList, index?: number): T[] => + slice.call(indexed, index || 0) /** * returns an array or an object wrapped in an array @@ -87,7 +94,7 @@ export const toArray = (indexed: IList, index?: number): T[] => slice.call * @returns {T[]} */ export const listify = (indexed: IList | T): T[] => { - return isArray(indexed) ? indexed as T[] : [indexed as T] + return isArrayLike(indexed) ? indexed as T[] : [indexed as T] } /** diff --git a/src/utils/math.ts b/src/utils/math.ts index c811afa4..17e28031 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -1,4 +1,6 @@ -import { _ } from './resources'; +import { _ } from './resources' -export const inRange = (val: number, min: number, max: number) => val !== _ && min <= val && val <= max -export const minMax = (val: number, min: number, max: number) => val < min ? min : val > max ? max : val +export const inRange = (val: number, min: number, max: number) => + val !== _ && min <= val && val <= max +export const minMax = (val: number, min: number, max: number) => + val < min ? min : val > max ? max : val diff --git a/src/utils/objects.ts b/src/utils/objects.ts index 14253b02..79d054fe 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -1,47 +1,57 @@ -import { isArray, isObject, getTypeString } from './type' +import { isArrayLike, isObject, getTypeString } from './type' import { _ } from '.' /** * Copies a single property from origin to destination */ -export const deepCopyProperty = (prop: string | number, origin: {}, dest: {}): void => { - const originProp = origin[prop] - let destProp = dest[prop] +export const deepCopyProperty = ( + prop: string | number, + origin: {}, + dest: {} +): void => { + const originProp = origin[prop] + let destProp = dest[prop] - // if the source and target don't have the same type, replace with target - const originType = getTypeString(originProp) - const destType = getTypeString(destProp) + // if the source and target don't have the same type, replace with target + const originType = getTypeString(originProp) + const destType = getTypeString(destProp) - if (originType !== destType) { - destProp = _ - } + if (originType !== destType) { + destProp = _ + } - if (isArray(originProp)) { - // note: a compromise until a solution for merging arrays becomes clear - dest[prop] = originProp.slice(0) - } else if (isObject(originProp)) { - // tslint:disable-next-line:no-use-before-declare - dest[prop] = deepCopyObject(originProp, destProp) - } else { - dest[prop] = originProp - } + if (isArrayLike(originProp)) { + // note: a compromise until a solution for merging arrays becomes clear + dest[prop] = originProp.slice(0) + } else if (isObject(originProp)) { + // tslint:disable-next-line:no-use-before-declare + dest[prop] = deepCopyObject(originProp, destProp) + } else { + dest[prop] = originProp + } } -export const assign = (target: T1, source: T2): T1 & T2 => { - const result = target as T1 & T2 - for (let prop in source) { - result[prop] = source[prop] - } - return result +export const assign = ( + target: T1, + source: T2 +): T1 & T2 => { + const result = target as T1 & T2 + for (let prop in source) { + result[prop] = source[prop] + } + return result } /** * performs a deep copy of properties from origin to destination */ -export const deepCopyObject = (origin: T1, dest?: T2): any => { - dest = dest || {} as T2 - for (let prop in origin) { - deepCopyProperty(prop, origin, dest) - } - return dest +export const deepCopyObject = ( + origin: T1, + dest?: T2 +): any => { + dest = dest || ({} as T2) + for (let prop in origin) { + deepCopyProperty(prop, origin, dest) + } + return dest } diff --git a/src/utils/random.ts b/src/utils/random.ts index 513a53cf..594122c6 100644 --- a/src/utils/random.ts +++ b/src/utils/random.ts @@ -8,13 +8,18 @@ * @memberOf JustAnimate */ export const shuffle = (choices: T[]): T => { - return choices[Math.floor(Math.random() * choices.length)] + return choices[Math.floor(Math.random() * choices.length)] } -export const random = (first: number, last: number, unit?: string, wholeNumbersOnly?: boolean): number | string => { - let val = first + (Math.random() * (last - first)) - if (wholeNumbersOnly === true) { - val = Math.floor(val) - } - return !unit ? val : val + unit +export const random = ( + first: number, + last: number, + unit?: string, + wholeNumbersOnly?: boolean +): number | string => { + let val = first + Math.random() * (last - first) + if (wholeNumbersOnly === true) { + val = Math.floor(val) + } + return !unit ? val : val + unit } diff --git a/src/utils/resources.ts b/src/utils/resources.ts index 6da7a5fb..28a5bd3f 100644 --- a/src/utils/resources.ts +++ b/src/utils/resources.ts @@ -1,22 +1,21 @@ export const _ = undefined as undefined -export const - measureExpression = /^[ ]*([\-]{0,1}[0-9]*[\.]{0,1}[0-9]*){1}[ ]*([a-z%]+){0,1}$/i, - unitExpression = /^[ ]*([+-][=]){0,1}[ ]*([\-]{0,1}[0-9]*[\.]{0,1}[0-9]+){1}[ ]*([a-z%]+){0,1}$/i +export const measureExpression = /^[ ]*([\-]{0,1}[0-9]*[\.]{0,1}[0-9]*){1}[ ]*([a-z%]+){0,1}$/i, + unitExpression = /^[ ]*([+-][=]){0,1}[ ]*([\-]{0,1}[0-9]*[\.]{0,1}[0-9]+){1}[ ]*([a-z%]+){0,1}$/i export const ALTERNATE = 'alternate', - CANCEL = 'cancel', - FATAL = 'fatal', - FINISH = 'finish', - FINISHED = 'finished', - IDLE = 'idle', - ITERATION = 'iteration', - NORMAL = 'normal', - PAUSE = 'pause', - PAUSED = 'paused', - PENDING = 'pending', - PLAY = 'play', - RUNNING = 'running', - SEEK = 'seek', - TRANSFORM = 'transform', - UPDATE = 'update' + CANCEL = 'cancel', + FATAL = 'fatal', + FINISH = 'finish', + FINISHED = 'finished', + IDLE = 'idle', + ITERATION = 'iteration', + NORMAL = 'normal', + PAUSE = 'pause', + PAUSED = 'paused', + PENDING = 'pending', + PLAY = 'play', + RUNNING = 'running', + SEEK = 'seek', + TRANSFORM = 'transform', + UPDATE = 'update' diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 9994c2b3..c30bd814 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -1,8 +1,11 @@ -import { isString } from './type'; +import { isString } from './type' -const camelCaseRegex = /([a-z])[- ]([a-z])/ig -const camelCaseReplacer = (_: string, p1: string, p2: string) => p1 + p2.toUpperCase() +const camelCaseRegex = /([a-z])[- ]([a-z])/gi +const camelCaseReplacer = (_: string, p1: string, p2: string) => + p1 + p2.toUpperCase() export function toCamelCase(value: string): string { - return isString(value) ? (value as string).replace(camelCaseRegex, camelCaseReplacer) : '' + return isString(value) + ? (value as string).replace(camelCaseRegex, camelCaseReplacer) + : '' } diff --git a/src/utils/type.ts b/src/utils/type.ts index a9f6853a..6363a770 100644 --- a/src/utils/type.ts +++ b/src/utils/type.ts @@ -1,27 +1,11 @@ export const isDefined = (a: any) => !!a || a === 0 || a === false - -/** - * Calls the native object.toString for real type comparisons - */ export const getTypeString = (val: any) => Object.prototype.toString.call(val) - -/** - * Tests if object is a function - */ -export const isFunction = (a: any) => getTypeString(a) === '[object Function]' +export const isFunction = (a: any) => typeof a === 'function' export const isNumber = (a: any) => typeof a === 'number' export const isObject = (a: any) => typeof a === 'object' && !!a export const isString = (a: any) => typeof a === 'string' - -/** - * Tests if object is an array - */ -export function isArray(a: any) { - return isDefined(a) && !isString(a) && !isFunction(a) && isNumber(a.length) +export function isArrayLike(a: any) { + return isDefined(a) && !isString(a) && !isFunction(a) && isNumber(a.length) } - -/** - * Returns true if the target appears to be an element. This helper is looking for a value tagName - * This is more useful than doing instanceof Element since WAAPI should support virtual elements - */ -export const isElement = (target: any) => !!target && typeof target.tagName === 'string' +export const isSVG = (target: SVGElement | any) => target instanceof SVGAElement +export const isDOM = (target: Node | any) => target.nodeType || isSVG(target) diff --git a/src/utils/units.ts b/src/utils/units.ts index a4ccbada..2a88a258 100644 --- a/src/utils/units.ts +++ b/src/utils/units.ts @@ -9,41 +9,41 @@ export const stepBackward = '-=' * Parses a string or number and returns the unit and numeric value */ export const parseUnit = (val: string | number, output?: Unit): Unit => { - output = output || {} as Unit - - if (!isDefined(val)) { - output.unit = _ - output.value = _ - } else if (isNumber(val)) { - output.unit = _ - output.value = val as number - } else { - const match = measureExpression.exec(val as string) as RegExpExecArray - const startString = match[1] - const unitTypeString = match[2] - - output.unit = unitTypeString || _ - output.value = startString ? parseFloat(startString) : _ - } - - return output + output = output || ({} as Unit) + + if (!isDefined(val)) { + output.unit = _ + output.value = _ + } else if (isNumber(val)) { + output.unit = _ + output.value = val as number + } else { + const match = measureExpression.exec(val as string) as RegExpExecArray + const startString = match[1] + const unitTypeString = match[2] + + output.unit = unitTypeString || _ + output.value = startString ? parseFloat(startString) : _ + } + + return output } /** * returns the unit as a number (resolves seconds to milliseconds) */ export const convertToMs = (val: string | number) => { - if (!isDefined(val) || isNumber(val)) { - return val as number - } - const match = measureExpression.exec(val as string) - const unit = match[2] - return +match[1] * (unit === 's' ? 1000 : unit === 'm' ? 60000 : 1) + if (!isDefined(val) || isNumber(val)) { + return val as number + } + const match = measureExpression.exec(val as string) + const unit = match[2] + return +match[1] * (unit === 's' ? 1000 : unit === 'm' ? 60000 : 1) } export type Unit = { - value: number; - unit?: string; + value: number + unit?: string } export type UnitResolver = (index: number) => Unit | string | number diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d9ed2ce5..134945d4 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,22 +1,18 @@ +const now = performance && performance.now ? performance.now : Date.now -/** - * Wrapper for performance now() with a fallback to Date.now() - */ -export const now = (): number => { - return performance && performance.now ? performance.now() : Date.now() -} +export { now } /** * Wrapper for raf with fallback to setTimeout */ -export const raf = (ctx: any, fn: Function): any => { - const callback = () => { fn(ctx) } - return requestAnimationFrame - ? requestAnimationFrame(callback) - : setTimeout(callback, 16) +export function raf(ctx: any, fn: Function): any { + const callback = () => fn(ctx) + return requestAnimationFrame + ? requestAnimationFrame(callback) + : setTimeout(callback, 16) } export function lazy(initializer: () => T) { - let value: T; - return () => value || (value = initializer()) + let value: T + return () => value || (value = initializer()) } diff --git a/tests/features/basic.ts b/tests/features/basic.ts index c1c478ee..28f74f78 100644 --- a/tests/features/basic.ts +++ b/tests/features/basic.ts @@ -1,4 +1,4 @@ -import { animate } from '../../src/helpers' +import { animate } from '../../src/main' import * as chai from 'chai' const { assert } = chai diff --git a/tests/features/staggering.ts b/tests/features/staggering.ts index ff9cf2f0..922e5e9a 100644 --- a/tests/features/staggering.ts +++ b/tests/features/staggering.ts @@ -1,4 +1,4 @@ -import { animate } from '../../src/helpers' +import { animate } from '../../src/main' import * as chai from 'chai' const { assert } = chai diff --git a/tests/features/timeline.add.ts b/tests/features/timeline.add.ts index 0d34fae0..c41d9497 100644 --- a/tests/features/timeline.add.ts +++ b/tests/features/timeline.add.ts @@ -1,4 +1,4 @@ -import { animate } from '../../src/helpers' +import { animate } from '../../src/main' import * as chai from 'chai' const { assert } = chai diff --git a/tests/features/timeline.to.ts b/tests/features/timeline.to.ts index 6217833f..003fa8d8 100644 --- a/tests/features/timeline.to.ts +++ b/tests/features/timeline.to.ts @@ -1,4 +1,4 @@ -import { animate } from '../../src/helpers' +import { animate } from '../../src/main' import * as chai from 'chai' const { assert } = chai diff --git a/tests/features/transforms.ts b/tests/features/transforms.ts index 25b51b65..4c06db17 100644 --- a/tests/features/transforms.ts +++ b/tests/features/transforms.ts @@ -1,4 +1,4 @@ -import { animate } from '../../src/helpers' +import { animate } from '../../src/main' import * as chai from 'chai' const { assert } = chai diff --git a/tests/utils/type.ts b/tests/utils/type.ts index 578f8096..149ab73d 100644 --- a/tests/utils/type.ts +++ b/tests/utils/type.ts @@ -1,22 +1,22 @@ import * as chai from 'chai' const { assert } = chai -import { isArray, isDefined, isFunction, isNumber, isString } from '../../src/utils' +import { isArrayLike, isDefined, isFunction, isNumber, isString } from '../../src/utils' describe('type', () => { describe('isArray()', () => { it('returns true when an array is passed', () => { - assert.equal(true, isArray([])) + assert.equal(true, isArrayLike([])) }) it('returns true when an array-like object is passed', () => { - assert.equal(true, isArray({ + assert.equal(true, isArrayLike({ length: 0 })) }) it('returns false when a normal object is passed', () => { - assert.equal(false, isArray({})) + assert.equal(false, isArrayLike({})) }) }) diff --git a/tsconfig.json b/tsconfig.json index 1a9e5f85..e4058abb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,7 @@ "forceConsistentCasingInFileNames": true, "inlineSourceMap": false, "module": "commonjs", - "moduleResolution": "node", - "newLine": "lf", + "moduleResolution": "node", "noFallthroughCasesInSwitch": true, "noImplicitAny": true, "noUnusedParameters": true,