etro 0.8.0 → 0.8.3
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 +3 -1
- package/.github/workflows/shipjs-trigger.yml +29 -0
- package/CHANGELOG.md +36 -13
- package/CODE_OF_CONDUCT.md +5 -5
- package/CONTRIBUTING.md +22 -72
- package/README.md +2 -2
- package/dist/effect/base.d.ts +14 -1
- package/dist/etro-cjs.js +189 -230
- package/dist/etro-iife.js +189 -230
- package/dist/layer/base.d.ts +13 -0
- package/eslint.conf.js +2 -1
- package/eslint.test-conf.js +1 -0
- package/examples/application/readme-screenshot.html +4 -8
- package/examples/application/video-player.html +3 -4
- package/examples/introduction/effects.html +23 -4
- package/karma.conf.js +4 -2
- package/package.json +8 -4
- package/scripts/gen-effect-samples.html +0 -3
- package/ship.config.js +80 -0
- package/src/effect/base.ts +29 -10
- package/src/effect/gaussian-blur.ts +10 -10
- package/src/effect/pixelate.ts +1 -2
- package/src/effect/shader.ts +18 -22
- package/src/effect/stack.ts +8 -4
- package/src/effect/transform.ts +13 -14
- package/src/event.ts +8 -14
- package/src/layer/audio-source.ts +16 -14
- package/src/layer/audio.ts +1 -2
- package/src/layer/base.ts +26 -7
- package/src/layer/visual.ts +11 -6
- package/src/movie.ts +70 -64
- package/src/util.ts +50 -57
- 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/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
- package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
- package/spec/assets/effect/grayscale.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 -421
- package/spec/event.spec.js +0 -39
- package/spec/layer.spec.js +0 -307
- package/spec/movie.spec.js +0 -346
- package/spec/util.spec.js +0 -294
package/src/event.ts
CHANGED
|
@@ -21,15 +21,13 @@ class TypeId {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
contains (other) {
|
|
24
|
-
if (other._parts.length > this._parts.length)
|
|
24
|
+
if (other._parts.length > this._parts.length)
|
|
25
25
|
return false
|
|
26
|
-
}
|
|
27
26
|
|
|
28
|
-
for (let i = 0; i < other._parts.length; i++)
|
|
29
|
-
if (other._parts[i] !== this._parts[i])
|
|
27
|
+
for (let i = 0; i < other._parts.length; i++)
|
|
28
|
+
if (other._parts[i] !== this._parts[i])
|
|
30
29
|
return false
|
|
31
|
-
|
|
32
|
-
}
|
|
30
|
+
|
|
33
31
|
return true
|
|
34
32
|
}
|
|
35
33
|
|
|
@@ -47,9 +45,8 @@ class TypeId {
|
|
|
47
45
|
* @param listener
|
|
48
46
|
*/
|
|
49
47
|
export function subscribe (target: EtroObject, type: string, listener: <T extends Event>(T) => void): void {
|
|
50
|
-
if (!listeners.has(target))
|
|
48
|
+
if (!listeners.has(target))
|
|
51
49
|
listeners.set(target, [])
|
|
52
|
-
}
|
|
53
50
|
|
|
54
51
|
listeners.get(target).push(
|
|
55
52
|
{ type: new TypeId(type), listener }
|
|
@@ -67,9 +64,8 @@ export function subscribe (target: EtroObject, type: string, listener: <T extend
|
|
|
67
64
|
export function unsubscribe (target: EtroObject, listener: <T extends Event>(T) => void): void {
|
|
68
65
|
// Make sure `listener` has been added with `subscribe`.
|
|
69
66
|
if (!listeners.has(target) ||
|
|
70
|
-
!listeners.get(target).map(pair => pair.listener).includes(listener))
|
|
67
|
+
!listeners.get(target).map(pair => pair.listener).includes(listener))
|
|
71
68
|
throw new Error('No matching event listener to remove')
|
|
72
|
-
}
|
|
73
69
|
|
|
74
70
|
const removed = listeners.get(target)
|
|
75
71
|
.filter(pair => pair.listener !== listener)
|
|
@@ -90,18 +86,16 @@ export function publish (target: EtroObject, type: string, event: Record<string,
|
|
|
90
86
|
|
|
91
87
|
const t = new TypeId(type)
|
|
92
88
|
|
|
93
|
-
if (!listeners.has(target))
|
|
89
|
+
if (!listeners.has(target))
|
|
94
90
|
// No event fired
|
|
95
91
|
return null
|
|
96
|
-
}
|
|
97
92
|
|
|
98
93
|
// Call event listeners for this event.
|
|
99
94
|
const listenersForType = []
|
|
100
95
|
for (let i = 0; i < listeners.get(target).length; i++) {
|
|
101
96
|
const item = listeners.get(target)[i]
|
|
102
|
-
if (t.contains(item.type))
|
|
97
|
+
if (t.contains(item.type))
|
|
103
98
|
listenersForType.push(item.listener)
|
|
104
|
-
}
|
|
105
99
|
}
|
|
106
100
|
|
|
107
101
|
for (let i = 0; i < listenersForType.length; i++) {
|
|
@@ -70,22 +70,22 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
70
70
|
|
|
71
71
|
const load = () => {
|
|
72
72
|
// TODO: && ?
|
|
73
|
-
if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0)
|
|
73
|
+
if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0)
|
|
74
74
|
throw new Error('Invalid options.duration or options.sourceStartTime')
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
this._unstretchedDuration = options.duration || (this.source.duration - this.sourceStartTime)
|
|
77
77
|
this.duration = this._unstretchedDuration / (this.playbackRate)
|
|
78
78
|
// onload will use `this`, and can't bind itself because it's before
|
|
79
79
|
// super()
|
|
80
80
|
onload && onload.bind(this)(this.source, options)
|
|
81
81
|
}
|
|
82
|
-
if (this.source.readyState >= 2)
|
|
82
|
+
if (this.source.readyState >= 2)
|
|
83
83
|
// this frame's data is available now
|
|
84
84
|
load()
|
|
85
|
-
|
|
85
|
+
else
|
|
86
86
|
// when this frame's data is available
|
|
87
87
|
this.source.addEventListener('loadedmetadata', load)
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
this.source.addEventListener('durationchange', () => {
|
|
90
90
|
this.duration = options.duration || (this.source.duration - this.sourceStartTime)
|
|
91
91
|
})
|
|
@@ -95,11 +95,10 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
95
95
|
super.attach(movie)
|
|
96
96
|
|
|
97
97
|
subscribe(movie, 'movie.seek', () => {
|
|
98
|
-
|
|
99
|
-
if (time < this.startTime || time >= this.startTime + this.duration) {
|
|
98
|
+
if (this.currentTime < 0 || this.currentTime >= this.duration)
|
|
100
99
|
return
|
|
101
|
-
|
|
102
|
-
this.source.currentTime =
|
|
100
|
+
|
|
101
|
+
this.source.currentTime = this.currentTime + this.sourceStartTime
|
|
103
102
|
})
|
|
104
103
|
|
|
105
104
|
// TODO: on unattach?
|
|
@@ -113,7 +112,7 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
113
112
|
})
|
|
114
113
|
|
|
115
114
|
// connect to audiocontext
|
|
116
|
-
this._audioNode = movie.actx.createMediaElementSource(this.source)
|
|
115
|
+
this._audioNode = this.audioNode || movie.actx.createMediaElementSource(this.source)
|
|
117
116
|
|
|
118
117
|
// Spy on connect and disconnect to remember if it connected to
|
|
119
118
|
// actx.destination (for Movie#record).
|
|
@@ -125,9 +124,9 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
125
124
|
const oldDisconnect = this._audioNode.disconnect.bind(this.audioNode)
|
|
126
125
|
this._audioNode.disconnect = <T extends IAudioDestinationNode<AudioContext>>(destination?: T | number, output?: number, input?: number): AudioNode => {
|
|
127
126
|
if (this._connectedToDestination &&
|
|
128
|
-
destination === movie.actx.destination)
|
|
127
|
+
destination === movie.actx.destination)
|
|
129
128
|
this._connectedToDestination = false
|
|
130
|
-
|
|
129
|
+
|
|
131
130
|
return oldDisconnect(destination, output, input)
|
|
132
131
|
}
|
|
133
132
|
|
|
@@ -135,6 +134,10 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
135
134
|
this.audioNode.connect(movie.actx.destination)
|
|
136
135
|
}
|
|
137
136
|
|
|
137
|
+
detach () {
|
|
138
|
+
this.audioNode.disconnect(this.movie.actx.destination)
|
|
139
|
+
}
|
|
140
|
+
|
|
138
141
|
start () {
|
|
139
142
|
this.source.currentTime = this.currentTime + this.sourceStartTime
|
|
140
143
|
this.source.play()
|
|
@@ -166,9 +169,8 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
166
169
|
|
|
167
170
|
set playbackRate (value) {
|
|
168
171
|
this._playbackRate = value
|
|
169
|
-
if (this._unstretchedDuration !== undefined)
|
|
172
|
+
if (this._unstretchedDuration !== undefined)
|
|
170
173
|
this.duration = this._unstretchedDuration / value
|
|
171
|
-
}
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
get startTime () {
|
package/src/layer/audio.ts
CHANGED
|
@@ -14,9 +14,8 @@ class Audio extends AudioSourceMixin<BaseOptions>(Base) {
|
|
|
14
14
|
*/
|
|
15
15
|
constructor (options: AudioOptions) {
|
|
16
16
|
super(options)
|
|
17
|
-
if (this.duration === undefined)
|
|
17
|
+
if (this.duration === undefined)
|
|
18
18
|
this.duration = (this).source.duration - this.sourceStartTime
|
|
19
|
-
}
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
getDefaultOptions (): AudioOptions {
|
package/src/layer/base.ts
CHANGED
|
@@ -70,22 +70,41 @@ class Base implements EtroObject {
|
|
|
70
70
|
return newThis
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Attaches this layer to `movie` if not already attached.
|
|
75
|
+
* @ignore
|
|
76
|
+
*/
|
|
77
|
+
tryAttach (movie: Movie): void {
|
|
78
|
+
if (this._occurrenceCount === 0)
|
|
79
|
+
this.attach(movie)
|
|
80
|
+
|
|
74
81
|
this._occurrenceCount++
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
attach (movie: Movie): void {
|
|
75
85
|
this._movie = movie
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Dettaches this layer from its movie if the number of times `tryDetach` has
|
|
90
|
+
* been called (including this call) equals the number of times `tryAttach`
|
|
91
|
+
* has been called.
|
|
92
|
+
*
|
|
93
|
+
* @ignore
|
|
94
|
+
*/
|
|
95
|
+
tryDetach (): void {
|
|
96
|
+
if (this.movie === null)
|
|
80
97
|
throw new Error('No movie to detach from')
|
|
81
|
-
}
|
|
82
98
|
|
|
83
99
|
this._occurrenceCount--
|
|
84
100
|
// If this layer occurs in another place in a `layers` array, do not unset
|
|
85
101
|
// _movie. (For calling `unshift` on the `layers` proxy)
|
|
86
|
-
if (this._occurrenceCount === 0)
|
|
87
|
-
this.
|
|
88
|
-
|
|
102
|
+
if (this._occurrenceCount === 0)
|
|
103
|
+
this.detach()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
detach (): void {
|
|
107
|
+
this._movie = null
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
/**
|
package/src/layer/visual.ts
CHANGED
|
@@ -68,9 +68,9 @@ class Visual extends Base {
|
|
|
68
68
|
set: (target, property, value) => {
|
|
69
69
|
if (!isNaN(Number(property))) {
|
|
70
70
|
// The property is a number (index)
|
|
71
|
-
if (target[property])
|
|
71
|
+
if (target[property])
|
|
72
72
|
target[property].detach()
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
value.attach(this)
|
|
75
75
|
}
|
|
76
76
|
target[property] = value
|
|
@@ -83,6 +83,12 @@ class Visual extends Base {
|
|
|
83
83
|
* Render visual output
|
|
84
84
|
*/
|
|
85
85
|
render (): void {
|
|
86
|
+
// Prevent empty canvas errors if the width or height is 0
|
|
87
|
+
const width = val(this, 'width', this.currentTime)
|
|
88
|
+
const height = val(this, 'height', this.currentTime)
|
|
89
|
+
if (width === 0 || height === 0)
|
|
90
|
+
return
|
|
91
|
+
|
|
86
92
|
this.beginRender()
|
|
87
93
|
this.doRender()
|
|
88
94
|
this.endRender()
|
|
@@ -116,19 +122,18 @@ class Visual extends Base {
|
|
|
116
122
|
endRender (): void {
|
|
117
123
|
const w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime)
|
|
118
124
|
const h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime)
|
|
119
|
-
if (w * h > 0)
|
|
125
|
+
if (w * h > 0)
|
|
120
126
|
this._applyEffects()
|
|
121
|
-
|
|
127
|
+
|
|
122
128
|
// else InvalidStateError for drawing zero-area image in some effects, right?
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
_applyEffects (): void {
|
|
126
132
|
for (let i = 0; i < this.effects.length; i++) {
|
|
127
133
|
const effect = this.effects[i]
|
|
128
|
-
if (effect.enabled)
|
|
134
|
+
if (effect && effect.enabled)
|
|
129
135
|
// Pass relative time
|
|
130
136
|
effect.apply(this, this.movie.currentTime - this.startTime)
|
|
131
|
-
}
|
|
132
137
|
}
|
|
133
138
|
}
|
|
134
139
|
|
package/src/movie.ts
CHANGED
|
@@ -95,7 +95,7 @@ export class Movie {
|
|
|
95
95
|
// Refresh screen when effect is removed, if the movie isn't playing
|
|
96
96
|
// already.
|
|
97
97
|
const value = target[property]
|
|
98
|
-
value.
|
|
98
|
+
value.tryDetach()
|
|
99
99
|
delete target[property]
|
|
100
100
|
publish(that, 'movie.change.effect.remove', { effect: value })
|
|
101
101
|
return true
|
|
@@ -107,10 +107,10 @@ export class Movie {
|
|
|
107
107
|
publish(that, 'movie.change.effect.remove', {
|
|
108
108
|
effect: target[property]
|
|
109
109
|
})
|
|
110
|
-
target[property].
|
|
110
|
+
target[property].tryDetach()
|
|
111
111
|
}
|
|
112
112
|
// Attach effect to movie
|
|
113
|
-
value.
|
|
113
|
+
value.tryAttach(that)
|
|
114
114
|
target[property] = value
|
|
115
115
|
// Refresh screen when effect is set, if the movie isn't playing
|
|
116
116
|
// already.
|
|
@@ -118,6 +118,7 @@ export class Movie {
|
|
|
118
118
|
} else {
|
|
119
119
|
target[property] = value
|
|
120
120
|
}
|
|
121
|
+
|
|
121
122
|
return true
|
|
122
123
|
}
|
|
123
124
|
})
|
|
@@ -127,12 +128,12 @@ export class Movie {
|
|
|
127
128
|
deleteProperty (target, property): boolean {
|
|
128
129
|
const oldDuration = this.duration
|
|
129
130
|
const value = target[property]
|
|
130
|
-
value.
|
|
131
|
+
value.tryDetach(that)
|
|
131
132
|
delete target[property]
|
|
132
133
|
const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
|
|
133
|
-
if (current)
|
|
134
|
+
if (current)
|
|
134
135
|
publish(that, 'movie.change.layer.remove', { layer: value })
|
|
135
|
-
|
|
136
|
+
|
|
136
137
|
publish(that, 'movie.change.duration', { oldDuration })
|
|
137
138
|
return true
|
|
138
139
|
},
|
|
@@ -144,20 +145,21 @@ export class Movie {
|
|
|
144
145
|
publish(that, 'movie.change.layer.remove', {
|
|
145
146
|
layer: target[property]
|
|
146
147
|
})
|
|
147
|
-
target[property].
|
|
148
|
+
target[property].tryDetach()
|
|
148
149
|
}
|
|
149
150
|
// Attach layer to movie
|
|
150
|
-
value.
|
|
151
|
+
value.tryAttach(that)
|
|
151
152
|
target[property] = value
|
|
152
153
|
// Refresh screen when a relevant layer is added or removed
|
|
153
154
|
const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
|
|
154
|
-
if (current)
|
|
155
|
+
if (current)
|
|
155
156
|
publish(that, 'movie.change.layer.add', { layer: value })
|
|
156
|
-
|
|
157
|
+
|
|
157
158
|
publish(that, 'movie.change.duration', { oldDuration })
|
|
158
159
|
} else {
|
|
159
160
|
target[property] = value
|
|
160
161
|
}
|
|
162
|
+
|
|
161
163
|
return true
|
|
162
164
|
}
|
|
163
165
|
})
|
|
@@ -179,15 +181,13 @@ export class Movie {
|
|
|
179
181
|
// newThis._updateInterval = 0.1; // time in seconds between each "timeupdate" event
|
|
180
182
|
// newThis._lastUpdate = -1;
|
|
181
183
|
|
|
182
|
-
if (newThis.autoRefresh)
|
|
184
|
+
if (newThis.autoRefresh)
|
|
183
185
|
newThis.refresh() // render single frame on creation
|
|
184
|
-
}
|
|
185
186
|
|
|
186
187
|
// Subscribe to own event "change" (child events propogate up)
|
|
187
188
|
subscribe(newThis, 'movie.change', () => {
|
|
188
|
-
if (newThis.autoRefresh && !newThis.rendering)
|
|
189
|
+
if (newThis.autoRefresh && !newThis.rendering)
|
|
189
190
|
newThis.refresh()
|
|
190
|
-
}
|
|
191
191
|
})
|
|
192
192
|
|
|
193
193
|
// Subscribe to own event "ended"
|
|
@@ -207,18 +207,17 @@ export class Movie {
|
|
|
207
207
|
*/
|
|
208
208
|
play (): Promise<void> {
|
|
209
209
|
return new Promise(resolve => {
|
|
210
|
-
if (!this.paused)
|
|
210
|
+
if (!this.paused)
|
|
211
211
|
throw new Error('Already playing')
|
|
212
|
-
}
|
|
213
212
|
|
|
214
213
|
this._paused = this._ended = false
|
|
215
214
|
this._lastPlayed = performance.now()
|
|
216
215
|
this._lastPlayedOffset = this.currentTime
|
|
217
216
|
|
|
218
|
-
if (!this.renderingFrame)
|
|
217
|
+
if (!this.renderingFrame)
|
|
219
218
|
// Not rendering (and not playing), so play.
|
|
220
219
|
this._render(true, undefined, resolve)
|
|
221
|
-
|
|
220
|
+
|
|
222
221
|
// Stop rendering frame if currently doing so, because playing has higher
|
|
223
222
|
// priority. This will effect the next _render call.
|
|
224
223
|
this._renderingFrame = false
|
|
@@ -250,13 +249,16 @@ export class Movie {
|
|
|
250
249
|
audio?: boolean,
|
|
251
250
|
mediaRecorderOptions?: Record<string, unknown>
|
|
252
251
|
}): Promise<Blob> {
|
|
253
|
-
if (options.video === false && options.audio === false)
|
|
252
|
+
if (options.video === false && options.audio === false)
|
|
254
253
|
throw new Error('Both video and audio cannot be disabled')
|
|
255
|
-
}
|
|
256
254
|
|
|
257
|
-
if (!this.paused)
|
|
255
|
+
if (!this.paused)
|
|
258
256
|
throw new Error('Cannot record movie while already playing or recording')
|
|
259
|
-
|
|
257
|
+
|
|
258
|
+
const mimeType = options.type || 'video/webm'
|
|
259
|
+
if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType))
|
|
260
|
+
throw new Error('Please pass a valid MIME type for the exported video')
|
|
261
|
+
|
|
260
262
|
return new Promise((resolve, reject) => {
|
|
261
263
|
const canvasCache = this.canvas
|
|
262
264
|
// Record on a temporary canvas context
|
|
@@ -287,12 +289,15 @@ export class Movie {
|
|
|
287
289
|
)
|
|
288
290
|
}
|
|
289
291
|
const stream = new MediaStream(tracks)
|
|
290
|
-
const
|
|
292
|
+
const mediaRecorderOptions = {
|
|
293
|
+
...(options.mediaRecorderOptions || {}),
|
|
294
|
+
mimeType
|
|
295
|
+
}
|
|
296
|
+
const mediaRecorder = new MediaRecorder(stream, mediaRecorderOptions)
|
|
291
297
|
mediaRecorder.ondataavailable = event => {
|
|
292
298
|
// if (this._paused) reject(new Error("Recording was interrupted"));
|
|
293
|
-
if (event.data.size > 0)
|
|
299
|
+
if (event.data.size > 0)
|
|
294
300
|
recordedChunks.push(event.data)
|
|
295
|
-
}
|
|
296
301
|
}
|
|
297
302
|
// TODO: publish to movie, not layers
|
|
298
303
|
mediaRecorder.onstop = () => {
|
|
@@ -306,7 +311,7 @@ export class Movie {
|
|
|
306
311
|
// Construct the exported video out of all the frame blobs.
|
|
307
312
|
resolve(
|
|
308
313
|
new Blob(recordedChunks, {
|
|
309
|
-
type:
|
|
314
|
+
type: mimeType
|
|
310
315
|
})
|
|
311
316
|
)
|
|
312
317
|
}
|
|
@@ -327,11 +332,13 @@ export class Movie {
|
|
|
327
332
|
pause (): Movie {
|
|
328
333
|
this._paused = true
|
|
329
334
|
// Deactivate all layers
|
|
330
|
-
for (let i = 0; i < this.layers.length; i++)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
+
for (let i = 0; i < this.layers.length; i++)
|
|
336
|
+
if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
|
|
337
|
+
const layer = this.layers[i]
|
|
338
|
+
layer.stop()
|
|
339
|
+
layer.active = false
|
|
340
|
+
}
|
|
341
|
+
|
|
335
342
|
publish(this, 'movie.pause', {})
|
|
336
343
|
return this
|
|
337
344
|
}
|
|
@@ -357,18 +364,17 @@ export class Movie {
|
|
|
357
364
|
if (!this.rendering) {
|
|
358
365
|
// (!this.paused || this._renderingFrame) is true so it's playing or it's
|
|
359
366
|
// rendering a single frame.
|
|
360
|
-
if (done)
|
|
367
|
+
if (done)
|
|
361
368
|
done()
|
|
362
|
-
|
|
369
|
+
|
|
363
370
|
return
|
|
364
371
|
}
|
|
365
372
|
|
|
366
373
|
this._updateCurrentTime(timestamp)
|
|
367
374
|
const recordingEnd = this.recording ? this._recordEndTime : this.duration
|
|
368
375
|
const recordingEnded = this.currentTime > recordingEnd
|
|
369
|
-
if (recordingEnded)
|
|
376
|
+
if (recordingEnded)
|
|
370
377
|
publish(this, 'movie.recordended', { movie: this })
|
|
371
|
-
}
|
|
372
378
|
|
|
373
379
|
// Bad for performance? (remember, it's calling Array.reduce)
|
|
374
380
|
const end = this.duration
|
|
@@ -384,24 +390,25 @@ export class Movie {
|
|
|
384
390
|
if (!this.repeat || this.recording) {
|
|
385
391
|
this._ended = true
|
|
386
392
|
// Deactivate all layers
|
|
387
|
-
for (let i = 0; i < this.layers.length; i++)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
+
for (let i = 0; i < this.layers.length; i++)
|
|
394
|
+
if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
|
|
395
|
+
const layer = this.layers[i]
|
|
396
|
+
// A layer that has been deleted before layers.length has been updated
|
|
397
|
+
// (see the layers proxy in the constructor).
|
|
398
|
+
if (!layer)
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
layer.stop()
|
|
402
|
+
layer.active = false
|
|
393
403
|
}
|
|
394
|
-
layer.stop()
|
|
395
|
-
layer.active = false
|
|
396
|
-
}
|
|
397
404
|
}
|
|
398
405
|
}
|
|
399
406
|
|
|
400
407
|
// Stop playback or recording if done
|
|
401
408
|
if (recordingEnded || (ended && !this.repeat)) {
|
|
402
|
-
if (done)
|
|
409
|
+
if (done)
|
|
403
410
|
done()
|
|
404
|
-
|
|
411
|
+
|
|
405
412
|
return
|
|
406
413
|
}
|
|
407
414
|
|
|
@@ -410,9 +417,8 @@ export class Movie {
|
|
|
410
417
|
const frameFullyLoaded = this._renderLayers()
|
|
411
418
|
this._applyEffects()
|
|
412
419
|
|
|
413
|
-
if (frameFullyLoaded)
|
|
420
|
+
if (frameFullyLoaded)
|
|
414
421
|
publish(this, 'movie.loadeddata', { movie: this })
|
|
415
|
-
}
|
|
416
422
|
|
|
417
423
|
// If didn't load in this instant, repeatedly frame-render until frame is
|
|
418
424
|
// loaded.
|
|
@@ -420,9 +426,9 @@ export class Movie {
|
|
|
420
426
|
// stop render loop.
|
|
421
427
|
if (!repeat || (this._renderingFrame && frameFullyLoaded)) {
|
|
422
428
|
this._renderingFrame = false
|
|
423
|
-
if (done)
|
|
429
|
+
if (done)
|
|
424
430
|
done()
|
|
425
|
-
|
|
431
|
+
|
|
426
432
|
return
|
|
427
433
|
}
|
|
428
434
|
|
|
@@ -461,12 +467,14 @@ export class Movie {
|
|
|
461
467
|
private _renderLayers () {
|
|
462
468
|
let frameFullyLoaded = true
|
|
463
469
|
for (let i = 0; i < this.layers.length; i++) {
|
|
470
|
+
if (!Object.prototype.hasOwnProperty.call(this.layers, i)) continue
|
|
471
|
+
|
|
464
472
|
const layer = this.layers[i]
|
|
465
473
|
// A layer that has been deleted before layers.length has been updated
|
|
466
474
|
// (see the layers proxy in the constructor).
|
|
467
|
-
if (!layer)
|
|
475
|
+
if (!layer)
|
|
468
476
|
continue
|
|
469
|
-
|
|
477
|
+
|
|
470
478
|
const reltime = this.currentTime - layer.startTime
|
|
471
479
|
// Cancel operation if layer disabled or outside layer time interval
|
|
472
480
|
if (!val(layer, 'enabled', reltime) ||
|
|
@@ -489,9 +497,9 @@ export class Movie {
|
|
|
489
497
|
}
|
|
490
498
|
|
|
491
499
|
// if the layer has an input file
|
|
492
|
-
if ('source' in layer)
|
|
500
|
+
if ('source' in layer)
|
|
493
501
|
frameFullyLoaded = frameFullyLoaded && (layer as unknown as AudioSource).source.readyState >= 2
|
|
494
|
-
|
|
502
|
+
|
|
495
503
|
layer.render()
|
|
496
504
|
|
|
497
505
|
// if the layer has visual component
|
|
@@ -499,11 +507,10 @@ export class Movie {
|
|
|
499
507
|
const canvas = (layer as Visual).canvas
|
|
500
508
|
// layer.canvas.width and layer.canvas.height should already be interpolated
|
|
501
509
|
// if the layer has an area (else InvalidStateError from canvas)
|
|
502
|
-
if (canvas.width * canvas.height > 0)
|
|
510
|
+
if (canvas.width * canvas.height > 0)
|
|
503
511
|
this.cctx.drawImage(canvas,
|
|
504
512
|
val(layer, 'x', reltime), val(layer, 'y', reltime), canvas.width, canvas.height
|
|
505
513
|
)
|
|
506
|
-
}
|
|
507
514
|
}
|
|
508
515
|
}
|
|
509
516
|
|
|
@@ -515,9 +522,9 @@ export class Movie {
|
|
|
515
522
|
const effect = this.effects[i]
|
|
516
523
|
// An effect that has been deleted before effects.length has been updated
|
|
517
524
|
// (see the effectsproxy in the constructor).
|
|
518
|
-
if (!effect)
|
|
525
|
+
if (!effect)
|
|
519
526
|
continue
|
|
520
|
-
|
|
527
|
+
|
|
521
528
|
effect.apply(this, this.currentTime)
|
|
522
529
|
}
|
|
523
530
|
}
|
|
@@ -537,9 +544,9 @@ export class Movie {
|
|
|
537
544
|
* Convienence method
|
|
538
545
|
*/
|
|
539
546
|
private _publishToLayers (type, event) {
|
|
540
|
-
for (let i = 0; i < this.layers.length; i++)
|
|
541
|
-
|
|
542
|
-
|
|
547
|
+
for (let i = 0; i < this.layers.length; i++)
|
|
548
|
+
if (Object.prototype.hasOwnProperty.call(this.layers, i))
|
|
549
|
+
publish(this.layers[i], type, event)
|
|
543
550
|
}
|
|
544
551
|
|
|
545
552
|
/**
|
|
@@ -631,12 +638,11 @@ export class Movie {
|
|
|
631
638
|
return new Promise((resolve, reject) => {
|
|
632
639
|
this._currentTime = time
|
|
633
640
|
publish(this, 'movie.seek', {})
|
|
634
|
-
if (refresh)
|
|
641
|
+
if (refresh)
|
|
635
642
|
// Pass promise callbacks to `refresh`
|
|
636
643
|
this.refresh().then(resolve).catch(reject)
|
|
637
|
-
|
|
644
|
+
else
|
|
638
645
|
resolve()
|
|
639
|
-
}
|
|
640
646
|
})
|
|
641
647
|
}
|
|
642
648
|
|