etro 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CONTRIBUTING.md +23 -20
  3. package/README.md +3 -2
  4. package/dist/custom-array.d.ts +10 -0
  5. package/dist/effect/base.d.ts +10 -1
  6. package/dist/effect/shader.d.ts +11 -1
  7. package/dist/effect/stack.d.ts +6 -2
  8. package/dist/etro-cjs.js +1156 -575
  9. package/dist/etro-iife.js +1156 -575
  10. package/dist/event.d.ts +10 -5
  11. package/dist/layer/audio-source.d.ts +9 -4
  12. package/dist/layer/audio.d.ts +15 -2
  13. package/dist/layer/base.d.ts +49 -3
  14. package/dist/layer/image.d.ts +15 -1
  15. package/dist/layer/text.d.ts +3 -0
  16. package/dist/layer/video.d.ts +13 -1
  17. package/dist/layer/visual.d.ts +6 -2
  18. package/dist/movie/effects.d.ts +6 -0
  19. package/dist/movie/index.d.ts +1 -0
  20. package/dist/movie/layers.d.ts +6 -0
  21. package/dist/movie/movie.d.ts +260 -0
  22. package/dist/object.d.ts +9 -6
  23. package/dist/util.d.ts +4 -10
  24. package/eslint.conf.js +2 -2
  25. package/karma.conf.js +4 -7
  26. package/package.json +8 -7
  27. package/src/custom-array.ts +43 -0
  28. package/src/effect/base.ts +23 -22
  29. package/src/effect/gaussian-blur.ts +11 -6
  30. package/src/effect/pixelate.ts +3 -3
  31. package/src/effect/shader.ts +33 -27
  32. package/src/effect/stack.ts +43 -30
  33. package/src/effect/transform.ts +14 -7
  34. package/src/event.ts +111 -21
  35. package/src/layer/audio-source.ts +60 -20
  36. package/src/layer/audio.ts +25 -3
  37. package/src/layer/base.ts +79 -26
  38. package/src/layer/image.ts +26 -2
  39. package/src/layer/text.ts +7 -0
  40. package/src/layer/video.ts +31 -4
  41. package/src/layer/visual-source.ts +43 -1
  42. package/src/layer/visual.ts +50 -28
  43. package/src/movie/effects.ts +26 -0
  44. package/src/movie/index.ts +1 -0
  45. package/src/movie/layers.ts +26 -0
  46. package/src/movie/movie.ts +855 -0
  47. package/src/object.ts +9 -6
  48. package/src/util.ts +68 -89
  49. package/dist/movie.d.ts +0 -201
  50. package/src/movie.ts +0 -744
  51. /package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +0 -0
  52. /package/scripts/{save-effect-samples.js → effect/save-effect-samples.js} +0 -0
