mand-mobile
Version:
A Vue.js 2.0 Mobile UI Toolkit
221 lines (189 loc) • 6.67 kB
JavaScript
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