etro 0.6.0 → 0.8.1
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/.github/workflows/nodejs.yml +4 -2
- package/CHANGELOG.md +85 -4
- package/CODE_OF_CONDUCT.md +5 -5
- package/CONTRIBUTING.md +33 -79
- package/README.md +93 -26
- package/dist/effect/base.d.ts +51 -0
- package/dist/effect/brightness.d.ts +16 -0
- package/dist/effect/channels.d.ts +23 -0
- package/dist/effect/chroma-key.d.ts +23 -0
- package/dist/effect/contrast.d.ts +15 -0
- package/dist/effect/elliptical-mask.d.ts +31 -0
- package/dist/effect/gaussian-blur.d.ts +60 -0
- package/dist/effect/grayscale.d.ts +7 -0
- package/dist/effect/index.d.ts +15 -0
- package/dist/effect/pixelate.d.ts +18 -0
- package/dist/effect/shader.d.ts +99 -0
- package/dist/effect/stack.d.ts +23 -0
- package/dist/effect/transform.d.ts +73 -0
- package/dist/etro-cjs.js +9387 -0
- package/dist/etro-iife.js +9390 -0
- package/dist/etro.d.ts +7 -0
- package/dist/event.d.ts +35 -0
- package/dist/index.d.ts +6 -0
- package/dist/layer/audio-source.d.ts +24 -0
- package/dist/layer/audio.d.ts +14 -0
- package/dist/layer/base.d.ts +82 -0
- package/dist/layer/image.d.ts +6 -0
- package/dist/layer/index.d.ts +11 -0
- package/dist/layer/text.d.ts +60 -0
- package/dist/layer/video.d.ts +11 -0
- package/dist/layer/visual-source.d.ts +32 -0
- package/dist/layer/visual.d.ts +58 -0
- package/dist/movie.d.ts +192 -0
- package/dist/object.d.ts +12 -0
- package/dist/util.d.ts +125 -0
- package/eslint.conf.js +2 -9
- package/eslint.example-conf.js +9 -0
- package/eslint.test-conf.js +1 -0
- package/eslint.typescript-conf.js +5 -0
- package/examples/application/readme-screenshot.html +16 -17
- package/examples/application/video-player.html +10 -11
- package/examples/application/webcam.html +6 -6
- package/examples/introduction/audio.html +30 -18
- package/examples/introduction/effects.html +37 -14
- package/examples/introduction/export.html +40 -27
- package/examples/introduction/functions.html +6 -4
- package/examples/introduction/hello-world1.html +9 -5
- package/examples/introduction/hello-world2.html +5 -5
- package/examples/introduction/keyframes.html +35 -23
- package/examples/introduction/media.html +26 -18
- package/examples/introduction/text.html +9 -5
- package/karma.conf.js +6 -4
- package/package.json +34 -13
- package/rollup.config.js +19 -3
- package/scripts/gen-effect-samples.html +27 -26
- package/scripts/save-effect-samples.js +14 -15
- package/src/effect/base.ts +115 -0
- package/src/effect/brightness.ts +43 -0
- package/src/effect/channels.ts +50 -0
- package/src/effect/chroma-key.ts +82 -0
- package/src/effect/contrast.ts +42 -0
- package/src/effect/elliptical-mask.ts +75 -0
- package/src/effect/gaussian-blur.ts +232 -0
- package/src/effect/grayscale.ts +34 -0
- package/src/effect/index.ts +22 -0
- package/src/effect/pixelate.ts +58 -0
- package/src/effect/shader.ts +557 -0
- package/src/effect/stack.ts +77 -0
- package/src/effect/transform.ts +193 -0
- package/src/etro.ts +26 -0
- package/src/event.ts +112 -0
- package/src/index.ts +8 -0
- package/src/layer/audio-source.ts +219 -0
- package/src/layer/audio.ts +34 -0
- package/src/layer/base.ts +175 -0
- package/src/layer/image.ts +8 -0
- package/src/layer/index.ts +13 -0
- package/src/layer/text.ts +138 -0
- package/src/layer/video.ts +15 -0
- package/src/layer/visual-source.ts +150 -0
- package/src/layer/visual.ts +197 -0
- package/src/movie.ts +701 -0
- package/src/object.ts +14 -0
- package/src/util.ts +466 -0
- package/tsconfig.json +8 -0
- package/dist/etro.js +0 -3397
- package/docs/effect.js.html +0 -1215
- package/docs/event.js.html +0 -145
- package/docs/index.html +0 -81
- package/docs/index.js.html +0 -92
- package/docs/layer.js.html +0 -888
- package/docs/module-effect-GaussianBlurComponent.html +0 -345
- package/docs/module-effect.Brightness.html +0 -339
- package/docs/module-effect.Channels.html +0 -319
- package/docs/module-effect.ChromaKey.html +0 -611
- package/docs/module-effect.Contrast.html +0 -339
- package/docs/module-effect.EllipticalMask.html +0 -200
- package/docs/module-effect.GaussianBlur.html +0 -202
- package/docs/module-effect.GaussianBlurHorizontal.html +0 -242
- package/docs/module-effect.GaussianBlurVertical.html +0 -242
- package/docs/module-effect.Pixelate.html +0 -330
- package/docs/module-effect.Shader.html +0 -1227
- package/docs/module-effect.Stack.html +0 -406
- package/docs/module-effect.Transform.Matrix.html +0 -193
- package/docs/module-effect.Transform.html +0 -1174
- package/docs/module-effect.html +0 -148
- package/docs/module-event.html +0 -473
- package/docs/module-index.html +0 -186
- package/docs/module-layer-Media.html +0 -1116
- package/docs/module-layer-MediaMixin.html +0 -164
- package/docs/module-layer.Audio.html +0 -1188
- package/docs/module-layer.Base.html +0 -629
- package/docs/module-layer.Image.html +0 -1421
- package/docs/module-layer.Text.html +0 -1731
- package/docs/module-layer.Video.html +0 -1938
- package/docs/module-layer.Visual.html +0 -1698
- package/docs/module-layer.html +0 -137
- package/docs/module-movie.html +0 -3118
- package/docs/module-util.Color.html +0 -702
- package/docs/module-util.Font.html +0 -395
- package/docs/module-util.html +0 -845
- package/docs/movie.js.html +0 -689
- package/docs/scripts/collapse.js +0 -20
- package/docs/scripts/linenumber.js +0 -25
- package/docs/scripts/nav.js +0 -12
- package/docs/scripts/polyfill.js +0 -4
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
- package/docs/scripts/prettify/lang-css.js +0 -2
- package/docs/scripts/prettify/prettify.js +0 -28
- package/docs/scripts/search.js +0 -83
- package/docs/styles/jsdoc.css +0 -671
- package/docs/styles/prettify.css +0 -79
- package/docs/util.js.html +0 -503
- package/screenshots/2019-08-17_0.png +0 -0
- package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
- package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
- package/spec/assets/effect/original.png +0 -0
- package/spec/assets/effect/pixelate.png +0 -0
- package/spec/assets/effect/transform/multiply.png +0 -0
- package/spec/assets/effect/transform/rotate.png +0 -0
- package/spec/assets/effect/transform/scale-fraction.png +0 -0
- package/spec/assets/effect/transform/scale.png +0 -0
- package/spec/assets/effect/transform/translate-fraction.png +0 -0
- package/spec/assets/effect/transform/translate.png +0 -0
- package/spec/assets/layer/audio.wav +0 -0
- package/spec/assets/layer/image.jpg +0 -0
- package/spec/effect.spec.js +0 -352
- package/spec/event.spec.js +0 -25
- package/spec/layer.spec.js +0 -128
- package/spec/movie.spec.js +0 -154
- package/spec/util.spec.js +0 -285
- package/src/effect.js +0 -1265
- package/src/event.js +0 -78
- package/src/index.js +0 -23
- package/src/layer.js +0 -875
- package/src/movie.js +0 -636
- package/src/util.js +0 -487
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Visual } from '../layer/index'
|
|
2
|
+
import { Movie } from '../movie'
|
|
3
|
+
import { val, Dynamic } from '../util'
|
|
4
|
+
import { Base } from './base'
|
|
5
|
+
|
|
6
|
+
export interface TransformOptions {
|
|
7
|
+
matrix: Dynamic<Transform.Matrix> // eslint-disable-line no-use-before-define
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Transforms a layer or movie using a transformation matrix. Use {@link
|
|
12
|
+
* Transform.Matrix} to either A) calculate those values based on a series of
|
|
13
|
+
* translations, scalings and rotations) or B) input the matrix values
|
|
14
|
+
* directly, using the optional argument in the constructor.
|
|
15
|
+
*/
|
|
16
|
+
class Transform extends Base {
|
|
17
|
+
/** Matrix that determines how to transform the target */
|
|
18
|
+
matrix: Dynamic<Transform.Matrix>
|
|
19
|
+
|
|
20
|
+
private _tmpMatrix: Transform.Matrix
|
|
21
|
+
private _tmpCanvas: HTMLCanvasElement
|
|
22
|
+
private _tmpCtx: CanvasRenderingContext2D
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param matrix - matrix that determines how to transform the target
|
|
26
|
+
*/
|
|
27
|
+
constructor (options: TransformOptions) {
|
|
28
|
+
super()
|
|
29
|
+
/**
|
|
30
|
+
* How to transform the target
|
|
31
|
+
*/
|
|
32
|
+
this.matrix = options.matrix
|
|
33
|
+
this._tmpMatrix = new Transform.Matrix()
|
|
34
|
+
this._tmpCanvas = document.createElement('canvas')
|
|
35
|
+
this._tmpCtx = this._tmpCanvas.getContext('2d')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
apply (target: Movie | Visual, reltime: number): void {
|
|
39
|
+
if (target.canvas.width !== this._tmpCanvas.width)
|
|
40
|
+
this._tmpCanvas.width = target.canvas.width
|
|
41
|
+
|
|
42
|
+
if (target.canvas.height !== this._tmpCanvas.height)
|
|
43
|
+
this._tmpCanvas.height = target.canvas.height
|
|
44
|
+
|
|
45
|
+
// Use data, since that's the underlying storage
|
|
46
|
+
this._tmpMatrix.data = val(this, 'matrix.data', reltime)
|
|
47
|
+
|
|
48
|
+
this._tmpCtx.setTransform(
|
|
49
|
+
this._tmpMatrix.a, this._tmpMatrix.b, this._tmpMatrix.c,
|
|
50
|
+
this._tmpMatrix.d, this._tmpMatrix.e, this._tmpMatrix.f
|
|
51
|
+
)
|
|
52
|
+
this._tmpCtx.drawImage(target.canvas, 0, 0)
|
|
53
|
+
// Assume it was identity for now
|
|
54
|
+
this._tmpCtx.setTransform(1, 0, 0, 0, 1, 0)
|
|
55
|
+
target.cctx.clearRect(0, 0, target.canvas.width, target.canvas.height)
|
|
56
|
+
target.cctx.drawImage(this._tmpCanvas, 0, 0)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
namespace Transform { // eslint-disable-line @typescript-eslint/no-namespace
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @class
|
|
64
|
+
* A 3x3 matrix for storing 2d transformations
|
|
65
|
+
*/
|
|
66
|
+
export class Matrix {
|
|
67
|
+
/**
|
|
68
|
+
* The identity matrix
|
|
69
|
+
*/
|
|
70
|
+
static IDENTITY = new Matrix()
|
|
71
|
+
private static _TMP_MATRIX = new Matrix()
|
|
72
|
+
|
|
73
|
+
data: number[]
|
|
74
|
+
|
|
75
|
+
constructor (data?: number[]) {
|
|
76
|
+
this.data = data || [
|
|
77
|
+
1, 0, 0,
|
|
78
|
+
0, 1, 0,
|
|
79
|
+
0, 0, 1
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
identity (): Matrix {
|
|
84
|
+
for (let i = 0; i < this.data.length; i++)
|
|
85
|
+
this.data[i] = Matrix.IDENTITY.data[i]
|
|
86
|
+
|
|
87
|
+
return this
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param x
|
|
92
|
+
* @param y
|
|
93
|
+
* @param [val]
|
|
94
|
+
*/
|
|
95
|
+
cell (x: number, y: number, val?: number): number {
|
|
96
|
+
if (val !== undefined)
|
|
97
|
+
this.data[3 * y + x] = val
|
|
98
|
+
|
|
99
|
+
return this.data[3 * y + x]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* For canvas context setTransform */
|
|
103
|
+
get a (): number {
|
|
104
|
+
return this.data[0]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get b (): number {
|
|
108
|
+
return this.data[3]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get c (): number {
|
|
112
|
+
return this.data[1]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get d (): number {
|
|
116
|
+
return this.data[4]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get e (): number {
|
|
120
|
+
return this.data[2]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get f (): number {
|
|
124
|
+
return this.data[5]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Combines <code>this</code> with another matrix <code>other</code>
|
|
129
|
+
* @param other
|
|
130
|
+
*/
|
|
131
|
+
multiply (other: Matrix): Matrix {
|
|
132
|
+
// copy to temporary matrix to avoid modifying `this` while reading from it
|
|
133
|
+
for (let x = 0; x < 3; x++)
|
|
134
|
+
for (let y = 0; y < 3; y++) {
|
|
135
|
+
let sum = 0
|
|
136
|
+
for (let i = 0; i < 3; i++)
|
|
137
|
+
sum += this.cell(x, i) * other.cell(i, y)
|
|
138
|
+
|
|
139
|
+
Matrix._TMP_MATRIX.cell(x, y, sum)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// copy data from TMP_MATRIX to this
|
|
143
|
+
for (let i = 0; i < Matrix._TMP_MATRIX.data.length; i++)
|
|
144
|
+
this.data[i] = Matrix._TMP_MATRIX.data[i]
|
|
145
|
+
|
|
146
|
+
return this
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param x
|
|
151
|
+
* @param y
|
|
152
|
+
*/
|
|
153
|
+
translate (x: number, y: number): Matrix {
|
|
154
|
+
this.multiply(new Matrix([
|
|
155
|
+
1, 0, x,
|
|
156
|
+
0, 1, y,
|
|
157
|
+
0, 0, 1
|
|
158
|
+
]))
|
|
159
|
+
|
|
160
|
+
return this
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param x
|
|
165
|
+
* @param y
|
|
166
|
+
*/
|
|
167
|
+
scale (x: number, y: number): Matrix {
|
|
168
|
+
this.multiply(new Matrix([
|
|
169
|
+
x, 0, 0,
|
|
170
|
+
0, y, 0,
|
|
171
|
+
0, 0, 1
|
|
172
|
+
]))
|
|
173
|
+
|
|
174
|
+
return this
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param a - the angle or rotation in radians
|
|
179
|
+
*/
|
|
180
|
+
rotate (a: number): Matrix {
|
|
181
|
+
const c = Math.cos(a); const s = Math.sin(a)
|
|
182
|
+
this.multiply(new Matrix([
|
|
183
|
+
c, s, 0,
|
|
184
|
+
-s, c, 0,
|
|
185
|
+
0, 0, 1
|
|
186
|
+
]))
|
|
187
|
+
|
|
188
|
+
return this
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { Transform }
|
package/src/etro.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Typedoc can't handle default exports. To let users import default export and
|
|
3
|
+
* make typedoc work, this module exports everything as named exports. Then,
|
|
4
|
+
* ./index imports everything from this module and exports it as a default
|
|
5
|
+
* export. Typedoc uses this file, and rollup and NPM use ./index
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// TODO: investigate possibility of changing movie (canvas) width/height after
|
|
9
|
+
// layers added. I think it's fine, but still make sure.
|
|
10
|
+
// TODO: create built-in audio gain node for volume control in movie and/or
|
|
11
|
+
// layer.
|
|
12
|
+
// TODO: figure out InvalidStateError in beginning only when reloaded
|
|
13
|
+
|
|
14
|
+
import * as layer from './layer/index'
|
|
15
|
+
import * as effect from './effect/index'
|
|
16
|
+
import * as event from './event'
|
|
17
|
+
import EtroObject from './object'
|
|
18
|
+
|
|
19
|
+
export * from './movie'
|
|
20
|
+
export * from './util'
|
|
21
|
+
export {
|
|
22
|
+
EtroObject,
|
|
23
|
+
layer,
|
|
24
|
+
effect,
|
|
25
|
+
event
|
|
26
|
+
}
|
package/src/event.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module event
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import EtroObject from './object'
|
|
6
|
+
|
|
7
|
+
export interface Event {
|
|
8
|
+
target: EtroObject
|
|
9
|
+
type: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An event type
|
|
14
|
+
* @private
|
|
15
|
+
*/
|
|
16
|
+
class TypeId {
|
|
17
|
+
private _parts: string[]
|
|
18
|
+
|
|
19
|
+
constructor (id) {
|
|
20
|
+
this._parts = id.split('.')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
contains (other) {
|
|
24
|
+
if (other._parts.length > this._parts.length)
|
|
25
|
+
return false
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < other._parts.length; i++)
|
|
28
|
+
if (other._parts[i] !== this._parts[i])
|
|
29
|
+
return false
|
|
30
|
+
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toString () {
|
|
35
|
+
return this._parts.join('.')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Listen for an event or category of events
|
|
41
|
+
*
|
|
42
|
+
* @param target - a etro object
|
|
43
|
+
* @param type - the id of the type (can contain subtypes, such as
|
|
44
|
+
* "type.subtype")
|
|
45
|
+
* @param listener
|
|
46
|
+
*/
|
|
47
|
+
export function subscribe (target: EtroObject, type: string, listener: <T extends Event>(T) => void): void {
|
|
48
|
+
if (!listeners.has(target))
|
|
49
|
+
listeners.set(target, [])
|
|
50
|
+
|
|
51
|
+
listeners.get(target).push(
|
|
52
|
+
{ type: new TypeId(type), listener }
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Remove an event listener
|
|
58
|
+
*
|
|
59
|
+
* @param target - a etro object
|
|
60
|
+
* @param type - the id of the type (can contain subtypes, such as
|
|
61
|
+
* "type.subtype")
|
|
62
|
+
* @param listener
|
|
63
|
+
*/
|
|
64
|
+
export function unsubscribe (target: EtroObject, listener: <T extends Event>(T) => void): void {
|
|
65
|
+
// Make sure `listener` has been added with `subscribe`.
|
|
66
|
+
if (!listeners.has(target) ||
|
|
67
|
+
!listeners.get(target).map(pair => pair.listener).includes(listener))
|
|
68
|
+
throw new Error('No matching event listener to remove')
|
|
69
|
+
|
|
70
|
+
const removed = listeners.get(target)
|
|
71
|
+
.filter(pair => pair.listener !== listener)
|
|
72
|
+
listeners.set(target, removed)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Emits an event to all listeners
|
|
77
|
+
*
|
|
78
|
+
* @param target - a etro object
|
|
79
|
+
* @param type - the id of the type (can contain subtypes, such as
|
|
80
|
+
* "type.subtype")
|
|
81
|
+
* @param event - any additional event data
|
|
82
|
+
*/
|
|
83
|
+
export function publish (target: EtroObject, type: string, event: Record<string, unknown>): Event {
|
|
84
|
+
(event as unknown as Event).target = target; // could be a proxy
|
|
85
|
+
(event as unknown as Event).type = type
|
|
86
|
+
|
|
87
|
+
const t = new TypeId(type)
|
|
88
|
+
|
|
89
|
+
if (!listeners.has(target))
|
|
90
|
+
// No event fired
|
|
91
|
+
return null
|
|
92
|
+
|
|
93
|
+
// Call event listeners for this event.
|
|
94
|
+
const listenersForType = []
|
|
95
|
+
for (let i = 0; i < listeners.get(target).length; i++) {
|
|
96
|
+
const item = listeners.get(target)[i]
|
|
97
|
+
if (t.contains(item.type))
|
|
98
|
+
listenersForType.push(item.listener)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < listenersForType.length; i++) {
|
|
102
|
+
const listener = listenersForType[i]
|
|
103
|
+
listener(event)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return event as unknown as Event
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const listeners: WeakMap<EtroObject, {
|
|
110
|
+
type: TypeId,
|
|
111
|
+
listener: (Event) => void
|
|
112
|
+
}[]> = new WeakMap()
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { AudioContext, IAudioNode, IAudioDestinationNode } from 'standardized-audio-context'
|
|
2
|
+
import { Movie } from '../movie'
|
|
3
|
+
import { subscribe } from '../event'
|
|
4
|
+
import { applyOptions, val } from '../util'
|
|
5
|
+
import { Base, BaseOptions } from './base'
|
|
6
|
+
|
|
7
|
+
type Constructor<T> = new (...args: unknown[]) => T
|
|
8
|
+
|
|
9
|
+
interface AudioSource extends Base {
|
|
10
|
+
readonly source: HTMLMediaElement
|
|
11
|
+
readonly audioNode: IAudioNode<AudioContext>
|
|
12
|
+
playbackRate: number
|
|
13
|
+
/** The audio source node for the media */
|
|
14
|
+
sourceStartTime: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AudioSourceOptions extends BaseOptions {
|
|
18
|
+
source: HTMLMediaElement
|
|
19
|
+
sourceStartTime?: number
|
|
20
|
+
muted?: boolean
|
|
21
|
+
volume?: number
|
|
22
|
+
playbackRate: number
|
|
23
|
+
onload?: (source: HTMLMediaElement, options: AudioSourceOptions) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A layer that gets its audio from an HTMLMediaElement
|
|
28
|
+
* @mixin AudioSourceMixin
|
|
29
|
+
*/
|
|
30
|
+
// TODO: Implement playback rate
|
|
31
|
+
// The generic is just for type-checking. The argument is for functionality
|
|
32
|
+
// (survives when compiled to javascript).
|
|
33
|
+
|
|
34
|
+
function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Constructor<Base>): Constructor<AudioSource> {
|
|
35
|
+
type MixedAudioSourceOptions = OptionsSuperclass & AudioSourceOptions
|
|
36
|
+
|
|
37
|
+
class MixedAudioSource extends superclass {
|
|
38
|
+
/**
|
|
39
|
+
* The raw html media element
|
|
40
|
+
*/
|
|
41
|
+
readonly source: HTMLMediaElement
|
|
42
|
+
|
|
43
|
+
private __startTime: number
|
|
44
|
+
private _audioNode: IAudioNode<AudioContext>
|
|
45
|
+
private _sourceStartTime: number
|
|
46
|
+
private _unstretchedDuration: number
|
|
47
|
+
private _playbackRate: number
|
|
48
|
+
private _initialized: boolean
|
|
49
|
+
private _connectedToDestination: boolean
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param options
|
|
53
|
+
* @param options.source
|
|
54
|
+
* @param options.onload
|
|
55
|
+
* @param [options.sourceStartTime=0] - at what time in the audio
|
|
56
|
+
* the layer starts
|
|
57
|
+
* @param [options.duration=media.duration-options.sourceStartTime]
|
|
58
|
+
* @param [options.muted=false]
|
|
59
|
+
* @param [options.volume=1]
|
|
60
|
+
* @param [options.playbackRate=1]
|
|
61
|
+
*/
|
|
62
|
+
constructor (options: MixedAudioSourceOptions) {
|
|
63
|
+
const onload = options.onload
|
|
64
|
+
// Don't set as instance property
|
|
65
|
+
delete options.onload
|
|
66
|
+
super(options)
|
|
67
|
+
this._initialized = false
|
|
68
|
+
this._sourceStartTime = options.sourceStartTime || 0
|
|
69
|
+
applyOptions(options, this)
|
|
70
|
+
|
|
71
|
+
const load = () => {
|
|
72
|
+
// TODO: && ?
|
|
73
|
+
if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0)
|
|
74
|
+
throw new Error('Invalid options.duration or options.sourceStartTime')
|
|
75
|
+
|
|
76
|
+
this._unstretchedDuration = options.duration || (this.source.duration - this.sourceStartTime)
|
|
77
|
+
this.duration = this._unstretchedDuration / (this.playbackRate)
|
|
78
|
+
// onload will use `this`, and can't bind itself because it's before
|
|
79
|
+
// super()
|
|
80
|
+
onload && onload.bind(this)(this.source, options)
|
|
81
|
+
}
|
|
82
|
+
if (this.source.readyState >= 2)
|
|
83
|
+
// this frame's data is available now
|
|
84
|
+
load()
|
|
85
|
+
else
|
|
86
|
+
// when this frame's data is available
|
|
87
|
+
this.source.addEventListener('loadedmetadata', load)
|
|
88
|
+
|
|
89
|
+
this.source.addEventListener('durationchange', () => {
|
|
90
|
+
this.duration = options.duration || (this.source.duration - this.sourceStartTime)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
attach (movie: Movie) {
|
|
95
|
+
super.attach(movie)
|
|
96
|
+
|
|
97
|
+
subscribe(movie, 'movie.seek', () => {
|
|
98
|
+
if (this.currentTime < 0 || this.currentTime >= this.duration)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
this.source.currentTime = this.currentTime + this.sourceStartTime
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// TODO: on unattach?
|
|
105
|
+
subscribe(movie, 'movie.audiodestinationupdate', event => {
|
|
106
|
+
// Connect to new destination if immeidately connected to the existing
|
|
107
|
+
// destination.
|
|
108
|
+
if (this._connectedToDestination) {
|
|
109
|
+
this.audioNode.disconnect(movie.actx.destination)
|
|
110
|
+
this.audioNode.connect(event.destination)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// connect to audiocontext
|
|
115
|
+
this._audioNode = this.audioNode || movie.actx.createMediaElementSource(this.source)
|
|
116
|
+
|
|
117
|
+
// Spy on connect and disconnect to remember if it connected to
|
|
118
|
+
// actx.destination (for Movie#record).
|
|
119
|
+
const oldConnect = this._audioNode.connect.bind(this.audioNode)
|
|
120
|
+
this._audioNode.connect = <T extends IAudioDestinationNode<AudioContext>>(destination: T, outputIndex?: number, inputIndex?: number): AudioNode => {
|
|
121
|
+
this._connectedToDestination = destination === movie.actx.destination
|
|
122
|
+
return oldConnect(destination, outputIndex, inputIndex)
|
|
123
|
+
}
|
|
124
|
+
const oldDisconnect = this._audioNode.disconnect.bind(this.audioNode)
|
|
125
|
+
this._audioNode.disconnect = <T extends IAudioDestinationNode<AudioContext>>(destination?: T | number, output?: number, input?: number): AudioNode => {
|
|
126
|
+
if (this._connectedToDestination &&
|
|
127
|
+
destination === movie.actx.destination)
|
|
128
|
+
this._connectedToDestination = false
|
|
129
|
+
|
|
130
|
+
return oldDisconnect(destination, output, input)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Connect to actx.destination by default (can be rewired by user)
|
|
134
|
+
this.audioNode.connect(movie.actx.destination)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
detach () {
|
|
138
|
+
this.audioNode.disconnect(this.movie.actx.destination)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
start () {
|
|
142
|
+
this.source.currentTime = this.currentTime + this.sourceStartTime
|
|
143
|
+
this.source.play()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
render () {
|
|
147
|
+
super.render()
|
|
148
|
+
// TODO: implement Issue: Create built-in audio node to support built-in
|
|
149
|
+
// audio nodes, as this does nothing rn
|
|
150
|
+
this.source.muted = val(this, 'muted', this.currentTime)
|
|
151
|
+
this.source.volume = val(this, 'volume', this.currentTime)
|
|
152
|
+
this.source.playbackRate = val(this, 'playbackRate', this.currentTime)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
stop () {
|
|
156
|
+
this.source.pause()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The audio source node for the media
|
|
161
|
+
*/
|
|
162
|
+
get audioNode () {
|
|
163
|
+
return this._audioNode
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get playbackRate () {
|
|
167
|
+
return this._playbackRate
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
set playbackRate (value) {
|
|
171
|
+
this._playbackRate = value
|
|
172
|
+
if (this._unstretchedDuration !== undefined)
|
|
173
|
+
this.duration = this._unstretchedDuration / value
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get startTime () {
|
|
177
|
+
return this.__startTime
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
set startTime (val) {
|
|
181
|
+
this.__startTime = val
|
|
182
|
+
if (this._initialized) {
|
|
183
|
+
const mediaProgress = this.movie.currentTime - this.startTime
|
|
184
|
+
this.source.currentTime = this.sourceStartTime + mediaProgress
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
set sourceStartTime (val) {
|
|
189
|
+
this._sourceStartTime = val
|
|
190
|
+
if (this._initialized) {
|
|
191
|
+
const mediaProgress = this.movie.currentTime - this.startTime
|
|
192
|
+
this.source.currentTime = mediaProgress + this.sourceStartTime
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Time in the media at which the layer starts
|
|
198
|
+
*/
|
|
199
|
+
get sourceStartTime () {
|
|
200
|
+
return this._sourceStartTime
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
getDefaultOptions (): MixedAudioSourceOptions {
|
|
204
|
+
return {
|
|
205
|
+
...superclass.prototype.getDefaultOptions(),
|
|
206
|
+
source: undefined, // required
|
|
207
|
+
sourceStartTime: 0,
|
|
208
|
+
duration: undefined, // important to include undefined keys, for applyOptions
|
|
209
|
+
muted: false,
|
|
210
|
+
volume: 1,
|
|
211
|
+
playbackRate: 1
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return MixedAudioSource
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export { AudioSource, AudioSourceOptions, AudioSourceMixin }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// TODO: rename to something more consistent with the naming convention of Visual and VisualSourceMixin
|
|
2
|
+
|
|
3
|
+
import { Base, BaseOptions } from './base'
|
|
4
|
+
import { AudioSourceMixin, AudioSourceOptions } from './audio-source'
|
|
5
|
+
|
|
6
|
+
type AudioOptions = AudioSourceOptions
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @extends AudioSource
|
|
10
|
+
*/
|
|
11
|
+
class Audio extends AudioSourceMixin<BaseOptions>(Base) {
|
|
12
|
+
/**
|
|
13
|
+
* Creates an audio layer
|
|
14
|
+
*/
|
|
15
|
+
constructor (options: AudioOptions) {
|
|
16
|
+
super(options)
|
|
17
|
+
if (this.duration === undefined)
|
|
18
|
+
this.duration = (this).source.duration - this.sourceStartTime
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getDefaultOptions (): AudioOptions {
|
|
22
|
+
return {
|
|
23
|
+
...Object.getPrototypeOf(this).getDefaultOptions(),
|
|
24
|
+
/**
|
|
25
|
+
* @name module:layer.Audio#sourceStartTime
|
|
26
|
+
* @desc Where in the media to start playing when the layer starts
|
|
27
|
+
*/
|
|
28
|
+
sourceStartTime: 0,
|
|
29
|
+
duration: undefined
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { Audio, AudioOptions }
|