etro 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CONTRIBUTING.md +23 -20
  3. package/README.md +3 -2
  4. package/dist/custom-array.d.ts +10 -0
  5. package/dist/effect/base.d.ts +10 -1
  6. package/dist/effect/shader.d.ts +11 -1
  7. package/dist/effect/stack.d.ts +6 -2
  8. package/dist/etro-cjs.js +1156 -575
  9. package/dist/etro-iife.js +1156 -575
  10. package/dist/event.d.ts +10 -5
  11. package/dist/layer/audio-source.d.ts +9 -4
  12. package/dist/layer/audio.d.ts +15 -2
  13. package/dist/layer/base.d.ts +49 -3
  14. package/dist/layer/image.d.ts +15 -1
  15. package/dist/layer/text.d.ts +3 -0
  16. package/dist/layer/video.d.ts +13 -1
  17. package/dist/layer/visual.d.ts +6 -2
  18. package/dist/movie/effects.d.ts +6 -0
  19. package/dist/movie/index.d.ts +1 -0
  20. package/dist/movie/layers.d.ts +6 -0
  21. package/dist/movie/movie.d.ts +260 -0
  22. package/dist/object.d.ts +9 -6
  23. package/dist/util.d.ts +4 -10
  24. package/eslint.conf.js +2 -2
  25. package/karma.conf.js +4 -7
  26. package/package.json +8 -7
  27. package/src/custom-array.ts +43 -0
  28. package/src/effect/base.ts +23 -22
  29. package/src/effect/gaussian-blur.ts +11 -6
  30. package/src/effect/pixelate.ts +3 -3
  31. package/src/effect/shader.ts +33 -27
  32. package/src/effect/stack.ts +43 -30
  33. package/src/effect/transform.ts +14 -7
  34. package/src/event.ts +111 -21
  35. package/src/layer/audio-source.ts +60 -20
  36. package/src/layer/audio.ts +25 -3
  37. package/src/layer/base.ts +79 -26
  38. package/src/layer/image.ts +26 -2
  39. package/src/layer/text.ts +7 -0
  40. package/src/layer/video.ts +31 -4
  41. package/src/layer/visual-source.ts +43 -1
  42. package/src/layer/visual.ts +50 -28
  43. package/src/movie/effects.ts +26 -0
  44. package/src/movie/index.ts +1 -0
  45. package/src/movie/layers.ts +26 -0
  46. package/src/movie/movie.ts +855 -0
  47. package/src/object.ts +9 -6
  48. package/src/util.ts +68 -89
  49. package/dist/movie.d.ts +0 -201
  50. package/src/movie.ts +0 -744
  51. /package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +0 -0
  52. /package/scripts/{save-effect-samples.js → effect/save-effect-samples.js} +0 -0
package/src/object.ts CHANGED
@@ -2,17 +2,20 @@ import { Movie } from './movie'
2
2
 
3
3
  /** A movie, layer or effect */
4
4
  export default interface EtroObject {
5
+ currentTime: number
5
6
  /** Used in etro internals */
6
7
  type: string
7
- /**
8
- * Which properties to not watch for changes, for `Movie#autoRefresh`
9
- *
10
- * @deprecated `Movie#autoRefresh` is deprecated
11
- */
12
- publicExcludes: string[]
13
8
  /** Map of property name to function to run on result of `val` */
14
9
  propertyFilters: Record<string, <T>(value: T) => T>
10
+ /**
11
+ * `true` if this object is ready to be played/rendered/applied, `false`
12
+ * otherwise
13
+ */
14
+ ready: boolean
15
15
  movie: Movie
16
16
 
17
+ /**
18
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
19
+ */
17
20
  getDefaultOptions(): object // eslint-disable-line @typescript-eslint/ban-types
18
21
  }
package/src/util.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import EtroObject from './object'
6
- import { publish } from './event'
7
6
  import { Movie } from './movie'
8
7
 