package/dist/etro-iife.js CHANGED
@@ -39,11 +39,68 @@ var etro = (function () {
39
39
  return t;
40
40
  };
41
41
  return __assign.apply(this, arguments);
42
- };
42
+ };
43
+
44
+ function __awaiter(thisArg, _arguments, P, generator) {
45
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
46
+ return new (P || (P = Promise))(function (resolve, reject) {
47
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
48
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
49
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
50
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
51
+ });
52
+ }
53
+
54
+ function __generator(thisArg, body) {
55
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
56
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
57
+ function verb(n) { return function (v) { return step([n, v]); }; }
58
+ function step(op) {
59
+ if (f) throw new TypeError("Generator is already executing.");
60
+ while (_) try {
61
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
62
+ if (y = 0, t) op = [op[0] & 2, t.value];
63
+ switch (op[0]) {
64
+ case 0: case 1: t = op; break;
65
+ case 4: _.label++; return { value: op[1], done: false };
66
+ case 5: _.label++; y = op[1]; op = [0]; continue;
67
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
68
+ default:
69
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
70
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
71
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
72
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
73
+ if (t[2]) _.ops.pop();
74
+ _.trys.pop(); continue;
75
+ }
76
+ op = body.call(thisArg, _);
77
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
78
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
79
+ }
80
+ }
43
81
 
44
82
  /**
45
83
  * @module event
46
84
  */
85
+ var DeprecatedEvent = /** @class */ (function () {
86
+ function DeprecatedEvent(replacement, message) {
87
+ if (message === void 0) { message = undefined; }
88
+ this.replacement = replacement;
89
+ this.message = message;
90
+ }
91
+ DeprecatedEvent.prototype.toString = function () {
92
+ var str = '';
93
+ if (this.replacement) {
94
+ str += "Use ".concat(this.replacement, " instead.");
95
+ }
96
+ if (this.message) {
97
+ str += " ".concat(this.message);
98
+ }
99
+ return str;
100
+ };
101
+ return DeprecatedEvent;
102
+ }());
103
+ var deprecatedEvents = {};
47
104
  /**
48
105
  * An event type
49
106
  * @private
@@ -53,11 +110,14 @@ var etro = (function () {
53
110
  this._parts = id.split('.');
54
111
  }
55
112
  TypeId.prototype.contains = function (other) {
56
- if (other._parts.length > this._parts.length)
113
+ if (other._parts.length > this._parts.length) {
57
114
  return false;
58
- for (var i = 0; i < other._parts.length; i++)
59
- if (other._parts[i] !== this._parts[i])
115
+ }
116
+ for (var i = 0; i < other._parts.length; i++) {
117
+ if (other._parts[i] !== this._parts[i]) {
60
118
  return false;
119
+ }
120
+ }
61
121
  return true;
62
122
  };
63
123
  TypeId.prototype.toString = function () {
@@ -65,23 +125,50 @@ var etro = (function () {
65
125
  };
66
126
  return TypeId;
67
127
  }());
128
+ function deprecate(type, newType, message) {
129
+ if (message === void 0) { message = undefined; }
130
+ deprecatedEvents[type] = new DeprecatedEvent(newType, message);
131
+ }
132
+ function subscribeOnce(target, type, listener) {
133
+ var wrapped = function (event) {
134
+ unsubscribe(target, wrapped);
135
+ listener(event);
136
+ };
137
+ subscribe(target, type, wrapped);
138
+ }
139
+ function subscribeMany(target, type, listener) {
140
+ if (!listeners.has(target)) {
141
+ listeners.set(target, []);
142
+ }
143
+ listeners.get(target).push({ type: new TypeId(type), listener: listener });
144
+ }
68
145
  /**
69
- * Listen for an event or category of events
146
+ * Listen for an event or category of events.
70
147
  *
71
- * @param target - a etro object
148
+ * @param target - an etro object
72
149
  * @param type - the id of the type (can contain subtypes, such as
73
150
  * "type.subtype")
74
151
  * @param listener
152
+ * @param options - options
153
+ * @param options.once - if true, the listener will only be called once
75
154
  */
76
- function subscribe(target, type, listener) {
77
- if (!listeners.has(target))
78
- listeners.set(target, []);
79
- listeners.get(target).push({ type: new TypeId(type), listener: listener });
155
+ function subscribe(target, type, listener, options) {
156
+ if (options === void 0) { options = {}; }
157
+ // Check if this event is deprecated.
158
+ if (Object.keys(deprecatedEvents).includes(type)) {
159
+ console.warn("Event ".concat(type, " is deprecated. ").concat(deprecatedEvents[type]));
160
+ }
161
+ if (options.once) {
162
+ subscribeOnce(target, type, listener);
163
+ }
164
+ else {
165
+ subscribeMany(target, type, listener);
166
+ }
80
167
  }
81
168
  /**
82
169
  * Remove an event listener
83
170
  *
84
- * @param target - a etro object
171
+ * @param target - an etro object
85
172
  * @param type - the id of the type (can contain subtypes, such as
86
173
  * "type.subtype")
87
174
  * @param listener
@@ -89,33 +176,36 @@ var etro = (function () {
89
176
  function unsubscribe(target, listener) {
90
177
  // Make sure `listener` has been added with `subscribe`.
91
178
  if (!listeners.has(target) ||
92
- !listeners.get(target).map(function (pair) { return pair.listener; }).includes(listener))
179
+ !listeners.get(target).map(function (pair) { return pair.listener; }).includes(listener)) {
93
180
  throw new Error('No matching event listener to remove');
181
+ }
94
182
  var removed = listeners.get(target)
95
183
  .filter(function (pair) { return pair.listener !== listener; });
96
184
  listeners.set(target, removed);
97
185
  }
98
186
  /**
99
- * Emits an event to all listeners
187
+ * Publish an event to all listeners without checking if it is deprecated.
100
188
  *
101
- * @param target - a etro object
102
- * @param type - the id of the type (can contain subtypes, such as
103
- * "type.subtype")
104
- * @param event - any additional event data
189
+ * @param target
190
+ * @param type
191
+ * @param event
192
+ * @returns
105
193
  */
106
- function publish(target, type, event) {
194
+ function _publish(target, type, event) {
107
195
  event.target = target; // could be a proxy
108
196
  event.type = type;
109
197
  var t = new TypeId(type);
110
- if (!listeners.has(target))
198
+ if (!listeners.has(target)) {
111
199
  // No event fired
112
200
  return null;
201
+ }
113
202
  // Call event listeners for this event.
114
203
  var listenersForType = [];
115
204
  for (var i = 0; i < listeners.get(target).length; i++) {
116
205
  var item = listeners.get(target)[i];
117
- if (t.contains(item.type))
206
+ if (t.contains(item.type)) {
118
207
  listenersForType.push(item.listener);
208
+ }
119
209
  }
120
210
  for (var i = 0; i < listenersForType.length; i++) {
121
211
  var listener = listenersForType[i];
@@ -123,10 +213,33 @@ var etro = (function () {
123
213
  }
124
214
  return event;
125
215
  }
216
+ /**
217
+ * Emits an event to all listeners
218
+ *
219
+ * @param target - an etro object
220
+ * @param type - the id of the type (can contain subtypes, such as
221
+ * "type.subtype")
222
+ * @param event - any additional event data
223
+ */
224
+ function publish(target, type, event) {
225
+ // Check if this event is deprecated only if it can be replaced.
226
+ if (Object.keys(deprecatedEvents).includes(type) && deprecatedEvents[type].replacement) {
227
+ throw new Error("Event ".concat(type, " is deprecated. ").concat(deprecatedEvents[type]));
228
+ }
229
+ // Check for deprecated events that this event replaces.
230
+ for (var deprecated in deprecatedEvents) {
231
+ var deprecatedEvent = deprecatedEvents[deprecated];
232
+ if (type === deprecatedEvent.replacement) {
233
+ _publish(target, deprecated, __assign({}, event));
234
+ }
235
+ }
236
+ return _publish(target, type, event);
237
+ }
126
238
  var listeners = new WeakMap();
127
239
 
128
240
  var event = /*#__PURE__*/Object.freeze({
129
241
  __proto__: null,
242
+ deprecate: deprecate,
130
243
  subscribe: subscribe,
131
244
  unsubscribe: unsubscribe,
132
245
  publish: publish
@@ -144,8 +257,9 @@ var etro = (function () {
144
257
  function getPropertyDescriptor(obj, name) {
145
258
  do {
146
259
  var propDesc = Object.getOwnPropertyDescriptor(obj, name);
147
- if (propDesc)
260
+ if (propDesc) {
148
261
  return propDesc;
262
+ }
149
263
  obj = Object.getPrototypeOf(obj);
150
264
  } while (obj);
151
265
  return undefined;
@@ -154,36 +268,45 @@ var etro = (function () {
154
268
  * Merges `options` with `defaultOptions`, and then copies the properties with
155
269
  * the keys in `defaultOptions` from the merged object to `destObj`.
156
270
  *
271
+ * @deprecated Each option should be copied individually, and the default value
272
+ * should be set in the constructor. See
273
+ * {@link https://github.com/etro-js/etro/issues/131} for more info.
274
+ *
157
275
  * @return
158
276
  */
159
277
  // TODO: Make methods like getDefaultOptions private
160
278
  function applyOptions(options, destObj) {
161
279
  var defaultOptions = destObj.getDefaultOptions();
162
280
  // Validate; make sure `keys` doesn't have any extraneous items
163
- for (var option in options)
281
+ for (var option in options) {
164
282
  // eslint-disable-next-line no-prototype-builtins
165
- if (!defaultOptions.hasOwnProperty(option))
283
+ if (!defaultOptions.hasOwnProperty(option)) {
166
284
  throw new Error("Invalid option: '" + option + "'");
285
+ }
286
+ }
167
287
  // Merge options and defaultOptions
168
288
  options = __assign(__assign({}, defaultOptions), options);
169
289
  // Copy options
170
290
  for (var option in options) {
171
291
  var propDesc = getPropertyDescriptor(destObj, option);
172
292
  // Update the property as long as the property has not been set (unless if it has a setter)
173
- if (!propDesc || propDesc.set)
293
+ if (!propDesc || propDesc.set) {
174
294
  destObj[option] = options[option];
295
+ }
175
296
  }
176
297
  }
177
298
  // This must be cleared at the start of each frame
178
299
  var valCache = new WeakMap();
179
300
  function cacheValue(element, path, value) {
180
301
  // Initiate movie cache
181
- if (!valCache.has(element.movie))
302
+ if (!valCache.has(element.movie)) {
182
303
  valCache.set(element.movie, new WeakMap());
304
+ }
183
305
  var movieCache = valCache.get(element.movie);
184
- // Iniitate element cache
185
- if (!movieCache.has(element))
306
+ // Initiate element cache
307
+ if (!movieCache.has(element)) {
186
308
  movieCache.set(element, {});
309
+ }
187
310
  var elementCache = movieCache.get(element);
188
311
  // Cache the value
189
312
  elementCache[path] = value;
@@ -223,13 +346,16 @@ var etro = (function () {
223
346
  return this;
224
347
  };
225
348
  KeyFrame.prototype.evaluate = function (time) {
226
- if (this.value.length === 0)
349
+ if (this.value.length === 0) {
227
350
  throw new Error('Empty keyframe');
228
- if (time === undefined)
351
+ }
352
+ if (time === undefined) {
229
353
  throw new Error('|time| is undefined or null');
354
+ }
230
355
  var firstTime = this.value[0][0];
231
- if (time < firstTime)
356
+ if (time < firstTime) {
232
357
  throw new Error('No keyframe point before |time|');
358
+ }
233
359
  // I think reduce are slow to do per-frame (or more)?
234
360
  for (var i = 0; i < this.value.length; i++) {
235
361
  var startTime = this.value[i][0];
@@ -238,7 +364,7 @@ var etro = (function () {
238
364
  if (i + 1 < this.value.length) {
239
365
  var endTime = this.value[i + 1][0];
240
366
  var endValue = this.value[i + 1][1];
241
- if (startTime <= time && time < endTime)
367
+ if (startTime <= time && time < endTime) {
242
368
  // No need for endValue if it is flat interpolation
243
369
  // TODO: support custom interpolation for 'other' types?
244
370
  if (!(typeof startValue === 'number' || typeof endValue === 'object')) {
@@ -254,6 +380,7 @@ var etro = (function () {
254
380
  endValue, // eslint-disable-line @typescript-eslint/ban-types
255
381
  percentProgress, this.interpolationKeys);
256
382
  }
383
+ }
257
384
  }
258
385
  else {
259
386
  // Repeat last value forever
@@ -279,23 +406,28 @@ var etro = (function () {
279
406
  // TODO: Is this function efficient?
280
407
  // TODO: Update doc @params to allow for keyframes
281
408
  function val(element, path, time) {
282
- if (hasCachedValue(element, path))
409
+ if (hasCachedValue(element, path)) {
283
410
  return getCachedValue(element, path);
411
+ }
284
412
  // Get property of element at path
285
413
  var pathParts = path.split('.');
286
414
  var property = element[pathParts.shift()];
287
- while (pathParts.length > 0)
415
+ while (pathParts.length > 0) {
288
416
  property = property[pathParts.shift()];
417
+ }
289
418
  // Property filter function
290
419
  var process = element.propertyFilters[path];
291
420
  var value;
292
- if (property instanceof KeyFrame)
421
+ if (property instanceof KeyFrame) {
293
422
  value = property.evaluate(time);
294
- else if (typeof property === 'function')
295
- value = property(element, time); // TODO? add more args
296
- else
423
+ }
424
+ else if (typeof property === 'function') {
425
+ value = property(element, time);
426
+ }
427
+ else {
297
428
  // Simple value
298
429
  value = property;
430
+ }
299
431
  return cacheValue(element, path, process ? process.call(element, value) : value);
300
432
  }
301
433
  /* export function floorInterp(x1, x2, t, objectKeys) {
@@ -306,15 +438,18 @@ var etro = (function () {
306
438
  }, Object.create(Object.getPrototypeOf(x1)));
307
439
  } */
308
440
  function linearInterp(x1, x2, t, objectKeys) {
309
- if (typeof x1 !== typeof x2)
441
+ if (typeof x1 !== typeof x2) {
310
442
  throw new Error('Type mismatch');
311
- if (typeof x1 !== 'number' && typeof x1 !== 'object')
443
+ }
444
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
312
445
  // Flat interpolation (floor)
313
446
  return x1;
447
+ }
314
448
  if (typeof x1 === 'object') { // to work with objects (including arrays)
315
449
  // TODO: make this code DRY
316
- if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2))
450
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
317
451
  throw new Error('Prototype mismatch');
452
+ }
318
453
  // Preserve prototype of objects
319
454
  var int = Object.create(Object.getPrototypeOf(x1));
320
455
  // Take the intersection of properties
@@ -322,8 +457,9 @@ var etro = (function () {
322
457
  for (var i = 0; i < keys.length; i++) {
323
458
  var key = keys[i];
324
459
  // eslint-disable-next-line no-prototype-builtins
325
- if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key))
460
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
326
461
  continue;
462
+ }
327
463
  int[key] = linearInterp(x1[key], x2[key], t);
328
464
  }
329
465
  return int;
@@ -331,14 +467,17 @@ var etro = (function () {
331
467
  return (1 - t) * x1 + t * x2;
332
468
  }
333
469
  function cosineInterp(x1, x2, t, objectKeys) {
334
- if (typeof x1 !== typeof x2)
470
+ if (typeof x1 !== typeof x2) {
335
471
  throw new Error('Type mismatch');
336
- if (typeof x1 !== 'number' && typeof x1 !== 'object')
472
+ }
473
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
337
474
  // Flat interpolation (floor)
338
475
  return x1;
476
+ }
339
477
  if (typeof x1 === 'object' && typeof x2 === 'object') { // to work with objects (including arrays)
340
- if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2))
478
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
341
479
  throw new Error('Prototype mismatch');
480
+ }
342
481
  // Preserve prototype of objects
343
482
  var int = Object.create(Object.getPrototypeOf(x1));
344
483
  // Take the intersection of properties
@@ -346,8 +485,9 @@ var etro = (function () {
346
485
  for (var i = 0; i < keys.length; i++) {
347
486
  var key = keys[i];
348
487
  // eslint-disable-next-line no-prototype-builtins
349
- if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key))
488
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
350
489
  continue;
490
+ }
351
491
  int[key] = cosineInterp(x1[key], x2[key], t);
352
492
  }
353
493
  return int;
@@ -427,17 +567,22 @@ var etro = (function () {
427
567
  */
428
568
  Font.prototype.toString = function () {
429
569
  var s = '';
430
- if (this.style !== 'normal')
570
+ if (this.style !== 'normal') {
431
571
  s += this.style + ' ';
432
- if (this.variant !== 'normal')
572
+ }
573
+ if (this.variant !== 'normal') {
433
574
  s += this.variant + ' ';
434
- if (this.weight !== 'normal')
575
+ }
576
+ if (this.weight !== 'normal') {
435
577
  s += this.weight + ' ';
436
- if (this.stretch !== 'normal')
578
+ }
579
+ if (this.stretch !== 'normal') {
437
580
  s += this.stretch + ' ';
581
+ }
438
582
  s += "".concat(this.size).concat(this.sizeUnit, " ");
439
- if (this.lineHeight !== 'normal')
583
+ if (this.lineHeight !== 'normal') {
440
584
  s += this.lineHeight + ' ';
585
+ }
441
586
  s += this.family;
442
587
  return s;
443
588
  };
@@ -477,61 +622,12 @@ var etro = (function () {
477
622
  width = width || canvas.width;
478
623
  height = height || canvas.height;
479
624
  var frame = ctx.getImageData(x, y, width, height);
480
- for (var i = 0, l = frame.data.length; i < l; i += 4)
625
+ for (var i = 0, l = frame.data.length; i < l; i += 4) {
481
626
  mapper(frame.data, i);
482
- if (flush)
627
+ }
628
+ if (flush) {
483
629
  ctx.putImageData(frame, x, y);
484
- }
485
- /**
486
- * <p>Emits "change" event when public properties updated, recursively.
487
- * <p>Must be called before any watchable properties are set, and only once in
488
- * the prototype chain.
489
- *
490
- * @deprecated Will be removed in the future (see issue #130)
491
- *
492
- * @param target - object to watch
493
- */
494
- function watchPublic(target) {
495
- var getPath = function (receiver, prop) {
496
- return (receiver === proxy ? '' : (paths.get(receiver) + '.')) + prop;
497
- };
498
- var callback = function (prop, val, receiver) {
499
- // Public API property updated, emit 'modify' event.
500
- publish(proxy, "".concat(target.type, ".change.modify"), { property: getPath(receiver, prop), newValue: val });
501
- };
502
- var canWatch = function (receiver, prop) { return !prop.startsWith('_') &&
503
- (receiver.publicExcludes === undefined || !receiver.publicExcludes.includes(prop)); };
504
- // The path to each child property (each is a unique proxy)
505
- var paths = new WeakMap();
506
- var handler = {
507
- set: function (obj, prop, val, receiver) {
508
- // Recurse
509
- if (typeof val === 'object' && val !== null && !paths.has(val) && canWatch(receiver, prop)) {
510
- val = new Proxy(val, handler);
511
- paths.set(val, getPath(receiver, prop));
512
- }
513
- // Set property or attribute
514
- // Search prototype chain for the closest setter
515
- var objProto = obj;
516
- while ((objProto = Object.getPrototypeOf(objProto))) {
517
- var propDesc = Object.getOwnPropertyDescriptor(objProto, prop);
518
- if (propDesc && propDesc.set) {
519
- // Call setter, supplying proxy as this (fixes event bugs)
520
- propDesc.set.call(receiver, val);
521
- break;
522
- }
523
- }
524
- if (!objProto)
525
- // Couldn't find setter; set value on instance
526
- obj[prop] = val;
527
- // Check if the property isn't blacklisted in publicExcludes.
528
- if (canWatch(receiver, prop))
529
- callback(prop, val, receiver);
530
- return true;
531
- }
532
- };
533
- var proxy = new Proxy(target, handler);
534
- return proxy;
630
+ }
535
631
  }
536
632
 
537
633
  /**
@@ -557,45 +653,70 @@ var etro = (function () {
557
653
  */
558
654
  function MixedAudioSource(options) {
559
655
  var _this = this;
656
+ var _a;
657
+ if (!options.source) {
658
+ throw new Error('Property "source" is required in options');
659
+ }
560
660
  var onload = options.onload;
561
661
  // Don't set as instance property
562
662
  delete options.onload;
563
- _this = _super.call(this, options) || this;
663
+ _this = _super.call(this, __assign(__assign({}, options), {
664
+ // Set a default duration so that the super constructor doesn't throw an
665
+ // error
666
+ duration: (_a = options.duration) !== null && _a !== void 0 ? _a : 0 })) || this;
564
667
  _this._initialized = false;
565
668
  _this._sourceStartTime = options.sourceStartTime || 0;
566
669
  applyOptions(options, _this);
567
670
  var load = function () {
568
671
  // TODO: && ?
569
- if ((options.duration || (_this.source.duration - _this.sourceStartTime)) < 0)
672
+ if ((options.duration || (_this.source.duration - _this.sourceStartTime)) < 0) {
570
673
  throw new Error('Invalid options.duration or options.sourceStartTime');
674
+ }
571
675
  _this._unstretchedDuration = options.duration || (_this.source.duration - _this.sourceStartTime);
572
676
  _this.duration = _this._unstretchedDuration / (_this.playbackRate);
573
677
  // onload will use `this`, and can't bind itself because it's before
574
678
  // super()
575
679
  onload && onload.bind(_this)(_this.source, options);
576
680
  };
577
- if (_this.source.readyState >= 2)
681
+ if (_this.source.readyState >= 2) {
578
682
  // this frame's data is available now
579
683
  load();
580
- else
684
+ }
685
+ else {
581
686
  // when this frame's data is available
582
687
  _this.source.addEventListener('loadedmetadata', load);
688
+ }
583
689
  _this.source.addEventListener('durationchange', function () {
584
690
  _this.duration = options.duration || (_this.source.duration - _this.sourceStartTime);
585
691
  });
586
692
  return _this;
587
693
  }
694
+ MixedAudioSource.prototype.whenReady = function () {
695
+ return __awaiter(this, void 0, void 0, function () {
696
+ var _this = this;
697
+ return __generator(this, function (_a) {
698
+ switch (_a.label) {
699
+ case 0: return [4 /*yield*/, _super.prototype.whenReady.call(this)];
700
+ case 1:
701
+ _a.sent();
702
+ if (!(this.source.readyState < 4)) return [3 /*break*/, 3];
703
+ return [4 /*yield*/, new Promise(function (resolve) {
704
+ _this.source.addEventListener('canplaythrough', resolve);
705
+ })];
706
+ case 2:
707
+ _a.sent();
708
+ _a.label = 3;
709
+ case 3: return [2 /*return*/];
710
+ }
711
+ });
712
+ });
713
+ };
588
714
  MixedAudioSource.prototype.attach = function (movie) {
589
715
  var _this = this;
590
716
  _super.prototype.attach.call(this, movie);
591
- subscribe(movie, 'movie.seek', function () {
592
- if (_this.currentTime < 0 || _this.currentTime >= _this.duration)
593
- return;
594
- _this.source.currentTime = _this.currentTime + _this.sourceStartTime;
595
- });
596
717
  // TODO: on unattach?
597
- subscribe(movie, 'movie.audiodestinationupdate', function (event) {
598
- // Connect to new destination if immeidately connected to the existing
718
+ subscribe(movie, 'audiodestinationupdate', function (event) {
719
+ // Connect to new destination if immediately connected to the existing
599
720
  // destination.
600
721
  if (_this._connectedToDestination) {
601
722
  _this.audioNode.disconnect(movie.actx.destination);
@@ -614,8 +735,9 @@ var etro = (function () {
614
735
  var oldDisconnect = this._audioNode.disconnect.bind(this.audioNode);
615
736
  this._audioNode.disconnect = function (destination, output, input) {
616
737
  if (_this._connectedToDestination &&
617
- destination === movie.actx.destination)
738
+ destination === movie.actx.destination) {
618
739
  _this._connectedToDestination = false;
740
+ }
619
741
  return oldDisconnect(destination, output, input);
620
742
  };
621
743
  // Connect to actx.destination by default (can be rewired by user)
@@ -631,6 +753,10 @@ var etro = (function () {
631
753
  this.source.currentTime = this.currentTime + this.sourceStartTime;
632
754
  this.source.play();
633
755
  };
756
+ MixedAudioSource.prototype.seek = function (time) {
757
+ _super.prototype.seek.call(this, time);
758
+ this.source.currentTime = this.currentTime + this.sourceStartTime;
759
+ };
634
760
  MixedAudioSource.prototype.render = function () {
635
761
  _super.prototype.render.call(this);
636
762
  // TODO: implement Issue: Create built-in audio node to support built-in
@@ -640,6 +766,7 @@ var etro = (function () {
640
766
  this.source.playbackRate = val(this, 'playbackRate', this.currentTime);
641
767
  };
642
768
  MixedAudioSource.prototype.stop = function () {
769
+ _super.prototype.stop.call(this);
643
770
  this.source.pause();
644
771
  };
645
772
  Object.defineProperty(MixedAudioSource.prototype, "audioNode", {
@@ -658,8 +785,9 @@ var etro = (function () {
658
785
  },
659
786
  set: function (value) {
660
787
  this._playbackRate = value;
661
- if (this._unstretchedDuration !== undefined)
788
+ if (this._unstretchedDuration !== undefined) {
662
789
  this.duration = this._unstretchedDuration / value;
790
+ }
663
791
  },
664
792
  enumerable: false,
665
793
  configurable: true
@@ -695,6 +823,18 @@ var etro = (function () {
695
823
  enumerable: false,
696
824
  configurable: true
697
825
  });
826
+ Object.defineProperty(MixedAudioSource.prototype, "ready", {
827
+ get: function () {
828
+ // Typescript doesn't support `super.ready` when targeting es5
829
+ var superReady = Object.getOwnPropertyDescriptor(superclass.prototype, 'ready').get.call(this);
830
+ return superReady && this.source.readyState === 4;
831
+ },
832
+ enumerable: false,
833
+ configurable: true
834
+ });
835
+ /**
836
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
837
+ */
698
838
  MixedAudioSource.prototype.getDefaultOptions = function () {
699
839
  return __assign(__assign({}, superclass.prototype.getDefaultOptions()), { source: undefined, sourceStartTime: 0, duration: undefined, muted: false, volume: 1, playbackRate: 1 });
700
840
  };
@@ -717,56 +857,74 @@ var etro = (function () {
717
857
  * movie's timeline
718
858
  */
719
859
  function Base(options) {
860
+ if (options.duration === null || options.duration === undefined) {
861
+ throw new Error('Property "duration" is required in BaseOptions');
862
+ }
863
+ if (options.startTime === null || options.startTime === undefined) {
864
+ throw new Error('Property "startTime" is required in BaseOptions');
865
+ }
720
866
  // Set startTime and duration properties manually, because they are
721
867
  // readonly. applyOptions ignores readonly properties.
722
868
  this._startTime = options.startTime;
723
869
  this._duration = options.duration;
724
- // Proxy that will be returned by constructor (for sending 'modified'
725
- // events).
726
- var newThis = watchPublic(this);
727
- // Don't send updates when initializing, so use this instead of newThis
728
870
  applyOptions(options, this);
729
871
  // Whether this layer is currently being rendered
730
872
  this.active = false;
731
873
  this.enabled = true;
732
- this._occurrenceCount = 0; // no occurances in parent
874
+ this._occurrenceCount = 0; // no occurrences in parent
733
875
  this._movie = null;
734
- // Propogate up to target
735
- subscribe(newThis, 'layer.change', function (event) {
736
- var typeOfChange = event.type.substring(event.type.lastIndexOf('.') + 1);
737
- var type = "movie.change.layer.".concat(typeOfChange);
738
- publish(newThis._movie, type, __assign(__assign({}, event), { target: newThis._movie, type: type }));
739
- });
740
- return newThis;
741
876
  }
877
+ /**
878
+ * Wait until this layer is ready to render
879
+ */
880
+ Base.prototype.whenReady = function () {
881
+ return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
882
+ return [2 /*return*/];
883
+ }); });
884
+ }; // eslint-disable-line @typescript-eslint/no-empty-function
742
885
  /**
743
886
  * Attaches this layer to `movie` if not already attached.
744
887
  * @ignore
745
888
  */
746
889
  Base.prototype.tryAttach = function (movie) {
747
- if (this._occurrenceCount === 0)
890
+ if (this._occurrenceCount === 0) {
748
891
  this.attach(movie);
892
+ }
749
893
  this._occurrenceCount++;
750
894
  };
895
+ /**
896
+ * Attaches this layer to `movie`
897
+ *
898
+ * Called when the layer is added to a movie's `layers` array.
899
+ *
900
+ * @param movie The movie to attach to
901
+ */
751
902
  Base.prototype.attach = function (movie) {
752
903
  this._movie = movie;
753
904
  };
754
905
  /**
755
- * Dettaches this layer from its movie if the number of times `tryDetach` has
906
+ * Detaches this layer from its movie if the number of times `tryDetach` has
756
907
  * been called (including this call) equals the number of times `tryAttach`
757
908
  * has been called.
758
909
  *
759
910
  * @ignore
760
911
  */
761
912
  Base.prototype.tryDetach = function () {
762
- if (this.movie === null)
913
+ if (this.movie === null) {
763
914
  throw new Error('No movie to detach from');
915
+ }
764
916
  this._occurrenceCount--;
765
917
  // If this layer occurs in another place in a `layers` array, do not unset
766
918
  // _movie. (For calling `unshift` on the `layers` proxy)
767
- if (this._occurrenceCount === 0)
919
+ if (this._occurrenceCount === 0) {
768
920
  this.detach();
921
+ }
769
922
  };
923
+ /**
924
+ * Detaches this layer from its movie
925
+ *
926
+ * Called when the layer is removed from a movie's `layers` array.
927
+ */
770
928
  Base.prototype.detach = function () {
771
929
  this._movie = null;
772
930
  };
@@ -774,14 +932,40 @@ var etro = (function () {
774
932
  * Called when the layer is activated
775
933
  */
776
934
  Base.prototype.start = function () { }; // eslint-disable-line @typescript-eslint/no-empty-function
935
+ /**
936
+ * Update {@link currentTime} when seeking
937
+ *
938
+ * This method is called when the movie seeks to a new time at the request of
939
+ * the user. {@link progress} is called when the movie's `currentTime` is
940
+ * updated due to playback.
941
+ *
942
+ * @param time - The new time in the layer
943
+ */
944
+ Base.prototype.seek = function (time) {
945
+ this._currentTime = time;
946
+ };
947
+ /**
948
+ * Update {@link currentTime} due to playback
949
+ *
950
+ * This method is called when the movie's `currentTime` is updated due to
951
+ * playback. {@link seek} is called when the movie seeks to a new time at the
952
+ * request of the user.
953
+ *
954
+ * @param time - The new time in the layer
955
+ */
956
+ Base.prototype.progress = function (time) {
957
+ this._currentTime = time;
958
+ };
777
959
  /**
778
960
  * Called when the movie renders and the layer is active
779
961
  */
780
962
  Base.prototype.render = function () { }; // eslint-disable-line @typescript-eslint/no-empty-function
781
963
  /**
782
- * Called when the layer is deactivated
964
+ * Called when the layer is deactivated
783
965
  */
784
- Base.prototype.stop = function () { }; // eslint-disable-line @typescript-eslint/no-empty-function
966
+ Base.prototype.stop = function () {
967
+ this._currentTime = undefined;
968
+ };
785
969
  Object.defineProperty(Base.prototype, "parent", {
786
970
  // TODO: is this needed?
787
971
  get: function () {
@@ -792,6 +976,7 @@ var etro = (function () {
792
976
  });
793
977
  Object.defineProperty(Base.prototype, "startTime", {
794
978
  /**
979
+ * The time in the movie at which this layer starts (in seconds)
795
980
  */
796
981
  get: function () {
797
982
  return this._startTime;
@@ -804,18 +989,17 @@ var etro = (function () {
804
989
  });
805
990
  Object.defineProperty(Base.prototype, "currentTime", {
806
991
  /**
807
- * The current time of the movie relative to this layer
992
+ * The current time of the movie relative to this layer (in seconds)
808
993
  */
809
994
  get: function () {
810
- return this._movie
811
- ? this._movie.currentTime - this.startTime
812
- : undefined;
995
+ return this._currentTime;
813
996
  },
814
997
  enumerable: false,
815
998
  configurable: true
816
999
  });
817
1000
  Object.defineProperty(Base.prototype, "duration", {
818
1001
  /**
1002
+ * The duration of this layer (in seconds)
819
1003
  */
820
1004
  get: function () {
821
1005
  return this._duration;
@@ -826,6 +1010,16 @@ var etro = (function () {
826
1010
  enumerable: false,
827
1011
  configurable: true
828
1012
  });
1013
+ Object.defineProperty(Base.prototype, "ready", {
1014
+ /**
1015
+ * `true` if this layer is ready to be rendered, `false` otherwise
1016
+ */
1017
+ get: function () {
1018
+ return true;
1019
+ },
1020
+ enumerable: false,
1021
+ configurable: true
1022
+ });
829
1023
  Object.defineProperty(Base.prototype, "movie", {
830
1024
  get: function () {
831
1025
  return this._movie;
@@ -833,6 +1027,9 @@ var etro = (function () {
833
1027
  enumerable: false,
834
1028
  configurable: true
835
1029
  });
1030
+ /**
1031
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1032
+ */
836
1033
  Base.prototype.getDefaultOptions = function () {
837
1034
  return {
838
1035
  startTime: undefined,
@@ -849,6 +1046,7 @@ var etro = (function () {
849
1046
 
850
1047
  // TODO: rename to something more consistent with the naming convention of Visual and VisualSourceMixin
851
1048
  /**
1049
+ * Layer for an HTML audio element
852
1050
  * @extends AudioSource
853
1051
  */
854
1052
  var Audio = /** @class */ (function (_super) {
@@ -857,11 +1055,21 @@ var etro = (function () {
857
1055
  * Creates an audio layer
858
1056
  */
859
1057
  function Audio(options) {
860
- var _this = _super.call(this, options) || this;
861
- if (_this.duration === undefined)
1058
+ var _this = this;
1059
+ if (typeof options.source === 'string') {
1060
+ var audio = document.createElement('audio');
1061
+ audio.src = options.source;
1062
+ options.source = audio;
1063
+ }
1064
+ _this = _super.call(this, options) || this;
1065
+ if (_this.duration === undefined) {
862
1066
  _this.duration = (_this).source.duration - _this.sourceStartTime;
1067
+ }
863
1068
  return _this;
864
1069
  }
1070
+ /**
1071
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1072
+ */
865
1073
  Audio.prototype.getDefaultOptions = function () {
866
1074
  return __assign(__assign({}, Object.getPrototypeOf(this).getDefaultOptions()), {
867
1075
  /**
@@ -873,40 +1081,101 @@ var etro = (function () {
873
1081
  return Audio;
874
1082
  }(AudioSourceMixin(Base)));
875
1083
 
876
- /** Any layer that renders to a canvas */
877
- var Visual = /** @class */ (function (_super) {
878
- __extends(Visual, _super);
879
- /**
880
- * Creates a visual layer
881
- */
882
- function Visual(options) {
883
- var _this = _super.call(this, options) || this;
884
- // Only validate extra if not subclassed, because if subclcass, there will
885
- // be extraneous options.
886
- applyOptions(options, _this);
887
- _this.canvas = document.createElement('canvas');
888
- _this.cctx = _this.canvas.getContext('2d');
889
- _this._effectsBack = [];
890
- _this.effects = new Proxy(_this._effectsBack, {
1084
+ var CustomArrayListener = /** @class */ (function () {
1085
+ function CustomArrayListener() {
1086
+ }
1087
+ return CustomArrayListener;
1088
+ }());
1089
+ /**
1090
+ * An array that notifies a listener when items are added or removed.
1091
+ */
1092
+ var CustomArray = /** @class */ (function (_super) {
1093
+ __extends(CustomArray, _super);
1094
+ function CustomArray(target, listener) {
1095
+ var _this = _super.call(this) || this;
1096
+ for (var _i = 0, target_1 = target; _i < target_1.length; _i++) {
1097
+ var item = target_1[_i];
1098
+ listener.onAdd(item);
1099
+ }
1100
+ // Create proxy
1101
+ return new Proxy(target, {
891
1102
  deleteProperty: function (target, property) {
892
1103
  var value = target[property];
893
- value.detach();
894
1104
  delete target[property];
1105
+ listener.onRemove(value);
895
1106
  return true;
896
1107
  },
897
1108
  set: function (target, property, value) {
1109
+ var oldValue = target[property];
1110
+ target[property] = value;
1111
+ // Check if property is a number (index)
898
1112
  if (!isNaN(Number(property))) {
899
- // The property is a number (index)
900
- if (target[property])
901
- target[property].detach();
902
- value.attach(_this);
1113
+ if (oldValue !== undefined) {
1114
+ listener.onRemove(oldValue);
1115
+ }
1116
+ listener.onAdd(value);
903
1117
  }
904
- target[property] = value;
905
1118
  return true;
906
1119
  }
907
1120
  });
1121
+ }
1122
+ return CustomArray;
1123
+ }(Array));
1124
+
1125
+ // eslint-disable-next-line no-use-before-define
1126
+ var VisualEffectsListener = /** @class */ (function (_super) {
1127
+ __extends(VisualEffectsListener, _super);
1128
+ // eslint-disable-next-line no-use-before-define
1129
+ function VisualEffectsListener(layer) {
1130
+ var _this = _super.call(this) || this;
1131
+ _this._layer = layer;
908
1132
  return _this;
909
1133
  }
1134
+ VisualEffectsListener.prototype.onAdd = function (effect) {
1135
+ effect.tryAttach(this._layer);
1136
+ };
1137
+ VisualEffectsListener.prototype.onRemove = function (effect) {
1138
+ effect.tryDetach();
1139
+ };
1140
+ return VisualEffectsListener;
1141
+ }(CustomArrayListener));
1142
+ var VisualEffects = /** @class */ (function (_super) {
1143
+ __extends(VisualEffects, _super);
1144
+ // eslint-disable-next-line no-use-before-define
1145
+ function VisualEffects(target, layer) {
1146
+ return _super.call(this, target, new VisualEffectsListener(layer)) || this;
1147
+ }
1148
+ return VisualEffects;
1149
+ }(CustomArray));
1150
+ /** Any layer that renders to a canvas */
1151
+ var Visual = /** @class */ (function (_super) {
1152
+ __extends(Visual, _super);
1153
+ /**
1154
+ * Creates a visual layer
1155
+ */
1156
+ function Visual(options) {
1157
+ var _this = _super.call(this, options) || this;
1158
+ applyOptions(options, _this);
1159
+ _this.canvas = document.createElement('canvas');
1160
+ _this.cctx = _this.canvas.getContext('2d');
1161
+ _this.effects = new VisualEffects([], _this);
1162
+ return _this;
1163
+ }
1164
+ Visual.prototype.whenReady = function () {
1165
+ return __awaiter(this, void 0, void 0, function () {
1166
+ return __generator(this, function (_a) {
1167
+ switch (_a.label) {
1168
+ case 0: return [4 /*yield*/, _super.prototype.whenReady.call(this)];
1169
+ case 1:
1170
+ _a.sent();
1171
+ return [4 /*yield*/, Promise.all(this.effects.map(function (effect) { return effect.whenReady(); }))];
1172
+ case 2:
1173
+ _a.sent();
1174
+ return [2 /*return*/];
1175
+ }
1176
+ });
1177
+ });
1178
+ };
910
1179
  /**
911
1180
  * Render visual output
912
1181
  */
@@ -914,8 +1183,9 @@ var etro = (function () {
914
1183
  // Prevent empty canvas errors if the width or height is 0
915
1184
  var width = val(this, 'width', this.currentTime);
916
1185
  var height = val(this, 'height', this.currentTime);
917
- if (width === 0 || height === 0)
1186
+ if (width === 0 || height === 0) {
918
1187
  return;
1188
+ }
919
1189
  this.beginRender();
920
1190
  this.doRender();
921
1191
  this.endRender();
@@ -946,20 +1216,22 @@ var etro = (function () {
946
1216
  Visual.prototype.endRender = function () {
947
1217
  var w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime);
948
1218
  var h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime);
949
- if (w * h > 0)
1219
+ if (w * h > 0) {
950
1220
  this._applyEffects();
1221
+ }
951
1222
  // else InvalidStateError for drawing zero-area image in some effects, right?
952
1223
  };
953
1224
  Visual.prototype._applyEffects = function () {
954
1225
  for (var i = 0; i < this.effects.length; i++) {
955
1226
  var effect = this.effects[i];
956
- if (effect && effect.enabled)
1227
+ if (effect && effect.enabled) {
957
1228
  // Pass relative time
958
1229
  effect.apply(this, this.movie.currentTime - this.startTime);
1230
+ }
959
1231
  }
960
1232
  };
961
1233
  /**
962
- * Convienence method for <code>effects.push()</code>
1234
+ * Convenience method for <code>effects.push()</code>
963
1235
  * @param effect
964
1236
  * @return the layer (for chaining)
965
1237
  */
@@ -967,6 +1239,18 @@ var etro = (function () {
967
1239
  this.effects.push(effect);
968
1240
  return this;
969
1241
  };
1242
+ Object.defineProperty(Visual.prototype, "ready", {
1243
+ get: function () {
1244
+ // Typescript doesn't support `super.ready` when targeting es5
1245
+ var superReady = Object.getOwnPropertyDescriptor(Base.prototype, 'ready').get.call(this);
1246
+ return superReady && this.effects.every(function (effect) { return effect.ready; });
1247
+ },
1248
+ enumerable: false,
1249
+ configurable: true
1250
+ });
1251
+ /**
1252
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1253
+ */
970
1254
  Visual.prototype.getDefaultOptions = function () {
971
1255
  return __assign(__assign({}, Base.prototype.getDefaultOptions()), {
972
1256
  /**
@@ -1025,17 +1309,60 @@ var etro = (function () {
1025
1309
  var MixedVisualSource = /** @class */ (function (_super) {
1026
1310
  __extends(MixedVisualSource, _super);
1027
1311
  function MixedVisualSource(options) {
1028
- var _this = _super.call(this, options) || this;
1312
+ var _this = this;
1313
+ if (!options.source) {
1314
+ throw new Error('Property "source" is required in options');
1315
+ }
1316
+ _this = _super.call(this, options) || this;
1029
1317
  applyOptions(options, _this);
1030
1318
  return _this;
1031
1319
  }
1320
+ MixedVisualSource.prototype.whenReady = function () {
1321
+ return __awaiter(this, void 0, void 0, function () {
1322
+ var _this = this;
1323
+ return __generator(this, function (_a) {
1324
+ switch (_a.label) {
1325
+ case 0: return [4 /*yield*/, _super.prototype.whenReady.call(this)];
1326
+ case 1:
1327
+ _a.sent();
1328
+ return [4 /*yield*/, new Promise(function (resolve) {
1329
+ if (_this.source instanceof HTMLImageElement) {
1330
+ // The source is an image; wait for it to load
1331
+ if (_this.source.complete) {
1332
+ resolve();
1333
+ }
1334
+ else {
1335
+ _this.source.addEventListener('load', function () {
1336
+ resolve();
1337
+ });
1338
+ }
1339
+ }
1340
+ else {
1341
+ // The source is a video; wait for the first frame to load
1342
+ if (_this.source.readyState === 4) {
1343
+ resolve();
1344
+ }
1345
+ else {
1346
+ _this.source.addEventListener('canplaythrough', function () {
1347
+ resolve();
1348
+ });
1349
+ }
1350
+ }
1351
+ })];
1352
+ case 2:
1353
+ _a.sent();
1354
+ return [2 /*return*/];
1355
+ }
1356
+ });
1357
+ });
1358
+ };
1032
1359
  MixedVisualSource.prototype.doRender = function () {
1033
1360
  // Clear/fill background
1034
1361
  _super.prototype.doRender.call(this);
1035
1362
  /*
1036
1363
  * Source dimensions crop the image. Dest dimensions set the size that
1037
1364
  * the image will be rendered at *on the layer*. Note that this is
1038
- * different than the layer dimensions (`this.width` and `this.height`).
1365
+ * different from the layer dimensions (`this.width` and `this.height`).
1039
1366
  * The main reason this distinction exists is so that an image layer can
1040
1367
  * be rotated without being cropped (see iss #46).
1041
1368
  */
@@ -1043,6 +1370,21 @@ var etro = (function () {
1043
1370
  // `destX` and `destY` are relative to the layer
1044
1371
  val(this, 'destX', this.currentTime), val(this, 'destY', this.currentTime), val(this, 'destWidth', this.currentTime), val(this, 'destHeight', this.currentTime));
1045
1372
  };
1373
+ Object.defineProperty(MixedVisualSource.prototype, "ready", {
1374
+ get: function () {
1375
+ // Typescript doesn't support `super.ready` when targeting es5
1376
+ var superReady = Object.getOwnPropertyDescriptor(superclass.prototype, 'ready').get.call(this);
1377
+ var sourceReady = this.source instanceof HTMLImageElement
1378
+ ? this.source.complete
1379
+ : this.source.readyState === 4;
1380
+ return superReady && sourceReady;
1381
+ },
1382
+ enumerable: false,
1383
+ configurable: true
1384
+ });
1385
+ /**
1386
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1387
+ */
1046
1388
  MixedVisualSource.prototype.getDefaultOptions = function () {
1047
1389
  return __assign(__assign({}, superclass.prototype.getDefaultOptions()), { source: undefined, sourceX: 0, sourceY: 0, sourceWidth: undefined, sourceHeight: undefined, destX: 0, destY: 0, destWidth: undefined, destHeight: undefined });
1048
1390
  };
@@ -1091,10 +1433,19 @@ var etro = (function () {
1091
1433
  return MixedVisualSource;
1092
1434
  }
1093
1435
 
1436
+ /**
1437
+ * Layer for an HTML image element
1438
+ * @extends VisualSource
1439
+ */
1094
1440
  var Image = /** @class */ (function (_super) {
1095
1441
  __extends(Image, _super);
1096
- function Image() {
1097
- return _super !== null && _super.apply(this, arguments) || this;
1442
+ function Image(options) {
1443
+ if (typeof (options.source) === 'string') {
1444
+ var img = document.createElement('img');
1445
+ img.src = options.source;
1446
+ options.source = img;
1447
+ }
1448
+ return _super.call(this, options) || this;
1098
1449
  }
1099
1450
  return Image;
1100
1451
  }(VisualSourceMixin(Visual)));
@@ -1108,9 +1459,12 @@ var etro = (function () {
1108
1459
  // TODO: is textX necessary? it seems inconsistent, because you can't define
1109
1460
  // width/height directly for a text layer
1110
1461
  function Text(options) {
1111
- var _this =
1462
+ var _this = this;
1463
+ if (!options.text) {
1464
+ throw new Error('Property "text" is required in TextOptions');
1465
+ }
1112
1466
  // Default to no (transparent) background
1113
- _super.call(this, __assign({ background: null }, options)) || this;
1467
+ _this = _super.call(this, __assign({ background: null }, options)) || this;
1114
1468
  applyOptions(options, _this);
1115
1469
  return _this;
1116
1470
  // this._prevText = undefined;
@@ -1157,22 +1511,33 @@ var etro = (function () {
1157
1511
  document.body.removeChild(s);
1158
1512
  return metrics;
1159
1513
  } */
1514
+ /**
1515
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1516
+ */
1160
1517
  Text.prototype.getDefaultOptions = function () {
1161
1518
  return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: parseColor('#fff'), textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr' });
1162
1519
  };
1163
1520
  return Text;
1164
1521
  }(Visual));
1165
1522
 
1166
- // Use mixins instead of `extend`ing two classes (which isn't supported by
1167
- // JavaScript).
1168
1523
  /**
1524
+ * Layer for an HTML video element
1169
1525
  * @extends AudioSource
1170
1526
  * @extends VisualSource
1171
1527
  */
1172
1528
  var Video = /** @class */ (function (_super) {
1173
1529
  __extends(Video, _super);
1174
- function Video() {
1175
- return _super !== null && _super.apply(this, arguments) || this;
1530
+ function Video(options) {
1531
+ var _a;
1532
+ if (typeof (options.source) === 'string') {
1533
+ var video = document.createElement('video');
1534
+ video.src = options.source;
1535
+ options.source = video;
1536
+ }
1537
+ return _super.call(this, __assign(__assign({}, options), {
1538
+ // Set a default duration so that the super constructor doesn't throw an
1539
+ // error
1540
+ duration: (_a = options.duration) !== null && _a !== void 0 ? _a : 0 })) || this;
1176
1541
  }
1177
1542
  return Video;
1178
1543
  }(AudioSourceMixin(VisualSourceMixin(Visual))));
@@ -1198,46 +1563,48 @@ var etro = (function () {
1198
1563
  */
1199
1564
  var Base$1 = /** @class */ (function () {
1200
1565
  function Base() {
1201
- var newThis = watchPublic(this); // proxy that will be returned by constructor
1202
- newThis.enabled = true;
1203
- newThis._occurrenceCount = 0;
1204
- newThis._target = null;
1205
- // Propogate up to target
1206
- subscribe(newThis, 'effect.change.modify', function (event) {
1207
- if (!newThis._target)
1208
- return;
1209
- var type = "".concat(newThis._target.type, ".change.effect.modify");
1210
- publish(newThis._target, type, __assign(__assign({}, event), { target: newThis._target, source: newThis, type: type }));
1211
- });
1212
- return newThis;
1566
+ this.enabled = true;
1567
+ this._occurrenceCount = 0;
1568
+ this._target = null;
1213
1569
  }
1570
+ /**
1571
+ * Wait until this effect is ready to be applied
1572
+ */
1573
+ Base.prototype.whenReady = function () {
1574
+ return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
1575
+ return [2 /*return*/];
1576
+ }); });
1577
+ }; // eslint-disable-line @typescript-eslint/no-empty-function
1214
1578
  /**
1215
1579
  * Attaches this effect to `target` if not already attached.
1216
1580
  * @ignore
1217
1581
  */
1218
1582
  Base.prototype.tryAttach = function (target) {
1219
- if (this._occurrenceCount === 0)
1583
+ if (this._occurrenceCount === 0) {
1220
1584
  this.attach(target);
1585
+ }
1221
1586
  this._occurrenceCount++;
1222
1587
  };
1223
1588
  Base.prototype.attach = function (movie) {
1224
1589
  this._target = movie;
1225
1590
  };
1226
1591
  /**
1227
- * Dettaches this effect from its target if the number of times `tryDetach`
1592
+ * Detaches this effect from its target if the number of times `tryDetach`
1228
1593
  * has been called (including this call) equals the number of times
1229
1594
  * `tryAttach` has been called.
1230
1595
  *
1231
1596
  * @ignore
1232
1597
  */
1233
1598
  Base.prototype.tryDetach = function () {
1234
- if (this._target === null)
1599
+ if (this._target === null) {
1235
1600
  throw new Error('No movie to detach from');
1601
+ }
1236
1602
  this._occurrenceCount--;
1237
1603
  // If this effect occurs in another place in the containing array, do not
1238
1604
  // unset _target. (For calling `unshift` on the `layers` proxy)
1239
- if (this._occurrenceCount === 0)
1605
+ if (this._occurrenceCount === 0) {
1240
1606
  this.detach();
1607
+ }
1241
1608
  };
1242
1609
  Base.prototype.detach = function () {
1243
1610
  this._target = null;
@@ -1262,6 +1629,14 @@ var etro = (function () {
1262
1629
  enumerable: false,
1263
1630
  configurable: true
1264
1631
  });
1632
+ Object.defineProperty(Base.prototype, "ready", {
1633
+ /** `true` if this effect is ready to be applied */
1634
+ get: function () {
1635
+ return true;
1636
+ },
1637
+ enumerable: false,
1638
+ configurable: true
1639
+ });
1265
1640
  Object.defineProperty(Base.prototype, "parent", {
1266
1641
  get: function () {
1267
1642
  return this._target;
@@ -1276,6 +1651,9 @@ var etro = (function () {
1276
1651
  enumerable: false,
1277
1652
  configurable: true
1278
1653
  });
1654
+ /**
1655
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1656
+ */
1279
1657
  Base.prototype.getDefaultOptions = function () {
1280
1658
  return {};
1281
1659
  };
@@ -1343,16 +1721,18 @@ var etro = (function () {
1343
1721
  Shader.prototype._initGl = function () {
1344
1722
  this._canvas = document.createElement('canvas');
1345
1723
  var gl = this._canvas.getContext('webgl');
1346
- if (gl === null)
1724
+ if (gl === null) {
1347
1725
  throw new Error('Unable to initialize WebGL. Your browser or machine may not support it.');
1726
+ }
1348
1727
  this._gl = gl;
1349
1728
  return gl;
1350
1729
  };
1351
1730
  Shader.prototype._initTextures = function (userUniforms, userTextures, sourceTextureOptions) {
1352
1731
  var gl = this._gl;
1353
1732
  var maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
1354
- if (userTextures.length > maxTextures)
1733
+ if (userTextures.length > maxTextures) {
1355
1734
  console.warn('Too many textures!');
1735
+ }
1356
1736
  this._userTextures = {};
1357
1737
  for (var name_1 in userTextures) {
1358
1738
  var userOptions = userTextures[name_1];
@@ -1365,8 +1745,9 @@ var etro = (function () {
1365
1745
  * textures, without having to define multiple properties in the effect
1366
1746
  * object.
1367
1747
  */
1368
- if (userUniforms[name_1])
1748
+ if (userUniforms[name_1]) {
1369
1749
  throw new Error("Texture - uniform naming conflict: ".concat(name_1, "!"));
1750
+ }
1370
1751
  // Add this as a "user uniform".
1371
1752
  userUniforms[name_1] = '1i'; // texture pointer
1372
1753
  }
@@ -1401,18 +1782,6 @@ var etro = (function () {
1401
1782
  this._uniformLocations[unprefixed] = gl.getUniformLocation(this._program, prefixed);
1402
1783
  }
1403
1784
  };
1404
- // Not needed, right?
1405
- /* watchWebGLOptions() {
1406
- const pubChange = () => {
1407
- this.publish("change", {});
1408
- };
1409
- for (let name in this._userTextures) {
1410
- watch(this, name, pubChange);
1411
- }
1412
- for (let name in this._userUniforms) {
1413
- watch(this, name, pubChange);
1414
- }
1415
- } */
1416
1785
  Shader.prototype.apply = function (target, reltime) {
1417
1786
  this._checkDimensions(target);
1418
1787
  this._refreshGl();
@@ -1499,16 +1868,22 @@ var etro = (function () {
1499
1868
  i++;
1500
1869
  }
1501
1870
  };
1871
+ /**
1872
+ * Set the shader's uniforms.
1873
+ * @param target The movie or layer to apply the shader to.
1874
+ * @param reltime The relative time of the movie or layer.
1875
+ */
1502
1876
  Shader.prototype._prepareUniforms = function (target, reltime) {
1503
1877
  var gl = this._gl;
1504
- // Set the shader uniforms.
1505
1878
  // Tell the shader we bound the texture to texture unit 0.
1506
1879
  // All base (Shader class) uniforms are optional.
1507
- if (this._uniformLocations.source)
1880
+ if (this._uniformLocations.source) {
1508
1881
  gl.uniform1i(this._uniformLocations.source, 0);
1882
+ }
1509
1883
  // All base (Shader class) uniforms are optional.
1510
- if (this._uniformLocations.size)
1884
+ if (this._uniformLocations.size) {
1511
1885
  gl.uniform2iv(this._uniformLocations.size, [target.canvas.width, target.canvas.height]);
1886
+ }
1512
1887
  for (var unprefixed in this._userUniforms) {
1513
1888
  var options = this._userUniforms[unprefixed];
1514
1889
  var value = val(this, unprefixed, reltime);
@@ -1532,11 +1907,13 @@ var etro = (function () {
1532
1907
  /**
1533
1908
  * Converts a value of a standard type for javascript to a standard type for
1534
1909
  * GLSL
1910
+ *
1535
1911
  * @param value - the raw value to prepare
1536
1912
  * @param outputType - the WebGL type of |value|; example:
1537
1913
  * <code>1f</code> for a float
1538
1914
  * @param reltime - current time, relative to the target
1539
- * @param [options] - Optional config
1915
+ * @param [options]
1916
+ * @returns the prepared value
1540
1917
  */
1541
1918
  Shader.prototype._prepareValue = function (value, outputType, reltime, options) {
1542
1919
  if (options === void 0) { options = {}; }
@@ -1559,35 +1936,40 @@ var etro = (function () {
1559
1936
  var i = 0;
1560
1937
  for (var name_4 in this._userTextures) {
1561
1938
  var testValue = val(this, name_4, reltime);
1562
- if (value === testValue)
1939
+ if (value === testValue) {
1563
1940
  value = Shader.INTERNAL_TEXTURE_UNITS + i; // after the internal texture units
1941
+ }
1564
1942
  i++;
1565
1943
  }
1566
1944
  }
1567
1945
  if (outputType === '3fv') {
1568
1946
  // allow 4-component vectors; TODO: why?
1569
- if (Array.isArray(value) && (value.length === 3 || value.length === 4))
1947
+ if (Array.isArray(value) && (value.length === 3 || value.length === 4)) {
1570
1948
  return value;
1949
+ }
1571
1950
  // kind of loose so this can be changed if needed
1572
- if (typeof value === 'object')
1951
+ if (typeof value === 'object') {
1573
1952
  return [
1574
1953
  value.r !== undefined ? value.r : def,
1575
1954
  value.g !== undefined ? value.g : def,
1576
1955
  value.b !== undefined ? value.b : def
1577
1956
  ];
1957
+ }
1578
1958
  throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1579
1959
  }
1580
1960
  if (outputType === '4fv') {
1581
- if (Array.isArray(value) && value.length === 4)
1961
+ if (Array.isArray(value) && value.length === 4) {
1582
1962
  return value;
1963
+ }
1583
1964
  // kind of loose so this can be changed if needed
1584
- if (typeof value === 'object')
1965
+ if (typeof value === 'object') {
1585
1966
  return [
1586
1967
  value.r !== undefined ? value.r : def,
1587
1968
  value.g !== undefined ? value.g : def,
1588
1969
  value.b !== undefined ? value.b : def,
1589
1970
  value.a !== undefined ? value.a : def
1590
1971
  ];
1972
+ }
1591
1973
  throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1592
1974
  }
1593
1975
  return value;
@@ -1681,8 +2063,9 @@ var etro = (function () {
1681
2063
  else {
1682
2064
  // No, it's not a power of 2. Turn off mips and set
1683
2065
  // wrapping to clamp to edge
1684
- if (wrapS !== gl.CLAMP_TO_EDGE || wrapT !== gl.CLAMP_TO_EDGE)
2066
+ if (wrapS !== gl.CLAMP_TO_EDGE || wrapT !== gl.CLAMP_TO_EDGE) {
1685
2067
  console.warn('Wrap mode is not CLAMP_TO_EDGE for a non-power-of-two texture. Defaulting to CLAMP_TO_EDGE');
2068
+ }
1686
2069
  gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1687
2070
  gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1688
2071
  }
@@ -1734,7 +2117,6 @@ var etro = (function () {
1734
2117
  Shader._IDENTITY_FRAGMENT_SOURCE = "\n precision mediump float;\n\n uniform sampler2D u_Source;\n\n varying highp vec2 v_TextureCoord;\n\n void main() {\n gl_FragColor = texture2D(u_Source, v_TextureCoord);\n }\n ";
1735
2118
  return Shader;
1736
2119
  }(Visual$1));
1737
- // Shader.prototype.getpublicExcludes = () =>
1738
2120
  var isPowerOf2 = function (value) { return (value && (value - 1)) === 0; };
1739
2121
 
1740
2122
  /**
@@ -1909,6 +2291,35 @@ var etro = (function () {
1909
2291
  return EllipticalMask;
1910
2292
  }(Visual$1));
1911
2293
 
2294
+ var StackEffectsListener = /** @class */ (function (_super) {
2295
+ __extends(StackEffectsListener, _super);
2296
+ function StackEffectsListener(stack) {
2297
+ var _this = _super.call(this) || this;
2298
+ _this._stack = stack;
2299
+ return _this;
2300
+ }
2301
+ StackEffectsListener.prototype.onAdd = function (effect) {
2302
+ if (!this._stack.parent) {
2303
+ return;
2304
+ }
2305
+ effect.tryAttach(this._stack.parent);
2306
+ };
2307
+ StackEffectsListener.prototype.onRemove = function (effect) {
2308
+ if (!this._stack.parent) {
2309
+ return;
2310
+ }
2311
+ effect.tryDetach();
2312
+ };
2313
+ return StackEffectsListener;
2314
+ }(CustomArrayListener));
2315
+ var StackEffects = /** @class */ (function (_super) {
2316
+ __extends(StackEffects, _super);
2317
+ // eslint-disable-next-line no-use-before-define
2318
+ function StackEffects(target, stack) {
2319
+ return _super.call(this, target, new StackEffectsListener(stack)) || this;
2320
+ }
2321
+ return StackEffects;
2322
+ }(CustomArray));
1912
2323
  /**
1913
2324
  * A sequence of effects to apply, treated as one effect. This can be useful
1914
2325
  * for defining reused effect sequences as one effect.
@@ -1917,48 +2328,28 @@ var etro = (function () {
1917
2328
  __extends(Stack, _super);
1918
2329
  function Stack(options) {
1919
2330
  var _this = _super.call(this) || this;
1920
- _this._effectsBack = [];
1921
- // TODO: Throw 'change' events in handlers
1922
- _this.effects = new Proxy(_this._effectsBack, {
1923
- deleteProperty: function (target, property) {
1924
- var value = target[property];
1925
- value.detach(); // Detach effect from movie
1926
- delete target[property];
1927
- return true;
1928
- },
1929
- set: function (target, property, value) {
1930
- // TODO: make sure type check works
1931
- if (!isNaN(Number(property))) { // if property is a number (index)
1932
- if (target[property])
1933
- target[property].detach(); // Detach old effect from movie
1934
- value.attach(this._target); // Attach effect to movie
1935
- }
1936
- target[property] = value;
1937
- return true;
1938
- }
1939
- });
2331
+ _this.effects = new StackEffects(options.effects, _this);
1940
2332
  options.effects.forEach(function (effect) { return _this.effects.push(effect); });
1941
2333
  return _this;
1942
- // TODO: Propogate 'change' events from children up
1943
2334
  }
1944
2335
  Stack.prototype.attach = function (movie) {
1945
2336
  _super.prototype.attach.call(this, movie);
1946
2337
  this.effects.filter(function (effect) { return !!effect; }).forEach(function (effect) {
1947
- effect.detach();
1948
- effect.attach(movie);
2338
+ effect.tryAttach(movie);
1949
2339
  });
1950
2340
  };
1951
2341
  Stack.prototype.detach = function () {
1952
2342
  _super.prototype.detach.call(this);
1953
2343
  this.effects.filter(function (effect) { return !!effect; }).forEach(function (effect) {
1954
- effect.detach();
2344
+ effect.tryDetach();
1955
2345
  });
1956
2346
  };
1957
2347
  Stack.prototype.apply = function (target, reltime) {
1958
2348
  for (var i = 0; i < this.effects.length; i++) {
1959
2349
  var effect = this.effects[i];
1960
- if (!effect)
2350
+ if (!effect) {
1961
2351
  continue;
2352
+ }
1962
2353
  effect.apply(target, reltime);
1963
2354
  }
1964
2355
  };
@@ -1977,7 +2368,7 @@ var etro = (function () {
1977
2368
  * Applies a Gaussian blur
1978
2369
  */
1979
2370
  // TODO: Improve performance
1980
- // TODO: Make sure this is truly gaussian even though it doens't require a
2371
+ // TODO: Make sure this is truly gaussian even though it doesn't require a
1981
2372
  // standard deviation
1982
2373
  var GaussianBlur = /** @class */ (function (_super) {
1983
2374
  __extends(GaussianBlur, _super);
@@ -2022,9 +2413,10 @@ var etro = (function () {
2022
2413
  }
2023
2414
  GaussianBlurComponent.prototype.apply = function (target, reltime) {
2024
2415
  var radiusVal = val(this, 'radius', reltime);
2025
- if (radiusVal !== this._radiusCache)
2416
+ if (radiusVal !== this._radiusCache) {
2026
2417
  // Regenerate gaussian distribution canvas.
2027
2418
  this.shape = GaussianBlurComponent._render1DKernel(GaussianBlurComponent._gen1DKernel(radiusVal));
2419
+ }
2028
2420
  this._radiusCache = radiusVal;
2029
2421
  _super.prototype.apply.call(this, target, reltime);
2030
2422
  };
@@ -2057,23 +2449,27 @@ var etro = (function () {
2057
2449
  var pascal = GaussianBlurComponent._genPascalRow(2 * radius + 1);
2058
2450
  // don't use `reduce` and `map` (overhead?)
2059
2451
  var sum = 0;
2060
- for (var i = 0; i < pascal.length; i++)
2452
+ for (var i = 0; i < pascal.length; i++) {
2061
2453
  sum += pascal[i];
2062
- for (var i = 0; i < pascal.length; i++)
2454
+ }
2455
+ for (var i = 0; i < pascal.length; i++) {
2063
2456
  pascal[i] /= sum;
2457
+ }
2064
2458
  return pascal;
2065
2459
  };
2066
2460
  GaussianBlurComponent._genPascalRow = function (index) {
2067
- if (index < 0)
2461
+ if (index < 0) {
2068
2462
  throw new Error("Invalid index ".concat(index));
2463
+ }
2069
2464
  var currRow = [1];
2070
2465
  for (var i = 1; i < index; i++) {
2071
2466
  var nextRow = [];
2072
2467
  nextRow.length = currRow.length + 1;
2073
2468
  // edges are always 1's
2074
2469
  nextRow[0] = nextRow[nextRow.length - 1] = 1;
2075
- for (var j = 1; j < nextRow.length - 1; j++)
2470
+ for (var j = 1; j < nextRow.length - 1; j++) {
2076
2471
  nextRow[j] = currRow[j - 1] + currRow[j];
2472
+ }
2077
2473
  currRow = nextRow;
2078
2474
  }
2079
2475
  return currRow;
@@ -2144,15 +2540,14 @@ var etro = (function () {
2144
2540
  pixelSize: '1i'
2145
2541
  }
2146
2542
  }) || this;
2147
- /**
2148
- */
2149
2543
  _this.pixelSize = options.pixelSize || 1;
2150
2544
  return _this;
2151
2545
  }
2152
2546
  Pixelate.prototype.apply = function (target, reltime) {
2153
2547
  var ps = val(this, 'pixelSize', reltime);
2154
- if (ps % 1 !== 0 || ps < 0)
2548
+ if (ps % 1 !== 0 || ps < 0) {
2155
2549
  throw new Error('Pixel size must be a nonnegative integer');
2550
+ }
2156
2551
  _super.prototype.apply.call(this, target, reltime);
2157
2552
  };
2158
2553
  return Pixelate;
@@ -2181,10 +2576,12 @@ var etro = (function () {
2181
2576
  return _this;
2182
2577
  }
2183
2578
  Transform.prototype.apply = function (target, reltime) {
2184
- if (target.canvas.width !== this._tmpCanvas.width)
2579
+ if (target.canvas.width !== this._tmpCanvas.width) {
2185
2580
  this._tmpCanvas.width = target.canvas.width;
2186
- if (target.canvas.height !== this._tmpCanvas.height)
2581
+ }
2582
+ if (target.canvas.height !== this._tmpCanvas.height) {
2187
2583
  this._tmpCanvas.height = target.canvas.height;
2584
+ }
2188
2585
  // Use data, since that's the underlying storage
2189
2586
  this._tmpMatrix.data = val(this, 'matrix.data', reltime);
2190
2587
  this._tmpCtx.setTransform(this._tmpMatrix.a, this._tmpMatrix.b, this._tmpMatrix.c, this._tmpMatrix.d, this._tmpMatrix.e, this._tmpMatrix.f);
@@ -2210,8 +2607,9 @@ var etro = (function () {
2210
2607
  ];
2211
2608
  }
2212
2609
  Matrix.prototype.identity = function () {
2213
- for (var i = 0; i < this.data.length; i++)
2610
+ for (var i = 0; i < this.data.length; i++) {
2214
2611
  this.data[i] = Matrix.IDENTITY.data[i];
2612
+ }
2215
2613
  return this;
2216
2614
  };
2217
2615
  /**
@@ -2220,8 +2618,9 @@ var etro = (function () {
2220
2618
  * @param [val]
2221
2619
  */
2222
2620
  Matrix.prototype.cell = function (x, y, val) {
2223
- if (val !== undefined)
2621
+ if (val !== undefined) {
2224
2622
  this.data[3 * y + x] = val;
2623
+ }
2225
2624
  return this.data[3 * y + x];
2226
2625
  };
2227
2626
  Object.defineProperty(Matrix.prototype, "a", {
@@ -2273,16 +2672,19 @@ var etro = (function () {
2273
2672
  */
2274
2673
  Matrix.prototype.multiply = function (other) {
2275
2674
  // copy to temporary matrix to avoid modifying `this` while reading from it
2276
- for (var x = 0; x < 3; x++)
2675
+ for (var x = 0; x < 3; x++) {
2277
2676
  for (var y = 0; y < 3; y++) {
2278
2677
  var sum = 0;
2279
- for (var i = 0; i < 3; i++)
2678
+ for (var i = 0; i < 3; i++) {
2280
2679
  sum += this.cell(x, i) * other.cell(i, y);
2680
+ }
2281
2681
  Matrix._TMP_MATRIX.cell(x, y, sum);
2282
2682
  }
2683
+ }
2283
2684
  // copy data from TMP_MATRIX to this
2284
- for (var i = 0; i < Matrix._TMP_MATRIX.data.length; i++)
2685
+ for (var i = 0; i < Matrix._TMP_MATRIX.data.length; i++) {
2285
2686
  this.data[i] = Matrix._TMP_MATRIX.data[i];
2687
+ }
2286
2688
  return this;
2287
2689
  };
2288
2690
  /**
@@ -2356,6 +2758,52 @@ var etro = (function () {
2356
2758
  Visual: Visual$1
2357
2759
  });
2358
2760
 
2761
+ var MovieEffectsListener = /** @class */ (function (_super) {
2762
+ __extends(MovieEffectsListener, _super);
2763
+ function MovieEffectsListener(movie) {
2764
+ var _this = _super.call(this) || this;
2765
+ _this._movie = movie;
2766
+ return _this;
2767
+ }
2768
+ MovieEffectsListener.prototype.onAdd = function (effect) {
2769
+ effect.tryAttach(this._movie);
2770
+ };
2771
+ MovieEffectsListener.prototype.onRemove = function (effect) {
2772
+ effect.tryDetach();
2773
+ };
2774
+ return MovieEffectsListener;
2775
+ }(CustomArrayListener));
2776
+ var MovieEffects = /** @class */ (function (_super) {
2777
+ __extends(MovieEffects, _super);
2778
+ function MovieEffects(target, movie) {
2779
+ return _super.call(this, target, new MovieEffectsListener(movie)) || this;
2780
+ }
2781
+ return MovieEffects;
2782
+ }(CustomArray));
2783
+
2784
+ var MovieLayersListener = /** @class */ (function (_super) {
2785
+ __extends(MovieLayersListener, _super);
2786
+ function MovieLayersListener(movie) {
2787
+ var _this = _super.call(this) || this;
2788
+ _this._movie = movie;
2789
+ return _this;
2790
+ }
2791
+ MovieLayersListener.prototype.onAdd = function (layer) {
2792
+ layer.tryAttach(this._movie);
2793
+ };
2794
+ MovieLayersListener.prototype.onRemove = function (layer) {
2795
+ layer.tryDetach();
2796
+ };
2797
+ return MovieLayersListener;
2798
+ }(CustomArrayListener));
2799
+ var MovieLayers = /** @class */ (function (_super) {
2800
+ __extends(MovieLayers, _super);
2801
+ function MovieLayers(target, movie) {
2802
+ return _super.call(this, target, new MovieLayersListener(movie)) || this;
2803
+ }
2804
+ return MovieLayers;
2805
+ }(CustomArray));
2806
+
2359
2807
  /**
2360
2808
  * @module movie
2361
2809
  */
@@ -2369,15 +2817,13 @@ var etro = (function () {
2369
2817
  *
2370
2818
  * Implements a pub/sub system.
2371
2819
  */
2372
- // TODO: Make record option to make recording video output to the user while
2373
- // it's recording
2374
2820
  // TODO: rename renderingFrame -> refreshing
2375
2821
  var Movie = /** @class */ (function () {
2376
2822
  /**
2377
2823
  * Creates a new movie.
2378
2824
  */
2379
2825
  function Movie(options) {
2380
- // TODO: move into multiple methods!
2826
+ this._recording = false;
2381
2827
  // Set actx option manually, because it's readonly.
2382
2828
  this.actx = options.actx ||
2383
2829
  options.audioContext ||
@@ -2385,236 +2831,294 @@ var etro = (function () {
2385
2831
  // eslint-disable-next-line new-cap
2386
2832
  new window.webkitAudioContext();
2387
2833
  delete options.actx;
2388
- // Proxy that will be returned by constructor
2389
- var newThis = watchPublic(this);
2834
+ // Check if required file canvas is provided
2835
+ if (!options.canvas) {
2836
+ throw new Error('Required option "canvas" not provided to Movie');
2837
+ }
2390
2838
  // Set canvas option manually, because it's readonly.
2391
- this._canvas = options.canvas;
2839
+ this._canvas = this._visibleCanvas = options.canvas;
2392
2840
  delete options.canvas;
2393
- // Don't send updates when initializing, so use this instead of newThis:
2394
2841
  this._cctx = this.canvas.getContext('2d'); // TODO: make private?
2842
+ // Set options on the movie
2395
2843
  applyOptions(options, this);
2396
- var that = newThis;
2397
- this._effectsBack = [];
2398
- this.effects = new Proxy(newThis._effectsBack, {
2399
- deleteProperty: function (target, property) {
2400
- // Refresh screen when effect is removed, if the movie isn't playing
2401
- // already.
2402
- var value = target[property];
2403
- value.tryDetach();
2404
- delete target[property];
2405
- publish(that, 'movie.change.effect.remove', { effect: value });
2406
- return true;
2407
- },
2408
- set: function (target, property, value) {
2409
- // Check if property is an number (an index)
2410
- if (!isNaN(Number(property))) {
2411
- if (target[property]) {
2412
- publish(that, 'movie.change.effect.remove', {
2413
- effect: target[property]
2414
- });
2415
- target[property].tryDetach();
2416
- }
2417
- // Attach effect to movie
2418
- value.tryAttach(that);
2419
- target[property] = value;
2420
- // Refresh screen when effect is set, if the movie isn't playing
2421
- // already.
2422
- publish(that, 'movie.change.effect.add', { effect: value });
2423
- }
2424
- else {
2425
- target[property] = value;
2426
- }
2427
- return true;
2428
- }
2429
- });
2430
- this._layersBack = [];
2431
- this.layers = new Proxy(newThis._layersBack, {
2432
- deleteProperty: function (target, property) {
2433
- var oldDuration = this.duration;
2434
- var value = target[property];
2435
- value.tryDetach(that);
2436
- delete target[property];
2437
- var current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration;
2438
- if (current)
2439
- publish(that, 'movie.change.layer.remove', { layer: value });
2440
- publish(that, 'movie.change.duration', { oldDuration: oldDuration });
2441
- return true;
2442
- },
2443
- set: function (target, property, value) {
2444
- var oldDuration = this.duration;
2445
- // Check if property is an number (an index)
2446
- if (!isNaN(Number(property))) {
2447
- if (target[property]) {
2448
- publish(that, 'movie.change.layer.remove', {
2449
- layer: target[property]
2450
- });
2451
- target[property].tryDetach();
2452
- }
2453
- // Attach layer to movie
2454
- value.tryAttach(that);
2455
- target[property] = value;
2456
- // Refresh screen when a relevant layer is added or removed
2457
- var current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration;
2458
- if (current)
2459
- publish(that, 'movie.change.layer.add', { layer: value });
2460
- publish(that, 'movie.change.duration', { oldDuration: oldDuration });
2461
- }
2462
- else {
2463
- target[property] = value;
2464
- }
2465
- return true;
2466
- }
2467
- });
2844
+ this.effects = new MovieEffects([], this);
2845
+ this.layers = new MovieLayers([], this);
2468
2846
  this._paused = true;
2469
2847
  this._ended = false;
2470
- // This variable helps prevent multiple frame-rendering loops at the same
2471
- // time (see `render`). It's only applicable when rendering.
2848
+ // This lock prevents multiple refresh loops at the same time (see
2849
+ // `render`). It's only valid while rendering.
2472
2850
  this._renderingFrame = false;
2473
2851
  this.currentTime = 0;
2474
- // For recording
2475
- this._mediaRecorder = null;
2476
- // -1 works well in inequalities
2477
- // The last time `play` was called
2852
+ // The last time `play` was called, -1 works well in comparisons
2478
2853
  this._lastPlayed = -1;
2479
- // What was `currentTime` when `play` was called
2854
+ // What `currentTime` was when `play` was called
2480
2855
  this._lastPlayedOffset = -1;
2481
- // newThis._updateInterval = 0.1; // time in seconds between each "timeupdate" event
2482
- // newThis._lastUpdate = -1;
2483
- if (newThis.autoRefresh)
2484
- newThis.refresh(); // render single frame on creation
2485
- // Subscribe to own event "change" (child events propogate up)
2486
- subscribe(newThis, 'movie.change', function () {
2487
- if (newThis.autoRefresh && !newThis.rendering)
2488
- newThis.refresh();
2489
- });
2490
- // Subscribe to own event "ended"
2491
- subscribe(newThis, 'movie.recordended', function () {
2492
- if (newThis.recording) {
2493
- newThis._mediaRecorder.requestData();
2494
- newThis._mediaRecorder.stop();
2495
- }
2496
- });
2497
- return newThis;
2498
2856
  }
2857
+ Movie.prototype._whenReady = function () {
2858
+ return __awaiter(this, void 0, void 0, function () {
2859
+ return __generator(this, function (_a) {
2860
+ switch (_a.label) {
2861
+ case 0: return [4 /*yield*/, Promise.all([
2862
+ Promise.all(this.layers.map(function (layer) { return layer.whenReady(); })),
2863
+ Promise.all(this.effects.map(function (effect) { return effect.whenReady(); }))
2864
+ ])];
2865
+ case 1:
2866
+ _a.sent();
2867
+ return [2 /*return*/];
2868
+ }
2869
+ });
2870
+ });
2871
+ };
2499
2872
  /**
2500
2873
  * Plays the movie
2501
- * @return fulfilled when the movie is done playing, never fails
2874
+ *
2875
+ * @param [options]
2876
+ * @param [options.onStart] Called when the movie starts playing
2877
+ *
2878
+ * @return Fulfilled when the movie is done playing, never fails
2502
2879
  */
2503
- Movie.prototype.play = function () {
2504
- var _this = this;
2505
- return new Promise(function (resolve) {
2506
- if (!_this.paused)
2507
- throw new Error('Already playing');
2508
- _this._paused = _this._ended = false;
2509
- _this._lastPlayed = performance.now();
2510
- _this._lastPlayedOffset = _this.currentTime;
2511
- if (!_this.renderingFrame)
2512
- // Not rendering (and not playing), so play.
2513
- _this._render(true, undefined, resolve);
2514
- // Stop rendering frame if currently doing so, because playing has higher
2515
- // priority. This will effect the next _render call.
2516
- _this._renderingFrame = false;
2517
- publish(_this, 'movie.play', {});
2880
+ Movie.prototype.play = function (options) {
2881
+ var _a;
2882
+ if (options === void 0) { options = {}; }
2883
+ return __awaiter(this, void 0, void 0, function () {
2884
+ var _this = this;
2885
+ return __generator(this, function (_b) {
2886
+ switch (_b.label) {
2887
+ case 0: return [4 /*yield*/, this._whenReady()];
2888
+ case 1:
2889
+ _b.sent();
2890
+ if (!this.paused) {
2891
+ throw new Error('Already playing');
2892
+ }
2893
+ this._paused = this._ended = false;
2894
+ this._lastPlayed = performance.now();
2895
+ this._lastPlayedOffset = this.currentTime;
2896
+ (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options);
2897
+ // For backwards compatibility
2898
+ publish(this, 'movie.play', {});
2899
+ // Repeatedly render frames until the movie ends
2900
+ return [4 /*yield*/, new Promise(function (resolve) {
2901
+ if (!_this.renderingFrame) {
2902
+ // Not rendering (and not playing), so play.
2903
+ _this._render(true, undefined, resolve);
2904
+ }
2905
+ // Stop rendering frame if currently doing so, because playing has higher
2906
+ // priority. This will affect the next _render call.
2907
+ _this._renderingFrame = false;
2908
+ })];
2909
+ case 2:
2910
+ // Repeatedly render frames until the movie ends
2911
+ _b.sent();
2912
+ return [2 /*return*/];
2913
+ }
2914
+ });
2915
+ });
2916
+ };
2917
+ /**
2918
+ * Updates the rendering canvas and audio destination to the visible canvas
2919
+ * and the audio context destination.
2920
+ */
2921
+ Movie.prototype._show = function () {
2922
+ this._canvas = this._visibleCanvas;
2923
+ this._cctx = this.canvas.getContext('2d');
2924
+ publish(this, 'audiodestinationupdate', { movie: this, destination: this.actx.destination });
2925
+ };
2926
+ /**
2927
+ * Streams the movie to a MediaStream
2928
+ *
2929
+ * @param options Options for the stream
2930
+ * @param options.frameRate The frame rate of the stream's video
2931
+ * @param options.duration The duration of the stream in seconds
2932
+ * @param options.video Whether to stream video. Defaults to true.
2933
+ * @param options.audio Whether to stream audio. Defaults to true.
2934
+ * @param options.onStart Called when the stream is started
2935
+ * @return Fulfilled when the stream is done, never fails
2936
+ */
2937
+ Movie.prototype.stream = function (options) {
2938
+ return __awaiter(this, void 0, void 0, function () {
2939
+ var tracks, visualStream, hasMediaTracks, audioDestination, audioStream;
2940
+ var _this = this;
2941
+ return __generator(this, function (_a) {
2942
+ switch (_a.label) {
2943
+ case 0:
2944
+ // Validate options
2945
+ if (!options || !options.frameRate) {
2946
+ throw new Error('Required option "frameRate" not provided to Movie.stream');
2947
+ }
2948
+ if (options.video === false && options.audio === false) {
2949
+ throw new Error('Both video and audio cannot be disabled');
2950
+ }
2951
+ if (!this.paused) {
2952
+ throw new Error("Cannot stream movie while it's already playing");
2953
+ }
2954
+ // Wait until all resources are loaded
2955
+ return [4 /*yield*/, this._whenReady()
2956
+ // Create a temporary canvas to stream from
2957
+ ];
2958
+ case 1:
2959
+ // Wait until all resources are loaded
2960
+ _a.sent();
2961
+ // Create a temporary canvas to stream from
2962
+ this._canvas = document.createElement('canvas');
2963
+ this.canvas.width = this._visibleCanvas.width;
2964
+ this.canvas.height = this._visibleCanvas.height;
2965
+ this._cctx = this.canvas.getContext('2d');
2966
+ tracks = [];
2967
+ // Add video track
2968
+ if (options.video !== false) {
2969
+ visualStream = this.canvas.captureStream(options.frameRate);
2970
+ tracks = tracks.concat(visualStream.getTracks());
2971
+ }
2972
+ hasMediaTracks = this.layers.some(function (layer) { return layer instanceof Audio || layer instanceof Video; });
2973
+ // If no media tracks present, don't include an audio stream, because
2974
+ // Chrome doesn't record silence when an audio stream is present.
2975
+ if (hasMediaTracks && options.audio !== false) {
2976
+ audioDestination = this.actx.createMediaStreamDestination();
2977
+ audioStream = audioDestination.stream;
2978
+ tracks = tracks.concat(audioStream.getTracks());
2979
+ // Notify layers and any other listeners of the new audio destination
2980
+ publish(this, 'audiodestinationupdate', { movie: this, destination: audioDestination });
2981
+ }
2982
+ // Create the stream
2983
+ this._currentStream = new MediaStream(tracks);
2984
+ // Play the movie
2985
+ this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2986
+ return [4 /*yield*/, this.play({
2987
+ onStart: function () {
2988
+ // Call the user's onStart callback
2989
+ options.onStart(_this._currentStream);
2990
+ }
2991
+ })
2992
+ // Clear the stream after the movie is done playing
2993
+ ];
2994
+ case 2:
2995
+ _a.sent();
2996
+ // Clear the stream after the movie is done playing
2997
+ this._currentStream.getTracks().forEach(function (track) {
2998
+ track.stop();
2999
+ });
3000
+ this._currentStream = null;
3001
+ this._show();
3002
+ return [2 /*return*/];
3003
+ }
3004
+ });
2518
3005
  });
2519
3006
  };
2520
3007
  /**
2521
3008
  * Plays the movie in the background and records it
2522
3009
  *
2523
3010
  * @param options
2524
- * @param frameRate
3011
+ * @param [options.frameRate] - Video frame rate
2525
3012
  * @param [options.video=true] - whether to include video in recording
2526
3013
  * @param [options.audio=true] - whether to include audio in recording
2527
- * @param [options.mediaRecorderOptions=undefined] - options to pass to the <code>MediaRecorder</code>
3014
+ * @param [options.mediaRecorderOptions=undefined] - Options to pass to the
3015
+ * `MediaRecorder` constructor
2528
3016
  * @param [options.type='video/webm'] - MIME type for exported video
2529
- * constructor
2530
- * @return resolves when done recording, rejects when internal media recorder errors
3017
+ * @param [options.onStart] - Called when the recording starts
3018
+ * @return Resolves when done recording, rejects when media recorder errors
2531
3019
  */
2532
- // TEST: *support recording that plays back with audio!*
2533
- // TODO: figure out how to do offline recording (faster than realtime).
2534
- // TODO: improve recording performance to increase frame rate?
3020
+ // TODO: Improve recording performance to increase frame rate
2535
3021
  Movie.prototype.record = function (options) {
2536
- var _this = this;
2537
- if (options.video === false && options.audio === false)
2538
- throw new Error('Both video and audio cannot be disabled');
2539
- if (!this.paused)
2540
- throw new Error('Cannot record movie while already playing or recording');
2541
- var mimeType = options.type || 'video/webm';
2542
- if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType))
2543
- throw new Error('Please pass a valid MIME type for the exported video');
2544
- return new Promise(function (resolve, reject) {
2545
- var canvasCache = _this.canvas;
2546
- // Record on a temporary canvas context
2547
- _this._canvas = document.createElement('canvas');
2548
- _this.canvas.width = canvasCache.width;
2549
- _this.canvas.height = canvasCache.height;
2550
- _this._cctx = _this.canvas.getContext('2d');
2551
- // frame blobs
2552
- var recordedChunks = [];
2553
- // Combine image + audio, or just pick one
2554
- var tracks = [];
2555
- if (options.video !== false) {
2556
- var visualStream = _this.canvas.captureStream(options.frameRate);
2557
- tracks = tracks.concat(visualStream.getTracks());
2558
- }
2559
- // Check if there's a layer that's an instance of an AudioSourceMixin
2560
- // (Audio or Video)
2561
- var hasMediaTracks = _this.layers.some(function (layer) { return layer instanceof Audio || layer instanceof Video; });
2562
- // If no media tracks present, don't include an audio stream, because
2563
- // Chrome doesn't record silence when an audio stream is present.
2564
- if (hasMediaTracks && options.audio !== false) {
2565
- var audioDestination = _this.actx.createMediaStreamDestination();
2566
- var audioStream = audioDestination.stream;
2567
- tracks = tracks.concat(audioStream.getTracks());
2568
- publish(_this, 'movie.audiodestinationupdate', { movie: _this, destination: audioDestination });
2569
- }
2570
- var stream = new MediaStream(tracks);
2571
- var mediaRecorderOptions = __assign(__assign({}, (options.mediaRecorderOptions || {})), { mimeType: mimeType });
2572
- var mediaRecorder = new MediaRecorder(stream, mediaRecorderOptions);
2573
- mediaRecorder.ondataavailable = function (event) {
2574
- // if (this._paused) reject(new Error("Recording was interrupted"));
2575
- if (event.data.size > 0)
2576
- recordedChunks.push(event.data);
2577
- };
2578
- // TODO: publish to movie, not layers
2579
- mediaRecorder.onstop = function () {
2580
- _this._paused = true;
2581
- _this._ended = true;
2582
- _this._canvas = canvasCache;
2583
- _this._cctx = _this.canvas.getContext('2d');
2584
- publish(_this, 'movie.audiodestinationupdate', { movie: _this, destination: _this.actx.destination });
2585
- _this._mediaRecorder = null;
2586
- // Construct the exported video out of all the frame blobs.
2587
- resolve(new Blob(recordedChunks, {
2588
- type: mimeType
2589
- }));
2590
- };
2591
- mediaRecorder.onerror = reject;
2592
- mediaRecorder.start();
2593
- _this._mediaRecorder = mediaRecorder;
2594
- _this._recordEndTime = options.duration ? _this.currentTime + options.duration : _this.duration;
2595
- _this.play();
2596
- publish(_this, 'movie.record', { options: options });
3022
+ var _a;
3023
+ return __awaiter(this, void 0, void 0, function () {
3024
+ var mimeType, stream, recordedChunks, mediaRecorderOptions;
3025
+ var _this = this;
3026
+ return __generator(this, function (_b) {
3027
+ switch (_b.label) {
3028
+ case 0:
3029
+ // Validate options
3030
+ if (options.video === false && options.audio === false) {
3031
+ throw new Error('Both video and audio cannot be disabled');
3032
+ }
3033
+ if (!this.paused) {
3034
+ throw new Error("Cannot record movie while it's already playing");
3035
+ }
3036
+ mimeType = options.type || 'video/webm';
3037
+ if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType)) {
3038
+ throw new Error('Please pass a valid MIME type for the exported video');
3039
+ }
3040
+ return [4 /*yield*/, new Promise(function (resolve) {
3041
+ _this.stream({
3042
+ frameRate: options.frameRate,
3043
+ duration: options.duration,
3044
+ video: options.video,
3045
+ audio: options.audio,
3046
+ onStart: resolve
3047
+ }).then(function () {
3048
+ // Stop the media recorder when the movie is done playing
3049
+ _this._recorder.requestData();
3050
+ _this._recorder.stop();
3051
+ });
3052
+ })
3053
+ // The array to store the recorded chunks
3054
+ ];
3055
+ case 1:
3056
+ stream = _b.sent();
3057
+ recordedChunks = [];
3058
+ mediaRecorderOptions = __assign(__assign({}, (options.mediaRecorderOptions || {})), { mimeType: mimeType });
3059
+ this._recorder = new MediaRecorder(stream, mediaRecorderOptions);
3060
+ this._recorder.ondataavailable = function (event) {
3061
+ // if (this._paused) reject(new Error("Recording was interrupted"));
3062
+ if (event.data.size > 0) {
3063
+ recordedChunks.push(event.data);
3064
+ }
3065
+ };
3066
+ // Start recording
3067
+ this._recorder.start();
3068
+ this._recording = true;
3069
+ // Notify caller that the media recorder has started
3070
+ (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options, this._recorder);
3071
+ // For backwards compatibility
3072
+ publish(this, 'movie.record', { options: options });
3073
+ // Wait until the media recorder is done recording and processing
3074
+ return [4 /*yield*/, new Promise(function (resolve, reject) {
3075
+ _this._recorder.onstop = function () {
3076
+ resolve();
3077
+ };
3078
+ _this._recorder.onerror = reject;
3079
+ })
3080
+ // Clean up
3081
+ ];
3082
+ case 2:
3083
+ // Wait until the media recorder is done recording and processing
3084
+ _b.sent();
3085
+ // Clean up
3086
+ this._paused = true;
3087
+ this._ended = true;
3088
+ this._recording = false;
3089
+ // Construct the exported video out of all the frame blobs.
3090
+ return [2 /*return*/, new Blob(recordedChunks, {
3091
+ type: mimeType
3092
+ })];
3093
+ }
3094
+ });
2597
3095
  });
2598
3096
  };
2599
3097
  /**
2600
- * Stops the movie, without reseting the playback position
2601
- * @return the movie (for chaining)
3098
+ * Stops the movie without resetting the playback position
3099
+ * @return The movie
2602
3100
  */
2603
3101
  Movie.prototype.pause = function () {
3102
+ // Update state
2604
3103
  this._paused = true;
2605
3104
  // Deactivate all layers
2606
- for (var i = 0; i < this.layers.length; i++)
3105
+ for (var i = 0; i < this.layers.length; i++) {
2607
3106
  if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2608
3107
  var layer = this.layers[i];
2609
- layer.stop();
2610
- layer.active = false;
3108
+ if (layer.active) {
3109
+ layer.stop();
3110
+ layer.active = false;
3111
+ }
2611
3112
  }
3113
+ }
3114
+ // For backwards compatibility, notify event listeners that the movie has
3115
+ // paused
2612
3116
  publish(this, 'movie.pause', {});
2613
3117
  return this;
2614
3118
  };
2615
3119
  /**
2616
3120
  * Stops playback and resets the playback position
2617
- * @return the movie (for chaining)
3121
+ * @return The movie
2618
3122
  */
2619
3123
  Movie.prototype.stop = function () {
2620
3124
  this.pause();
@@ -2623,8 +3127,8 @@ var etro = (function () {
2623
3127
  };
2624
3128
  /**
2625
3129
  * @param [timestamp=performance.now()]
2626
- * @param [done=undefined] - called when done playing or when the current frame is loaded
2627
- * @private
3130
+ * @param [done=undefined] - Called when done playing or when the current
3131
+ * frame is loaded
2628
3132
  */
2629
3133
  Movie.prototype._render = function (repeat, timestamp, done) {
2630
3134
  var _this = this;
@@ -2632,179 +3136,207 @@ var etro = (function () {
2632
3136
  if (done === void 0) { done = undefined; }
2633
3137
  clearCachedValues(this);
2634
3138
  if (!this.rendering) {
2635
- // (!this.paused || this._renderingFrame) is true so it's playing or it's
3139
+ // (this.paused && !this._renderingFrame) is true so it's playing or it's
2636
3140
  // rendering a single frame.
2637
- if (done)
3141
+ if (done) {
2638
3142
  done();
3143
+ }
2639
3144
  return;
2640
3145
  }
2641
- var end = this.recording ? this._recordEndTime : this.duration;
2642
- this._updateCurrentTime(timestamp, end);
2643
- // TODO: Is calling duration every frame bad for performance? (remember,
2644
- // it's calling Array.reduce)
2645
- if (this.currentTime === end) {
2646
- if (this.recording)
2647
- publish(this, 'movie.recordended', { movie: this });
2648
- if (this.currentTime === this.duration)
2649
- publish(this, 'movie.ended', { movie: this, repeat: this.repeat });
2650
- // TODO: only reset currentTime if repeating
2651
- if (this.repeat) {
2652
- // Don't use setter, which publishes 'movie.seek'. Instead, update the
2653
- // value and publish a 'movie.timeupdate' event.
3146
+ if (this.ready) {
3147
+ publish(this, 'movie.loadeddata', { movie: this });
3148
+ // If the movie is streaming or recording, resume the media recorder
3149
+ if (this._recording && this._recorder.state === 'paused') {
3150
+ this._recorder.resume();
3151
+ }
3152
+ // If the movie is streaming or recording, end at the specified duration.
3153
+ // Otherwise, end at the movie's duration, because play() does not
3154
+ // support playing a portion of the movie yet.
3155
+ // TODO: Is calling duration every frame bad for performance? (remember,
3156
+ // it's calling Array.reduce)
3157
+ var end = this._currentStream ? this._endTime : this.duration;
3158
+ this._updateCurrentTime(timestamp, end);
3159
+ if (this.currentTime === end) {
3160
+ if (this.recording) {
3161
+ publish(this, 'movie.recordended', { movie: this });
3162
+ }
3163
+ if (this.currentTime === this.duration) {
3164
+ publish(this, 'movie.ended', { movie: this, repeat: this.repeat });
3165
+ }
3166
+ // Don't use setter, which publishes 'seek'. Instead, update the
3167
+ // value and publish a 'imeupdate' event.
2654
3168
  this._currentTime = 0;
2655
3169
  publish(this, 'movie.timeupdate', { movie: this });
2656
- }
2657
- this._lastPlayed = performance.now();
2658
- this._lastPlayedOffset = 0; // this.currentTime
2659
- this._renderingFrame = false;
2660
- // Stop playback or recording if done (except if it's playing and repeat
2661
- // is true)
2662
- if (!(!this.recording && this.repeat)) {
2663
- this._paused = true;
2664
- this._ended = true;
2665
- // Deactivate all layers
2666
- for (var i = 0; i < this.layers.length; i++)
2667
- if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2668
- var layer = this.layers[i];
2669
- // A layer that has been deleted before layers.length has been updated
2670
- // (see the layers proxy in the constructor).
2671
- if (!layer || !layer.active)
2672
- continue;
2673
- layer.stop();
2674
- layer.active = false;
3170
+ this._lastPlayed = performance.now();
3171
+ this._lastPlayedOffset = 0; // this.currentTime
3172
+ this._renderingFrame = false;
3173
+ // Stop playback or recording if done (except if it's playing and repeat
3174
+ // is true)
3175
+ if (!(!this.recording && this.repeat)) {
3176
+ this._paused = true;
3177
+ this._ended = true;
3178
+ // Deactivate all layers
3179
+ for (var i = 0; i < this.layers.length; i++) {
3180
+ if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
3181
+ var layer = this.layers[i];
3182
+ // A layer that has been deleted before layers.length has been updated
3183
+ // (see the layers proxy in the constructor).
3184
+ if (!layer || !layer.active) {
3185
+ continue;
3186
+ }
3187
+ layer.stop();
3188
+ layer.active = false;
3189
+ }
3190
+ }
3191
+ publish(this, 'movie.pause', {});
3192
+ if (done) {
3193
+ done();
2675
3194
  }
2676
- if (done)
2677
- done();
2678
- return;
3195
+ return;
3196
+ }
2679
3197
  }
3198
+ // Do render
3199
+ this._renderBackground(timestamp);
3200
+ this._renderLayers();
3201
+ this._applyEffects();
2680
3202
  }
2681
- // Do render
2682
- this._renderBackground(timestamp);
2683
- var frameFullyLoaded = this._renderLayers();
2684
- this._applyEffects();
2685
- if (frameFullyLoaded)
2686
- publish(this, 'movie.loadeddata', { movie: this });
2687
- // If didn't load in this instant, repeatedly frame-render until frame is
2688
- // loaded.
2689
- // If the expression below is false, don't publish an event, just silently
2690
- // stop render loop.
2691
- if (!repeat || (this._renderingFrame && frameFullyLoaded)) {
3203
+ else {
3204
+ // If we are recording, pause the media recorder until the movie is
3205
+ // ready.
3206
+ if (this.recording && this._recorder.state === 'recording') {
3207
+ this._recorder.pause();
3208
+ }
3209
+ }
3210
+ // If the frame didn't load this instant, repeatedly frame-render until it
3211
+ // is loaded.
3212
+ // If the expression below is true, don't publish an event, just silently
3213
+ // stop the render loop.
3214
+ if (this._renderingFrame && this.ready) {
2692
3215
  this._renderingFrame = false;
2693
- if (done)
3216
+ if (done) {
2694
3217
  done();
3218
+ }
2695
3219
  return;
2696
3220
  }
3221
+ // TODO: Is making a new arrow function every frame bad for performance?
2697
3222
  window.requestAnimationFrame(function () {
2698
3223
  _this._render(repeat, undefined, done);
2699
- }); // TODO: research performance cost
3224
+ });
2700
3225
  };
2701
3226
  Movie.prototype._updateCurrentTime = function (timestampMs, end) {
2702
- // If we're only instant-rendering (current frame only), it doens't matter
2703
- // if it's paused or not.
3227
+ // If we're only frame-rendering (current frame only), it doesn't matter if
3228
+ // it's paused or not.
2704
3229
  if (!this._renderingFrame) {
2705
- // if ((timestamp - this._lastUpdate) >= this._updateInterval) {
2706
3230
  var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
2707
- var currentTime = this._lastPlayedOffset + sinceLastPlayed; // don't use setter
3231
+ var currentTime = this._lastPlayedOffset + sinceLastPlayed;
2708
3232
  if (this.currentTime !== currentTime) {
3233
+ // Update the current time (don't use setter)
2709
3234
  this._currentTime = currentTime;
3235
+ // For backwards compatibility, publish a 'movie.timeupdate' event.
2710
3236
  publish(this, 'movie.timeupdate', { movie: this });
2711
3237
  }
2712
- // this._lastUpdate = timestamp;
2713
- // }
2714
- if (this.currentTime > end)
2715
- this.currentTime = end;
3238
+ if (this.currentTime > end) {
3239
+ this._currentTime = end;
3240
+ }
2716
3241
  }
2717
3242
  };
2718
3243
  Movie.prototype._renderBackground = function (timestamp) {
2719
3244
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3245
+ // Evaluate background color (since it's a dynamic property)
2720
3246
  var background = val(this, 'background', timestamp);
2721
- if (background) { // TODO: check val'd result
3247
+ if (background) {
2722
3248
  this.cctx.fillStyle = background;
2723
3249
  this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
2724
3250
  }
2725
3251
  };
2726
3252
  /**
2727
- * @return whether or not video frames are loaded
2728
3253
  * @param [timestamp=performance.now()]
2729
- * @private
2730
3254
  */
2731
3255
  Movie.prototype._renderLayers = function () {
2732
- var frameFullyLoaded = true;
2733
3256
  for (var i = 0; i < this.layers.length; i++) {
2734
- if (!Object.prototype.hasOwnProperty.call(this.layers, i))
3257
+ if (!Object.prototype.hasOwnProperty.call(this.layers, i)) {
2735
3258
  continue;
3259
+ }
2736
3260
  var layer = this.layers[i];
2737
3261
  // A layer that has been deleted before layers.length has been updated
2738
3262
  // (see the layers proxy in the constructor).
2739
- if (!layer)
3263
+ if (!layer) {
2740
3264
  continue;
2741
- var reltime = this.currentTime - layer.startTime;
3265
+ }
2742
3266
  // Cancel operation if layer disabled or outside layer time interval
3267
+ var reltime = this.currentTime - layer.startTime;
2743
3268
  if (!val(layer, 'enabled', reltime) ||
2744
3269
  // TODO > or >= ?
2745
3270
  this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
2746
3271
  // Layer is not active.
2747
3272
  // If only rendering this frame, we are not "starting" the layer.
2748
3273
  if (layer.active && !this._renderingFrame) {
2749
- // TODO: make a `deactivate()` method?
2750
3274
  layer.stop();
2751
3275
  layer.active = false;
2752
3276
  }
2753
3277
  continue;
2754
3278
  }
3279
+ // If we are playing (not refreshing), update the layer's progress
3280
+ if (!this._renderingFrame) {
3281
+ layer.progress(reltime);
3282
+ }
2755
3283
  // If only rendering this frame, we are not "starting" the layer
2756
3284
  if (!layer.active && val(layer, 'enabled', reltime) && !this._renderingFrame) {
2757
- // TODO: make an `activate()` method?
2758
3285
  layer.start();
2759
3286
  layer.active = true;
2760
3287
  }
2761
- // if the layer has an input file
2762
- if ('source' in layer)
2763
- frameFullyLoaded = frameFullyLoaded && layer.source.readyState >= 2;
2764
3288
  layer.render();
2765
3289
  // if the layer has visual component
2766
3290
  if (layer instanceof Visual) {
2767
3291
  var canvas = layer.canvas;
2768
- // layer.canvas.width and layer.canvas.height should already be interpolated
2769
- // if the layer has an area (else InvalidStateError from canvas)
2770
- if (canvas.width * canvas.height > 0)
3292
+ if (canvas.width * canvas.height > 0) {
2771
3293
  this.cctx.drawImage(canvas, val(layer, 'x', reltime), val(layer, 'y', reltime), canvas.width, canvas.height);
3294
+ }
2772
3295
  }
2773
3296
  }
2774
- return frameFullyLoaded;
2775
3297
  };
2776
3298
  Movie.prototype._applyEffects = function () {
2777
3299
  for (var i = 0; i < this.effects.length; i++) {
2778
3300
  var effect = this.effects[i];
2779
3301
  // An effect that has been deleted before effects.length has been updated
2780
3302
  // (see the effectsproxy in the constructor).
2781
- if (!effect)
3303
+ if (!effect) {
2782
3304
  continue;
3305
+ }
2783
3306
  effect.apply(this, this.currentTime);
2784
3307
  }
2785
3308
  };
2786
3309
  /**
2787
- * Refreshes the screen (only use this if auto-refresh is disabled)
2788
- * @return - resolves when the frame is loaded
3310
+ * Refreshes the screen
3311
+ *
3312
+ * Only use this if auto-refresh is disabled
3313
+ *
3314
+ * @return - Promise that resolves when the frame is loaded
2789
3315
  */
2790
3316
  Movie.prototype.refresh = function () {
2791
3317
  var _this = this;
3318
+ // Refreshing while playing can interrupt playback
3319
+ if (!this.paused) {
3320
+ throw new Error('Already playing');
3321
+ }
2792
3322
  return new Promise(function (resolve) {
2793
3323
  _this._renderingFrame = true;
2794
3324
  _this._render(false, undefined, resolve);
2795
3325
  });
2796
3326
  };
2797
3327
  /**
2798
- * Convienence method
3328
+ * Convienence method (TODO: remove)
2799
3329
  */
2800
3330
  Movie.prototype._publishToLayers = function (type, event) {
2801
- for (var i = 0; i < this.layers.length; i++)
2802
- if (Object.prototype.hasOwnProperty.call(this.layers, i))
3331
+ for (var i = 0; i < this.layers.length; i++) {
3332
+ if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2803
3333
  publish(this.layers[i], type, event);
3334
+ }
3335
+ }
2804
3336
  };
2805
3337
  Object.defineProperty(Movie.prototype, "rendering", {
2806
3338
  /**
2807
- * If the movie is playing, recording or refreshing
3339
+ * `true` if the movie is playing, recording or refreshing
2808
3340
  */
2809
3341
  get: function () {
2810
3342
  return !this.paused || this._renderingFrame;
@@ -2814,7 +3346,7 @@ var etro = (function () {
2814
3346
  });
2815
3347
  Object.defineProperty(Movie.prototype, "renderingFrame", {
2816
3348
  /**
2817
- * If the movie is refreshing current frame
3349
+ * `true` if the movie is refreshing the current frame
2818
3350
  */
2819
3351
  get: function () {
2820
3352
  return this._renderingFrame;
@@ -2824,17 +3356,19 @@ var etro = (function () {
2824
3356
  });
2825
3357
  Object.defineProperty(Movie.prototype, "recording", {
2826
3358
  /**
2827
- * If the movie is recording
3359
+ * `true` if the movie is recording
2828
3360
  */
2829
3361
  get: function () {
2830
- return !!this._mediaRecorder;
3362
+ return this._recording;
2831
3363
  },
2832
3364
  enumerable: false,
2833
3365
  configurable: true
2834
3366
  });
2835
3367
  Object.defineProperty(Movie.prototype, "duration", {
2836
3368
  /**
2837
- * The combined duration of all layers
3369
+ * The duration of the movie in seconds
3370
+ *
3371
+ * Calculated from the end time of the last layer
2838
3372
  */
2839
3373
  // TODO: dirty flag?
2840
3374
  get: function () {
@@ -2844,16 +3378,16 @@ var etro = (function () {
2844
3378
  configurable: true
2845
3379
  });
2846
3380
  /**
2847
- * Convienence method for <code>layers.push()</code>
3381
+ * Convenience method for `layers.push()`
2848
3382
  * @param layer
2849
- * @return the movie
3383
+ * @return The movie
2850
3384
  */
2851
3385
  Movie.prototype.addLayer = function (layer) {
2852
3386
  this.layers.push(layer);
2853
3387
  return this;
2854
3388
  };
2855
3389
  /**
2856
- * Convienence method for <code>effects.push()</code>
3390
+ * Convenience method for `effects.push()`
2857
3391
  * @param effect
2858
3392
  * @return the movie
2859
3393
  */
@@ -2863,6 +3397,7 @@ var etro = (function () {
2863
3397
  };
2864
3398
  Object.defineProperty(Movie.prototype, "paused", {
2865
3399
  /**
3400
+ * `true` if the movie is paused
2866
3401
  */
2867
3402
  get: function () {
2868
3403
  return this._paused;
@@ -2872,7 +3407,7 @@ var etro = (function () {
2872
3407
  });
2873
3408
  Object.defineProperty(Movie.prototype, "ended", {
2874
3409
  /**
2875
- * If the playback position is at the end of the movie
3410
+ * `true` if the playback position is at the end of the movie
2876
3411
  */
2877
3412
  get: function () {
2878
3413
  return this._ended;
@@ -2880,50 +3415,89 @@ var etro = (function () {
2880
3415
  enumerable: false,
2881
3416
  configurable: true
2882
3417
  });
3418
+ /**
3419
+ * Skips to the provided playback position, updating {@link currentTime}.
3420
+ *
3421
+ * @param time - The new playback position (in seconds)
3422
+ */
3423
+ Movie.prototype.seek = function (time) {
3424
+ this._currentTime = time;
3425
+ // Call `seek` on every layer
3426
+ for (var i = 0; i < this.layers.length; i++) {
3427
+ var layer = this.layers[i];
3428
+ if (layer) {
3429
+ var relativeTime = time - layer.startTime;
3430
+ if (relativeTime >= 0 && relativeTime <= layer.duration) {
3431
+ layer.seek(relativeTime);
3432
+ }
3433
+ else {
3434
+ layer.seek(undefined);
3435
+ }
3436
+ }
3437
+ }
3438
+ // For backwards compatibility, publish a `seek` event
3439
+ publish(this, 'movie.seek', {});
3440
+ };
2883
3441
  Object.defineProperty(Movie.prototype, "currentTime", {
2884
3442
  /**
2885
- * The current playback position
3443
+ * The current playback position in seconds
2886
3444
  */
2887
3445
  get: function () {
2888
3446
  return this._currentTime;
2889
3447
  },
3448
+ /**
3449
+ * Skips to the provided playback position, updating {@link currentTime}.
3450
+ *
3451
+ * @param time - The new playback position (in seconds)
3452
+ *
3453
+ * @deprecated Use `seek` instead
3454
+ */
2890
3455
  set: function (time) {
2891
- this._currentTime = time;
2892
- publish(this, 'movie.seek', {});
2893
- // Render single frame to match new time
2894
- if (this.autoRefresh)
2895
- this.refresh();
3456
+ this.seek(time);
2896
3457
  },
2897
3458
  enumerable: false,
2898
3459
  configurable: true
2899
3460
  });
2900
3461
  /**
2901
- * Sets the current playback position. This is a more powerful version of
2902
- * `set currentTime`.
3462
+ * Skips to the provided playback position, updating {@link currentTime}.
2903
3463
  *
2904
- * @param time - the new cursor's time value in seconds
2905
- * @param [refresh=true] - whether to render a single frame
2906
- * @return resolves when the current frame is rendered if
2907
- * <code>refresh</code> is true, otherwise resolves immediately
3464
+ * @param time - The new time (in seconds)
3465
+ * @param [refresh=true] - Render a single frame?
3466
+ * @return Promise that resolves when the current frame is rendered if
3467
+ * `refresh` is true; otherwise resolves immediately.
2908
3468
  *
3469
+ * @deprecated Call {@link seek} and {@link refresh} separately
2909
3470
  */
2910
- // TODO: Refresh if only auto-refreshing is enabled
3471
+ // TODO: Refresh only if auto-refreshing is enabled
2911
3472
  Movie.prototype.setCurrentTime = function (time, refresh) {
2912
3473
  var _this = this;
2913
3474
  if (refresh === void 0) { refresh = true; }
2914
3475
  return new Promise(function (resolve, reject) {
2915
- _this._currentTime = time;
2916
- publish(_this, 'movie.seek', {});
2917
- if (refresh)
3476
+ _this.seek(time);
3477
+ if (refresh) {
2918
3478
  // Pass promise callbacks to `refresh`
2919
3479
  _this.refresh().then(resolve).catch(reject);
2920
- else
3480
+ }
3481
+ else {
2921
3482
  resolve();
3483
+ }
2922
3484
  });
2923
3485
  };
3486
+ Object.defineProperty(Movie.prototype, "ready", {
3487
+ /**
3488
+ * `true` if the movie is ready for playback
3489
+ */
3490
+ get: function () {
3491
+ var layersReady = this.layers.every(function (layer) { return layer.ready; });
3492
+ var effectsReady = this.effects.every(function (effect) { return effect.ready; });
3493
+ return layersReady && effectsReady;
3494
+ },
3495
+ enumerable: false,
3496
+ configurable: true
3497
+ });
2924
3498
  Object.defineProperty(Movie.prototype, "canvas", {
2925
3499
  /**
2926
- * The rendering canvas
3500
+ * The HTML canvas element used for rendering
2927
3501
  */
2928
3502
  get: function () {
2929
3503
  return this._canvas;
@@ -2933,7 +3507,7 @@ var etro = (function () {
2933
3507
  });
2934
3508
  Object.defineProperty(Movie.prototype, "cctx", {
2935
3509
  /**
2936
- * The rendering canvas's context
3510
+ * The canvas context used for rendering
2937
3511
  */
2938
3512
  get: function () {
2939
3513
  return this._cctx;
@@ -2943,7 +3517,7 @@ var etro = (function () {
2943
3517
  });
2944
3518
  Object.defineProperty(Movie.prototype, "width", {
2945
3519
  /**
2946
- * The width of the rendering canvas
3520
+ * The width of the output canvas
2947
3521
  */
2948
3522
  get: function () {
2949
3523
  return this.canvas.width;
@@ -2956,7 +3530,7 @@ var etro = (function () {
2956
3530
  });
2957
3531
  Object.defineProperty(Movie.prototype, "height", {
2958
3532
  /**
2959
- * The height of the rendering canvas
3533
+ * The height of the output canvas
2960
3534
  */
2961
3535
  get: function () {
2962
3536
  return this.canvas.height;
@@ -2968,12 +3542,18 @@ var etro = (function () {
2968
3542
  configurable: true
2969
3543
  });
2970
3544
  Object.defineProperty(Movie.prototype, "movie", {
3545
+ /**
3546
+ * @return The movie
3547
+ */
2971
3548
  get: function () {
2972
3549
  return this;
2973
3550
  },
2974
3551
  enumerable: false,
2975
3552
  configurable: true
2976
3553
  });
3554
+ /**
3555
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
3556
+ */
2977
3557
  Movie.prototype.getDefaultOptions = function () {
2978
3558
  return {
2979
3559
  canvas: undefined,
@@ -2985,21 +3565,23 @@ var etro = (function () {
2985
3565
  /**
2986
3566
  * @name module:movie#repeat
2987
3567
  */
2988
- repeat: false,
2989
- /**
2990
- * @name module:movie#autoRefresh
2991
- * @desc Whether to refresh when changes are made that would effect the current frame
2992
- */
2993
- autoRefresh: true
3568
+ repeat: false
2994
3569
  };
2995
3570
  };
2996
3571
  return Movie;
2997
3572
  }());
2998
- // id for events (independent of instance, but easy to access when on prototype chain)
3573
+ // Id for events
2999
3574
  Movie.prototype.type = 'movie';
3000
- // TODO: refactor so we don't need to explicitly exclude some of these
3001
- Movie.prototype.publicExcludes = ['canvas', 'cctx', 'actx', 'layers', 'effects'];
3002
- Movie.prototype.propertyFilters = {};
3575
+ Movie.prototype.propertyFilters = {};
3576
+ deprecate('movie.audiodestinationupdate', 'audiodestinationupdate');
3577
+ deprecate('movie.ended', undefined);
3578
+ deprecate('movie.loadeddata', undefined);
3579
+ deprecate('movie.pause', undefined, 'Wait for `play()`, `stream()`, or `record()` to resolve instead.');
3580
+ deprecate('movie.play', undefined, 'Provide an `onStart` callback to `play()`, `stream()`, or `record()` instead.');
3581
+ deprecate('movie.record', undefined, 'Provide an `onStart` callback to `record()` instead.');
3582
+ deprecate('movie.recordended', undefined, 'Wait for `record()` to resolve instead.');
3583
+ deprecate('movie.seek', undefined, 'Override the `seek` method on layers instead.');
3584
+ deprecate('movie.timeupdate', undefined, 'Override the `progress` method on layers instead.');
3003
3585
 
3004
3586
  /*
3005
3587
  * Typedoc can't handle default exports. To let users import default export and
@@ -3025,8 +3607,7 @@ var etro = (function () {
3025
3607
  parseColor: parseColor,
3026
3608
  Font: Font,
3027
3609
  parseFont: parseFont,
3028
- mapPixels: mapPixels,
3029
- watchPublic: watchPublic
3610
+ mapPixels: mapPixels
3030
3611
  });
3031
3612
 
3032
3613
  /**