|
|
import {root} from './env'
/* istanbul ignore file */const Animate = (global => { /* istanbul ignore next */ const time = Date.now || (() => { return +new Date() }) const desiredFrames = 60 const millisecondsPerSecond = 1000
let running = {} let counter = 1
return { /** * A requestAnimationFrame wrapper / polyfill. * * @param callback {Function} The callback to be invoked before the next repaint. * @param root {HTMLElement} The root element for the repaint */ requestAnimationFrame: (() => { // Check for request animation Frame support
const requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame let isNative = !!requestFrame
if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) { isNative = false }
if (isNative) { return (callback, root) => { requestFrame(callback, root) } }
const TARGET_FPS = 60 let requests = {} let requestCount = 0 let rafHandle = 1 let intervalHandle = null let lastActive = +new Date()
return callback => { const callbackHandle = rafHandle++
// Store callback
requests[callbackHandle] = callback requestCount++
// Create timeout at first request
if (intervalHandle === null) { intervalHandle = setInterval(() => { const time = +new Date() const currentRequests = requests
// Reset data structure before executing callbacks
requests = {} requestCount = 0
for (const key in currentRequests) { if (currentRequests.hasOwnProperty(key)) { currentRequests[key](time) lastActive = time } }
// Disable the timeout when nothing happens for a certain
// period of time
if (time - lastActive > 2500) { clearInterval(intervalHandle) intervalHandle = null } }, 1000 / TARGET_FPS) }
return callbackHandle } })(),
/** * Stops the given animation. * * @param id {Integer} Unique animation ID * @return {Boolean} Whether the animation was stopped (aka, was running before) */ stop(id) { const cleared = running[id] != null cleared && (running[id] = null) return cleared },
/** * Whether the given animation is still running. * * @param id {Integer} Unique animation ID * @return {Boolean} Whether the animation is still running */ isRunning(id) { return running[id] != null },
/** * Start the animation. * * @param stepCallback {Function} Pointer to function which is executed on every step. * Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }` * @param verifyCallback {Function} Executed before every animation step. * Signature of the method should be `function() { return continueWithAnimation; }` * @param completedCallback {Function} * Signature of the method should be `function(droppedFrames, finishedAnimation) {}` * @param duration {Integer} Milliseconds to run the animation * @param easingMethod {Function} Pointer to easing function * Signature of the method should be `function(percent) { return modifiedValue; }` * @param root {Element ? document.body} Render root, when available. Used for internal * usage of requestAnimationFrame. * @return {Integer} Identifier of animation. Can be used to stop it any time. */ start(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) { const start = time() let lastFrame = start let percent = 0 let dropCounter = 0 const id = counter++
if (!root) { // root = document.body
}
// Compacting running db automatically every few new animations
if (id % 20 === 0) { const newRunning = {} for (const usedId in running) { newRunning[usedId] = true } running = newRunning }
// This is the internal step method which is called every few milliseconds
const step = virtual => { // Normalize virtual value
const render = virtual !== true
// Get current time
const now = time()
// Verification is executed before next animation step
if (!running[id] || (verifyCallback && !verifyCallback(id))) { running[id] = null completedCallback && completedCallback(desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, false) return }
// For the current rendering to apply let's update omitted steps in memory.
// This is important to bring internal state variables up-to-date with progress in time.
if (render) { const droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1 for (let j = 0; j < Math.min(droppedFrames, 4); j++) { step(true) dropCounter++ } }
// Compute percent value
if (duration) { percent = (now - start) / duration if (percent > 1) { percent = 1 } }
// Execute step callback, then...
let value = easingMethod ? easingMethod(percent) : percent value = isNaN(value) ? 0 : value if ((stepCallback(value, now, render) === false || percent === 1) && render) { running[id] = null completedCallback && completedCallback( desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, percent === 1 || duration == null, ) } else if (render) { lastFrame = now this.requestAnimationFrame(step, root) } }
// Mark as running
running[id] = true
// Init first step
this.requestAnimationFrame(step, root)
// Return unique animation ID
return id }, }})(root)
export const easeOutCubic = pos => { return Math.pow(pos - 1, 3) + 1}
export const easeInOutCubic = pos => { if ((pos /= 0.5) < 1) { return 0.5 * Math.pow(pos, 3) }
return 0.5 * (Math.pow(pos - 2, 3) + 2)}
export default Animate
|