9
8
  /**
@@ -15,8 +14,9 @@ import { Movie } from './movie'
15
14
  function getPropertyDescriptor (obj: unknown, name: string | number | symbol): PropertyDescriptor {
16
15
  do {
17
16
  const propDesc = Object.getOwnPropertyDescriptor(obj, name)
18
- if (propDesc)
17
+ if (propDesc) {
19
18
  return propDesc
19
+ }
20
20
 
21
21
  obj = Object.getPrototypeOf(obj)
22
22
  } while (obj)
@@ -27,6 +27,10 @@ function getPropertyDescriptor (obj: unknown, name: string | number | symbol): P
27
27
  * Merges `options` with `defaultOptions`, and then copies the properties with
28
28
  * the keys in `defaultOptions` from the merged object to `destObj`.
29
29
  *
30
+ * @deprecated Each option should be copied individually, and the default value
31
+ * should be set in the constructor. See
32
+ * {@link https://github.com/etro-js/etro/issues/131} for more info.
33
+ *
30
34
  * @return
31
35
  */
32
36
  // TODO: Make methods like getDefaultOptions private
@@ -34,10 +38,12 @@ export function applyOptions (options: object, destObj: EtroObject): void { // e
34
38
  const defaultOptions = destObj.getDefaultOptions()
35
39
 
36
40
  // Validate; make sure `keys` doesn't have any extraneous items
37
- for (const option in options)
41
+ for (const option in options) {
38
42
  // eslint-disable-next-line no-prototype-builtins
39
- if (!defaultOptions.hasOwnProperty(option))
43
+ if (!defaultOptions.hasOwnProperty(option)) {
40
44
  throw new Error("Invalid option: '" + option + "'")
45
+ }
46
+ }
41
47
 
42
48
  // Merge options and defaultOptions
43
49
  options = { ...defaultOptions, ...options }
@@ -46,8 +52,9 @@ export function applyOptions (options: object, destObj: EtroObject): void { // e
46
52
  for (const option in options) {
47
53
  const propDesc = getPropertyDescriptor(destObj, option)
48
54
  // Update the property as long as the property has not been set (unless if it has a setter)
49
- if (!propDesc || propDesc.set)
55
+ if (!propDesc || propDesc.set) {
50
56
  destObj[option] = options[option]
57
+ }
51
58
  }
52
59
  }
53
60
 
@@ -55,14 +62,16 @@ export function applyOptions (options: object, destObj: EtroObject): void { // e
55
62
  const valCache = new WeakMap()
56
63
  function cacheValue (element: EtroObject, path: string, value: unknown) {
57
64
  // Initiate movie cache
58
- if (!valCache.has(element.movie))
65
+ if (!valCache.has(element.movie)) {
59
66
  valCache.set(element.movie, new WeakMap())
67
+ }
60
68
 
61
69
  const movieCache = valCache.get(element.movie)
62
70
 
63
- // Iniitate element cache
64
- if (!movieCache.has(element))
71
+ // Initiate element cache
72
+ if (!movieCache.has(element)) {
65
73
  movieCache.set(element, {})
74
+ }
66
75
 
67
76
  const elementCache = movieCache.get(element)
68
77
 
@@ -107,15 +116,18 @@ export class KeyFrame<T> {
107
116
  }
108
117
 
109
118
  evaluate (time: number): T {
110
- if (this.value.length === 0)
119
+ if (this.value.length === 0) {
111
120
  throw new Error('Empty keyframe')
121
+ }
112
122
 
113
- if (time === undefined)
123
+ if (time === undefined) {
114
124
  throw new Error('|time| is undefined or null')
125
+ }
115
126
 
116
127
  const firstTime: number = this.value[0][0] as number
117
- if (time < firstTime)
128
+ if (time < firstTime) {
118
129
  throw new Error('No keyframe point before |time|')
130
+ }
119
131
 
120
132
  // I think reduce are slow to do per-frame (or more)?
121
133
  for (let i = 0; i < this.value.length; i++) {
@@ -126,7 +138,7 @@ export class KeyFrame<T> {
126
138
  if (i + 1 < this.value.length) {
127
139
  const endTime = this.value[i + 1][0] as number
128
140
  const endValue = this.value[i + 1][1] as T
129
- if (startTime <= time && time < endTime)
141
+ if (startTime <= time && time < endTime) {
130
142
  // No need for endValue if it is flat interpolation
131
143
  // TODO: support custom interpolation for 'other' types?
132
144
  if (!(typeof startValue === 'number' || typeof endValue === 'object')) {
@@ -142,6 +154,7 @@ export class KeyFrame<T> {
142
154
  percentProgress, this.interpolationKeys
143
155
  ) as unknown as T
144
156
  }
157
+ }
145
158
  } else {
146
159
  // Repeat last value forever
147
160
  return startValue
@@ -169,26 +182,29 @@ export type Dynamic<T> = T | KeyFrame<T> | ((element: EtroObject, time: number)
169
182
  // TODO: Is this function efficient?
170
183
  // TODO: Update doc @params to allow for keyframes
171
184
  export function val (element: EtroObject, path: string, time: number): any { // eslint-disable-line @typescript-eslint/no-explicit-any
172
- if (hasCachedValue(element, path))
185
+ if (hasCachedValue(element, path)) {
173
186
  return getCachedValue(element, path)
187
+ }
174
188
 
175
189
  // Get property of element at path
176
190
  const pathParts = path.split('.')
177
191
  let property = element[pathParts.shift()]
178
- while (pathParts.length > 0)
192
+ while (pathParts.length > 0) {
179
193
  property = property[pathParts.shift()]
194
+ }
180
195
 
181
196
  // Property filter function
182
197
  const process = element.propertyFilters[path]
183
198
 
184
199
  let value
185
- if (property instanceof KeyFrame)
200
+ if (property instanceof KeyFrame) {
186
201
  value = property.evaluate(time)
187
- else if (typeof property === 'function')
188
- value = property(element, time) // TODO? add more args
189
- else
202
+ } else if (typeof property === 'function') {
203
+ value = property(element, time)
204
+ } else {
190
205
  // Simple value
191
206
  value = property
207
+ }
192
208
 
193
209
  return cacheValue(element, path, process ? process.call(element, value) : value)
194
210
  }
@@ -202,17 +218,20 @@ export function val (element: EtroObject, path: string, time: number): any { //
202
218
  } */
203
219
 
204
220
  export function linearInterp (x1: number | object, x2: number | object, t: number, objectKeys?: string[]): number | object { // eslint-disable-line @typescript-eslint/ban-types
205
- if (typeof x1 !== typeof x2)
221
+ if (typeof x1 !== typeof x2) {
206
222
  throw new Error('Type mismatch')
223
+ }
207
224
 
208
- if (typeof x1 !== 'number' && typeof x1 !== 'object')
225
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
209
226
  // Flat interpolation (floor)
210
227
  return x1
228
+ }
211
229
 
212
230
  if (typeof x1 === 'object') { // to work with objects (including arrays)
213
231
  // TODO: make this code DRY
214
- if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2))
232
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
215
233
  throw new Error('Prototype mismatch')
234
+ }
216
235
 
217
236
  // Preserve prototype of objects
218
237
  const int = Object.create(Object.getPrototypeOf(x1))
@@ -221,8 +240,9 @@ export function linearInterp (x1: number | object, x2: number | object, t: numbe
221
240
  for (let i = 0; i < keys.length; i++) {
222
241
  const key = keys[i]
223
242
  // eslint-disable-next-line no-prototype-builtins
224
- if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key))
243
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
225
244
  continue
245
+ }
226
246
 
227
247
  int[key] = linearInterp(x1[key], x2[key], t)
228
248
  }
@@ -232,16 +252,19 @@ export function linearInterp (x1: number | object, x2: number | object, t: numbe
232
252
  }
233
253
 
234
254
  export function cosineInterp (x1: number | object, x2: number | object, t: number, objectKeys?: string[]): number | object { // eslint-disable-line @typescript-eslint/ban-types
235
- if (typeof x1 !== typeof x2)
255
+ if (typeof x1 !== typeof x2) {
236
256
  throw new Error('Type mismatch')
257
+ }
237
258
 
238
- if (typeof x1 !== 'number' && typeof x1 !== 'object')
259
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
239
260
  // Flat interpolation (floor)
240
261
  return x1
262
+ }
241
263
 
242
264
  if (typeof x1 === 'object' && typeof x2 === 'object') { // to work with objects (including arrays)
243
- if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2))
265
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
244
266
  throw new Error('Prototype mismatch')
267
+ }
245
268
 
246
269
  // Preserve prototype of objects
247
270
  const int = Object.create(Object.getPrototypeOf(x1))
@@ -250,8 +273,9 @@ export function cosineInterp (x1: number | object, x2: number | object, t: numbe
250
273
  for (let i = 0; i < keys.length; i++) {
251
274
  const key = keys[i]
252
275
  // eslint-disable-next-line no-prototype-builtins
253
- if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key))
276
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
254
277
  continue
278
+ }
255
279
 
256
280
  int[key] = cosineInterp(x1[key], x2[key], t)
257
281
  }
