es6-tween
Version:
ES6 implementation of amazing tween.js
805 lines (703 loc) • 21.8 kB
JavaScript
import { add, now, Plugins, remove, isRunning, isLagSmoothing } from './core'
import Easing from './Easing'
import Interpolation from './Interpolation'
import NodeCache, { Store } from './NodeCache'
import Selector from './selector'
import {
decompose,
decomposeString,
recompose,
deepCopy,
SET_NESTED,
EVENT_CALLBACK,
CHAINED_TWEENS,
EVENT_UPDATE,
EVENT_COMPLETE,
EVENT_START,
EVENT_REPEAT,
EVENT_REVERSE,
EVENT_PAUSE,
EVENT_PLAY,
EVENT_RESTART,
EVENT_STOP,
EVENT_SEEK,
FRAME_MS,
TOO_LONG_FRAME_MS
} from './constants'
let _id = 0 // Unique ID
const defaultEasing = Easing.Linear.None
/**
* Tween main constructor
* @constructor
* @class
* @namespace TWEEN.Tween
* @param {Object|Element} node Node Element or Tween initial object
* @param {Object=} object If Node Element is using, second argument is used for Tween initial object
* @example let tween = new Tween(myNode, {width:'100px'}).to({width:'300px'}, 2000).start()
*/
class Tween {
/**
* Easier way to call the Tween
* @param {Element} node DOM Element
* @param {object} object - Initial value
* @param {object} to - Target value
* @param {object} params - Options of tweens
* @example Tween.fromTo(node, {x:0}, {x:200}, {duration:1000})
* @memberof TWEEN.Tween
* @static
*/
static fromTo (node, object, to, params = {}) {
params.quickRender = params.quickRender ? params.quickRender : !to
const tween = new Tween(node, object).to(to, params)
if (params.quickRender) {
tween.render().update(tween._startTime)
tween._rendered = false
tween._onStartCallbackFired = false
}
return tween
}
/**
* Easier way calling constructor only applies the `to` value, useful for CSS Animation
* @param {Element} node DOM Element
* @param {object} to - Target value
* @param {object} params - Options of tweens
* @example Tween.to(node, {x:200}, {duration:1000})
* @memberof TWEEN.Tween
* @static
*/
static to (node, to, params) {
return Tween.fromTo(node, null, to, params)
}
/**
* Easier way calling constructor only applies the `from` value, useful for CSS Animation
* @param {Element} node DOM Element
* @param {object} from - Initial value
* @param {object} params - Options of tweens
* @example Tween.from(node, {x:200}, {duration:1000})
* @memberof TWEEN.Tween
* @static
*/
static from (node, from, params) {
return Tween.fromTo(node, from, null, params)
}
constructor (node, object) {
this.id = _id++
if (!!node && typeof node === 'object' && !object && !node.nodeType) {
object = this.object = node
node = null
} else if (!!node && (node.nodeType || node.length || typeof node === 'string')) {
node = this.node = Selector(node)
object = this.object = NodeCache(node, object, this)
}
this._valuesEnd = null
this._valuesStart = Array.isArray(object) ? [] : {}
this._duration = 1000
this._easingFunction = defaultEasing
this._easingReverse = defaultEasing
this._interpolationFunction = Interpolation.Linear
this._startTime = 0
this._initTime = 0
this._delayTime = 0
this._repeat = 0
this._r = 0
this._isPlaying = false
this._yoyo = false
this._reversed = false
this._onStartCallbackFired = false
this._pausedTime = null
this._isFinite = true
this._maxListener = 15
this._chainedTweensCount = 0
this._prevTime = null
return this
}
/**
* Sets max `event` listener's count to Events system
* @param {number} count - Event listener's count
* @memberof TWEEN.Tween
*/
setMaxListener (count = 15) {
this._maxListener = count
return this
}
/**
* Adds `event` to Events system
* @param {string} event - Event listener name
* @param {Function} callback - Event listener callback
* @memberof TWEEN.Tween
*/
on (event, callback) {
const { _maxListener } = this
const callbackName = event + EVENT_CALLBACK
for (let i = 0; i < _maxListener; i++) {
const callbackId = callbackName + i
if (!this[callbackId]) {
this[callbackId] = callback
break
}
}
return this
}
/**
* Adds `event` to Events system.
* Removes itself after fired once
* @param {string} event - Event listener name
* @param {Function} callback - Event listener callback
* @memberof TWEEN.Tween
*/
once (event, callback) {
const { _maxListener } = this
const callbackName = event + EVENT_CALLBACK
for (let i = 0; i < _maxListener; i++) {
const callbackId = callbackName + i
if (!this[callbackId]) {
this[callbackId] = (...args) => {
callback.apply(this, args)
this[callbackId] = null
}
break
}
}
return this
}
/**
* Removes `event` from Events system
* @param {string} event - Event listener name
* @param {Function} callback - Event listener callback
* @memberof TWEEN.Tween
*/
off (event, callback) {
const { _maxListener } = this
const callbackName = event + EVENT_CALLBACK
for (let i = 0; i < _maxListener; i++) {
const callbackId = callbackName + i
if (this[callbackId] === callback) {
this[callbackId] = null
}
}
return this
}
/**
* Emits/Fired/Trigger `event` from Events system listeners
* @param {string} event - Event listener name
* @memberof TWEEN.Tween
*/
emit (event, arg1, arg2, arg3) {
const { _maxListener } = this
const callbackName = event + EVENT_CALLBACK
if (!this[callbackName + 0]) {
return this
}
for (let i = 0; i < _maxListener; i++) {
const callbackId = callbackName + i
if (this[callbackId]) {
this[callbackId](arg1, arg2, arg3)
}
}
return this
}
/**
* @return {boolean} State of playing of tween
* @example tween.isPlaying() // returns `true` if tween in progress
* @memberof TWEEN.Tween
*/
isPlaying () {
return this._isPlaying
}
/**
* @return {boolean} State of started of tween
* @example tween.isStarted() // returns `true` if tween in started
* @memberof TWEEN.Tween
*/
isStarted () {
return this._onStartCallbackFired
}
/**
* Reverses the tween state/direction
* @example tween.reverse()
* @param {boolean=} state Set state of current reverse
* @memberof TWEEN.Tween
*/
reverse (state) {
const { _reversed } = this
this._reversed = state !== undefined ? state : !_reversed
return this
}
/**
* @return {boolean} State of reversed
* @example tween.reversed() // returns `true` if tween in reversed state
* @memberof TWEEN.Tween
*/
reversed () {
return this._reversed
}
/**
* Pauses tween
* @example tween.pause()
* @memberof TWEEN.Tween
*/
pause () {
if (!this._isPlaying) {
return this
}
this._isPlaying = false
remove(this)
this._pausedTime = now()
return this.emit(EVENT_PAUSE, this.object)
}
/**
* Play/Resume the tween
* @example tween.play()
* @memberof TWEEN.Tween
*/
play () {
if (this._isPlaying) {
return this
}
this._isPlaying = true
this._startTime += now() - this._pausedTime
this._initTime = this._startTime
add(this)
this._pausedTime = now()
return this.emit(EVENT_PLAY, this.object)
}
/**
* Restarts tween from initial value
* @param {boolean=} noDelay If this param is set to `true`, restarts tween without `delay`
* @example tween.restart()
* @memberof TWEEN.Tween
*/
restart (noDelay) {
this._repeat = this._r
this.reassignValues()
add(this)
return this.emit(EVENT_RESTART, this.object)
}
/**
* Seek tween value by `time`. Note: Not works as excepted. PR are welcome
* @param {Time} time Tween update time
* @param {boolean=} keepPlaying When this param is set to `false`, tween pausing after seek
* @example tween.seek(500)
* @memberof TWEEN.Tween
* @deprecated Not works as excepted, so we deprecated this method
*/
seek (time, keepPlaying) {
const { _duration, _initTime, _startTime, _reversed } = this
let updateTime = _initTime + time
this._isPlaying = true
if (updateTime < _startTime && _startTime >= _initTime) {
this._startTime -= _duration
this._reversed = !_reversed
}
this.update(time, false)
this.emit(EVENT_SEEK, time, this.object)
return keepPlaying ? this : this.pause()
}
/**
* Sets tween duration
* @param {number} amount Duration is milliseconds
* @example tween.duration(2000)
* @memberof TWEEN.Tween
* @deprecated Not works as excepted and useless, so we deprecated this method
*/
duration (amount) {
this._duration = typeof amount === 'function' ? amount(this._duration) : amount
return this
}
/**
* Sets target value and duration
* @param {object} properties Target value (to value)
* @param {number|Object=} [duration=1000] Duration of tween
* @example let tween = new Tween({x:0}).to({x:100}, 2000)
* @memberof TWEEN.Tween
*/
to (properties, duration = 1000, maybeUsed) {
this._valuesEnd = properties
if (typeof duration === 'number' || typeof duration === 'function') {
this._duration = typeof duration === 'function' ? duration(this._duration) : duration
} else if (typeof duration === 'object') {
for (const prop in duration) {
if (typeof this[prop] === 'function') {
const [arg1 = null, arg2 = null, arg3 = null, arg4 = null] = Array.isArray(duration[prop])
? duration[prop]
: [duration[prop]]
this[prop](arg1, arg2, arg3, arg4)
}
}
}
return this
}
/**
* Renders and computes value at first render
* @private
* @memberof TWEEN.Tween
*/
render () {
if (this._rendered) {
return this
}
let { _valuesStart, _valuesEnd, object, node, InitialValues } = this
SET_NESTED(object)
SET_NESTED(_valuesEnd)
if (node && node.queueID && Store[node.queueID]) {
const prevTweenByNode = Store[node.queueID]
if (prevTweenByNode.propNormaliseRequired && prevTweenByNode.tween !== this) {
for (const property in _valuesEnd) {
if (prevTweenByNode.tween._valuesEnd[property] !== undefined) {
// delete prevTweenByNode.tween._valuesEnd[property];
}
}
prevTweenByNode.normalisedProp = true
prevTweenByNode.propNormaliseRequired = false
}
}
if (node && InitialValues) {
if (!object || Object.keys(object).length === 0) {
object = this.object = NodeCache(node, InitialValues(node, _valuesEnd), this)
} else if (!_valuesEnd || Object.keys(_valuesEnd).length === 0) {
_valuesEnd = this._valuesEnd = InitialValues(node, object)
}
}
if (!_valuesStart.processed) {
for (const property in _valuesEnd) {
let start = object && object[property] && deepCopy(object[property])
let end = _valuesEnd[property]
if (Plugins[property] && Plugins[property].init) {
Plugins[property].init.call(this, start, end, property, object)
if (start === undefined && _valuesStart[property]) {
start = _valuesStart[property]
}
if (Plugins[property].skipProcess) {
continue
}
}
if (
(typeof start === 'number' && isNaN(start)) ||
start === null ||
end === null ||
start === false ||
end === false ||
start === undefined ||
end === undefined ||
start === end
) {
continue
}
_valuesStart[property] = start
if (Array.isArray(end)) {
if (!Array.isArray(start)) {
end.unshift(start)
for (let i = 0, len = end.length; i < len; i++) {
if (typeof end[i] === 'string') {
end[i] = decomposeString(end[i])
}
}
} else {
if (end.isString && object[property].isString && !start.isString) {
start.isString = true
} else {
decompose(property, object, _valuesStart, _valuesEnd)
}
}
} else {
decompose(property, object, _valuesStart, _valuesEnd)
}
if (typeof start === 'number' && typeof end === 'string' && end[1] === '=') {
continue
}
}
_valuesStart.processed = true
}
if (Tween.Renderer && this.node && Tween.Renderer.init) {
Tween.Renderer.init.call(this, object, _valuesStart, _valuesEnd)
this.__render = true
}
this._rendered = true
return this
}
/**
* Start the tweening
* @param {number|string} time setting manual time instead of Current browser timestamp or like `+1000` relative to current timestamp
* @example tween.start()
* @memberof TWEEN.Tween
*/
start (time) {
this._startTime = time !== undefined ? (typeof time === 'string' ? now() + parseFloat(time) : time) : now()
this._startTime += this._delayTime
this._initTime = this._prevTime = this._startTime
this._onStartCallbackFired = false
this._rendered = false
this._isPlaying = true
add(this)
return this
}
/**
* Stops the tween
* @example tween.stop()
* @memberof TWEEN.Tween
*/
stop () {
let { _isPlaying, _isFinite, object, _startTime, _duration, _r, _yoyo, _reversed } = this
if (!_isPlaying) {
return this
}
let atStart = _isFinite ? (_r + 1) % 2 === 1 : !_reversed
this._reversed = false
if (_yoyo && atStart) {
this.update(_startTime)
} else {
this.update(_startTime + _duration)
}
remove(this)
return this.emit(EVENT_STOP, object)
}
/**
* Set delay of tween
* @param {number} amount Sets tween delay / wait duration
* @example tween.delay(500)
* @memberof TWEEN.Tween
*/
delay (amount) {
this._delayTime = typeof amount === 'function' ? amount(this._delayTime) : amount
return this
}
/**
* Chained tweens
* @param {any} arguments Arguments list
* @example tween.chainedTweens(tween1, tween2)
* @memberof TWEEN.Tween
*/
chainedTweens () {
this._chainedTweensCount = arguments.length
if (!this._chainedTweensCount) {
return this
}
for (let i = 0, len = this._chainedTweensCount; i < len; i++) {
this[CHAINED_TWEENS + i] = arguments[i]
}
return this
}
/**
* Sets how times tween is repeating
* @param {amount} amount the times of repeat
* @example tween.repeat(5)
* @memberof TWEEN.Tween
*/
repeat (amount) {
this._repeat = !this._duration ? 0 : typeof amount === 'function' ? amount(this._repeat) : amount
this._r = this._repeat
this._isFinite = isFinite(amount)
return this
}
/**
* Set delay of each repeat alternate of tween
* @param {number} amount Sets tween repeat alternate delay / repeat alternate wait duration
* @example tween.reverseDelay(500)
* @memberof TWEEN.Tween
*/
reverseDelay (amount) {
this._reverseDelayTime = typeof amount === 'function' ? amount(this._reverseDelayTime) : amount
return this
}
/**
* Set `yoyo` state (enables reverse in repeat)
* @param {boolean} state Enables alternate direction for repeat
* @param {Function=} _easingReverse Easing function in reverse direction
* @example tween.yoyo(true)
* @memberof TWEEN.Tween
*/
yoyo (state, _easingReverse) {
this._yoyo = typeof state === 'function' ? state(this._yoyo) : state === null ? this._yoyo : state
if (!state) {
this._reversed = false
}
this._easingReverse = _easingReverse || null
return this
}
/**
* Set easing
* @param {Function} _easingFunction Easing function, applies in non-reverse direction if Tween#yoyo second argument is applied
* @example tween.easing(Easing.Elastic.InOut)
* @memberof TWEEN.Tween
*/
easing (_easingFunction) {
this._easingFunction = _easingFunction
return this
}
/**
* Set interpolation
* @param {Function} _interpolationFunction Interpolation function
* @example tween.interpolation(Interpolation.Bezier)
* @memberof TWEEN.Tween
*/
interpolation (_interpolationFunction) {
if (typeof _interpolationFunction === 'function') {
this._interpolationFunction = _interpolationFunction
}
return this
}
/**
* Reassigns value for rare-case like Tween#restart or for Timeline
* @private
* @memberof TWEEN.Tween
*/
reassignValues (time) {
const { _valuesStart, object, _delayTime } = this
this._isPlaying = true
this._startTime = time !== undefined ? time : now()
this._startTime += _delayTime
this._reversed = false
add(this)
for (const property in _valuesStart) {
const start = _valuesStart[property]
object[property] = start
}
return this
}
/**
* Updates initial object to target value by given `time`
* @param {Time} time Current time
* @param {boolean=} preserve Prevents from removing tween from store
* @param {boolean=} forceTime Forces to be frame rendered, even mismatching time
* @example tween.update(100)
* @memberof TWEEN.Tween
*/
update (time, preserve, forceTime) {
let {
_onStartCallbackFired,
_easingFunction,
_interpolationFunction,
_easingReverse,
_repeat,
_delayTime,
_reverseDelayTime,
_yoyo,
_reversed,
_startTime,
_prevTime,
_duration,
_valuesStart,
_valuesEnd,
object,
_isFinite,
_isPlaying,
__render,
_chainedTweensCount
} = this
let elapsed
let currentEasing
let property
let propCount = 0
if (!_duration) {
elapsed = 1
_repeat = 0
} else {
time = time !== undefined ? time : now()
let delta = time - _prevTime
this._prevTime = time
if (delta > TOO_LONG_FRAME_MS && isRunning() && isLagSmoothing()) {
time -= delta - FRAME_MS
}
if (!_isPlaying || (time < _startTime && !forceTime)) {
return true
}
elapsed = (time - _startTime) / _duration
elapsed = elapsed > 1 ? 1 : elapsed
elapsed = _reversed ? 1 - elapsed : elapsed
}
if (!_onStartCallbackFired) {
if (!this._rendered) {
this.render()
this._rendered = true
}
this.emit(EVENT_START, object)
this._onStartCallbackFired = true
}
currentEasing = _reversed ? _easingReverse || _easingFunction : _easingFunction
if (!object) {
return true
}
for (property in _valuesEnd) {
const start = _valuesStart[property]
if ((start === undefined || start === null) && !(Plugins[property] && Plugins[property].update)) {
continue
}
const end = _valuesEnd[property]
const value = currentEasing[property]
? currentEasing[property](elapsed)
: typeof currentEasing === 'function'
? currentEasing(elapsed)
: defaultEasing(elapsed)
const _interpolationFunctionCall = _interpolationFunction[property]
? _interpolationFunction[property]
: typeof _interpolationFunction === 'function'
? _interpolationFunction
: Interpolation.Linear
if (typeof end === 'number') {
object[property] = start + (end - start) * value
} else if (Array.isArray(end) && !end.isString && !Array.isArray(start)) {
object[property] = _interpolationFunctionCall(end, value, object[property])
} else if (end && end.update) {
end.update(value)
} else if (typeof end === 'function') {
object[property] = end(value)
} else if (typeof end === 'string' && typeof start === 'number') {
object[property] = start + parseFloat(end[0] + end.substr(2)) * value
} else {
recompose(property, object, _valuesStart, _valuesEnd, value, elapsed)
}
if (Plugins[property] && Plugins[property].update) {
Plugins[property].update.call(this, object[property], start, end, value, elapsed, property)
}
propCount++
}
if (!propCount) {
remove(this)
return false
}
if (__render && Tween.Renderer && Tween.Renderer.update) {
Tween.Renderer.update.call(this, object, elapsed)
}
this.emit(EVENT_UPDATE, object, elapsed, time)
if (elapsed === 1 || (_reversed && elapsed === 0)) {
if (_repeat > 0 && _duration > 0) {
if (_isFinite) {
this._repeat--
}
if (_yoyo) {
this._reversed = !_reversed
} else {
for (property in _valuesEnd) {
let end = _valuesEnd[property]
if (typeof end === 'string' && typeof _valuesStart[property] === 'number') {
_valuesStart[property] += parseFloat(end[0] + end.substr(2))
}
}
}
this.emit(_yoyo && !_reversed ? EVENT_REVERSE : EVENT_REPEAT, object)
if (_reversed && _reverseDelayTime) {
this._startTime = time - _reverseDelayTime
} else {
this._startTime = time + _delayTime
}
return true
} else {
if (!preserve) {
this._isPlaying = false
remove(this)
_id--
}
this.emit(EVENT_COMPLETE, object)
this._repeat = this._r
if (_chainedTweensCount) {
for (let i = 0; i < _chainedTweensCount; i++) {
this[CHAINED_TWEENS + i].start(time + _duration)
}
}
return false
}
}
return true
}
}
export default Tween