etro 0.6.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/.env +2 -0
- package/.github/workflows/nodejs.yml +27 -0
- package/CHANGELOG.md +109 -0
- package/CODE_OF_CONDUCT.md +77 -0
- package/CONTRIBUTING.md +155 -0
- package/LICENSE +674 -0
- package/README.md +57 -0
- package/dist/etro.js +3397 -0
- package/docs/effect.js.html +1215 -0
- package/docs/event.js.html +145 -0
- package/docs/index.html +81 -0
- package/docs/index.js.html +92 -0
- package/docs/layer.js.html +888 -0
- package/docs/module-effect-GaussianBlurComponent.html +345 -0
- package/docs/module-effect.Brightness.html +339 -0
- package/docs/module-effect.Channels.html +319 -0
- package/docs/module-effect.ChromaKey.html +611 -0
- package/docs/module-effect.Contrast.html +339 -0
- package/docs/module-effect.EllipticalMask.html +200 -0
- package/docs/module-effect.GaussianBlur.html +202 -0
- package/docs/module-effect.GaussianBlurHorizontal.html +242 -0
- package/docs/module-effect.GaussianBlurVertical.html +242 -0
- package/docs/module-effect.Pixelate.html +330 -0
- package/docs/module-effect.Shader.html +1227 -0
- package/docs/module-effect.Stack.html +406 -0
- package/docs/module-effect.Transform.Matrix.html +193 -0
- package/docs/module-effect.Transform.html +1174 -0
- package/docs/module-effect.html +148 -0
- package/docs/module-event.html +473 -0
- package/docs/module-index.html +186 -0
- package/docs/module-layer-Media.html +1116 -0
- package/docs/module-layer-MediaMixin.html +164 -0
- package/docs/module-layer.Audio.html +1188 -0
- package/docs/module-layer.Base.html +629 -0
- package/docs/module-layer.Image.html +1421 -0
- package/docs/module-layer.Text.html +1731 -0
- package/docs/module-layer.Video.html +1938 -0
- package/docs/module-layer.Visual.html +1698 -0
- package/docs/module-layer.html +137 -0
- package/docs/module-movie.html +3118 -0
- package/docs/module-util.Color.html +702 -0
- package/docs/module-util.Font.html +395 -0
- package/docs/module-util.html +845 -0
- package/docs/movie.js.html +689 -0
- package/docs/scripts/collapse.js +20 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +83 -0
- package/docs/styles/jsdoc.css +671 -0
- package/docs/styles/prettify.css +79 -0
- package/docs/util.js.html +503 -0
- package/eslint.conf.js +28 -0
- package/eslint.test-conf.js +4 -0
- package/examples/application/readme-screenshot.html +86 -0
- package/examples/application/video-player.html +131 -0
- package/examples/application/webcam.html +28 -0
- package/examples/introduction/audio.html +52 -0
- package/examples/introduction/effects.html +56 -0
- package/examples/introduction/export.html +70 -0
- package/examples/introduction/functions.html +35 -0
- package/examples/introduction/hello-world1.html +33 -0
- package/examples/introduction/hello-world2.html +32 -0
- package/examples/introduction/keyframes.html +67 -0
- package/examples/introduction/media.html +55 -0
- package/examples/introduction/text.html +27 -0
- package/jsdoc.conf.json +3 -0
- package/karma.conf.js +60 -0
- package/package.json +63 -0
- package/private-todo.txt +70 -0
- package/rename-file.sh +18 -0
- package/rename-versions.sh +14 -0
- package/rename.sh +22 -0
- package/rollup.config.js +31 -0
- package/screenshots/2019-08-17_0.png +0 -0
- package/scripts/gen-effect-samples.html +99 -0
- package/scripts/save-effect-samples.js +43 -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 +352 -0
- package/spec/event.spec.js +25 -0
- package/spec/layer.spec.js +128 -0
- package/spec/movie.spec.js +154 -0
- package/spec/util.spec.js +285 -0
- package/src/effect.js +1265 -0
- package/src/event.js +78 -0
- package/src/index.js +23 -0
- package/src/layer.js +875 -0
- package/src/movie.js +636 -0
- package/src/util.js +487 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>movie.js - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
|
|
21
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
22
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
23
|
+
<div class="navicon"></div>
|
|
24
|
+
</label>
|
|
25
|
+
|
|
26
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
27
|
+
|
|
28
|
+
<nav >
|
|
29
|
+
|
|
30
|
+
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="module-effect.Brightness.html">Brightness</a></li><li><a href="module-effect.Channels.html">Channels</a></li><li><a href="module-effect.ChromaKey.html">ChromaKey</a></li><li><a href="module-effect.Contrast.html">Contrast</a></li><li><a href="module-effect.EllipticalMask.html">EllipticalMask</a></li><li><a href="module-effect.GaussianBlur.html">GaussianBlur</a></li><li><a href="module-effect.GaussianBlurHorizontal.html">GaussianBlurHorizontal</a></li><li><a href="module-effect.GaussianBlurVertical.html">GaussianBlurVertical</a></li><li><a href="module-effect.Pixelate.html">Pixelate</a></li><li><a href="module-effect.Shader.html">Shader</a><ul class='methods'><li data-type='method'><a href="module-effect.Shader.html#._initBuffer">_initBuffer</a></li><li data-type='method'><a href="module-effect.Shader.html#._loadTexture">_loadTexture</a></li><li data-type='method'><a href="module-effect.Shader.html#_prepareValue">_prepareValue</a></li></ul></li><li><a href="module-effect.Stack.html">Stack</a><ul class='methods'><li data-type='method'><a href="module-effect.Stack.html#addEffect">addEffect</a></li></ul></li><li><a href="module-effect.Transform.html">Transform</a><ul class='methods'><li data-type='method'><a href="module-effect.Transform.html#.Matrix#cell">Matrix#cell</a></li><li data-type='method'><a href="module-effect.Transform.html#.Matrix#multiply">Matrix#multiply</a></li><li data-type='method'><a href="module-effect.Transform.html#.Matrix#rotate">Matrix#rotate</a></li><li data-type='method'><a href="module-effect.Transform.html#.Matrix#scale">Matrix#scale</a></li><li data-type='method'><a href="module-effect.Transform.html#.Matrix#translate">Matrix#translate</a></li></ul></li><li><a href="module-effect.Transform.Matrix.html">Matrix</a></li><li><a href="module-effect-GaussianBlurComponent.html">GaussianBlurComponent</a></li><li><a href="module-layer.Audio.html">Audio</a></li><li><a href="module-layer.Base.html">Base</a><ul class='methods'><li data-type='method'><a href="module-layer.Base.html#_render">_render</a></li></ul></li><li><a href="module-layer.Image.html">Image</a></li><li><a href="module-layer.Text.html">Text</a></li><li><a href="module-layer.Video.html">Video</a></li><li><a href="module-layer.Visual.html">Visual</a><ul class='methods'><li data-type='method'><a href="module-layer.Visual.html#_render">_render</a></li><li data-type='method'><a href="module-layer.Visual.html#addEffect">addEffect</a></li></ul></li><li><a href="module-layer-Media.html">Media</a></li><li><a href="module-movie.html">movie</a><ul class='methods'><li data-type='method'><a href="module-movie.html#addEffect">addEffect</a></li><li data-type='method'><a href="module-movie.html#addLayer">addLayer</a></li><li data-type='method'><a href="module-movie.html#pause">pause</a></li><li data-type='method'><a href="module-movie.html#play">play</a></li><li data-type='method'><a href="module-movie.html#publishToLayers">publishToLayers</a></li><li data-type='method'><a href="module-movie.html#record">record</a></li><li data-type='method'><a href="module-movie.html#refresh">refresh</a></li><li data-type='method'><a href="module-movie.html#setCurrentTime">setCurrentTime</a></li><li data-type='method'><a href="module-movie.html#stop">stop</a></li></ul></li><li><a href="module-util.Color.html">Color</a><ul class='methods'><li data-type='method'><a href="module-util.Color.html#toString">toString</a></li></ul></li><li><a href="module-util.Font.html">Font</a><ul class='methods'><li data-type='method'><a href="module-util.Font.html#toString">toString</a></li></ul></li></ul><h3>Modules</h3><ul><li><a href="module-effect.html">effect</a></li><li><a href="module-event.html">event</a><ul class='methods'><li data-type='method'><a href="module-event.html#.publish">publish</a></li><li data-type='method'><a href="module-event.html#.subscribe">subscribe</a></li></ul></li><li><a href="module-index.html">index</a></li><li><a href="module-layer.html">layer</a></li><li><a href="module-movie.html">movie</a><ul class='methods'><li data-type='method'><a href="module-movie.html#addEffect">addEffect</a></li><li data-type='method'><a href="module-movie.html#addLayer">addLayer</a></li><li data-type='method'><a href="module-movie.html#pause">pause</a></li><li data-type='method'><a href="module-movie.html#play">play</a></li><li data-type='method'><a href="module-movie.html#publishToLayers">publishToLayers</a></li><li data-type='method'><a href="module-movie.html#record">record</a></li><li data-type='method'><a href="module-movie.html#refresh">refresh</a></li><li data-type='method'><a href="module-movie.html#setCurrentTime">setCurrentTime</a></li><li data-type='method'><a href="module-movie.html#stop">stop</a></li></ul></li><li><a href="module-util.html">util</a><ul class='methods'><li data-type='method'><a href="module-util.html#.parseColor">parseColor</a></li><li data-type='method'><a href="module-util.html#.parseFont">parseFont</a></li><li data-type='method'><a href="module-util.html#.watchPublic">watchPublic</a></li><li data-type='method'><a href="module-util.html#~isKeyFrames">isKeyFrames</a></li></ul></li></ul><h3>Mixins</h3><ul><li><a href="module-layer-MediaMixin.html">MediaMixin</a></li></ul>
|
|
31
|
+
</nav>
|
|
32
|
+
|
|
33
|
+
<div id="main">
|
|
34
|
+
|
|
35
|
+
<h1 class="page-title">movie.js</h1>
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
<section>
|
|
44
|
+
<article>
|
|
45
|
+
<pre class="prettyprint source linenums"><code>/**
|
|
46
|
+
* @module movie
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { subscribe, publish } from './event.js'
|
|
50
|
+
import { val, applyOptions, watchPublic } from './util.js'
|
|
51
|
+
import { Audio as AudioLayer, Video as VideoLayer } from './layer.js' // `Media` mixins
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Contains all layers and movie information<br>
|
|
55
|
+
* Implements a sub/pub system (adapted from https://gist.github.com/lizzie/4993046)
|
|
56
|
+
*
|
|
57
|
+
* @todo Implement event "durationchange", and more
|
|
58
|
+
* @todo Add width and height options
|
|
59
|
+
* @todo Make record option to make recording video output to the user while it's recording
|
|
60
|
+
* @todo rename renderingFrame -> refreshing
|
|
61
|
+
*/
|
|
62
|
+
export default class Movie {
|
|
63
|
+
/**
|
|
64
|
+
* Creates a new <code>Movie</code> instance (project)
|
|
65
|
+
*
|
|
66
|
+
* @param {HTMLCanvasElement} canvas - the canvas to display image data on
|
|
67
|
+
* @param {object} [options] - various optional arguments
|
|
68
|
+
* @param {BaseAudioContext} [options.audioContext=new AudioContext()]
|
|
69
|
+
* @param {string} [options.background="#000"] - the background color of the movijse,
|
|
70
|
+
* or <code>null</code> for a transparent background
|
|
71
|
+
* @param {boolean} [options.repeat=false] - whether to loop playbackjs
|
|
72
|
+
* @param {boolean} [options.autoRefresh=true] - whether to call `.refresh()` on init and when relevant layers
|
|
73
|
+
* are added/removed
|
|
74
|
+
*/
|
|
75
|
+
constructor (canvas, options = {}) {
|
|
76
|
+
// TODO: move into multiple methods!
|
|
77
|
+
// Rename audioContext -> _actx
|
|
78
|
+
if ('audioContext' in options) {
|
|
79
|
+
options._actx = options.audioContext
|
|
80
|
+
}
|
|
81
|
+
delete options.audioContext // TODO: move up a line :P
|
|
82
|
+
|
|
83
|
+
const newThis = watchPublic(this) // proxy that will be returned by constructor
|
|
84
|
+
// Don't send updates when initializing, so use this instead of newThis:
|
|
85
|
+
// output canvas
|
|
86
|
+
this._canvas = canvas
|
|
87
|
+
// output canvas context
|
|
88
|
+
this._cctx = canvas.getContext('2d') // TODO: make private?
|
|
89
|
+
applyOptions(options, this)
|
|
90
|
+
|
|
91
|
+
// proxy arrays
|
|
92
|
+
const that = newThis
|
|
93
|
+
|
|
94
|
+
this._effectsBack = []
|
|
95
|
+
this._effects = new Proxy(newThis._effectsBack, {
|
|
96
|
+
apply: function (target, thisArg, argumentsList) {
|
|
97
|
+
return thisArg[target].apply(newThis, argumentsList)
|
|
98
|
+
},
|
|
99
|
+
deleteProperty: function (target, property) {
|
|
100
|
+
// Refresh screen when effect is removed, if the movie isn't playing already.
|
|
101
|
+
const value = target[property]
|
|
102
|
+
publish(that, 'movie.change.effect.remove', { source: value })
|
|
103
|
+
publish(target[property], 'effect.detach', { source: that })
|
|
104
|
+
delete target[property]
|
|
105
|
+
return true
|
|
106
|
+
},
|
|
107
|
+
set: function (target, property, value) {
|
|
108
|
+
if (!isNaN(property)) { // if property is an number (index)
|
|
109
|
+
if (target[property]) {
|
|
110
|
+
delete target[property] // call deleteProperty
|
|
111
|
+
}
|
|
112
|
+
publish(value, 'effect.attach', { source: that }) // Attach effect to movie (first)
|
|
113
|
+
// Refresh screen when effect is set, if the movie isn't playing already.
|
|
114
|
+
publish(that, 'movie.change.effect.add', { source: value })
|
|
115
|
+
}
|
|
116
|
+
target[property] = value
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
this._layersBack = []
|
|
122
|
+
this._layers = new Proxy(newThis._layersBack, {
|
|
123
|
+
apply: function (target, thisArg, argumentsList) {
|
|
124
|
+
return thisArg[target].apply(newThis, argumentsList)
|
|
125
|
+
},
|
|
126
|
+
deleteProperty: function (target, property) {
|
|
127
|
+
const value = target[property]
|
|
128
|
+
const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
|
|
129
|
+
if (current) {
|
|
130
|
+
publish(that, 'movie.change.layer.remove', { source: value })
|
|
131
|
+
}
|
|
132
|
+
delete target[property]
|
|
133
|
+
return true
|
|
134
|
+
},
|
|
135
|
+
set: function (target, property, value) {
|
|
136
|
+
target[property] = value
|
|
137
|
+
if (!isNaN(property)) { // if property is an number (index)
|
|
138
|
+
publish(value, 'layer.attach', { movie: that }) // Attach layer to movie (first)
|
|
139
|
+
// Refresh screen when a relevant layer is added or removed
|
|
140
|
+
const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
|
|
141
|
+
if (current) {
|
|
142
|
+
publish(that, 'movie.change.layer.add', { source: that })
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return true
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
this._paused = true
|
|
149
|
+
this._ended = false
|
|
150
|
+
// to prevent multiple frame-rendering loops at the same time (see `render`)
|
|
151
|
+
this._renderingFrame = false // only applicable when rendering
|
|
152
|
+
this._currentTime = 0
|
|
153
|
+
|
|
154
|
+
this._mediaRecorder = null // for recording
|
|
155
|
+
|
|
156
|
+
// NOTE: -1 works well in inequalities
|
|
157
|
+
this._lastPlayed = -1 // the last time `play` was called
|
|
158
|
+
this._lastPlayedOffset = -1 // what was `currentTime` when `play` was called
|
|
159
|
+
// newThis._updateInterval = 0.1; // time in seconds between each "timeupdate" event
|
|
160
|
+
// newThis._lastUpdate = -1;
|
|
161
|
+
|
|
162
|
+
if (newThis.autoRefresh) {
|
|
163
|
+
newThis.refresh() // render single frame on init
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Subscribe to own event "change" (child events propogate up)
|
|
167
|
+
subscribe(newThis, 'movie.change', () => {
|
|
168
|
+
if (newThis.autoRefresh && !newThis.rendering) {
|
|
169
|
+
newThis.refresh()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Subscribe to own event "ended"
|
|
174
|
+
subscribe(newThis, 'movie.ended', () => {
|
|
175
|
+
if (newThis.recording) {
|
|
176
|
+
newThis._mediaRecorder.requestData() // I shouldn't have to call newThis right? err
|
|
177
|
+
newThis._mediaRecorder.stop()
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return newThis
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Plays the movie
|
|
186
|
+
* @return {Promise} fulfilled when done playing, never fails
|
|
187
|
+
*/
|
|
188
|
+
play () {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
if (!this.paused) {
|
|
191
|
+
throw new Error('Already playing')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this._paused = this._ended = false
|
|
195
|
+
this._lastPlayed = performance.now()
|
|
196
|
+
this._lastPlayedOffset = this.currentTime
|
|
197
|
+
|
|
198
|
+
if (!this._renderingFrame) {
|
|
199
|
+
// Not rendering (and not playing), so play
|
|
200
|
+
this._render(undefined, resolve)
|
|
201
|
+
}
|
|
202
|
+
// Stop rendering frame if currently doing so, because playing has higher priority.
|
|
203
|
+
this._renderingFrame = false // this will effect the next _render call
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// TEST: *support recording that plays back with audio!*
|
|
208
|
+
// TODO: figure out a way to record faster than playing (i.e. not in real time)
|
|
209
|
+
// TODO: improve recording performance to increase frame rate?
|
|
210
|
+
/**
|
|
211
|
+
* Plays the movie in the background and records it
|
|
212
|
+
*
|
|
213
|
+
* @param {number} framerate
|
|
214
|
+
* @param {object} [options]
|
|
215
|
+
* @param {boolean} [options.video=true] - whether to include video in recording
|
|
216
|
+
* @param {boolean} [options.audio=true] - whether to include audio in recording
|
|
217
|
+
* @param {object} [options.mediaRecorderOptions=undefined] - options to pass to the <code>MediaRecorder</code>
|
|
218
|
+
* constructor
|
|
219
|
+
* @return {Promise} resolves when done recording, rejects when internal media recorder errors
|
|
220
|
+
*/
|
|
221
|
+
record (framerate, options = {}) {
|
|
222
|
+
if (options.video === options.audio === false) {
|
|
223
|
+
throw new Error('Both video and audio cannot be disabled')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!this.paused) {
|
|
227
|
+
throw new Error('Cannot record movie while already playing or recording')
|
|
228
|
+
}
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
// https://developers.google.com/web/updates/2016/01/mediarecorder
|
|
231
|
+
const canvasCache = this.canvas
|
|
232
|
+
// record on a temporary canvas context
|
|
233
|
+
this._canvas = document.createElement('canvas')
|
|
234
|
+
this.canvas.width = canvasCache.width
|
|
235
|
+
this.canvas.height = canvasCache.height
|
|
236
|
+
this._cctx = this.canvas.getContext('2d')
|
|
237
|
+
|
|
238
|
+
const recordedChunks = [] // frame blobs
|
|
239
|
+
// combine image + audio, or just pick one
|
|
240
|
+
let tracks = []
|
|
241
|
+
if (options.video !== false) {
|
|
242
|
+
const visualStream = this.canvas.captureStream(framerate)
|
|
243
|
+
tracks = tracks.concat(visualStream.getTracks())
|
|
244
|
+
}
|
|
245
|
+
// Check if there's a layer that's an instance of a Media mixin (Audio or Video)
|
|
246
|
+
const hasMediaTracks = this.layers.some(layer => layer instanceof AudioLayer || layer instanceof VideoLayer)
|
|
247
|
+
// If no media tracks present, don't include an audio stream, because Chrome doesn't record silence
|
|
248
|
+
// when an audio stream is present.
|
|
249
|
+
if (hasMediaTracks && options.audio !== false) {
|
|
250
|
+
const audioDestination = this.actx.createMediaStreamDestination()
|
|
251
|
+
const audioStream = audioDestination.stream
|
|
252
|
+
tracks = tracks.concat(audioStream.getTracks())
|
|
253
|
+
this.publishToLayers('movie.audiodestinationupdate', { movie: this, destination: audioDestination })
|
|
254
|
+
}
|
|
255
|
+
const stream = new MediaStream(tracks)
|
|
256
|
+
const mediaRecorder = new MediaRecorder(stream, options.mediaRecorderOptions)
|
|
257
|
+
// TODO: publish to movie, not layers
|
|
258
|
+
mediaRecorder.ondataavailable = event => {
|
|
259
|
+
// if (this._paused) reject(new Error("Recording was interrupted"));
|
|
260
|
+
if (event.data.size > 0) {
|
|
261
|
+
recordedChunks.push(event.data)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
mediaRecorder.onstop = () => {
|
|
265
|
+
this._ended = true
|
|
266
|
+
this._canvas = canvasCache
|
|
267
|
+
this._cctx = this.canvas.getContext('2d')
|
|
268
|
+
this.publishToLayers(
|
|
269
|
+
'movie.audiodestinationupdate',
|
|
270
|
+
{ movie: this, destination: this.actx.destination }
|
|
271
|
+
)
|
|
272
|
+
this._mediaRecorder = null
|
|
273
|
+
// construct super-blob
|
|
274
|
+
// this is the exported video as a blob!
|
|
275
|
+
resolve(new Blob(recordedChunks, { type: 'video/webm' }/*, {"type" : "audio/ogg; codecs=opus"} */))
|
|
276
|
+
}
|
|
277
|
+
mediaRecorder.onerror = reject
|
|
278
|
+
|
|
279
|
+
mediaRecorder.start()
|
|
280
|
+
this._mediaRecorder = mediaRecorder
|
|
281
|
+
this.play()
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Stops the movie, without reseting the playback position
|
|
287
|
+
* @return {Movie} the movie (for chaining)
|
|
288
|
+
*/
|
|
289
|
+
pause () {
|
|
290
|
+
this._paused = true
|
|
291
|
+
// disable all layers
|
|
292
|
+
const event = { movie: this }
|
|
293
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
294
|
+
const layer = this.layers[i]
|
|
295
|
+
publish(layer, 'layer.stop', event)
|
|
296
|
+
layer._active = false
|
|
297
|
+
}
|
|
298
|
+
return this
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Stops playback and resets the playback position
|
|
303
|
+
* @return {Movie} the movie (for chaining)
|
|
304
|
+
*/
|
|
305
|
+
stop () {
|
|
306
|
+
this.pause()
|
|
307
|
+
this.currentTime = 0 // use setter?
|
|
308
|
+
return this
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @param {number} [timestamp=performance.now()]
|
|
313
|
+
* @param {function} [done=undefined] - called when done playing or when the current frame is loaded
|
|
314
|
+
* @private
|
|
315
|
+
*/
|
|
316
|
+
_render (timestamp = performance.now(), done = undefined) {
|
|
317
|
+
if (!this.rendering) {
|
|
318
|
+
// (!this.paused || this._renderingFrame) is true (it's playing or it's rendering a single frame)
|
|
319
|
+
done && done()
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this._updateCurrentTime(timestamp)
|
|
324
|
+
// bad for performance? (remember, it's calling Array.reduce)
|
|
325
|
+
const end = this.duration
|
|
326
|
+
const ended = this.currentTime >= end
|
|
327
|
+
if (ended) {
|
|
328
|
+
publish(this, 'movie.ended', { movie: this, repeat: this.repeat })
|
|
329
|
+
this._currentTime = 0 // don't use setter
|
|
330
|
+
publish(this, 'movie.timeupdate', { movie: this })
|
|
331
|
+
this._lastPlayed = performance.now()
|
|
332
|
+
this._lastPlayedOffset = 0 // this.currentTime
|
|
333
|
+
this._renderingFrame = false
|
|
334
|
+
if (!this.repeat || this.recording) {
|
|
335
|
+
this._ended = true
|
|
336
|
+
// disable all layers
|
|
337
|
+
const event = { movie: this }
|
|
338
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
339
|
+
const layer = this.layers[i]
|
|
340
|
+
publish(layer, 'layer.stop', event)
|
|
341
|
+
layer._active = false
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
done && done()
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// do render
|
|
349
|
+
this._renderBackground(timestamp)
|
|
350
|
+
const frameFullyLoaded = this._renderLayers(timestamp)
|
|
351
|
+
this._applyEffects()
|
|
352
|
+
|
|
353
|
+
if (frameFullyLoaded) {
|
|
354
|
+
publish(this, 'movie.loadeddata', { movie: this })
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// if instant didn't load, repeatedly frame-render until frame is loaded
|
|
358
|
+
// if the expression below is false, don't publish an event, just silently stop render loop
|
|
359
|
+
if (this._renderingFrame && frameFullyLoaded) {
|
|
360
|
+
this._renderingFrame = false
|
|
361
|
+
done && done()
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
window.requestAnimationFrame(timestamp => {
|
|
366
|
+
this._render(timestamp)
|
|
367
|
+
}) // TODO: research performance cost
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_updateCurrentTime (timestamp) {
|
|
371
|
+
// if we're only instant-rendering (current frame only), it doens't matter if it's paused or not
|
|
372
|
+
if (!this._renderingFrame) {
|
|
373
|
+
// if ((timestamp - this._lastUpdate) >= this._updateInterval) {
|
|
374
|
+
const sinceLastPlayed = (timestamp - this._lastPlayed) / 1000
|
|
375
|
+
this._currentTime = this._lastPlayedOffset + sinceLastPlayed // don't use setter
|
|
376
|
+
publish(this, 'movie.timeupdate', { movie: this })
|
|
377
|
+
// this._lastUpdate = timestamp;
|
|
378
|
+
// }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_renderBackground (timestamp) {
|
|
383
|
+
this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
384
|
+
if (this.background) {
|
|
385
|
+
this.cctx.fillStyle = val(this.background, this, timestamp)
|
|
386
|
+
this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @return {boolean} whether or not video frames are loaded
|
|
392
|
+
* @param {number} [timestamp=performance.now()]
|
|
393
|
+
* @private
|
|
394
|
+
*/
|
|
395
|
+
_renderLayers (timestamp) {
|
|
396
|
+
let frameFullyLoaded = true
|
|
397
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
398
|
+
const layer = this.layers[i]
|
|
399
|
+
// Cancel operation if outside layer time interval
|
|
400
|
+
// > or >= ?
|
|
401
|
+
if (this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
|
|
402
|
+
// outside time interval
|
|
403
|
+
// if only rendering this frame (instant==true), we are not "starting" the layer
|
|
404
|
+
if (layer.active && !this._renderingFrame) {
|
|
405
|
+
// TODO: make a `deactivate()` method?
|
|
406
|
+
// console.log("stop");
|
|
407
|
+
publish(layer, 'layer.stop', { movie: this })
|
|
408
|
+
layer._active = false
|
|
409
|
+
}
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
// if only rendering this frame, we are not "starting" the layer
|
|
413
|
+
if (!layer.active && !this._renderingFrame) {
|
|
414
|
+
// TODO: make an `activate()` method?
|
|
415
|
+
// console.log("start");
|
|
416
|
+
publish(layer, 'layer.start', { movie: this })
|
|
417
|
+
layer._active = true
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (layer.media) {
|
|
421
|
+
frameFullyLoaded = frameFullyLoaded && layer.media.readyState >= 2
|
|
422
|
+
} // frame loaded
|
|
423
|
+
const reltime = this.currentTime - layer.startTime
|
|
424
|
+
layer._render(reltime) // pass relative time for convenience
|
|
425
|
+
|
|
426
|
+
// if the layer has visual component
|
|
427
|
+
if (layer.canvas) {
|
|
428
|
+
// layer.canvas.width and layer.canvas.height should already be interpolated
|
|
429
|
+
// if the layer has an area (else InvalidStateError from canvas)
|
|
430
|
+
if (layer.canvas.width * layer.canvas.height > 0) {
|
|
431
|
+
this.cctx.drawImage(layer.canvas,
|
|
432
|
+
val(layer.x, layer, reltime), val(layer.y, layer, reltime), layer.canvas.width, layer.canvas.height
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return frameFullyLoaded
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
_applyEffects () {
|
|
442
|
+
for (let i = 0; i < this.effects.length; i++) {
|
|
443
|
+
const effect = this.effects[i]
|
|
444
|
+
effect.apply(this, this.currentTime)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Refreshes the screen (only use this if auto-refresh is disabled)
|
|
450
|
+
* @return {Promise} - resolves when the frame is loaded
|
|
451
|
+
*/
|
|
452
|
+
refresh () {
|
|
453
|
+
if (this.rendering) {
|
|
454
|
+
throw new Error('Cannot refresh frame while already rendering')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return new Promise((resolve, reject) => {
|
|
458
|
+
this._renderingFrame = true
|
|
459
|
+
this._render(undefined, resolve)
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Convienence method
|
|
465
|
+
* @todo Make private
|
|
466
|
+
*/
|
|
467
|
+
publishToLayers (type, event) {
|
|
468
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
469
|
+
publish(this.layers[i], type, event)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* If the movie is playing, recording or refreshing
|
|
475
|
+
* @type boolean
|
|
476
|
+
*/
|
|
477
|
+
get rendering () {
|
|
478
|
+
return !this.paused || this._renderingFrame
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* If the movie is refreshing current frame
|
|
483
|
+
* @type boolean
|
|
484
|
+
*/
|
|
485
|
+
get renderingFrame () {
|
|
486
|
+
return this._renderingFrame
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* If the movie is recording
|
|
491
|
+
* @type boolean
|
|
492
|
+
*/
|
|
493
|
+
get recording () {
|
|
494
|
+
return !!this._mediaRecorder
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* The combined duration of all layers
|
|
499
|
+
* @type number
|
|
500
|
+
*/
|
|
501
|
+
get duration () { // TODO: dirty flag?
|
|
502
|
+
return this.layers.reduce((end, layer) => Math.max(layer.startTime + layer.duration, end), 0)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* @type layer.Base[]
|
|
507
|
+
*/
|
|
508
|
+
get layers () {
|
|
509
|
+
return this._layers
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// (proxy)
|
|
513
|
+
/**
|
|
514
|
+
* Convienence method for <code>layers.push()</code>
|
|
515
|
+
* @param {BaseLayer} layer
|
|
516
|
+
* @return {Movie} the movie (for chaining)
|
|
517
|
+
*/
|
|
518
|
+
addLayer (layer) {
|
|
519
|
+
this.layers.push(layer); return this
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @type effect.Base[]
|
|
524
|
+
*/
|
|
525
|
+
get effects () {
|
|
526
|
+
return this._effects // private (because it's a proxy)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Convienence method for <code>effects.push()</code>
|
|
531
|
+
* @param {BaseEffect} effect
|
|
532
|
+
* @return {Movie} the movie (for chaining)
|
|
533
|
+
*/
|
|
534
|
+
addEffect (effect) {
|
|
535
|
+
this.effects.push(effect); return this
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* @type boolean
|
|
540
|
+
*/
|
|
541
|
+
get paused () {
|
|
542
|
+
return this._paused
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* If the playback position is at the end of the movie
|
|
547
|
+
* @type boolean
|
|
548
|
+
*/
|
|
549
|
+
get ended () {
|
|
550
|
+
return this._ended
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* The current playback position
|
|
555
|
+
* @type number
|
|
556
|
+
*/
|
|
557
|
+
get currentTime () {
|
|
558
|
+
return this._currentTime
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Sets the current playback position. This is a more powerful version of `set currentTime`.
|
|
563
|
+
*
|
|
564
|
+
* @param {number} time - the new cursor's time value in seconds
|
|
565
|
+
* @param {boolean} [refresh=true] - whether to render a single frame to match new time or not
|
|
566
|
+
* @return {Promise} resolves when the current frame is rendered if <code>refresh</code> is true,
|
|
567
|
+
* otherwise resolves immediately
|
|
568
|
+
*
|
|
569
|
+
* @todo Refresh ionly f auto-refreshing is enabled
|
|
570
|
+
*/
|
|
571
|
+
setCurrentTime (time, refresh = true) {
|
|
572
|
+
return new Promise((resolve, reject) => {
|
|
573
|
+
this._currentTime = time
|
|
574
|
+
publish(this, 'movie.seek', {})
|
|
575
|
+
if (refresh) {
|
|
576
|
+
// pass promise callbacks to `refresh`
|
|
577
|
+
this.refresh().then(resolve).catch(reject)
|
|
578
|
+
} else {
|
|
579
|
+
resolve()
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
set currentTime (time) {
|
|
585
|
+
this._currentTime = time
|
|
586
|
+
publish(this, 'movie.seek', {})
|
|
587
|
+
this.refresh() // render single frame to match new time
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* The rendering canvas
|
|
592
|
+
* @type HTMLCanvasElement
|
|
593
|
+
*/
|
|
594
|
+
get canvas () {
|
|
595
|
+
return this._canvas
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* The rendering canvas's context
|
|
600
|
+
* @type CanvasRenderingContext2D
|
|
601
|
+
*/
|
|
602
|
+
get cctx () {
|
|
603
|
+
return this._cctx
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* The audio context to which audio is played
|
|
608
|
+
* @type BaseAudioContext
|
|
609
|
+
*/
|
|
610
|
+
get actx () {
|
|
611
|
+
return this._actx
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* The width of the rendering canvas
|
|
616
|
+
* @type number
|
|
617
|
+
*/
|
|
618
|
+
get width () {
|
|
619
|
+
return this.canvas.width
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* The height of the rendering canvas
|
|
624
|
+
* @type number
|
|
625
|
+
*/
|
|
626
|
+
get height () {
|
|
627
|
+
return this.canvas.height
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
set width (width) {
|
|
631
|
+
this.canvas.width = width
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
set height (height) {
|
|
635
|
+
this.canvas.height = height
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// id for events (independent of instance, but easy to access when on prototype chain)
|
|
640
|
+
Movie.prototype._type = 'movie'
|
|
641
|
+
Movie.prototype.getDefaultOptions = function () {
|
|
642
|
+
return {
|
|
643
|
+
_actx: new AudioContext(),
|
|
644
|
+
/**
|
|
645
|
+
* @name module:movie#background
|
|
646
|
+
* @type string
|
|
647
|
+
* @desc The css color for the background, or <code>null</code> for transparency
|
|
648
|
+
*/
|
|
649
|
+
background: '#000',
|
|
650
|
+
/**
|
|
651
|
+
* @name module:movie#repeat
|
|
652
|
+
* @type boolean
|
|
653
|
+
*/
|
|
654
|
+
repeat: false,
|
|
655
|
+
/**
|
|
656
|
+
* @name module:movie#autoRefresh
|
|
657
|
+
* @type boolean
|
|
658
|
+
* @desc Whether to refresh when changes are made that would effect the current frame
|
|
659
|
+
*/
|
|
660
|
+
autoRefresh: true
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// TODO: refactor so we don't need to explicitly exclude some of these
|
|
664
|
+
Movie.prototype._publicExcludes = ['canvas', 'cctx', 'actx', 'layers', 'effects']
|
|
665
|
+
</code></pre>
|
|
666
|
+
</article>
|
|
667
|
+
</section>
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<br class="clear">
|
|
677
|
+
|
|
678
|
+
<footer>
|
|
679
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.3</a> on Sun Oct 13 2019 15:38:43 GMT-0400 (Eastern Daylight Time) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
680
|
+
</footer>
|
|
681
|
+
|
|
682
|
+
<script>prettyPrint();</script>
|
|
683
|
+
<script src="scripts/polyfill.js"></script>
|
|
684
|
+
<script src="scripts/linenumber.js"></script>
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
</body>
|
|
689
|
+
</html>
|