-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathwhen.js
More file actions
159 lines (137 loc) · 4.25 KB
/
when.js
File metadata and controls
159 lines (137 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
export const defaultContext = {
add: addAnimator,
remove: removeAnimator,
}
export const unattached = {
add: _ => {},
remove: _ => {}
}
export function always() { return true }
export function When(condition=always, ctx=defaultContext) {
if (new.target == null) return new When(condition, ctx)
this.condition = condition
this.running = false
this._handlers = {start: [], frame: [], at: [], end: [], changed: []}
this._resetDone()
this._ctx = ctx
ctx.add && ctx.add(this)
}
const fire = type => ({
[type](cb) { this._handlers[type].push(cb); return this },
[`_fire_${type}`](ts, currentBuild, lastBuild) {
const cbs = this._handlers[type]
const count = cbs.length
for (let i = 0; i !== count; ++i)
cbs[i].apply(this, [ts, currentBuild, lastBuild])
}
})
Object.assign(When.prototype, ...['start', 'frame', 'at', 'end', 'changed'].map(fire))
When.prototype._resetDone = function() {
this.done = new Promise(_ => this._resolveDone = _)
}
When.prototype.withName = function(name) {
this.name = name
return this
}
When.prototype.withDuration = function(duration) {
this.duration = duration
return this
}
When.prototype.remove = function() {
this._ctx.remove(this)
return this
}
When.prototype.step = function(ts, currentBuild, lastBuild) {
const shouldRun = this.condition[match](ts, currentBuild, lastBuild)
if (shouldRun && !this.running) {
this._fire_start(ts, currentBuild, lastBuild)
this.startedAt = ts
this.running = true
}
if (!shouldRun && this.running) {
this._fire_end(ts, currentBuild, lastBuild)
this.endedAt = ts
this.running = false
this._resolveDone(this)
this._resetDone()
}
if (this.running) {
if (currentBuild !== lastBuild) this._fire_changed(ts, currentBuild, lastBuild)
this._fire_frame(ts, currentBuild, lastBuild)
if (typeof this.duration === 'number') {
const t = (ts - this.startedAt) / this.duration
this.t = t
this._fire_at(t, currentBuild, lastBuild)
}
}
}
export const For = (duration, ctx=defaultContext) => {
let endTime
const anim =
When(ts => !anim.running || ts < endTime, ctx).withDuration(duration)
.start(ts => endTime = ts + duration)
.end(() => ctx.remove(anim))
return anim
}
export const every = interval => {
let lastTick = null
return cb => function (ts, currentBuild, lastBuild) {
const currentTick = Math.floor((ts - this.startedAt) / interval)
if (currentTick !== lastTick)
cb.apply(this, [ts, currentTick, currentBuild, lastBuild])
lastTick = currentTick
}
}
/****** Unit utilities ******/
export const sec = seconds => 1000 * seconds
sec.symbol = Symbol('seconds')
sec[Symbol.toPrimitive] = () => sec.symbol
Object.defineProperty(Number.prototype, sec, {
get() { return sec(this.valueOf()) }
})
Object.defineProperty(String.prototype, sec, {
get() { return sec(+this) }
})
/****** Animation utils ******/
export const lerp = (from, to, map=_=>_) => {
const delta = to - from
return t => map(from + t * delta)
}
/****** Animator framework ******/
global.__animators = global.__animators || []
global.__cascadeDebug = (debug=true) =>
debug
? log = console
: log = debuggingOff
const debuggingOff = { log() {}, table() {}, }
let log = debuggingOff
export function addAnimator(animator) {
global.__animators.push(animator)
log.log('added animator', animator)
log.table(global.__animators)
}
export function removeAnimator(animator) {
const animators = global.__animators
const idx = animators.indexOf(animator)
if (idx >= 0)
animators.splice(idx, 1)
animator._resolveDone()
}
export const runAnimatorStep = (ts, currentBuild, prevBuild) => {
const animators = global.__animators
let i = animators.length; while (i --> 0)
animators[i].step(ts, currentBuild, prevBuild)
}
/****** Condition helpers ******/
export const match = Symbol('when/match')
Object.defineProperty(Function.prototype, match, { get() { return this } })
HTMLElement.prototype[match] = function(_, current) {
return current === this
}
export const buildInRange = (from, to) =>
(ts, current) =>
current &&
current.order >= from.order &&
current.order <= to.order
export const any = (...matchers) => (ts, current, next) =>
matchers.some(m => m [match] (ts, current, next))