@@ -345,12 +369,22 @@ export class Font {
345
369
  */
346
370
  toString (): string {
347
371
  let s = ''
348
- if (this.style !== 'normal') s += this.style + ' '
349
- if (this.variant !== 'normal') s += this.variant + ' '
350
- if (this.weight !== 'normal') s += this.weight + ' '
351
- if (this.stretch !== 'normal') s += this.stretch + ' '
372
+ if (this.style !== 'normal') {
373
+ s += this.style + ' '
374
+ }
375
+ if (this.variant !== 'normal') {
376
+ s += this.variant + ' '
377
+ }
378
+ if (this.weight !== 'normal') {
379
+ s += this.weight + ' '
380
+ }
381
+ if (this.stretch !== 'normal') {
382
+ s += this.stretch + ' '
383
+ }
352
384
  s += `${this.size}${this.sizeUnit} `
353
- if (this.lineHeight !== 'normal') s += this.lineHeight + ' '
385
+ if (this.lineHeight !== 'normal') {
386
+ s += this.lineHeight + ' '
387
+ }
354
388
  s += this.family
355
389
 
356
390
  return s
@@ -403,66 +437,11 @@ export function mapPixels (
403
437
  width = width || canvas.width
404
438
  height = height || canvas.height
405
439
  const frame = ctx.getImageData(x, y, width, height)
406
- for (let i = 0, l = frame.data.length; i < l; i += 4)
440
+ for (let i = 0, l = frame.data.length; i < l; i += 4) {
407
441
  mapper(frame.data, i)
408
-
409
- if (flush)
410
- ctx.putImageData(frame, x, y)
411
- }
412
-
413
- /**
414
- * <p>Emits "change" event when public properties updated, recursively.
415
- * <p>Must be called before any watchable properties are set, and only once in
416
- * the prototype chain.
417
- *
418
- * @deprecated Will be removed in the future (see issue #130)
419
- *
420
- * @param target - object to watch
421
- */
422
- export function watchPublic (target: EtroObject): EtroObject {
423
- const getPath = (receiver, prop) =>
424
- (receiver === proxy ? '' : (paths.get(receiver) + '.')) + prop
425
- const callback = function (prop, val, receiver) {
426
- // Public API property updated, emit 'modify' event.
427
- publish(proxy, `${target.type}.change.modify`, { property: getPath(receiver, prop), newValue: val })
428
442
  }
429
- const canWatch = (receiver, prop) => !prop.startsWith('_') &&
430
- (receiver.publicExcludes === undefined || !receiver.publicExcludes.includes(prop))
431
-
432
- // The path to each child property (each is a unique proxy)
433
- const paths = new WeakMap()
434
-
435
- const handler = {
436
- set (obj, prop, val, receiver) {
437
- // Recurse
438
- if (typeof val === 'object' && val !== null && !paths.has(val) && canWatch(receiver, prop)) {
439
- val = new Proxy(val, handler)
440
- paths.set(val, getPath(receiver, prop))
441
- }
442
443
 
443
- // Set property or attribute
444
- // Search prototype chain for the closest setter
445
- let objProto = obj
446
- while ((objProto = Object.getPrototypeOf(objProto))) {
447
- const propDesc = Object.getOwnPropertyDescriptor(objProto, prop)
448
- if (propDesc && propDesc.set) {
449
- // Call setter, supplying proxy as this (fixes event bugs)
450
- propDesc.set.call(receiver, val)
451
- break
452
- }
453
- }
454
- if (!objProto)
455
- // Couldn't find setter; set value on instance
456
- obj[prop] = val
457
-
458
- // Check if the property isn't blacklisted in publicExcludes.
459
- if (canWatch(receiver, prop))
460
- callback(prop, val, receiver)
461
-
462
- return true
463
- }
444
+ if (flush) {
445
+ ctx.putImageData(frame, x, y)
464
446
  }
465
-
466
- const proxy = new Proxy(target, handler)
467
- return proxy
468
447
  }
package/dist/movie.d.ts DELETED
@@ -1,201 +0,0 @@
1
- /**
2
- * @module movie
3
- */
4
- import { Dynamic, Color } from './util';
5
- import { Base as BaseLayer } from './layer/index';
6
- import { Base as BaseEffect } from './effect/index';
7
- declare global {
8
- interface Window {
9
- webkitAudioContext: typeof AudioContext;
10
- }
11
- interface HTMLCanvasElement {
12
- captureStream(frameRate?: number): MediaStream;
13
- }
14
- }
15
- export declare class MovieOptions {
16
- /** The html canvas element to use for playback */
17
- canvas: HTMLCanvasElement;
18
- /** The audio context to use for playback, defaults to a new audio context */
19
- actx?: AudioContext;
20
- /** @deprecated Use <code>actx</code> instead */
21
- audioContext?: AudioContext;
22
- /** The background color of the movie as a cSS string */
23
- background?: Dynamic<Color>;
24
- repeat?: boolean;
25
- /** Call `refresh` when the user changes a property on the movie or any of its layers or effects */
26
- autoRefresh?: boolean;
27
- }
28
- /**
29
- * The movie contains everything included in the render.
30
- *
31
- * Implements a pub/sub system.
32
- */
33
- export declare class Movie {
34
- type: string;
35
- /**
36
- * @deprecated Auto-refresh will be removed in the future (see issue #130).
37
- */
38
- publicExcludes: string[];
39
- propertyFilters: Record<string, <T>(value: T) => T>;
40
- repeat: boolean;
41
- /**
42
- * Call `refresh` when the user changes a property on the movie or any of its
43
- * layers or effects
44
- *
45
- * @deprecated Auto-refresh will be removed in the future. If you want to
46
- * refresh the canvas, call `refresh`. See issue #130.
47
- */
48
- autoRefresh: boolean;
49
- /** The background color of the movie as a cSS string */
50
- background: Dynamic<Color>;
51
- /** The audio context to which audio output is sent during playback */
52
- readonly actx: AudioContext;
53
- readonly effects: BaseEffect[];
54
- readonly layers: BaseLayer[];
55
- private _canvas;
56
- private _cctx;
57
- private _effectsBack;
58
- private _layersBack;
59
- private _currentTime;
60
- private _paused;
61
- private _ended;
62
- private _renderingFrame;
63
- private _recordEndTime;
64
- private _mediaRecorder;
65
- private _lastPlayed;
66
- private _lastPlayedOffset;
67
- /**
68
- * Creates a new movie.
69
- */
70
- constructor(options: MovieOptions);
71
- /**
72
- * Plays the movie
73
- * @return fulfilled when the movie is done playing, never fails
74
- */
75
- play(): Promise<void>;
76
- /**
77
- * Plays the movie in the background and records it
78
- *
79
- * @param options
80
- * @param frameRate
81
- * @param [options.video=true] - whether to include video in recording
82
- * @param [options.audio=true] - whether to include audio in recording
83
- * @param [options.mediaRecorderOptions=undefined] - options to pass to the <code>MediaRecorder</code>
84
- * @param [options.type='video/webm'] - MIME type for exported video
85
- * constructor
86
- * @return resolves when done recording, rejects when internal media recorder errors
87
- */
88
- record(options: {
89
- frameRate: number;
90
- duration?: number;
91
- type?: string;
92
- video?: boolean;
93
- audio?: boolean;
94
- mediaRecorderOptions?: Record<string, unknown>;
95
- }): Promise<Blob>;
96
- /**
97
- * Stops the movie, without reseting the playback position
98
- * @return the movie (for chaining)
99
- */
100
- pause(): Movie;
101
- /**
102
- * Stops playback and resets the playback position
103
- * @return the movie (for chaining)
104
- */
105
- stop(): Movie;
106
- /**
107
- * @param [timestamp=performance.now()]
108
- * @param [done=undefined] - called when done playing or when the current frame is loaded
109
- * @private
110
- */
111
- private _render;
112
- private _updateCurrentTime;
113
- private _renderBackground;
114
- /**
115
- * @return whether or not video frames are loaded
116
- * @param [timestamp=performance.now()]
117
- * @private
118
- */
119
- private _renderLayers;
120
- private _applyEffects;
121
- /**
122
- * Refreshes the screen (only use this if auto-refresh is disabled)
123
- * @return - resolves when the frame is loaded
124
- */
125
- refresh(): Promise<null>;
126
- /**
127
- * Convienence method
128
- */
129
- private _publishToLayers;
130
- /**
131
- * If the movie is playing, recording or refreshing
132
- */
133
- get rendering(): boolean;
134
- /**
135
- * If the movie is refreshing current frame
136
- */
137
- get renderingFrame(): boolean;
138
- /**
139
- * If the movie is recording
140
- */
141
- get recording(): boolean;
142
- /**
143
- * The combined duration of all layers
144
- */
145
- get duration(): number;
146
- /**
147
- * Convienence method for <code>layers.push()</code>
148
- * @param layer
149
- * @return the movie
150
- */
151
- addLayer(layer: BaseLayer): Movie;
152
- /**
153
- * Convienence method for <code>effects.push()</code>
154
- * @param effect
155
- * @return the movie
156
- */
157
- addEffect(effect: BaseEffect): Movie;
158
- /**
159
- */
160
- get paused(): boolean;
161
- /**
162
- * If the playback position is at the end of the movie
163
- */
164
- get ended(): boolean;
165
- /**
166
- * The current playback position
167
- */
168
- get currentTime(): number;
169
- set currentTime(time: number);
170
- /**
171
- * Sets the current playback position. This is a more powerful version of
172
- * `set currentTime`.
173
- *
174
- * @param time - the new cursor's time value in seconds
175
- * @param [refresh=true] - whether to render a single frame
176
- * @return resolves when the current frame is rendered if
177
- * <code>refresh</code> is true, otherwise resolves immediately
178
- *
179
- */
180
- setCurrentTime(time: number, refresh?: boolean): Promise<void>;
181
- /**
182
- * The rendering canvas
183
- */
184
- get canvas(): HTMLCanvasElement;
185
- /**
186
- * The rendering canvas's context
187
- */
188
- get cctx(): CanvasRenderingContext2D;
189
- /**
190
- * The width of the rendering canvas
191
- */
192
- get width(): number;
193
- set width(width: number);
194
- /**
195
- * The height of the rendering canvas
196
- */
197
- get height(): number;
198
- set height(height: number);
199
- get movie(): Movie;
200
- getDefaultOptions(): MovieOptions;
201
- }