etro 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +25 -34
- package/README.md +9 -17
- package/dist/custom-array.d.ts +10 -0
- package/dist/effect/base.d.ts +10 -1
- package/dist/effect/shader.d.ts +11 -1
- package/dist/effect/stack.d.ts +6 -2
- package/dist/etro-cjs.js +1182 -592
- package/dist/etro-iife.js +1182 -592
- package/dist/event.d.ts +10 -5
- package/dist/layer/audio-source.d.ts +9 -4
- package/dist/layer/audio.d.ts +15 -2
- package/dist/layer/base.d.ts +49 -3
- package/dist/layer/image.d.ts +15 -1
- package/dist/layer/text.d.ts +6 -3
- package/dist/layer/video.d.ts +13 -1
- package/dist/layer/visual-source.d.ts +18 -3
- package/dist/layer/visual.d.ts +11 -7
- package/dist/movie/effects.d.ts +6 -0
- package/dist/movie/index.d.ts +1 -0
- package/dist/movie/layers.d.ts +6 -0
- package/dist/movie/movie.d.ts +260 -0
- package/dist/object.d.ts +9 -2
- package/dist/util.d.ts +4 -10
- package/eslint.conf.js +4 -2
- package/eslint.test-conf.js +1 -2
- package/karma.conf.js +10 -14
- package/package.json +23 -22
- package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +24 -0
- package/scripts/{save-effect-samples.js → effect/save-effect-samples.js} +1 -1
- package/src/custom-array.ts +43 -0
- package/src/effect/base.ts +23 -22
- package/src/effect/gaussian-blur.ts +11 -6
- package/src/effect/pixelate.ts +3 -3
- package/src/effect/shader.ts +33 -27
- package/src/effect/stack.ts +43 -30
- package/src/effect/transform.ts +16 -9
- package/src/event.ts +111 -21
- package/src/layer/audio-source.ts +60 -20
- package/src/layer/audio.ts +25 -3
- package/src/layer/base.ts +79 -25
- package/src/layer/image.ts +26 -2
- package/src/layer/text.ts +11 -4
- package/src/layer/video.ts +31 -4
- package/src/layer/visual-source.ts +70 -8
- package/src/layer/visual.ts +57 -35
- package/src/movie/effects.ts +26 -0
- package/src/movie/index.ts +1 -0
- package/src/movie/layers.ts +26 -0
- package/src/movie/movie.ts +855 -0
- package/src/object.ts +9 -2
- package/src/util.ts +68 -89
- package/tsconfig.json +3 -1
- package/dist/movie.d.ts +0 -201
- package/examples/application/readme-screenshot.html +0 -85
- package/examples/application/video-player.html +0 -130
- package/examples/application/webcam.html +0 -28
- package/examples/introduction/audio.html +0 -64
- package/examples/introduction/effects.html +0 -79
- package/examples/introduction/export.html +0 -83
- package/examples/introduction/functions.html +0 -37
- package/examples/introduction/hello-world1.html +0 -37
- package/examples/introduction/hello-world2.html +0 -32
- package/examples/introduction/keyframes.html +0 -79
- package/examples/introduction/media.html +0 -63
- package/examples/introduction/text.html +0 -31
- package/private-todo.txt +0 -70
- package/src/movie.ts +0 -742
package/src/effect/stack.ts
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import { Movie } from '../movie'
|
|
2
2
|
import { Visual } from './visual'
|
|
3
3
|
import { Visual as VisualLayer } from '../layer'
|
|
4
|
+
import { CustomArray, CustomArrayListener } from '../custom-array'
|
|
5
|
+
|
|
6
|
+
class StackEffectsListener extends CustomArrayListener<Visual> {
|
|
7
|
+
// eslint-disable-next-line no-use-before-define
|
|
8
|
+
private _stack: Stack
|
|
9
|
+
|
|
10
|
+
constructor (stack: Stack) {
|
|
11
|
+
super()
|
|
12
|
+
this._stack = stack
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
onAdd (effect: Visual) {
|
|
16
|
+
if (!this._stack.parent) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
effect.tryAttach(this._stack.parent)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
onRemove (effect: Visual) {
|
|
24
|
+
if (!this._stack.parent) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
effect.tryDetach()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class StackEffects extends CustomArray<Visual> {
|
|
33
|
+
// eslint-disable-next-line no-use-before-define
|
|
34
|
+
constructor (target: Visual[], stack: Stack) {
|
|
35
|
+
super(target, new StackEffectsListener(stack))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
4
38
|
|
|
5
39
|
export interface StackOptions {
|
|
6
40
|
effects: Visual[]
|
|
@@ -11,58 +45,37 @@ export interface StackOptions {
|
|
|
11
45
|
* for defining reused effect sequences as one effect.
|
|
12
46
|
*/
|
|
13
47
|
export class Stack extends Visual {
|
|
14
|
-
readonly effects:
|
|
15
|
-
|
|
16
|
-
private _effectsBack: Visual[]
|
|
48
|
+
readonly effects: StackEffects
|
|
17
49
|
|
|
18
50
|
constructor (options: StackOptions) {
|
|
19
51
|
super()
|
|
20
52
|
|
|
21
|
-
this.
|
|
22
|
-
// TODO: Throw 'change' events in handlers
|
|
23
|
-
this.effects = new Proxy(this._effectsBack, {
|
|
24
|
-
deleteProperty: function (target: Visual[], property: number | string): boolean {
|
|
25
|
-
const value = target[property]
|
|
26
|
-
value.detach() // Detach effect from movie
|
|
27
|
-
delete target[property]
|
|
28
|
-
return true
|
|
29
|
-
},
|
|
30
|
-
set: function (target: Visual[], property: number | string, value: Visual): boolean {
|
|
31
|
-
// TODO: make sure type check works
|
|
32
|
-
if (!isNaN(Number(property))) { // if property is a number (index)
|
|
33
|
-
if (target[property])
|
|
34
|
-
target[property].detach() // Detach old effect from movie
|
|
35
|
-
|
|
36
|
-
value.attach(this._target) // Attach effect to movie
|
|
37
|
-
}
|
|
38
|
-
target[property] = value
|
|
39
|
-
return true
|
|
40
|
-
}
|
|
41
|
-
})
|
|
53
|
+
this.effects = new StackEffects(options.effects, this)
|
|
42
54
|
options.effects.forEach(effect => this.effects.push(effect))
|
|
43
|
-
|
|
44
|
-
// TODO: Propogate 'change' events from children up
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
attach (movie: Movie): void {
|
|
48
58
|
super.attach(movie)
|
|
59
|
+
|
|
49
60
|
this.effects.filter(effect => !!effect).forEach(effect => {
|
|
50
|
-
effect.
|
|
51
|
-
effect.attach(movie)
|
|
61
|
+
effect.tryAttach(movie)
|
|
52
62
|
})
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
detach (): void {
|
|
56
66
|
super.detach()
|
|
67
|
+
|
|
57
68
|
this.effects.filter(effect => !!effect).forEach(effect => {
|
|
58
|
-
effect.
|
|
69
|
+
effect.tryDetach()
|
|
59
70
|
})
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
apply (target: Movie | VisualLayer, reltime: number): void {
|
|
63
74
|
for (let i = 0; i < this.effects.length; i++) {
|
|
64
75
|
const effect = this.effects[i]
|
|
65
|
-
if (!effect)
|
|
76
|
+
if (!effect) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
66
79
|
effect.apply(target, reltime)
|
|
67
80
|
}
|
|
68
81
|
}
|
package/src/effect/transform.ts
CHANGED
|
@@ -15,9 +15,9 @@ export interface TransformOptions {
|
|
|
15
15
|
*/
|
|
16
16
|
class Transform extends Visual {
|
|
17
17
|
/** Matrix that determines how to transform the target */
|
|
18
|
-
matrix: Dynamic<Transform.Matrix>
|
|
18
|
+
matrix: Dynamic<Transform.Matrix> // eslint-disable-line no-use-before-define
|
|
19
19
|
|
|
20
|
-
private _tmpMatrix: Transform.Matrix
|
|
20
|
+
private _tmpMatrix: Transform.Matrix // eslint-disable-line no-use-before-define
|
|
21
21
|
private _tmpCanvas: HTMLCanvasElement
|
|
22
22
|
private _tmpCtx: CanvasRenderingContext2D
|
|
23
23
|
|
|
@@ -36,11 +36,13 @@ class Transform extends Visual {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
apply (target: Movie | VisualLayer, reltime: number): void {
|
|
39
|
-
if (target.canvas.width !== this._tmpCanvas.width)
|
|
39
|
+
if (target.canvas.width !== this._tmpCanvas.width) {
|
|
40
40
|
this._tmpCanvas.width = target.canvas.width
|
|
41
|
+
}
|
|
41
42
|
|
|
42
|
-
if (target.canvas.height !== this._tmpCanvas.height)
|
|
43
|
+
if (target.canvas.height !== this._tmpCanvas.height) {
|
|
43
44
|
this._tmpCanvas.height = target.canvas.height
|
|
45
|
+
}
|
|
44
46
|
|
|
45
47
|
// Use data, since that's the underlying storage
|
|
46
48
|
this._tmpMatrix.data = val(this, 'matrix.data', reltime)
|
|
@@ -81,8 +83,9 @@ namespace Transform { // eslint-disable-line @typescript-eslint/no-namespace
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
identity (): Matrix {
|
|
84
|
-
for (let i = 0; i < this.data.length; i++)
|
|
86
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
85
87
|
this.data[i] = Matrix.IDENTITY.data[i]
|
|
88
|
+
}
|
|
86
89
|
|
|
87
90
|
return this
|
|
88
91
|
}
|
|
@@ -93,8 +96,9 @@ namespace Transform { // eslint-disable-line @typescript-eslint/no-namespace
|
|
|
93
96
|
* @param [val]
|
|
94
97
|
*/
|
|
95
98
|
cell (x: number, y: number, val?: number): number {
|
|
96
|
-
if (val !== undefined)
|
|
99
|
+
if (val !== undefined) {
|
|
97
100
|
this.data[3 * y + x] = val
|
|
101
|
+
}
|
|
98
102
|
|
|
99
103
|
return this.data[3 * y + x]
|
|
100
104
|
}
|
|
@@ -130,18 +134,21 @@ namespace Transform { // eslint-disable-line @typescript-eslint/no-namespace
|
|
|
130
134
|
*/
|
|
131
135
|
multiply (other: Matrix): Matrix {
|
|
132
136
|
// copy to temporary matrix to avoid modifying `this` while reading from it
|
|
133
|
-
for (let x = 0; x < 3; x++)
|
|
137
|
+
for (let x = 0; x < 3; x++) {
|
|
134
138
|
for (let y = 0; y < 3; y++) {
|
|
135
139
|
let sum = 0
|
|
136
|
-
for (let i = 0; i < 3; i++)
|
|
140
|
+
for (let i = 0; i < 3; i++) {
|
|
137
141
|
sum += this.cell(x, i) * other.cell(i, y)
|
|
142
|
+
}
|
|
138
143
|
|
|
139
144
|
Matrix._TMP_MATRIX.cell(x, y, sum)
|
|
140
145
|
}
|
|
146
|
+
}
|
|
141
147
|
|
|
142
148
|
// copy data from TMP_MATRIX to this
|
|
143
|
-
for (let i = 0; i < Matrix._TMP_MATRIX.data.length; i++)
|
|
149
|
+
for (let i = 0; i < Matrix._TMP_MATRIX.data.length; i++) {
|
|
144
150
|
this.data[i] = Matrix._TMP_MATRIX.data[i]
|
|
151
|
+
}
|
|
145
152
|
|
|
146
153
|
return this
|
|
147
154
|
}
|
package/src/event.ts
CHANGED
|
@@ -4,6 +4,32 @@
|
|
|
4
4
|
|
|
5
5
|
import EtroObject from './object'
|
|
6
6
|
|
|
7
|
+
class DeprecatedEvent {
|
|
8
|
+
replacement: string
|
|
9
|
+
message: string
|
|
10
|
+
|
|
11
|
+
constructor (replacement: string, message: string = undefined) {
|
|
12
|
+
this.replacement = replacement
|
|
13
|
+
this.message = message
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
toString () {
|
|
17
|
+
let str = ''
|
|
18
|
+
|
|
19
|
+
if (this.replacement) {
|
|
20
|
+
str += `Use ${this.replacement} instead.`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (this.message) {
|
|
24
|
+
str += ` ${this.message}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return str
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const deprecatedEvents: Record<string, DeprecatedEvent> = {}
|
|
32
|
+
|
|
7
33
|
export interface Event {
|
|
8
34
|
target: EtroObject
|
|
9
35
|
type: string
|
|
@@ -21,12 +47,15 @@ class TypeId {
|
|
|
21
47
|
}
|
|
22
48
|
|
|
23
49
|
contains (other) {
|
|
24
|
-
if (other._parts.length > this._parts.length)
|
|
50
|
+
if (other._parts.length > this._parts.length) {
|
|
25
51
|
return false
|
|
52
|
+
}
|
|
26
53
|
|
|
27
|
-
for (let i = 0; i < other._parts.length; i++)
|
|
28
|
-
if (other._parts[i] !== this._parts[i])
|
|
54
|
+
for (let i = 0; i < other._parts.length; i++) {
|
|
55
|
+
if (other._parts[i] !== this._parts[i]) {
|
|
29
56
|
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
30
59
|
|
|
31
60
|
return true
|
|
32
61
|
}
|
|
@@ -36,27 +65,60 @@ class TypeId {
|
|
|
36
65
|
}
|
|
37
66
|
}
|
|
38
67
|
|
|
68
|
+
export function deprecate (type: string, newType: string, message: string = undefined): void {
|
|
69
|
+
deprecatedEvents[type] = new DeprecatedEvent(newType, message)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function subscribeOnce (target: EtroObject, type: string, listener: <T extends Event>(T) => void): void {
|
|
73
|
+
const wrapped = event => {
|
|
74
|
+
unsubscribe(target, wrapped)
|
|
75
|
+
listener(event)
|
|
76
|
+
}
|
|
77
|
+
subscribe(target, type, wrapped)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function subscribeMany (target: EtroObject, type: string, listener: <T extends Event>(T) => void): void {
|
|
81
|
+
if (!listeners.has(target)) {
|
|
82
|
+
listeners.set(target, [])
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
listeners.get(target).push(
|
|
86
|
+
{ type: new TypeId(type), listener }
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
39
90
|
/**
|
|
40
|
-
* Listen for an event or category of events
|
|
91
|
+
* Listen for an event or category of events.
|
|
41
92
|
*
|
|
42
|
-
* @param target -
|
|
93
|
+
* @param target - an etro object
|
|
43
94
|
* @param type - the id of the type (can contain subtypes, such as
|
|
44
95
|
* "type.subtype")
|
|
45
96
|
* @param listener
|
|
97
|
+
* @param options - options
|
|
98
|
+
* @param options.once - if true, the listener will only be called once
|
|
46
99
|
*/
|
|
47
|
-
export function subscribe (
|
|
48
|
-
|
|
49
|
-
|
|
100
|
+
export function subscribe (
|
|
101
|
+
target: EtroObject,
|
|
102
|
+
type: string,
|
|
103
|
+
listener: <T extends Event>(T) => void,
|
|
104
|
+
options: { once?: boolean } = {}
|
|
105
|
+
): void {
|
|
106
|
+
// Check if this event is deprecated.
|
|
107
|
+
if (Object.keys(deprecatedEvents).includes(type)) {
|
|
108
|
+
console.warn(`Event ${type} is deprecated. ${deprecatedEvents[type]}`)
|
|
109
|
+
}
|
|
50
110
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
111
|
+
if (options.once) {
|
|
112
|
+
subscribeOnce(target, type, listener)
|
|
113
|
+
} else {
|
|
114
|
+
subscribeMany(target, type, listener)
|
|
115
|
+
}
|
|
54
116
|
}
|
|
55
117
|
|
|
56
118
|
/**
|
|
57
119
|
* Remove an event listener
|
|
58
120
|
*
|
|
59
|
-
* @param target -
|
|
121
|
+
* @param target - an etro object
|
|
60
122
|
* @param type - the id of the type (can contain subtypes, such as
|
|
61
123
|
* "type.subtype")
|
|
62
124
|
* @param listener
|
|
@@ -64,8 +126,9 @@ export function subscribe (target: EtroObject, type: string, listener: <T extend
|
|
|
64
126
|
export function unsubscribe (target: EtroObject, listener: <T extends Event>(T) => void): void {
|
|
65
127
|
// Make sure `listener` has been added with `subscribe`.
|
|
66
128
|
if (!listeners.has(target) ||
|
|
67
|
-
!listeners.get(target).map(pair => pair.listener).includes(listener))
|
|
129
|
+
!listeners.get(target).map(pair => pair.listener).includes(listener)) {
|
|
68
130
|
throw new Error('No matching event listener to remove')
|
|
131
|
+
}
|
|
69
132
|
|
|
70
133
|
const removed = listeners.get(target)
|
|
71
134
|
.filter(pair => pair.listener !== listener)
|
|
@@ -73,29 +136,31 @@ export function unsubscribe (target: EtroObject, listener: <T extends Event>(T)
|
|
|
73
136
|
}
|
|
74
137
|
|
|
75
138
|
/**
|
|
76
|
-
*
|
|
139
|
+
* Publish an event to all listeners without checking if it is deprecated.
|
|
77
140
|
*
|
|
78
|
-
* @param target
|
|
79
|
-
* @param type
|
|
80
|
-
*
|
|
81
|
-
* @
|
|
141
|
+
* @param target
|
|
142
|
+
* @param type
|
|
143
|
+
* @param event
|
|
144
|
+
* @returns
|
|
82
145
|
*/
|
|
83
|
-
|
|
146
|
+
function _publish (target: EtroObject, type: string, event: Record<string, unknown>): Event {
|
|
84
147
|
(event as unknown as Event).target = target; // could be a proxy
|
|
85
148
|
(event as unknown as Event).type = type
|
|
86
149
|
|
|
87
150
|
const t = new TypeId(type)
|
|
88
151
|
|
|
89
|
-
if (!listeners.has(target))
|
|
152
|
+
if (!listeners.has(target)) {
|
|
90
153
|
// No event fired
|
|
91
154
|
return null
|
|
155
|
+
}
|
|
92
156
|
|
|
93
157
|
// Call event listeners for this event.
|
|
94
158
|
const listenersForType = []
|
|
95
159
|
for (let i = 0; i < listeners.get(target).length; i++) {
|
|
96
160
|
const item = listeners.get(target)[i]
|
|
97
|
-
if (t.contains(item.type))
|
|
161
|
+
if (t.contains(item.type)) {
|
|
98
162
|
listenersForType.push(item.listener)
|
|
163
|
+
}
|
|
99
164
|
}
|
|
100
165
|
|
|
101
166
|
for (let i = 0; i < listenersForType.length; i++) {
|
|
@@ -106,6 +171,31 @@ export function publish (target: EtroObject, type: string, event: Record<string,
|
|
|
106
171
|
return event as unknown as Event
|
|
107
172
|
}
|
|
108
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Emits an event to all listeners
|
|
176
|
+
*
|
|
177
|
+
* @param target - an etro object
|
|
178
|
+
* @param type - the id of the type (can contain subtypes, such as
|
|
179
|
+
* "type.subtype")
|
|
180
|
+
* @param event - any additional event data
|
|
181
|
+
*/
|
|
182
|
+
export function publish (target: EtroObject, type: string, event: Record<string, unknown>): Event {
|
|
183
|
+
// Check if this event is deprecated only if it can be replaced.
|
|
184
|
+
if (Object.keys(deprecatedEvents).includes(type) && deprecatedEvents[type].replacement) {
|
|
185
|
+
throw new Error(`Event ${type} is deprecated. ${deprecatedEvents[type]}`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for deprecated events that this event replaces.
|
|
189
|
+
for (const deprecated in deprecatedEvents) {
|
|
190
|
+
const deprecatedEvent = deprecatedEvents[deprecated]
|
|
191
|
+
if (type === deprecatedEvent.replacement) {
|
|
192
|
+
_publish(target, deprecated, { ...event })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return _publish(target, type, event)
|
|
197
|
+
}
|
|
198
|
+
|
|
109
199
|
const listeners: WeakMap<EtroObject, {
|
|
110
200
|
type: TypeId,
|
|
111
201
|
listener: (Event) => void
|
|
@@ -6,19 +6,24 @@ import { Base, BaseOptions } from './base'
|
|
|
6
6
|
type Constructor<T> = new (...args: unknown[]) => T
|
|
7
7
|
|
|
8
8
|
interface AudioSource extends Base {
|
|
9
|
-
|
|
9
|
+
/** HTML media element (an audio or video element) */
|
|
10
|
+
readonly source: HTMLAudioElement
|
|
11
|
+
/** Audio source node for the media */
|
|
10
12
|
readonly audioNode: AudioNode
|
|
11
13
|
playbackRate: number
|
|
12
|
-
/**
|
|
14
|
+
/** Seconds to skip ahead by */
|
|
13
15
|
sourceStartTime: number
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
interface AudioSourceOptions extends BaseOptions {
|
|
18
|
+
interface AudioSourceOptions extends Omit<BaseOptions, 'duration'> {
|
|
19
|
+
duration?: number
|
|
20
|
+
/** HTML media element (an audio or video element) */
|
|
17
21
|
source: HTMLMediaElement
|
|
22
|
+
/** Seconds to skip ahead by */
|
|
18
23
|
sourceStartTime?: number
|
|
19
24
|
muted?: boolean
|
|
20
25
|
volume?: number
|
|
21
|
-
playbackRate
|
|
26
|
+
playbackRate?: number
|
|
22
27
|
onload?: (source: HTMLMediaElement, options: AudioSourceOptions) => void
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -59,18 +64,31 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
59
64
|
* @param [options.playbackRate=1]
|
|
60
65
|
*/
|
|
61
66
|
constructor (options: MixedAudioSourceOptions) {
|
|
67
|
+
if (!options.source) {
|
|
68
|
+
throw new Error('Property "source" is required in options')
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
const onload = options.onload
|
|
63
72
|
// Don't set as instance property
|
|
64
73
|
delete options.onload
|
|
65
|
-
|
|
74
|
+
|
|
75
|
+
super({
|
|
76
|
+
...options,
|
|
77
|
+
|
|
78
|
+
// Set a default duration so that the super constructor doesn't throw an
|
|
79
|
+
// error
|
|
80
|
+
duration: options.duration ?? 0
|
|
81
|
+
})
|
|
82
|
+
|
|
66
83
|
this._initialized = false
|
|
67
84
|
this._sourceStartTime = options.sourceStartTime || 0
|
|
68
85
|
applyOptions(options, this)
|
|
69
86
|
|
|
70
87
|
const load = () => {
|
|
71
88
|
// TODO: && ?
|
|
72
|
-
if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0)
|
|
89
|
+
if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0) {
|
|
73
90
|
throw new Error('Invalid options.duration or options.sourceStartTime')
|
|
91
|
+
}
|
|
74
92
|
|
|
75
93
|
this._unstretchedDuration = options.duration || (this.source.duration - this.sourceStartTime)
|
|
76
94
|
this.duration = this._unstretchedDuration / (this.playbackRate)
|
|
@@ -78,31 +96,34 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
78
96
|
// super()
|
|
79
97
|
onload && onload.bind(this)(this.source, options)
|
|
80
98
|
}
|
|
81
|
-
if (this.source.readyState >= 2)
|
|
99
|
+
if (this.source.readyState >= 2) {
|
|
82
100
|
// this frame's data is available now
|
|
83
101
|
load()
|
|
84
|
-
else
|
|
102
|
+
} else {
|
|
85
103
|
// when this frame's data is available
|
|
86
104
|
this.source.addEventListener('loadedmetadata', load)
|
|
105
|
+
}
|
|
87
106
|
|
|
88
107
|
this.source.addEventListener('durationchange', () => {
|
|
89
108
|
this.duration = options.duration || (this.source.duration - this.sourceStartTime)
|
|
90
109
|
})
|
|
91
110
|
}
|
|
92
111
|
|
|
112
|
+
async whenReady (): Promise<void> {
|
|
113
|
+
await super.whenReady()
|
|
114
|
+
if (this.source.readyState < 4) {
|
|
115
|
+
await new Promise(resolve => {
|
|
116
|
+
this.source.addEventListener('canplaythrough', resolve)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
93
121
|
attach (movie: Movie) {
|
|
94
122
|
super.attach(movie)
|
|
95
123
|
|
|
96
|
-
subscribe(movie, 'movie.seek', () => {
|
|
97
|
-
if (this.currentTime < 0 || this.currentTime >= this.duration)
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
this.source.currentTime = this.currentTime + this.sourceStartTime
|
|
101
|
-
})
|
|
102
|
-
|
|
103
124
|
// TODO: on unattach?
|
|
104
|
-
subscribe(movie, '
|
|
105
|
-
// Connect to new destination if
|
|
125
|
+
subscribe(movie, 'audiodestinationupdate', event => {
|
|
126
|
+
// Connect to new destination if immediately connected to the existing
|
|
106
127
|
// destination.
|
|
107
128
|
if (this._connectedToDestination) {
|
|
108
129
|
this.audioNode.disconnect(movie.actx.destination)
|
|
@@ -123,8 +144,9 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
123
144
|
const oldDisconnect = this._audioNode.disconnect.bind(this.audioNode)
|
|
124
145
|
this._audioNode.disconnect = <T extends AudioDestinationNode>(destination?: T | number, output?: number, input?: number): AudioNode => {
|
|
125
146
|
if (this._connectedToDestination &&
|
|
126
|
-
destination === movie.actx.destination)
|
|
147
|
+
destination === movie.actx.destination) {
|
|
127
148
|
this._connectedToDestination = false
|
|
149
|
+
}
|
|
128
150
|
|
|
129
151
|
return oldDisconnect(destination, output, input)
|
|
130
152
|
}
|
|
@@ -145,6 +167,12 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
145
167
|
this.source.play()
|
|
146
168
|
}
|
|
147
169
|
|
|
170
|
+
seek (time: number): void {
|
|
171
|
+
super.seek(time)
|
|
172
|
+
|
|
173
|
+
this.source.currentTime = this.currentTime + this.sourceStartTime
|
|
174
|
+
}
|
|
175
|
+
|
|
148
176
|
render () {
|
|
149
177
|
super.render()
|
|
150
178
|
// TODO: implement Issue: Create built-in audio node to support built-in
|
|
@@ -155,6 +183,8 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
155
183
|
}
|
|
156
184
|
|
|
157
185
|
stop () {
|
|
186
|
+
super.stop()
|
|
187
|
+
|
|
158
188
|
this.source.pause()
|
|
159
189
|
}
|
|
160
190
|
|
|
@@ -171,8 +201,9 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
171
201
|
|
|
172
202
|
set playbackRate (value) {
|
|
173
203
|
this._playbackRate = value
|
|
174
|
-
if (this._unstretchedDuration !== undefined)
|
|
204
|
+
if (this._unstretchedDuration !== undefined) {
|
|
175
205
|
this.duration = this._unstretchedDuration / value
|
|
206
|
+
}
|
|
176
207
|
}
|
|
177
208
|
|
|
178
209
|
get startTime () {
|
|
@@ -202,7 +233,16 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
202
233
|
return this._sourceStartTime
|
|
203
234
|
}
|
|
204
235
|
|
|
205
|
-
|
|
236
|
+
get ready (): boolean {
|
|
237
|
+
// Typescript doesn't support `super.ready` when targeting es5
|
|
238
|
+
const superReady = Object.getOwnPropertyDescriptor(superclass.prototype, 'ready').get.call(this)
|
|
239
|
+
return superReady && this.source.readyState === 4
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @deprecated See {@link https://github.com/etro-js/etro/issues/131}
|
|
244
|
+
*/
|
|
245
|
+
getDefaultOptions () {
|
|
206
246
|
return {
|
|
207
247
|
...superclass.prototype.getDefaultOptions(),
|
|
208
248
|
source: undefined, // required
|
package/src/layer/audio.ts
CHANGED
|
@@ -3,22 +3,44 @@
|
|
|
3
3
|
import { Base, BaseOptions } from './base'
|
|
4
4
|
import { AudioSourceMixin, AudioSourceOptions } from './audio-source'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface AudioOptions extends Omit<AudioSourceOptions, 'source'> {
|
|
7
|
+
/**
|
|
8
|
+
* The raw html `<audio>` element
|
|
9
|
+
*/
|
|
10
|
+
source: string | HTMLAudioElement
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
/**
|
|
14
|
+
* Layer for an HTML audio element
|
|
9
15
|
* @extends AudioSource
|
|
10
16
|
*/
|
|
11
17
|
class Audio extends AudioSourceMixin<BaseOptions>(Base) {
|
|
18
|
+
/**
|
|
19
|
+
* The raw html `<audio>` element
|
|
20
|
+
*/
|
|
21
|
+
source: HTMLAudioElement
|
|
22
|
+
|
|
12
23
|
/**
|
|
13
24
|
* Creates an audio layer
|
|
14
25
|
*/
|
|
15
26
|
constructor (options: AudioOptions) {
|
|
27
|
+
if (typeof options.source === 'string') {
|
|
28
|
+
const audio = document.createElement('audio')
|
|
29
|
+
audio.src = options.source
|
|
30
|
+
options.source = audio
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
super(options)
|
|
17
|
-
|
|
34
|
+
|
|
35
|
+
if (this.duration === undefined) {
|
|
18
36
|
this.duration = (this).source.duration - this.sourceStartTime
|
|
37
|
+
}
|
|
19
38
|
}
|
|
20
39
|
|
|
21
|
-
|
|
40
|
+
/**
|
|
41
|
+
* @deprecated See {@link https://github.com/etro-js/etro/issues/131}
|
|
42
|
+
*/
|
|
43
|
+
getDefaultOptions () {
|
|
22
44
|
return {
|
|
23
45
|
...Object.getPrototypeOf(this).getDefaultOptions(),
|
|
24
46
|
/**
|