cvs
Version:
A basic canvas framework for 2d drawing.
383 lines (325 loc) • 9.29 kB
JavaScript
// borrow from sprite-timeline (https://github.com/spritejs/sprite-timeline)
function createNowTime (syncLocker = true) {
let nowtime = null
if (Date.now) {
nowtime = Date.now
} else {
nowtime = () => new Date().getTime()
}
return nowtime
}
/*
delay = 100 -> delay = {delay: 100}
delay = {entropy: 100} -> delay = {delay: 100, isEntropy: true}
*/
function formatDelay (delay) {
if (typeof delay === 'number') {
delay = { delay }
} else if ('entropy' in delay) {
delay = { delay: delay.entropy, isEntropy: true }
}
return delay
}
const _nowtime = createNowTime()
const defaultOptions = {
originTime: 0,
playbackRate: 1.0
}
const _timeMark = Symbol('timeMark'),
_playbackRate = Symbol('playbackRate'),
_timers = Symbol('timers'),
_originTime = Symbol('originTime'),
_setTimer = Symbol('setTimer'),
_parent = Symbol('parent')
class Timeline {
constructor (options, parent) {
if (options instanceof Timeline) {
parent = options
options = {}
}
options = Object.assign({}, defaultOptions, options)
if (parent) {
this[_parent] = parent
}
const nowtime = options.nowtime || _nowtime
if (!parent) {
const createTime = nowtime()
Object.defineProperty(this, 'globalTime', {
get () {
return nowtime() - createTime
}
})
} else {
Object.defineProperty(this, 'globalTime', {
get () {
return parent.currentTime
}
})
}
// timeMark records the reference points on timeline
// Each time we change the playbackRate or currentTime or entropy
// A new timeMark will be generated
// timeMark sorted by entropy
// If you reset entropy, all the timeMarks behind the new entropy
// should be dropped
this[_timeMark] = [
{
globalTime: this.globalTime,
localTime: -options.originTime,
entropy: -options.originTime,
playbackRate: options.playbackRate,
globalEntropy: 0
}
]
if (this[_parent]) {
this[_timeMark][0].globalEntropy = this[_parent].entropy
}
this[_originTime] = options.originTime
this[_playbackRate] = options.playbackRate
this[_timers] = new Map()
}
get parent () {
return this[_parent]
}
get lastTimeMark () {
return this[_timeMark][this[_timeMark].length - 1]
}
markTime ({
time = this.currentTime,
entropy = this.entropy,
playbackRate = this.playbackRate
} = {}) {
const timeMark = {
globalTime: this.globalTime,
localTime: time,
entropy,
playbackRate,
globalEntropy: this.globalEntropy
}
this[_timeMark].push(timeMark)
}
get currentTime () {
const { localTime, globalTime } = this.lastTimeMark
return localTime + (this.globalTime - globalTime) * this.playbackRate
}
set currentTime (time) {
const from = this.currentTime,
to = time,
timers = this[_timers]
this.markTime({ time })
;[...timers].forEach(([id, timer]) => {
if (!timers.has(id)) return // Need check because it maybe clearTimeout by former handler().
const { isEntropy, delay, heading } = timer.time,
{ handler, startTime } = timer
if (!isEntropy) {
const endTime = startTime + delay
if (
delay === 0 ||
(heading !== false && (to - from) * delay <= 0) ||
(from <= endTime && endTime <= to) ||
(from >= endTime && endTime >= to)
) {
handler()
this.clearTimeout(id)
}
} else if (delay === 0) {
handler()
this.clearTimeout(id)
}
})
this.updateTimers()
}
// Both currentTime and entropy should be influenced by playbackRate.
// If current playbackRate is negative, the currentTime should go backwards
// while the entropy remain to go forwards.
// Both of the initial values is set to -originTime
get entropy () {
const { entropy, globalEntropy } = this.lastTimeMark
return (
entropy +
Math.abs((this.globalEntropy - globalEntropy) * this.playbackRate)
)
}
get globalEntropy () {
return this[_parent] ? this[_parent].entropy : this.globalTime
}
// get globalTime() {
// if(this[_parent]) {
// return this[_parent].currentTime;
// }
// return nowtime();
// }
// change entropy will NOT cause currentTime changing but may influence the pass
// and the future of the timeline. (It may change the result of seek***Time)
// While entropy is set, all the marks behind will be droped
set entropy (entropy) {
if (this.entropy > entropy) {
const idx = this.seekTimeMark(entropy)
this[_timeMark].length = idx + 1
}
this.markTime({ entropy })
this.updateTimers()
}
fork (options) {
return new Timeline(options, this)
}
seekGlobalTime (seekEntropy) {
const idx = this.seekTimeMark(seekEntropy),
timeMark = this[_timeMark][idx]
const { entropy, playbackRate, globalTime } = timeMark
return globalTime + (seekEntropy - entropy) / Math.abs(playbackRate)
}
seekLocalTime (seekEntropy) {
const idx = this.seekTimeMark(seekEntropy),
timeMark = this[_timeMark][idx]
const { localTime, entropy, playbackRate } = timeMark
if (playbackRate > 0) {
return localTime + (seekEntropy - entropy)
}
return localTime - (seekEntropy - entropy)
}
seekTimeMark (entropy) {
const timeMark = this[_timeMark]
let l = 0,
r = timeMark.length - 1
if (entropy <= timeMark[l].entropy) {
return l
}
if (entropy >= timeMark[r].entropy) {
return r
}
let m = Math.floor((l + r) / 2) // binary search
while (m > l && m < r) {
if (entropy === timeMark[m].entropy) {
return m
}
if (entropy < timeMark[m].entropy) {
r = m
} else if (entropy > timeMark[m].entropy) {
l = m
}
m = Math.floor((l + r) / 2)
}
return l
}
get playbackRate () {
return this[_playbackRate]
}
set playbackRate (rate) {
if (rate !== this.playbackRate) {
this.markTime({ playbackRate: rate })
this[_playbackRate] = rate
this.updateTimers()
}
}
get paused () {
if (this.playbackRate === 0) return true
let parent = this.parent
while (parent) {
if (parent.playbackRate === 0) return true
parent = parent.parent
}
return false
}
updateTimers () {
const timers = [...this[_timers]]
timers.forEach(([id, timer]) => {
this[_setTimer](timer.handler, timer.time, id)
})
}
clearTimeout (id) {
const timer = this[_timers].get(id)
if (timer && timer.timerID != null) {
if (this[_parent]) {
this[_parent].clearTimeout(timer.timerID)
} else {
clearTimeout(timer.timerID)
}
}
this[_timers].delete(id)
}
clearInterval (id) {
return this.clearTimeout(id)
}
clear () {
// clear all running timers
const timers = this[_timers]
;[...timers.keys()].forEach(id => {
this.clearTimeout(id)
})
}
/*
setTimeout(func, {delay: 100, isEntropy: true})
setTimeout(func, {entropy: 100})
setTimeout(func, 100})
*/
setTimeout (handler, time = { delay: 0 }) {
return this[_setTimer](handler, time)
}
setInterval (handler, time = { delay: 0 }) {
const that = this
const id = this[_setTimer](function step () {
// reset timer before handler cause we may clearTimeout in handler()
that[_setTimer](step, time, id)
handler()
}, time)
return id
}
[_setTimer] (handler, time, id = Symbol('timerID')) {
time = formatDelay(time)
const timer = this[_timers].get(id)
let delay,
timerID = null,
startTime,
startEntropy
if (timer) {
this.clearTimeout(id)
if (time.isEntropy) {
delay =
(time.delay - (this.entropy - timer.startEntropy)) /
Math.abs(this.playbackRate)
} else {
delay =
(time.delay - (this.currentTime - timer.startTime)) /
this.playbackRate
}
startTime = timer.startTime
startEntropy = timer.startEntropy
} else {
delay =
time.delay /
(time.isEntropy ? Math.abs(this.playbackRate) : this.playbackRate)
startTime = this.currentTime
startEntropy = this.entropy
}
const parent = this[_parent],
globalTimeout = parent ? parent.setTimeout.bind(parent) : setTimeout
const heading = time.heading
// console.log(heading, parent, delay)
if (!parent && heading === false && delay < 0) {
delay = Infinity
}
// if playbackRate is zero, delay will be infinity.
// For wxapp bugs, cannot use Number.isFinite yet.
if (isFinite(delay) || parent) {
// eslint-disable-line no-restricted-globals
delay = Math.ceil(delay)
if (globalTimeout !== setTimeout) {
delay = { delay, heading }
}
timerID = globalTimeout(() => {
this[_timers].delete(id)
handler()
}, delay)
}
this[_timers].set(id, {
timerID,
handler,
time,
startTime,
startEntropy
})
return id
}
}
export default Timeline