etro 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/CONTRIBUTING.md +25 -20
  3. package/README.md +12 -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 +1153 -577
  9. package/dist/etro-iife.js +1153 -577
  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 +22 -4
  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,16 @@ 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)
862
- _this.duration = (_this).source.duration - _this.sourceStartTime;
863
- return _this;
1058
+ if (typeof options.source === 'string') {
1059
+ var audio = document.createElement('audio');
1060
+ audio.src = options.source;
1061
+ options.source = audio;
1062
+ }
1063
+ return _super.call(this, options) || this;
864
1064
  }
1065
+ /**
1066
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1067
+ */
865
1068
  Audio.prototype.getDefaultOptions = function () {
866
1069
  return __assign(__assign({}, Object.getPrototypeOf(this).getDefaultOptions()), {
867
1070
  /**
@@ -873,40 +1076,101 @@ var etro = (function () {
873
1076
  return Audio;
874
1077
  }(AudioSourceMixin(Base)));
875
1078
 
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, {
1079
+ var CustomArrayListener = /** @class */ (function () {
1080
+ function CustomArrayListener() {
1081
+ }
1082
+ return CustomArrayListener;
1083
+ }());
1084
+ /**
1085
+ * An array that notifies a listener when items are added or removed.
1086
+ */
1087
+ var CustomArray = /** @class */ (function (_super) {
1088
+ __extends(CustomArray, _super);
1089
+ function CustomArray(target, listener) {
1090
+ var _this = _super.call(this) || this;
1091
+ for (var _i = 0, target_1 = target; _i < target_1.length; _i++) {
1092
+ var item = target_1[_i];
1093
+ listener.onAdd(item);
1094
+ }
1095
+ // Create proxy
1096
+ return new Proxy(target, {
891
1097
  deleteProperty: function (target, property) {
892
1098
  var value = target[property];
893
- value.detach();
894
1099
  delete target[property];
1100
+ listener.onRemove(value);
895
1101
  return true;
896
1102
  },
897
1103
  set: function (target, property, value) {
1104
+ var oldValue = target[property];
1105
+ target[property] = value;
1106
+ // Check if property is a number (index)
898
1107
  if (!isNaN(Number(property))) {
899
- // The property is a number (index)
900
- if (target[property])
901
- target[property].detach();
902
- value.attach(_this);
1108
+ if (oldValue !== undefined) {
1109
+ listener.onRemove(oldValue);
1110
+ }
1111
+ listener.onAdd(value);
903
1112
  }
904
- target[property] = value;
905
1113
  return true;
906
1114
  }
907
1115
  });
1116
+ }
1117
+ return CustomArray;
1118
+ }(Array));
1119
+
1120
+ // eslint-disable-next-line no-use-before-define
1121
+ var VisualEffectsListener = /** @class */ (function (_super) {
1122
+ __extends(VisualEffectsListener, _super);
1123
+ // eslint-disable-next-line no-use-before-define
1124
+ function VisualEffectsListener(layer) {
1125
+ var _this = _super.call(this) || this;
1126
+ _this._layer = layer;
908
1127
  return _this;
909
1128
  }
1129
+ VisualEffectsListener.prototype.onAdd = function (effect) {
1130
+ effect.tryAttach(this._layer);
1131
+ };
1132
+ VisualEffectsListener.prototype.onRemove = function (effect) {
1133
+ effect.tryDetach();
1134
+ };
1135
+ return VisualEffectsListener;
1136
+ }(CustomArrayListener));
1137
+ var VisualEffects = /** @class */ (function (_super) {
1138
+ __extends(VisualEffects, _super);
1139
+ // eslint-disable-next-line no-use-before-define
1140
+ function VisualEffects(target, layer) {
1141
+ return _super.call(this, target, new VisualEffectsListener(layer)) || this;
1142
+ }
1143
+ return VisualEffects;
1144
+ }(CustomArray));
1145
+ /** Any layer that renders to a canvas */
1146
+ var Visual = /** @class */ (function (_super) {
1147
+ __extends(Visual, _super);
1148
+ /**
1149
+ * Creates a visual layer
1150
+ */
1151
+ function Visual(options) {
1152
+ var _this = _super.call(this, options) || this;
1153
+ applyOptions(options, _this);
1154
+ _this.canvas = document.createElement('canvas');
1155
+ _this.cctx = _this.canvas.getContext('2d');
1156
+ _this.effects = new VisualEffects([], _this);
1157
+ return _this;
1158
+ }
1159
+ Visual.prototype.whenReady = function () {
1160
+ return __awaiter(this, void 0, void 0, function () {
1161
+ return __generator(this, function (_a) {
1162
+ switch (_a.label) {
1163
+ case 0: return [4 /*yield*/, _super.prototype.whenReady.call(this)];
1164
+ case 1:
1165
+ _a.sent();
1166
+ return [4 /*yield*/, Promise.all(this.effects.map(function (effect) { return effect.whenReady(); }))];
1167
+ case 2:
1168
+ _a.sent();
1169
+ return [2 /*return*/];
1170
+ }
1171
+ });
1172
+ });
1173
+ };
910
1174
  /**
911
1175
  * Render visual output
912
1176
  */
@@ -914,8 +1178,9 @@ var etro = (function () {
914
1178
  // Prevent empty canvas errors if the width or height is 0
915
1179
  var width = val(this, 'width', this.currentTime);
916
1180
  var height = val(this, 'height', this.currentTime);
917
- if (width === 0 || height === 0)
1181
+ if (width === 0 || height === 0) {
918
1182
  return;
1183
+ }
919
1184
  this.beginRender();
920
1185
  this.doRender();
921
1186
  this.endRender();
@@ -946,20 +1211,22 @@ var etro = (function () {
946
1211
  Visual.prototype.endRender = function () {
947
1212
  var w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime);
948
1213
  var h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime);
949
- if (w * h > 0)
1214
+ if (w * h > 0) {
950
1215
  this._applyEffects();
1216
+ }
951
1217
  // else InvalidStateError for drawing zero-area image in some effects, right?
952
1218
  };
953
1219
  Visual.prototype._applyEffects = function () {
954
1220
  for (var i = 0; i < this.effects.length; i++) {
955
1221
  var effect = this.effects[i];
956
- if (effect && effect.enabled)
1222
+ if (effect && effect.enabled) {
957
1223
  // Pass relative time
958
1224
  effect.apply(this, this.movie.currentTime - this.startTime);
1225
+ }
959
1226
  }
960
1227
  };
961
1228
  /**
962
- * Convienence method for <code>effects.push()</code>
1229
+ * Convenience method for <code>effects.push()</code>
963
1230
  * @param effect
964
1231
  * @return the layer (for chaining)
965
1232
  */
@@ -967,6 +1234,18 @@ var etro = (function () {
967
1234
  this.effects.push(effect);
968
1235
  return this;
969
1236
  };
1237
+ Object.defineProperty(Visual.prototype, "ready", {
1238
+ get: function () {
1239
+ // Typescript doesn't support `super.ready` when targeting es5
1240
+ var superReady = Object.getOwnPropertyDescriptor(Base.prototype, 'ready').get.call(this);
1241
+ return superReady && this.effects.every(function (effect) { return effect.ready; });
1242
+ },
1243
+ enumerable: false,
1244
+ configurable: true
1245
+ });
1246
+ /**
1247
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1248
+ */
970
1249
  Visual.prototype.getDefaultOptions = function () {
971
1250
  return __assign(__assign({}, Base.prototype.getDefaultOptions()), {
972
1251
  /**
@@ -1025,17 +1304,60 @@ var etro = (function () {
1025
1304
  var MixedVisualSource = /** @class */ (function (_super) {
1026
1305
  __extends(MixedVisualSource, _super);
1027
1306
  function MixedVisualSource(options) {
1028
- var _this = _super.call(this, options) || this;
1307
+ var _this = this;
1308
+ if (!options.source) {
1309
+ throw new Error('Property "source" is required in options');
1310
+ }
1311
+ _this = _super.call(this, options) || this;
1029
1312
  applyOptions(options, _this);
1030
1313
  return _this;
1031
1314
  }
1315
+ MixedVisualSource.prototype.whenReady = function () {
1316
+ return __awaiter(this, void 0, void 0, function () {
1317
+ var _this = this;
1318
+ return __generator(this, function (_a) {
1319
+ switch (_a.label) {
1320
+ case 0: return [4 /*yield*/, _super.prototype.whenReady.call(this)];
1321
+ case 1:
1322
+ _a.sent();
1323
+ return [4 /*yield*/, new Promise(function (resolve) {
1324
+ if (_this.source instanceof HTMLImageElement) {
1325
+ // The source is an image; wait for it to load
1326
+ if (_this.source.complete) {
1327
+ resolve();
1328
+ }
1329
+ else {
1330
+ _this.source.addEventListener('load', function () {
1331
+ resolve();
1332
+ });
1333
+ }
1334
+ }
1335
+ else {
1336
+ // The source is a video; wait for the first frame to load
1337
+ if (_this.source.readyState === 4) {
1338
+ resolve();
1339
+ }
1340
+ else {
1341
+ _this.source.addEventListener('canplaythrough', function () {
1342
+ resolve();
1343
+ });
1344
+ }
1345
+ }
1346
+ })];
1347
+ case 2:
1348
+ _a.sent();
1349
+ return [2 /*return*/];
1350
+ }
1351
+ });
1352
+ });
1353
+ };
1032
1354
  MixedVisualSource.prototype.doRender = function () {
1033
1355
  // Clear/fill background
1034
1356
  _super.prototype.doRender.call(this);
1035
1357
  /*
1036
1358
  * Source dimensions crop the image. Dest dimensions set the size that
1037
1359
  * the image will be rendered at *on the layer*. Note that this is
1038
- * different than the layer dimensions (`this.width` and `this.height`).
1360
+ * different from the layer dimensions (`this.width` and `this.height`).
1039
1361
  * The main reason this distinction exists is so that an image layer can
1040
1362
  * be rotated without being cropped (see iss #46).
1041
1363
  */
@@ -1043,6 +1365,21 @@ var etro = (function () {
1043
1365
  // `destX` and `destY` are relative to the layer
1044
1366
  val(this, 'destX', this.currentTime), val(this, 'destY', this.currentTime), val(this, 'destWidth', this.currentTime), val(this, 'destHeight', this.currentTime));
1045
1367
  };
1368
+ Object.defineProperty(MixedVisualSource.prototype, "ready", {
1369
+ get: function () {
1370
+ // Typescript doesn't support `super.ready` when targeting es5
1371
+ var superReady = Object.getOwnPropertyDescriptor(superclass.prototype, 'ready').get.call(this);
1372
+ var sourceReady = this.source instanceof HTMLImageElement
1373
+ ? this.source.complete
1374
+ : this.source.readyState === 4;
1375
+ return superReady && sourceReady;
1376
+ },
1377
+ enumerable: false,
1378
+ configurable: true
1379
+ });
1380
+ /**
1381
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1382
+ */
1046
1383
  MixedVisualSource.prototype.getDefaultOptions = function () {
1047
1384
  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
1385
  };
@@ -1091,10 +1428,19 @@ var etro = (function () {
1091
1428
  return MixedVisualSource;
1092
1429
  }
1093
1430
 
1431
+ /**
1432
+ * Layer for an HTML image element
1433
+ * @extends VisualSource
1434
+ */
1094
1435
  var Image = /** @class */ (function (_super) {
1095
1436
  __extends(Image, _super);
1096
- function Image() {
1097
- return _super !== null && _super.apply(this, arguments) || this;
1437
+ function Image(options) {
1438
+ if (typeof (options.source) === 'string') {
1439
+ var img = document.createElement('img');
1440
+ img.src = options.source;
1441
+ options.source = img;
1442
+ }
1443
+ return _super.call(this, options) || this;
1098
1444
  }
1099
1445
  return Image;
1100
1446
  }(VisualSourceMixin(Visual)));
@@ -1108,9 +1454,12 @@ var etro = (function () {
1108
1454
  // TODO: is textX necessary? it seems inconsistent, because you can't define
1109
1455
  // width/height directly for a text layer
1110
1456
  function Text(options) {
1111
- var _this =
1457
+ var _this = this;
1458
+ if (!options.text) {
1459
+ throw new Error('Property "text" is required in TextOptions');
1460
+ }
1112
1461
  // Default to no (transparent) background
1113
- _super.call(this, __assign({ background: null }, options)) || this;
1462
+ _this = _super.call(this, __assign({ background: null }, options)) || this;
1114
1463
  applyOptions(options, _this);
1115
1464
  return _this;
1116
1465
  // this._prevText = undefined;
@@ -1157,22 +1506,33 @@ var etro = (function () {
1157
1506
  document.body.removeChild(s);
1158
1507
  return metrics;
1159
1508
  } */
1509
+ /**
1510
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1511
+ */
1160
1512
  Text.prototype.getDefaultOptions = function () {
1161
1513
  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
1514
  };
1163
1515
  return Text;
1164
1516
  }(Visual));
1165
1517
 
1166
- // Use mixins instead of `extend`ing two classes (which isn't supported by
1167
- // JavaScript).
1168
1518
  /**
1519
+ * Layer for an HTML video element
1169
1520
  * @extends AudioSource
1170
1521
  * @extends VisualSource
1171
1522
  */
1172
1523
  var Video = /** @class */ (function (_super) {
1173
1524
  __extends(Video, _super);
1174
- function Video() {
1175
- return _super !== null && _super.apply(this, arguments) || this;
1525
+ function Video(options) {
1526
+ var _a;
1527
+ if (typeof (options.source) === 'string') {
1528
+ var video = document.createElement('video');
1529
+ video.src = options.source;
1530
+ options.source = video;
1531
+ }
1532
+ return _super.call(this, __assign(__assign({}, options), {
1533
+ // Set a default duration so that the super constructor doesn't throw an
1534
+ // error
1535
+ duration: (_a = options.duration) !== null && _a !== void 0 ? _a : 0 })) || this;
1176
1536
  }
1177
1537
  return Video;
1178
1538
  }(AudioSourceMixin(VisualSourceMixin(Visual))));
@@ -1198,46 +1558,48 @@ var etro = (function () {
1198
1558
  */
1199
1559
  var Base$1 = /** @class */ (function () {
1200
1560
  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;
1561
+ this.enabled = true;
1562
+ this._occurrenceCount = 0;
1563
+ this._target = null;
1213
1564
  }
1565
+ /**
1566
+ * Wait until this effect is ready to be applied
1567
+ */
1568
+ Base.prototype.whenReady = function () {
1569
+ return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
1570
+ return [2 /*return*/];
1571
+ }); });
1572
+ }; // eslint-disable-line @typescript-eslint/no-empty-function
1214
1573
  /**
1215
1574
  * Attaches this effect to `target` if not already attached.
1216
1575
  * @ignore
1217
1576
  */
1218
1577
  Base.prototype.tryAttach = function (target) {
1219
- if (this._occurrenceCount === 0)
1578
+ if (this._occurrenceCount === 0) {
1220
1579
  this.attach(target);
1580
+ }
1221
1581
  this._occurrenceCount++;
1222
1582
  };
1223
1583
  Base.prototype.attach = function (movie) {
1224
1584
  this._target = movie;
1225
1585
  };
1226
1586
  /**
1227
- * Dettaches this effect from its target if the number of times `tryDetach`
1587
+ * Detaches this effect from its target if the number of times `tryDetach`
1228
1588
  * has been called (including this call) equals the number of times
1229
1589
  * `tryAttach` has been called.
1230
1590
  *
1231
1591
  * @ignore
1232
1592
  */
1233
1593
  Base.prototype.tryDetach = function () {
1234
- if (this._target === null)
1594
+ if (this._target === null) {
1235
1595
  throw new Error('No movie to detach from');
1596
+ }
1236
1597
  this._occurrenceCount--;
1237
1598
  // If this effect occurs in another place in the containing array, do not
1238
1599
  // unset _target. (For calling `unshift` on the `layers` proxy)
1239
- if (this._occurrenceCount === 0)
1600
+ if (this._occurrenceCount === 0) {
1240
1601
  this.detach();
1602
+ }
1241
1603
  };
1242
1604
  Base.prototype.detach = function () {
1243
1605
  this._target = null;
@@ -1262,6 +1624,14 @@ var etro = (function () {
1262
1624
  enumerable: false,
1263
1625
  configurable: true
1264
1626
  });
1627
+ Object.defineProperty(Base.prototype, "ready", {
1628
+ /** `true` if this effect is ready to be applied */
1629
+ get: function () {
1630
+ return true;
1631
+ },
1632
+ enumerable: false,
1633
+ configurable: true
1634
+ });
1265
1635
  Object.defineProperty(Base.prototype, "parent", {
1266
1636
  get: function () {
1267
1637
  return this._target;
@@ -1276,6 +1646,9 @@ var etro = (function () {
1276
1646
  enumerable: false,
1277
1647
  configurable: true
1278
1648
  });
1649
+ /**
1650
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1651
+ */
1279
1652
  Base.prototype.getDefaultOptions = function () {
1280
1653
  return {};
1281
1654
  };
@@ -1343,16 +1716,18 @@ var etro = (function () {
1343
1716
  Shader.prototype._initGl = function () {
1344
1717
  this._canvas = document.createElement('canvas');
1345
1718
  var gl = this._canvas.getContext('webgl');
1346
- if (gl === null)
1719
+ if (gl === null) {
1347
1720
  throw new Error('Unable to initialize WebGL. Your browser or machine may not support it.');
1721
+ }
1348
1722
  this._gl = gl;
1349
1723
  return gl;
1350
1724
  };
1351
1725
  Shader.prototype._initTextures = function (userUniforms, userTextures, sourceTextureOptions) {
1352
1726
  var gl = this._gl;
1353
1727
  var maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
1354
- if (userTextures.length > maxTextures)
1728
+ if (userTextures.length > maxTextures) {
1355
1729
  console.warn('Too many textures!');
1730
+ }
1356
1731
  this._userTextures = {};
1357
1732
  for (var name_1 in userTextures) {
1358
1733
  var userOptions = userTextures[name_1];
@@ -1365,8 +1740,9 @@ var etro = (function () {
1365
1740
  * textures, without having to define multiple properties in the effect
1366
1741
  * object.
1367
1742
  */
1368
- if (userUniforms[name_1])
1743
+ if (userUniforms[name_1]) {
1369
1744
  throw new Error("Texture - uniform naming conflict: ".concat(name_1, "!"));
1745
+ }
1370
1746
  // Add this as a "user uniform".
1371
1747
  userUniforms[name_1] = '1i'; // texture pointer
1372
1748
  }
@@ -1401,18 +1777,6 @@ var etro = (function () {
1401
1777
  this._uniformLocations[unprefixed] = gl.getUniformLocation(this._program, prefixed);
1402
1778
  }
1403
1779
  };
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
1780
  Shader.prototype.apply = function (target, reltime) {
1417
1781
  this._checkDimensions(target);
1418
1782
  this._refreshGl();
@@ -1499,16 +1863,22 @@ var etro = (function () {
1499
1863
  i++;
1500
1864
  }
1501
1865
  };
1866
+ /**
1867
+ * Set the shader's uniforms.
1868
+ * @param target The movie or layer to apply the shader to.
1869
+ * @param reltime The relative time of the movie or layer.
1870
+ */
1502
1871
  Shader.prototype._prepareUniforms = function (target, reltime) {
1503
1872
  var gl = this._gl;
1504
- // Set the shader uniforms.
1505
1873
  // Tell the shader we bound the texture to texture unit 0.
1506
1874
  // All base (Shader class) uniforms are optional.
1507
- if (this._uniformLocations.source)
1875
+ if (this._uniformLocations.source) {
1508
1876
  gl.uniform1i(this._uniformLocations.source, 0);
1877
+ }
1509
1878
  // All base (Shader class) uniforms are optional.
1510
- if (this._uniformLocations.size)
1879
+ if (this._uniformLocations.size) {
1511
1880
  gl.uniform2iv(this._uniformLocations.size, [target.canvas.width, target.canvas.height]);
1881
+ }
1512
1882
  for (var unprefixed in this._userUniforms) {
1513
1883
  var options = this._userUniforms[unprefixed];
1514
1884
  var value = val(this, unprefixed, reltime);
@@ -1532,11 +1902,13 @@ var etro = (function () {
1532
1902
  /**
1533
1903
  * Converts a value of a standard type for javascript to a standard type for
1534
1904
  * GLSL
1905
+ *
1535
1906
  * @param value - the raw value to prepare
1536
1907
  * @param outputType - the WebGL type of |value|; example:
1537
1908
  * <code>1f</code> for a float
1538
1909
  * @param reltime - current time, relative to the target
1539
- * @param [options] - Optional config
1910
+ * @param [options]
1911
+ * @returns the prepared value
1540
1912
  */
1541
1913
  Shader.prototype._prepareValue = function (value, outputType, reltime, options) {
1542
1914
  if (options === void 0) { options = {}; }
@@ -1559,35 +1931,40 @@ var etro = (function () {
1559
1931
  var i = 0;
1560
1932
  for (var name_4 in this._userTextures) {
1561
1933
  var testValue = val(this, name_4, reltime);
1562
- if (value === testValue)
1934
+ if (value === testValue) {
1563
1935
  value = Shader.INTERNAL_TEXTURE_UNITS + i; // after the internal texture units
1936
+ }
1564
1937
  i++;
1565
1938
  }
1566
1939
  }
1567
1940
  if (outputType === '3fv') {
1568
1941
  // allow 4-component vectors; TODO: why?
1569
- if (Array.isArray(value) && (value.length === 3 || value.length === 4))
1942
+ if (Array.isArray(value) && (value.length === 3 || value.length === 4)) {
1570
1943
  return value;
1944
+ }
1571
1945
  // kind of loose so this can be changed if needed
1572
- if (typeof value === 'object')
1946
+ if (typeof value === 'object') {
1573
1947
  return [
1574
1948
  value.r !== undefined ? value.r : def,
1575
1949
  value.g !== undefined ? value.g : def,
1576
1950
  value.b !== undefined ? value.b : def
1577
1951
  ];
1952
+ }
1578
1953
  throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1579
1954
  }
1580
1955
  if (outputType === '4fv') {
1581
- if (Array.isArray(value) && value.length === 4)
1956
+ if (Array.isArray(value) && value.length === 4) {
1582
1957
  return value;
1958
+ }
1583
1959
  // kind of loose so this can be changed if needed
1584
- if (typeof value === 'object')
1960
+ if (typeof value === 'object') {
1585
1961
  return [
1586
1962
  value.r !== undefined ? value.r : def,
1587
1963
  value.g !== undefined ? value.g : def,
1588
1964
  value.b !== undefined ? value.b : def,
1589
1965
  value.a !== undefined ? value.a : def
1590
1966
  ];
1967
+ }
1591
1968
  throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1592
1969
  }
1593
1970
  return value;
@@ -1681,8 +2058,9 @@ var etro = (function () {
1681
2058
  else {
1682
2059
  // No, it's not a power of 2. Turn off mips and set
1683
2060
  // wrapping to clamp to edge
1684
- if (wrapS !== gl.CLAMP_TO_EDGE || wrapT !== gl.CLAMP_TO_EDGE)
2061
+ if (wrapS !== gl.CLAMP_TO_EDGE || wrapT !== gl.CLAMP_TO_EDGE) {
1685
2062
  console.warn('Wrap mode is not CLAMP_TO_EDGE for a non-power-of-two texture. Defaulting to CLAMP_TO_EDGE');
2063
+ }
1686
2064
  gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1687
2065
  gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1688
2066
  }
@@ -1734,7 +2112,6 @@ var etro = (function () {
1734
2112
  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
2113
  return Shader;
1736
2114
  }(Visual$1));
1737
- // Shader.prototype.getpublicExcludes = () =>
1738
2115
  var isPowerOf2 = function (value) { return (value && (value - 1)) === 0; };
1739
2116
 
1740
2117
  /**
@@ -1909,6 +2286,35 @@ var etro = (function () {
1909
2286
  return EllipticalMask;
1910
2287
  }(Visual$1));
1911
2288
 
2289
+ var StackEffectsListener = /** @class */ (function (_super) {
2290
+ __extends(StackEffectsListener, _super);
2291
+ function StackEffectsListener(stack) {
2292
+ var _this = _super.call(this) || this;
2293
+ _this._stack = stack;
2294
+ return _this;
2295
+ }
2296
+ StackEffectsListener.prototype.onAdd = function (effect) {
2297
+ if (!this._stack.parent) {
2298
+ return;
2299
+ }
2300
+ effect.tryAttach(this._stack.parent);
2301
+ };
2302
+ StackEffectsListener.prototype.onRemove = function (effect) {
2303
+ if (!this._stack.parent) {
2304
+ return;
2305
+ }
2306
+ effect.tryDetach();
2307
+ };
2308
+ return StackEffectsListener;
2309
+ }(CustomArrayListener));
2310
+ var StackEffects = /** @class */ (function (_super) {
2311
+ __extends(StackEffects, _super);
2312
+ // eslint-disable-next-line no-use-before-define
2313
+ function StackEffects(target, stack) {
2314
+ return _super.call(this, target, new StackEffectsListener(stack)) || this;
2315
+ }
2316
+ return StackEffects;
2317
+ }(CustomArray));
1912
2318
  /**
1913
2319
  * A sequence of effects to apply, treated as one effect. This can be useful
1914
2320
  * for defining reused effect sequences as one effect.
@@ -1917,48 +2323,28 @@ var etro = (function () {
1917
2323
  __extends(Stack, _super);
1918
2324
  function Stack(options) {
1919
2325
  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
- });
2326
+ _this.effects = new StackEffects(options.effects, _this);
1940
2327
  options.effects.forEach(function (effect) { return _this.effects.push(effect); });
1941
2328
  return _this;
1942
- // TODO: Propogate 'change' events from children up
1943
2329
  }
1944
2330
  Stack.prototype.attach = function (movie) {
1945
2331
  _super.prototype.attach.call(this, movie);
1946
2332
  this.effects.filter(function (effect) { return !!effect; }).forEach(function (effect) {
1947
- effect.detach();
1948
- effect.attach(movie);
2333
+ effect.tryAttach(movie);
1949
2334
  });
1950
2335
  };
1951
2336
  Stack.prototype.detach = function () {
1952
2337
  _super.prototype.detach.call(this);
1953
2338
  this.effects.filter(function (effect) { return !!effect; }).forEach(function (effect) {
1954
- effect.detach();
2339
+ effect.tryDetach();
1955
2340
  });
1956
2341
  };
1957
2342
  Stack.prototype.apply = function (target, reltime) {
1958
2343
  for (var i = 0; i < this.effects.length; i++) {
1959
2344
  var effect = this.effects[i];
1960
- if (!effect)
2345
+ if (!effect) {
1961
2346
  continue;
2347
+ }
1962
2348
  effect.apply(target, reltime);
1963
2349
  }
1964
2350
  };
@@ -1977,7 +2363,7 @@ var etro = (function () {
1977
2363
  * Applies a Gaussian blur
1978
2364
  */
1979
2365
  // TODO: Improve performance
1980
- // TODO: Make sure this is truly gaussian even though it doens't require a
2366
+ // TODO: Make sure this is truly gaussian even though it doesn't require a
1981
2367
  // standard deviation
1982
2368
  var GaussianBlur = /** @class */ (function (_super) {
1983
2369
  __extends(GaussianBlur, _super);
@@ -2022,9 +2408,10 @@ var etro = (function () {
2022
2408
  }
2023
2409
  GaussianBlurComponent.prototype.apply = function (target, reltime) {
2024
2410
  var radiusVal = val(this, 'radius', reltime);
2025
- if (radiusVal !== this._radiusCache)
2411
+ if (radiusVal !== this._radiusCache) {
2026
2412
  // Regenerate gaussian distribution canvas.
2027
2413
  this.shape = GaussianBlurComponent._render1DKernel(GaussianBlurComponent._gen1DKernel(radiusVal));
2414
+ }
2028
2415
  this._radiusCache = radiusVal;
2029
2416
  _super.prototype.apply.call(this, target, reltime);
2030
2417
  };
@@ -2057,23 +2444,27 @@ var etro = (function () {
2057
2444
  var pascal = GaussianBlurComponent._genPascalRow(2 * radius + 1);
2058
2445
  // don't use `reduce` and `map` (overhead?)
2059
2446
  var sum = 0;
2060
- for (var i = 0; i < pascal.length; i++)
2447
+ for (var i = 0; i < pascal.length; i++) {
2061
2448
  sum += pascal[i];
2062
- for (var i = 0; i < pascal.length; i++)
2449
+ }
2450
+ for (var i = 0; i < pascal.length; i++) {
2063
2451
  pascal[i] /= sum;
2452
+ }
2064
2453
  return pascal;
2065
2454
  };
2066
2455
  GaussianBlurComponent._genPascalRow = function (index) {
2067
- if (index < 0)
2456
+ if (index < 0) {
2068
2457
  throw new Error("Invalid index ".concat(index));
2458
+ }
2069
2459
  var currRow = [1];
2070
2460
  for (var i = 1; i < index; i++) {
2071
2461
  var nextRow = [];
2072
2462
  nextRow.length = currRow.length + 1;
2073
2463
  // edges are always 1's
2074
2464
  nextRow[0] = nextRow[nextRow.length - 1] = 1;
2075
- for (var j = 1; j < nextRow.length - 1; j++)
2465
+ for (var j = 1; j < nextRow.length - 1; j++) {
2076
2466
  nextRow[j] = currRow[j - 1] + currRow[j];
2467
+ }
2077
2468
  currRow = nextRow;
2078
2469
  }
2079
2470
  return currRow;
@@ -2144,15 +2535,14 @@ var etro = (function () {
2144
2535
  pixelSize: '1i'
2145
2536
  }
2146
2537
  }) || this;
2147
- /**
2148
- */
2149
2538
  _this.pixelSize = options.pixelSize || 1;
2150
2539
  return _this;
2151
2540
  }
2152
2541
  Pixelate.prototype.apply = function (target, reltime) {
2153
2542
  var ps = val(this, 'pixelSize', reltime);
2154
- if (ps % 1 !== 0 || ps < 0)
2543
+ if (ps % 1 !== 0 || ps < 0) {
2155
2544
  throw new Error('Pixel size must be a nonnegative integer');
2545
+ }
2156
2546
  _super.prototype.apply.call(this, target, reltime);
2157
2547
  };
2158
2548
  return Pixelate;
@@ -2181,10 +2571,12 @@ var etro = (function () {
2181
2571
  return _this;
2182
2572
  }
2183
2573
  Transform.prototype.apply = function (target, reltime) {
2184
- if (target.canvas.width !== this._tmpCanvas.width)
2574
+ if (target.canvas.width !== this._tmpCanvas.width) {
2185
2575
  this._tmpCanvas.width = target.canvas.width;
2186
- if (target.canvas.height !== this._tmpCanvas.height)
2576
+ }
2577
+ if (target.canvas.height !== this._tmpCanvas.height) {
2187
2578
  this._tmpCanvas.height = target.canvas.height;
2579
+ }
2188
2580
  // Use data, since that's the underlying storage
2189
2581
  this._tmpMatrix.data = val(this, 'matrix.data', reltime);
2190
2582
  this._tmpCtx.setTransform(this._tmpMatrix.a, this._tmpMatrix.b, this._tmpMatrix.c, this._tmpMatrix.d, this._tmpMatrix.e, this._tmpMatrix.f);
@@ -2210,8 +2602,9 @@ var etro = (function () {
2210
2602
  ];
2211
2603
  }
2212
2604
  Matrix.prototype.identity = function () {
2213
- for (var i = 0; i < this.data.length; i++)
2605
+ for (var i = 0; i < this.data.length; i++) {
2214
2606
  this.data[i] = Matrix.IDENTITY.data[i];
2607
+ }
2215
2608
  return this;
2216
2609
  };
2217
2610
  /**
@@ -2220,8 +2613,9 @@ var etro = (function () {
2220
2613
  * @param [val]
2221
2614
  */
2222
2615
  Matrix.prototype.cell = function (x, y, val) {
2223
- if (val !== undefined)
2616
+ if (val !== undefined) {
2224
2617
  this.data[3 * y + x] = val;
2618
+ }
2225
2619
  return this.data[3 * y + x];
2226
2620
  };
2227
2621
  Object.defineProperty(Matrix.prototype, "a", {
@@ -2273,16 +2667,19 @@ var etro = (function () {
2273
2667
  */
2274
2668
  Matrix.prototype.multiply = function (other) {
2275
2669
  // copy to temporary matrix to avoid modifying `this` while reading from it
2276
- for (var x = 0; x < 3; x++)
2670
+ for (var x = 0; x < 3; x++) {
2277
2671
  for (var y = 0; y < 3; y++) {
2278
2672
  var sum = 0;
2279
- for (var i = 0; i < 3; i++)
2673
+ for (var i = 0; i < 3; i++) {
2280
2674
  sum += this.cell(x, i) * other.cell(i, y);
2675
+ }
2281
2676
  Matrix._TMP_MATRIX.cell(x, y, sum);
2282
2677
  }
2678
+ }
2283
2679
  // copy data from TMP_MATRIX to this
2284
- for (var i = 0; i < Matrix._TMP_MATRIX.data.length; i++)
2680
+ for (var i = 0; i < Matrix._TMP_MATRIX.data.length; i++) {
2285
2681
  this.data[i] = Matrix._TMP_MATRIX.data[i];
2682
+ }
2286
2683
  return this;
2287
2684
  };
2288
2685
  /**
@@ -2356,6 +2753,52 @@ var etro = (function () {
2356
2753
  Visual: Visual$1
2357
2754
  });
2358
2755
 
2756
+ var MovieEffectsListener = /** @class */ (function (_super) {
2757
+ __extends(MovieEffectsListener, _super);
2758
+ function MovieEffectsListener(movie) {
2759
+ var _this = _super.call(this) || this;
2760
+ _this._movie = movie;
2761
+ return _this;
2762
+ }
2763
+ MovieEffectsListener.prototype.onAdd = function (effect) {
2764
+ effect.tryAttach(this._movie);
2765
+ };
2766
+ MovieEffectsListener.prototype.onRemove = function (effect) {
2767
+ effect.tryDetach();
2768
+ };
2769
+ return MovieEffectsListener;
2770
+ }(CustomArrayListener));
2771
+ var MovieEffects = /** @class */ (function (_super) {
2772
+ __extends(MovieEffects, _super);
2773
+ function MovieEffects(target, movie) {
2774
+ return _super.call(this, target, new MovieEffectsListener(movie)) || this;
2775
+ }
2776
+ return MovieEffects;
2777
+ }(CustomArray));
2778
+
2779
+ var MovieLayersListener = /** @class */ (function (_super) {
2780
+ __extends(MovieLayersListener, _super);
2781
+ function MovieLayersListener(movie) {
2782
+ var _this = _super.call(this) || this;
2783
+ _this._movie = movie;
2784
+ return _this;
2785
+ }
2786
+ MovieLayersListener.prototype.onAdd = function (layer) {
2787
+ layer.tryAttach(this._movie);
2788
+ };
2789
+ MovieLayersListener.prototype.onRemove = function (layer) {
2790
+ layer.tryDetach();
2791
+ };
2792
+ return MovieLayersListener;
2793
+ }(CustomArrayListener));
2794
+ var MovieLayers = /** @class */ (function (_super) {
2795
+ __extends(MovieLayers, _super);
2796
+ function MovieLayers(target, movie) {
2797
+ return _super.call(this, target, new MovieLayersListener(movie)) || this;
2798
+ }
2799
+ return MovieLayers;
2800
+ }(CustomArray));
2801
+
2359
2802
  /**
2360
2803
  * @module movie
2361
2804
  */
@@ -2369,15 +2812,13 @@ var etro = (function () {
2369
2812
  *
2370
2813
  * Implements a pub/sub system.
2371
2814
  */
2372
- // TODO: Make record option to make recording video output to the user while
2373
- // it's recording
2374
2815
  // TODO: rename renderingFrame -> refreshing
2375
2816
  var Movie = /** @class */ (function () {
2376
2817
  /**
2377
2818
  * Creates a new movie.
2378
2819
  */
2379
2820
  function Movie(options) {
2380
- // TODO: move into multiple methods!
2821
+ this._recording = false;
2381
2822
  // Set actx option manually, because it's readonly.
2382
2823
  this.actx = options.actx ||
2383
2824
  options.audioContext ||
@@ -2385,236 +2826,294 @@ var etro = (function () {
2385
2826
  // eslint-disable-next-line new-cap
2386
2827
  new window.webkitAudioContext();
2387
2828
  delete options.actx;
2388
- // Proxy that will be returned by constructor
2389
- var newThis = watchPublic(this);
2829
+ // Check if required file canvas is provided
2830
+ if (!options.canvas) {
2831
+ throw new Error('Required option "canvas" not provided to Movie');
2832
+ }
2390
2833
  // Set canvas option manually, because it's readonly.
2391
- this._canvas = options.canvas;
2834
+ this._canvas = this._visibleCanvas = options.canvas;
2392
2835
  delete options.canvas;
2393
- // Don't send updates when initializing, so use this instead of newThis:
2394
2836
  this._cctx = this.canvas.getContext('2d'); // TODO: make private?
2837
+ // Set options on the movie
2395
2838
  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
- });
2839
+ this.effects = new MovieEffects([], this);
2840
+ this.layers = new MovieLayers([], this);
2468
2841
  this._paused = true;
2469
2842
  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.
2843
+ // This lock prevents multiple refresh loops at the same time (see
2844
+ // `render`). It's only valid while rendering.
2472
2845
  this._renderingFrame = false;
2473
2846
  this.currentTime = 0;
2474
- // For recording
2475
- this._mediaRecorder = null;
2476
- // -1 works well in inequalities
2477
- // The last time `play` was called
2847
+ // The last time `play` was called, -1 works well in comparisons
2478
2848
  this._lastPlayed = -1;
2479
- // What was `currentTime` when `play` was called
2849
+ // What `currentTime` was when `play` was called
2480
2850
  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
2851
  }
2852
+ Movie.prototype._whenReady = function () {
2853
+ return __awaiter(this, void 0, void 0, function () {
2854
+ return __generator(this, function (_a) {
2855
+ switch (_a.label) {
2856
+ case 0: return [4 /*yield*/, Promise.all([
2857
+ Promise.all(this.layers.map(function (layer) { return layer.whenReady(); })),
2858
+ Promise.all(this.effects.map(function (effect) { return effect.whenReady(); }))
2859
+ ])];
2860
+ case 1:
2861
+ _a.sent();
2862
+ return [2 /*return*/];
2863
+ }
2864
+ });
2865
+ });
2866
+ };
2499
2867
  /**
2500
2868
  * Plays the movie
2501
- * @return fulfilled when the movie is done playing, never fails
2869
+ *
2870
+ * @param [options]
2871
+ * @param [options.onStart] Called when the movie starts playing
2872
+ *
2873
+ * @return Fulfilled when the movie is done playing, never fails
2502
2874
  */
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', {});
2875
+ Movie.prototype.play = function (options) {
2876
+ var _a;
2877
+ if (options === void 0) { options = {}; }
2878
+ return __awaiter(this, void 0, void 0, function () {
2879
+ var _this = this;
2880
+ return __generator(this, function (_b) {
2881
+ switch (_b.label) {
2882
+ case 0: return [4 /*yield*/, this._whenReady()];
2883
+ case 1:
2884
+ _b.sent();
2885
+ if (!this.paused) {
2886
+ throw new Error('Already playing');
2887
+ }
2888
+ this._paused = this._ended = false;
2889
+ this._lastPlayed = performance.now();
2890
+ this._lastPlayedOffset = this.currentTime;
2891
+ (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options);
2892
+ // For backwards compatibility
2893
+ publish(this, 'movie.play', {});
2894
+ // Repeatedly render frames until the movie ends
2895
+ return [4 /*yield*/, new Promise(function (resolve) {
2896
+ if (!_this.renderingFrame) {
2897
+ // Not rendering (and not playing), so play.
2898
+ _this._render(true, undefined, resolve);
2899
+ }
2900
+ // Stop rendering frame if currently doing so, because playing has higher
2901
+ // priority. This will affect the next _render call.
2902
+ _this._renderingFrame = false;
2903
+ })];
2904
+ case 2:
2905
+ // Repeatedly render frames until the movie ends
2906
+ _b.sent();
2907
+ return [2 /*return*/];
2908
+ }
2909
+ });
2910
+ });
2911
+ };
2912
+ /**
2913
+ * Updates the rendering canvas and audio destination to the visible canvas
2914
+ * and the audio context destination.
2915
+ */
2916
+ Movie.prototype._show = function () {
2917
+ this._canvas = this._visibleCanvas;
2918
+ this._cctx = this.canvas.getContext('2d');
2919
+ publish(this, 'audiodestinationupdate', { movie: this, destination: this.actx.destination });
2920
+ };
2921
+ /**
2922
+ * Streams the movie to a MediaStream
2923
+ *
2924
+ * @param options Options for the stream
2925
+ * @param options.frameRate The frame rate of the stream's video
2926
+ * @param options.duration The duration of the stream in seconds
2927
+ * @param options.video Whether to stream video. Defaults to true.
2928
+ * @param options.audio Whether to stream audio. Defaults to true.
2929
+ * @param options.onStart Called when the stream is started
2930
+ * @return Fulfilled when the stream is done, never fails
2931
+ */
2932
+ Movie.prototype.stream = function (options) {
2933
+ return __awaiter(this, void 0, void 0, function () {
2934
+ var tracks, visualStream, hasMediaTracks, audioDestination, audioStream;
2935
+ var _this = this;
2936
+ return __generator(this, function (_a) {
2937
+ switch (_a.label) {
2938
+ case 0:
2939
+ // Validate options
2940
+ if (!options || !options.frameRate) {
2941
+ throw new Error('Required option "frameRate" not provided to Movie.stream');
2942
+ }
2943
+ if (options.video === false && options.audio === false) {
2944
+ throw new Error('Both video and audio cannot be disabled');
2945
+ }
2946
+ if (!this.paused) {
2947
+ throw new Error("Cannot stream movie while it's already playing");
2948
+ }
2949
+ // Wait until all resources are loaded
2950
+ return [4 /*yield*/, this._whenReady()
2951
+ // Create a temporary canvas to stream from
2952
+ ];
2953
+ case 1:
2954
+ // Wait until all resources are loaded
2955
+ _a.sent();
2956
+ // Create a temporary canvas to stream from
2957
+ this._canvas = document.createElement('canvas');
2958
+ this.canvas.width = this._visibleCanvas.width;
2959
+ this.canvas.height = this._visibleCanvas.height;
2960
+ this._cctx = this.canvas.getContext('2d');
2961
+ tracks = [];
2962
+ // Add video track
2963
+ if (options.video !== false) {
2964
+ visualStream = this.canvas.captureStream(options.frameRate);
2965
+ tracks = tracks.concat(visualStream.getTracks());
2966
+ }
2967
+ hasMediaTracks = this.layers.some(function (layer) { return layer instanceof Audio || layer instanceof Video; });
2968
+ // If no media tracks present, don't include an audio stream, because
2969
+ // Chrome doesn't record silence when an audio stream is present.
2970
+ if (hasMediaTracks && options.audio !== false) {
2971
+ audioDestination = this.actx.createMediaStreamDestination();
2972
+ audioStream = audioDestination.stream;
2973
+ tracks = tracks.concat(audioStream.getTracks());
2974
+ // Notify layers and any other listeners of the new audio destination
2975
+ publish(this, 'audiodestinationupdate', { movie: this, destination: audioDestination });
2976
+ }
2977
+ // Create the stream
2978
+ this._currentStream = new MediaStream(tracks);
2979
+ // Play the movie
2980
+ this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2981
+ return [4 /*yield*/, this.play({
2982
+ onStart: function () {
2983
+ // Call the user's onStart callback
2984
+ options.onStart(_this._currentStream);
2985
+ }
2986
+ })
2987
+ // Clear the stream after the movie is done playing
2988
+ ];
2989
+ case 2:
2990
+ _a.sent();
2991
+ // Clear the stream after the movie is done playing
2992
+ this._currentStream.getTracks().forEach(function (track) {
2993
+ track.stop();
2994
+ });
2995
+ this._currentStream = null;
2996
+ this._show();
2997
+ return [2 /*return*/];
2998
+ }
2999
+ });
2518
3000
  });
2519
3001
  };
2520
3002
  /**
2521
3003
  * Plays the movie in the background and records it
2522
3004
  *
2523
3005
  * @param options
2524
- * @param frameRate
3006
+ * @param [options.frameRate] - Video frame rate
2525
3007
  * @param [options.video=true] - whether to include video in recording
2526
3008
  * @param [options.audio=true] - whether to include audio in recording
2527
- * @param [options.mediaRecorderOptions=undefined] - options to pass to the <code>MediaRecorder</code>
3009
+ * @param [options.mediaRecorderOptions=undefined] - Options to pass to the
3010
+ * `MediaRecorder` constructor
2528
3011
  * @param [options.type='video/webm'] - MIME type for exported video
2529
- * constructor
2530
- * @return resolves when done recording, rejects when internal media recorder errors
3012
+ * @param [options.onStart] - Called when the recording starts
3013
+ * @return Resolves when done recording, rejects when media recorder errors
2531
3014
  */
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?
3015
+ // TODO: Improve recording performance to increase frame rate
2535
3016
  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 });
3017
+ var _a;
3018
+ return __awaiter(this, void 0, void 0, function () {
3019
+ var mimeType, stream, recordedChunks, mediaRecorderOptions;
3020
+ var _this = this;
3021
+ return __generator(this, function (_b) {
3022
+ switch (_b.label) {
3023
+ case 0:
3024
+ // Validate options
3025
+ if (options.video === false && options.audio === false) {
3026
+ throw new Error('Both video and audio cannot be disabled');
3027
+ }
3028
+ if (!this.paused) {
3029
+ throw new Error("Cannot record movie while it's already playing");
3030
+ }
3031
+ mimeType = options.type || 'video/webm';
3032
+ if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType)) {
3033
+ throw new Error('Please pass a valid MIME type for the exported video');
3034
+ }
3035
+ return [4 /*yield*/, new Promise(function (resolve) {
3036
+ _this.stream({
3037
+ frameRate: options.frameRate,
3038
+ duration: options.duration,
3039
+ video: options.video,
3040
+ audio: options.audio,
3041
+ onStart: resolve
3042
+ }).then(function () {
3043
+ // Stop the media recorder when the movie is done playing
3044
+ _this._recorder.requestData();
3045
+ _this._recorder.stop();
3046
+ });
3047
+ })
3048
+ // The array to store the recorded chunks
3049
+ ];
3050
+ case 1:
3051
+ stream = _b.sent();
3052
+ recordedChunks = [];
3053
+ mediaRecorderOptions = __assign(__assign({}, (options.mediaRecorderOptions || {})), { mimeType: mimeType });
3054
+ this._recorder = new MediaRecorder(stream, mediaRecorderOptions);
3055
+ this._recorder.ondataavailable = function (event) {
3056
+ // if (this._paused) reject(new Error("Recording was interrupted"));
3057
+ if (event.data.size > 0) {
3058
+ recordedChunks.push(event.data);
3059
+ }
3060
+ };
3061
+ // Start recording
3062
+ this._recorder.start();
3063
+ this._recording = true;
3064
+ // Notify caller that the media recorder has started
3065
+ (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options, this._recorder);
3066
+ // For backwards compatibility
3067
+ publish(this, 'movie.record', { options: options });
3068
+ // Wait until the media recorder is done recording and processing
3069
+ return [4 /*yield*/, new Promise(function (resolve, reject) {
3070
+ _this._recorder.onstop = function () {
3071
+ resolve();
3072
+ };
3073
+ _this._recorder.onerror = reject;
3074
+ })
3075
+ // Clean up
3076
+ ];
3077
+ case 2:
3078
+ // Wait until the media recorder is done recording and processing
3079
+ _b.sent();
3080
+ // Clean up
3081
+ this._paused = true;
3082
+ this._ended = true;
3083
+ this._recording = false;
3084
+ // Construct the exported video out of all the frame blobs.
3085
+ return [2 /*return*/, new Blob(recordedChunks, {
3086
+ type: mimeType
3087
+ })];
3088
+ }
3089
+ });
2597
3090
  });
2598
3091
  };
2599
3092
  /**
2600
- * Stops the movie, without reseting the playback position
2601
- * @return the movie (for chaining)
3093
+ * Stops the movie without resetting the playback position
3094
+ * @return The movie
2602
3095
  */
2603
3096
  Movie.prototype.pause = function () {
3097
+ // Update state
2604
3098
  this._paused = true;
2605
3099
  // Deactivate all layers
2606
- for (var i = 0; i < this.layers.length; i++)
3100
+ for (var i = 0; i < this.layers.length; i++) {
2607
3101
  if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2608
3102
  var layer = this.layers[i];
2609
- layer.stop();
2610
- layer.active = false;
3103
+ if (layer.active) {
3104
+ layer.stop();
3105
+ layer.active = false;
3106
+ }
2611
3107
  }
3108
+ }
3109
+ // For backwards compatibility, notify event listeners that the movie has
3110
+ // paused
2612
3111
  publish(this, 'movie.pause', {});
2613
3112
  return this;
2614
3113
  };
2615
3114
  /**
2616
3115
  * Stops playback and resets the playback position
2617
- * @return the movie (for chaining)
3116
+ * @return The movie
2618
3117
  */
2619
3118
  Movie.prototype.stop = function () {
2620
3119
  this.pause();
@@ -2623,8 +3122,8 @@ var etro = (function () {
2623
3122
  };
2624
3123
  /**
2625
3124
  * @param [timestamp=performance.now()]
2626
- * @param [done=undefined] - called when done playing or when the current frame is loaded
2627
- * @private
3125
+ * @param [done=undefined] - Called when done playing or when the current
3126
+ * frame is loaded
2628
3127
  */
2629
3128
  Movie.prototype._render = function (repeat, timestamp, done) {
2630
3129
  var _this = this;
@@ -2632,179 +3131,207 @@ var etro = (function () {
2632
3131
  if (done === void 0) { done = undefined; }
2633
3132
  clearCachedValues(this);
2634
3133
  if (!this.rendering) {
2635
- // (!this.paused || this._renderingFrame) is true so it's playing or it's
3134
+ // (this.paused && !this._renderingFrame) is true so it's playing or it's
2636
3135
  // rendering a single frame.
2637
- if (done)
3136
+ if (done) {
2638
3137
  done();
3138
+ }
2639
3139
  return;
2640
3140
  }
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.
3141
+ if (this.ready) {
3142
+ publish(this, 'movie.loadeddata', { movie: this });
3143
+ // If the movie is streaming or recording, resume the media recorder
3144
+ if (this._recording && this._recorder.state === 'paused') {
3145
+ this._recorder.resume();
3146
+ }
3147
+ // If the movie is streaming or recording, end at the specified duration.
3148
+ // Otherwise, end at the movie's duration, because play() does not
3149
+ // support playing a portion of the movie yet.
3150
+ // TODO: Is calling duration every frame bad for performance? (remember,
3151
+ // it's calling Array.reduce)
3152
+ var end = this._currentStream ? this._endTime : this.duration;
3153
+ this._updateCurrentTime(timestamp, end);
3154
+ if (this.currentTime === end) {
3155
+ if (this.recording) {
3156
+ publish(this, 'movie.recordended', { movie: this });
3157
+ }
3158
+ if (this.currentTime === this.duration) {
3159
+ publish(this, 'movie.ended', { movie: this, repeat: this.repeat });
3160
+ }
3161
+ // Don't use setter, which publishes 'seek'. Instead, update the
3162
+ // value and publish a 'imeupdate' event.
2654
3163
  this._currentTime = 0;
2655
3164
  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;
3165
+ this._lastPlayed = performance.now();
3166
+ this._lastPlayedOffset = 0; // this.currentTime
3167
+ this._renderingFrame = false;
3168
+ // Stop playback or recording if done (except if it's playing and repeat
3169
+ // is true)
3170
+ if (!(!this.recording && this.repeat)) {
3171
+ this._paused = true;
3172
+ this._ended = true;
3173
+ // Deactivate all layers
3174
+ for (var i = 0; i < this.layers.length; i++) {
3175
+ if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
3176
+ var layer = this.layers[i];
3177
+ // A layer that has been deleted before layers.length has been updated
3178
+ // (see the layers proxy in the constructor).
3179
+ if (!layer || !layer.active) {
3180
+ continue;
3181
+ }
3182
+ layer.stop();
3183
+ layer.active = false;
3184
+ }
3185
+ }
3186
+ publish(this, 'movie.pause', {});
3187
+ if (done) {
3188
+ done();
2675
3189
  }
2676
- if (done)
2677
- done();
2678
- return;
3190
+ return;
3191
+ }
2679
3192
  }
3193
+ // Do render
3194
+ this._renderBackground(timestamp);
3195
+ this._renderLayers();
3196
+ this._applyEffects();
2680
3197
  }
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)) {
3198
+ else {
3199
+ // If we are recording, pause the media recorder until the movie is
3200
+ // ready.
3201
+ if (this.recording && this._recorder.state === 'recording') {
3202
+ this._recorder.pause();
3203
+ }
3204
+ }
3205
+ // If the frame didn't load this instant, repeatedly frame-render until it
3206
+ // is loaded.
3207
+ // If the expression below is true, don't publish an event, just silently
3208
+ // stop the render loop.
3209
+ if (this._renderingFrame && this.ready) {
2692
3210
  this._renderingFrame = false;
2693
- if (done)
3211
+ if (done) {
2694
3212
  done();
3213
+ }
2695
3214
  return;
2696
3215
  }
3216
+ // TODO: Is making a new arrow function every frame bad for performance?
2697
3217
  window.requestAnimationFrame(function () {
2698
3218
  _this._render(repeat, undefined, done);
2699
- }); // TODO: research performance cost
3219
+ });
2700
3220
  };
2701
3221
  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.
3222
+ // If we're only frame-rendering (current frame only), it doesn't matter if
3223
+ // it's paused or not.
2704
3224
  if (!this._renderingFrame) {
2705
- // if ((timestamp - this._lastUpdate) >= this._updateInterval) {
2706
3225
  var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
2707
- var currentTime = this._lastPlayedOffset + sinceLastPlayed; // don't use setter
3226
+ var currentTime = this._lastPlayedOffset + sinceLastPlayed;
2708
3227
  if (this.currentTime !== currentTime) {
3228
+ // Update the current time (don't use setter)
2709
3229
  this._currentTime = currentTime;
3230
+ // For backwards compatibility, publish a 'movie.timeupdate' event.
2710
3231
  publish(this, 'movie.timeupdate', { movie: this });
2711
3232
  }
2712
- // this._lastUpdate = timestamp;
2713
- // }
2714
- if (this.currentTime > end)
2715
- this.currentTime = end;
3233
+ if (this.currentTime > end) {
3234
+ this._currentTime = end;
3235
+ }
2716
3236
  }
2717
3237
  };
2718
3238
  Movie.prototype._renderBackground = function (timestamp) {
2719
3239
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3240
+ // Evaluate background color (since it's a dynamic property)
2720
3241
  var background = val(this, 'background', timestamp);
2721
- if (background) { // TODO: check val'd result
3242
+ if (background) {
2722
3243
  this.cctx.fillStyle = background;
2723
3244
  this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
2724
3245
  }
2725
3246
  };
2726
3247
  /**
2727
- * @return whether or not video frames are loaded
2728
3248
  * @param [timestamp=performance.now()]
2729
- * @private
2730
3249
  */
2731
3250
  Movie.prototype._renderLayers = function () {
2732
- var frameFullyLoaded = true;
2733
3251
  for (var i = 0; i < this.layers.length; i++) {
2734
- if (!Object.prototype.hasOwnProperty.call(this.layers, i))
3252
+ if (!Object.prototype.hasOwnProperty.call(this.layers, i)) {
2735
3253
  continue;
3254
+ }
2736
3255
  var layer = this.layers[i];
2737
3256
  // A layer that has been deleted before layers.length has been updated
2738
3257
  // (see the layers proxy in the constructor).
2739
- if (!layer)
3258
+ if (!layer) {
2740
3259
  continue;
2741
- var reltime = this.currentTime - layer.startTime;
3260
+ }
2742
3261
  // Cancel operation if layer disabled or outside layer time interval
3262
+ var reltime = this.currentTime - layer.startTime;
2743
3263
  if (!val(layer, 'enabled', reltime) ||
2744
3264
  // TODO > or >= ?
2745
3265
  this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
2746
3266
  // Layer is not active.
2747
3267
  // If only rendering this frame, we are not "starting" the layer.
2748
3268
  if (layer.active && !this._renderingFrame) {
2749
- // TODO: make a `deactivate()` method?
2750
3269
  layer.stop();
2751
3270
  layer.active = false;
2752
3271
  }
2753
3272
  continue;
2754
3273
  }
3274
+ // If we are playing (not refreshing), update the layer's progress
3275
+ if (!this._renderingFrame) {
3276
+ layer.progress(reltime);
3277
+ }
2755
3278
  // If only rendering this frame, we are not "starting" the layer
2756
3279
  if (!layer.active && val(layer, 'enabled', reltime) && !this._renderingFrame) {
2757
- // TODO: make an `activate()` method?
2758
3280
  layer.start();
2759
3281
  layer.active = true;
2760
3282
  }
2761
- // if the layer has an input file
2762
- if ('source' in layer)
2763
- frameFullyLoaded = frameFullyLoaded && layer.source.readyState >= 2;
2764
3283
  layer.render();
2765
3284
  // if the layer has visual component
2766
3285
  if (layer instanceof Visual) {
2767
3286
  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)
3287
+ if (canvas.width * canvas.height > 0) {
2771
3288
  this.cctx.drawImage(canvas, val(layer, 'x', reltime), val(layer, 'y', reltime), canvas.width, canvas.height);
3289
+ }
2772
3290
  }
2773
3291
  }
2774
- return frameFullyLoaded;
2775
3292
  };
2776
3293
  Movie.prototype._applyEffects = function () {
2777
3294
  for (var i = 0; i < this.effects.length; i++) {
2778
3295
  var effect = this.effects[i];
2779
3296
  // An effect that has been deleted before effects.length has been updated
2780
3297
  // (see the effectsproxy in the constructor).
2781
- if (!effect)
3298
+ if (!effect) {
2782
3299
  continue;
3300
+ }
2783
3301
  effect.apply(this, this.currentTime);
2784
3302
  }
2785
3303
  };
2786
3304
  /**
2787
- * Refreshes the screen (only use this if auto-refresh is disabled)
2788
- * @return - resolves when the frame is loaded
3305
+ * Refreshes the screen
3306
+ *
3307
+ * Only use this if auto-refresh is disabled
3308
+ *
3309
+ * @return - Promise that resolves when the frame is loaded
2789
3310
  */
2790
3311
  Movie.prototype.refresh = function () {
2791
3312
  var _this = this;
3313
+ // Refreshing while playing can interrupt playback
3314
+ if (!this.paused) {
3315
+ throw new Error('Already playing');
3316
+ }
2792
3317
  return new Promise(function (resolve) {
2793
3318
  _this._renderingFrame = true;
2794
3319
  _this._render(false, undefined, resolve);
2795
3320
  });
2796
3321
  };
2797
3322
  /**
2798
- * Convienence method
3323
+ * Convienence method (TODO: remove)
2799
3324
  */
2800
3325
  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))
3326
+ for (var i = 0; i < this.layers.length; i++) {
3327
+ if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2803
3328
  publish(this.layers[i], type, event);
3329
+ }
3330
+ }
2804
3331
  };
2805
3332
  Object.defineProperty(Movie.prototype, "rendering", {
2806
3333
  /**
2807
- * If the movie is playing, recording or refreshing
3334
+ * `true` if the movie is playing, recording or refreshing
2808
3335
  */
2809
3336
  get: function () {
2810
3337
  return !this.paused || this._renderingFrame;
@@ -2814,7 +3341,7 @@ var etro = (function () {
2814
3341
  });
2815
3342
  Object.defineProperty(Movie.prototype, "renderingFrame", {
2816
3343
  /**
2817
- * If the movie is refreshing current frame
3344
+ * `true` if the movie is refreshing the current frame
2818
3345
  */
2819
3346
  get: function () {
2820
3347
  return this._renderingFrame;
@@ -2824,17 +3351,19 @@ var etro = (function () {
2824
3351
  });
2825
3352
  Object.defineProperty(Movie.prototype, "recording", {
2826
3353
  /**
2827
- * If the movie is recording
3354
+ * `true` if the movie is recording
2828
3355
  */
2829
3356
  get: function () {
2830
- return !!this._mediaRecorder;
3357
+ return this._recording;
2831
3358
  },
2832
3359
  enumerable: false,
2833
3360
  configurable: true
2834
3361
  });
2835
3362
  Object.defineProperty(Movie.prototype, "duration", {
2836
3363
  /**
2837
- * The combined duration of all layers
3364
+ * The duration of the movie in seconds
3365
+ *
3366
+ * Calculated from the end time of the last layer
2838
3367
  */
2839
3368
  // TODO: dirty flag?
2840
3369
  get: function () {
@@ -2844,16 +3373,16 @@ var etro = (function () {
2844
3373
  configurable: true
2845
3374
  });
2846
3375
  /**
2847
- * Convienence method for <code>layers.push()</code>
3376
+ * Convenience method for `layers.push()`
2848
3377
  * @param layer
2849
- * @return the movie
3378
+ * @return The movie
2850
3379
  */
2851
3380
  Movie.prototype.addLayer = function (layer) {
2852
3381
  this.layers.push(layer);
2853
3382
  return this;
2854
3383
  };
2855
3384
  /**
2856
- * Convienence method for <code>effects.push()</code>
3385
+ * Convenience method for `effects.push()`
2857
3386
  * @param effect
2858
3387
  * @return the movie
2859
3388
  */
@@ -2863,6 +3392,7 @@ var etro = (function () {
2863
3392
  };
2864
3393
  Object.defineProperty(Movie.prototype, "paused", {
2865
3394
  /**
3395
+ * `true` if the movie is paused
2866
3396
  */
2867
3397
  get: function () {
2868
3398
  return this._paused;
@@ -2872,7 +3402,7 @@ var etro = (function () {
2872
3402
  });
2873
3403
  Object.defineProperty(Movie.prototype, "ended", {
2874
3404
  /**
2875
- * If the playback position is at the end of the movie
3405
+ * `true` if the playback position is at the end of the movie
2876
3406
  */
2877
3407
  get: function () {
2878
3408
  return this._ended;
@@ -2880,50 +3410,89 @@ var etro = (function () {
2880
3410
  enumerable: false,
2881
3411
  configurable: true
2882
3412
  });
3413
+ /**
3414
+ * Skips to the provided playback position, updating {@link currentTime}.
3415
+ *
3416
+ * @param time - The new playback position (in seconds)
3417
+ */
3418
+ Movie.prototype.seek = function (time) {
3419
+ this._currentTime = time;
3420
+ // Call `seek` on every layer
3421
+ for (var i = 0; i < this.layers.length; i++) {
3422
+ var layer = this.layers[i];
3423
+ if (layer) {
3424
+ var relativeTime = time - layer.startTime;
3425
+ if (relativeTime >= 0 && relativeTime <= layer.duration) {
3426
+ layer.seek(relativeTime);
3427
+ }
3428
+ else {
3429
+ layer.seek(undefined);
3430
+ }
3431
+ }
3432
+ }
3433
+ // For backwards compatibility, publish a `seek` event
3434
+ publish(this, 'movie.seek', {});
3435
+ };
2883
3436
  Object.defineProperty(Movie.prototype, "currentTime", {
2884
3437
  /**
2885
- * The current playback position
3438
+ * The current playback position in seconds
2886
3439
  */
2887
3440
  get: function () {
2888
3441
  return this._currentTime;
2889
3442
  },
3443
+ /**
3444
+ * Skips to the provided playback position, updating {@link currentTime}.
3445
+ *
3446
+ * @param time - The new playback position (in seconds)
3447
+ *
3448
+ * @deprecated Use `seek` instead
3449
+ */
2890
3450
  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();
3451
+ this.seek(time);
2896
3452
  },
2897
3453
  enumerable: false,
2898
3454
  configurable: true
2899
3455
  });
2900
3456
  /**
2901
- * Sets the current playback position. This is a more powerful version of
2902
- * `set currentTime`.
3457
+ * Skips to the provided playback position, updating {@link currentTime}.
2903
3458
  *
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
3459
+ * @param time - The new time (in seconds)
3460
+ * @param [refresh=true] - Render a single frame?
3461
+ * @return Promise that resolves when the current frame is rendered if
3462
+ * `refresh` is true; otherwise resolves immediately.
2908
3463
  *
3464
+ * @deprecated Call {@link seek} and {@link refresh} separately
2909
3465
  */
2910
- // TODO: Refresh if only auto-refreshing is enabled
3466
+ // TODO: Refresh only if auto-refreshing is enabled
2911
3467
  Movie.prototype.setCurrentTime = function (time, refresh) {
2912
3468
  var _this = this;
2913
3469
  if (refresh === void 0) { refresh = true; }
2914
3470
  return new Promise(function (resolve, reject) {
2915
- _this._currentTime = time;
2916
- publish(_this, 'movie.seek', {});
2917
- if (refresh)
3471
+ _this.seek(time);
3472
+ if (refresh) {
2918
3473
  // Pass promise callbacks to `refresh`
2919
3474
  _this.refresh().then(resolve).catch(reject);
2920
- else
3475
+ }
3476
+ else {
2921
3477
  resolve();
3478
+ }
2922
3479
  });
2923
3480
  };
3481
+ Object.defineProperty(Movie.prototype, "ready", {
3482
+ /**
3483
+ * `true` if the movie is ready for playback
3484
+ */
3485
+ get: function () {
3486
+ var layersReady = this.layers.every(function (layer) { return layer.ready; });
3487
+ var effectsReady = this.effects.every(function (effect) { return effect.ready; });
3488
+ return layersReady && effectsReady;
3489
+ },
3490
+ enumerable: false,
3491
+ configurable: true
3492
+ });
2924
3493
  Object.defineProperty(Movie.prototype, "canvas", {
2925
3494
  /**
2926
- * The rendering canvas
3495
+ * The HTML canvas element used for rendering
2927
3496
  */
2928
3497
  get: function () {
2929
3498
  return this._canvas;
@@ -2933,7 +3502,7 @@ var etro = (function () {
2933
3502
  });
2934
3503
  Object.defineProperty(Movie.prototype, "cctx", {
2935
3504
  /**
2936
- * The rendering canvas's context
3505
+ * The canvas context used for rendering
2937
3506
  */
2938
3507
  get: function () {
2939
3508
  return this._cctx;
@@ -2943,7 +3512,7 @@ var etro = (function () {
2943
3512
  });
2944
3513
  Object.defineProperty(Movie.prototype, "width", {
2945
3514
  /**
2946
- * The width of the rendering canvas
3515
+ * The width of the output canvas
2947
3516
  */
2948
3517
  get: function () {
2949
3518
  return this.canvas.width;
@@ -2956,7 +3525,7 @@ var etro = (function () {
2956
3525
  });
2957
3526
  Object.defineProperty(Movie.prototype, "height", {
2958
3527
  /**
2959
- * The height of the rendering canvas
3528
+ * The height of the output canvas
2960
3529
  */
2961
3530
  get: function () {
2962
3531
  return this.canvas.height;
@@ -2968,12 +3537,18 @@ var etro = (function () {
2968
3537
  configurable: true
2969
3538
  });
2970
3539
  Object.defineProperty(Movie.prototype, "movie", {
3540
+ /**
3541
+ * @return The movie
3542
+ */
2971
3543
  get: function () {
2972
3544
  return this;
2973
3545
  },
2974
3546
  enumerable: false,
2975
3547
  configurable: true
2976
3548
  });
3549
+ /**
3550
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
3551
+ */
2977
3552
  Movie.prototype.getDefaultOptions = function () {
2978
3553
  return {
2979
3554
  canvas: undefined,
@@ -2985,21 +3560,23 @@ var etro = (function () {
2985
3560
  /**
2986
3561
  * @name module:movie#repeat
2987
3562
  */
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
3563
+ repeat: false
2994
3564
  };
2995
3565
  };
2996
3566
  return Movie;
2997
3567
  }());
2998
- // id for events (independent of instance, but easy to access when on prototype chain)
3568
+ // Id for events
2999
3569
  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 = {};
3570
+ Movie.prototype.propertyFilters = {};
3571
+ deprecate('movie.audiodestinationupdate', 'audiodestinationupdate');
3572
+ deprecate('movie.ended', undefined);
3573
+ deprecate('movie.loadeddata', undefined);
3574
+ deprecate('movie.pause', undefined, 'Wait for `play()`, `stream()`, or `record()` to resolve instead.');
3575
+ deprecate('movie.play', undefined, 'Provide an `onStart` callback to `play()`, `stream()`, or `record()` instead.');
3576
+ deprecate('movie.record', undefined, 'Provide an `onStart` callback to `record()` instead.');
3577
+ deprecate('movie.recordended', undefined, 'Wait for `record()` to resolve instead.');
3578
+ deprecate('movie.seek', undefined, 'Override the `seek` method on layers instead.');
3579
+ deprecate('movie.timeupdate', undefined, 'Override the `progress` method on layers instead.');
3003
3580
 
3004
3581
  /*
3005
3582
  * Typedoc can't handle default exports. To let users import default export and
@@ -3025,8 +3602,7 @@ var etro = (function () {
3025
3602
  parseColor: parseColor,
3026
3603
  Font: Font,
3027
3604
  parseFont: parseFont,
3028
- mapPixels: mapPixels,
3029
- watchPublic: watchPublic
3605
+ mapPixels: mapPixels
3030
3606
  });
3031
3607
 
3032
3608
  /**