etro 0.7.0 → 0.8.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 (106) hide show
  1. package/.github/workflows/nodejs.yml +1 -1
  2. package/CHANGELOG.md +45 -1
  3. package/CONTRIBUTING.md +23 -19
  4. package/README.md +81 -26
  5. package/dist/effect/base.d.ts +38 -0
  6. package/dist/effect/brightness.d.ts +16 -0
  7. package/dist/effect/channels.d.ts +23 -0
  8. package/dist/effect/chroma-key.d.ts +23 -0
  9. package/dist/effect/contrast.d.ts +15 -0
  10. package/dist/effect/elliptical-mask.d.ts +31 -0
  11. package/dist/effect/gaussian-blur.d.ts +60 -0
  12. package/dist/effect/grayscale.d.ts +7 -0
  13. package/dist/effect/index.d.ts +15 -0
  14. package/dist/effect/pixelate.d.ts +18 -0
  15. package/dist/effect/shader.d.ts +99 -0
  16. package/dist/effect/stack.d.ts +23 -0
  17. package/dist/effect/transform.d.ts +73 -0
  18. package/dist/etro-cjs.js +9337 -3331
  19. package/dist/etro-iife.js +9279 -3273
  20. package/dist/etro.d.ts +7 -0
  21. package/dist/event.d.ts +35 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/layer/audio-source.d.ts +24 -0
  24. package/dist/layer/audio.d.ts +14 -0
  25. package/dist/layer/base.d.ts +69 -0
  26. package/dist/layer/image.d.ts +6 -0
  27. package/dist/layer/index.d.ts +11 -0
  28. package/dist/layer/text.d.ts +60 -0
  29. package/dist/layer/video.d.ts +11 -0
  30. package/dist/layer/visual-source.d.ts +32 -0
  31. package/dist/layer/visual.d.ts +58 -0
  32. package/dist/movie.d.ts +192 -0
  33. package/dist/object.d.ts +12 -0
  34. package/dist/util.d.ts +125 -0
  35. package/eslint.conf.js +0 -8
  36. package/eslint.example-conf.js +9 -0
  37. package/eslint.typescript-conf.js +5 -0
  38. package/examples/application/readme-screenshot.html +12 -9
  39. package/examples/application/video-player.html +7 -7
  40. package/examples/application/webcam.html +6 -6
  41. package/examples/introduction/audio.html +30 -18
  42. package/examples/introduction/effects.html +14 -10
  43. package/examples/introduction/export.html +32 -25
  44. package/examples/introduction/functions.html +6 -4
  45. package/examples/introduction/hello-world1.html +9 -5
  46. package/examples/introduction/hello-world2.html +5 -5
  47. package/examples/introduction/keyframes.html +35 -23
  48. package/examples/introduction/media.html +26 -18
  49. package/examples/introduction/text.html +9 -5
  50. package/karma.conf.js +1 -1
  51. package/package.json +29 -13
  52. package/rollup.config.js +15 -4
  53. package/scripts/gen-effect-samples.html +29 -25
  54. package/scripts/save-effect-samples.js +14 -15
  55. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  56. package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
  57. package/spec/assets/effect/grayscale.png +0 -0
  58. package/spec/assets/effect/original.png +0 -0
  59. package/spec/assets/effect/pixelate.png +0 -0
  60. package/spec/assets/effect/transform/multiply.png +0 -0
  61. package/spec/assets/effect/transform/rotate.png +0 -0
  62. package/spec/assets/effect/transform/scale-fraction.png +0 -0
  63. package/spec/assets/effect/transform/scale.png +0 -0
  64. package/spec/assets/effect/transform/translate-fraction.png +0 -0
  65. package/spec/assets/effect/transform/translate.png +0 -0
  66. package/spec/effect.spec.js +126 -57
  67. package/spec/event.spec.js +14 -0
  68. package/spec/layer.spec.js +175 -18
  69. package/spec/movie.spec.js +191 -7
  70. package/spec/util.spec.js +14 -5
  71. package/src/effect/base.ts +96 -0
  72. package/src/effect/brightness.ts +43 -0
  73. package/src/effect/channels.ts +50 -0
  74. package/src/effect/chroma-key.ts +82 -0
  75. package/src/effect/contrast.ts +42 -0
  76. package/src/effect/elliptical-mask.ts +75 -0
  77. package/src/effect/gaussian-blur.ts +232 -0
  78. package/src/effect/grayscale.ts +34 -0
  79. package/src/effect/index.ts +22 -0
  80. package/src/effect/pixelate.ts +59 -0
  81. package/src/effect/shader.ts +561 -0
  82. package/src/effect/stack.ts +74 -0
  83. package/src/effect/transform.ts +194 -0
  84. package/src/etro.ts +26 -0
  85. package/src/event.ts +118 -0
  86. package/src/index.ts +8 -0
  87. package/src/layer/audio-source.ts +217 -0
  88. package/src/layer/audio.ts +35 -0
  89. package/src/layer/base.ts +156 -0
  90. package/src/layer/image.ts +8 -0
  91. package/src/layer/index.ts +13 -0
  92. package/src/layer/text.ts +138 -0
  93. package/src/layer/video.ts +15 -0
  94. package/src/layer/visual-source.ts +150 -0
  95. package/src/layer/visual.ts +198 -0
  96. package/src/movie.ts +709 -0
  97. package/src/object.ts +14 -0
  98. package/src/util.ts +473 -0
  99. package/tsconfig.json +8 -0
  100. package/screenshots/2019-08-17_0.png +0 -0
  101. package/src/effect.js +0 -1268
  102. package/src/event.js +0 -78
  103. package/src/index.js +0 -23
  104. package/src/layer.js +0 -897
  105. package/src/movie.js +0 -637
  106. package/src/util.js +0 -505
@@ -70,7 +70,8 @@ function compareImageData (original, effect, path) {
70
70
  return new Promise(resolve => {
71
71
  const result = copyCanvas(original)
72
72
  const ctx = result.getContext('2d')
73
- effect.apply({ canvas: result, cctx: ctx, movie: new etro.Movie(dummyCanvas) }) // movie should be unique, to prevent caching!
73
+ const dummyMovie = new etro.Movie({ canvas: dummyCanvas })
74
+ effect.apply({ canvas: result, cctx: ctx, movie: dummyMovie }) // movie should be unique, to prevent caching!
74
75
  const actual = ctx.getImageData(0, 0, result.width, result.height)
75
76
 
76
77
  getImageData(path).then(expected => {
@@ -114,7 +115,7 @@ describe('Effects', function () {
114
115
  let effect
115
116
 
116
117
  beforeEach(function () {
117
- effect = new etro.effect.Base(0, 3)
118
+ effect = new etro.effect.Base()
118
119
  })
119
120
 
120
121
  it("should be of type 'effect'", function () {
@@ -129,19 +130,46 @@ describe('Effects', function () {
129
130
  })
130
131
 
131
132
  describe('Stack', function () {
132
- let effects, stack
133
+ let stack
133
134
 
134
135
  beforeEach(function () {
135
- effects = [
136
- new etro.effect.Brightness(10),
137
- new etro.effect.Contrast(1.5)
136
+ const effects = [
137
+ new etro.effect.Brightness({ brightness: 10 }),
138
+ new etro.effect.Contrast({ contrast: 1.5 })
138
139
  ]
139
- stack = new etro.effect.Stack(effects)
140
- stack.attach(new etro.Movie(dummyCanvas))
140
+ stack = new etro.effect.Stack({ effects })
141
+ stack.attach(new etro.Movie({ canvas: dummyCanvas }))
141
142
  })
142
143
 
143
144
  it('should attach its children to the target when attached', function () {
144
- expect(effects.every(child => child._target === stack._target)).toBe(true)
145
+ expect(stack.effects.every(child => child._target === stack._target)).toBe(true)
146
+ })
147
+
148
+ it('should attach a new child', function () {
149
+ const child = new etro.effect.Grayscale()
150
+ spyOn(child, 'attach')
151
+
152
+ stack.effects.push(child)
153
+
154
+ expect(child.attach).toHaveBeenCalled()
155
+ })
156
+
157
+ it('should detach each child that is removed', function () {
158
+ const child = stack.effects[0]
159
+ spyOn(child, 'detach')
160
+
161
+ stack.effects.shift() // remove first element
162
+
163
+ expect(child.detach).toHaveBeenCalled()
164
+ })
165
+
166
+ it('should detach a child that is replaced', function () {
167
+ const child = stack.effects[0]
168
+ spyOn(child, 'detach')
169
+
170
+ stack.effects[0] = new etro.effect.Base()
171
+
172
+ expect(child.detach).toHaveBeenCalled()
145
173
  })
146
174
 
147
175
  it('should be the same as applying individual effects', function () {
@@ -149,18 +177,38 @@ describe('Effects', function () {
149
177
  const result = copyCanvas(original)
150
178
  const resultCtx = result.getContext('2d')
151
179
 
152
- effects.forEach(effect => effect.apply({
153
- canvas: result, cctx: resultCtx, movie: new etro.Movie(dummyCanvas)
180
+ stack.effects.forEach(effect => effect.apply({
181
+ canvas: result, cctx: resultCtx, movie: new etro.Movie({ canvas: dummyCanvas })
154
182
  }))
155
183
  const expected = resultCtx.getImageData(0, 0, result.width, result.height)
156
184
 
157
185
  resultCtx.drawImage(original, 0, 0) // reset
158
186
  stack.apply({
159
- canvas: result, cctx: resultCtx, movie: new etro.Movie(dummyCanvas)
187
+ canvas: result, cctx: resultCtx, movie: new etro.Movie({ canvas: dummyCanvas })
160
188
  })
161
189
  const actual = resultCtx.getImageData(0, 0, result.width, result.height)
162
190
  expect(actual).toEqual(expected)
163
191
  })
192
+
193
+ it('children array should implement common array methods', function () {
194
+ const dummy = () => new etro.effect.Base()
195
+ const calls = {
196
+ concat: [[dummy()]],
197
+ every: [layer => true],
198
+ includes: [dummy()],
199
+ pop: [],
200
+ push: [dummy()],
201
+ unshift: [dummy()]
202
+ }
203
+ for (const method in calls) {
204
+ const args = calls[method]
205
+ const copy = [...stack.effects]
206
+ const expectedResult = Array.prototype[method].apply(copy, args)
207
+ const actualResult = stack.effects[method](...args)
208
+ expect(actualResult).toEqual(expectedResult)
209
+ expect(stack.effects).toEqual(copy)
210
+ }
211
+ })
164
212
  })
165
213
 
166
214
  describe('Shader', function () {
@@ -168,7 +216,7 @@ describe('Effects', function () {
168
216
 
169
217
  beforeEach(function () {
170
218
  effect = new etro.effect.Shader()
171
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
219
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
172
220
  })
173
221
 
174
222
  it('should construct', function () {})
@@ -176,7 +224,8 @@ describe('Effects', function () {
176
224
  it('should not change the target if no arguments are passed', function () {
177
225
  const { ctx, imageData: originalData } = createRandomCanvas(2)
178
226
  // apply effect to a fake layer containing `ctx`
179
- effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
227
+ const dummyMovie = new etro.Movie({ canvas: dummyCanvas })
228
+ effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: dummyMovie })
180
229
  // Verify no change
181
230
  const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
182
231
  expect(imageData).toEqual(originalData)
@@ -185,11 +234,12 @@ describe('Effects', function () {
185
234
 
186
235
  describe('Brightness', function () {
187
236
  it('should change the brightness', function () {
188
- const effect = new etro.effect.Brightness(5)
189
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
237
+ const effect = new etro.effect.Brightness({ brightness: 5 })
238
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
190
239
  const ctx = createPixel(RED)
191
240
  // Apply effect to a fake layer containing `ctx`
192
- effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
241
+ const dummyMovie = new etro.Movie({ canvas: dummyCanvas })
242
+ effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: dummyMovie })
193
243
  // Verify brightness changed
194
244
  const imageData = ctx.getImageData(0, 0, 1, 1)
195
245
  expect(imageData.data).toEqual(RED.map((c, i) => c % 4 === 3 ? c
@@ -199,11 +249,12 @@ describe('Effects', function () {
199
249
 
200
250
  describe('Contrast', function () {
201
251
  it('should change the contrast', function () {
202
- const effect = new etro.effect.Contrast(5)
203
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
252
+ const effect = new etro.effect.Contrast({ contrast: 5 })
253
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
204
254
  const ctx = createPixel(RED)
205
255
  // Apply effect to a fake layer containing `ctx`
206
- effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
256
+ const dummyMovie = new etro.Movie({ canvas: dummyCanvas })
257
+ effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: dummyMovie })
207
258
  // Verify brightness changed
208
259
  const imageData = ctx.getImageData(0, 0, 1, 1)
209
260
  expect(imageData.data).toEqual(RED.map((c, i) => c % 4 === 3 ? c
@@ -211,13 +262,26 @@ describe('Effects', function () {
211
262
  })
212
263
  })
213
264
 
265
+ describe('Grayscale', function () {
266
+ it('should desaturate the target', function (done) {
267
+ const effect = new etro.effect.Grayscale()
268
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
269
+ const path = 'grayscale.png'
270
+ whenOriginalLoaded(original =>
271
+ compareImageData(original, effect, path).then(done))
272
+ })
273
+ })
274
+
214
275
  describe('Channels', function () {
215
276
  it('should multiply each channel by a constant', function () {
216
- const effect = new etro.effect.Channels({ r: 0.5, g: 1.25, b: 2 })
217
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
277
+ const effect = new etro.effect.Channels({
278
+ channels: { r: 0.5, g: 1.25, b: 2 }
279
+ })
280
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
218
281
  const ctx = createPixel(RED)
219
282
  // Apply effect to a fake layer containing `ctx`
220
- effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
283
+ const dummyMovie = new etro.Movie({ canvas: dummyCanvas })
284
+ effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: dummyMovie })
221
285
  // Verify brightness changed
222
286
  const imageData = ctx.getImageData(0, 0, 1, 1)
223
287
  expect(imageData.data).toEqual(new Uint8ClampedArray([
@@ -233,14 +297,17 @@ describe('Effects', function () {
233
297
  let effect
234
298
 
235
299
  beforeEach(function () {
236
- effect = new etro.effect.ChromaKey({ r: 250 }, 5) // will hit r=255, because threshold is 5
237
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
300
+ effect = new etro.effect.ChromaKey({
301
+ target: { r: 250 },
302
+ threshold: 5
303
+ }) // will hit r=255, because threshold is 5
304
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
238
305
  })
239
306
 
240
307
  it('should make the target color transparent', function () {
241
308
  const ctx = createPixel(RED)
242
309
  // Apply effect to a fake layer containing `ctx`
243
- effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
310
+ effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie({ canvas: dummyCanvas }) })
244
311
  // Verify brightness changed
245
312
  const imageData = ctx.getImageData(0, 0, 1, 1)
246
313
  const alpha = imageData.data[3]
@@ -250,7 +317,7 @@ describe('Effects', function () {
250
317
  it('should not make other colors transparent', function () {
251
318
  const ctx = createPixel(BLUE)
252
319
  // Apply effect to a fake layer containing `ctx`
253
- effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
320
+ effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie({ canvas: dummyCanvas }) })
254
321
  // Verify brightness changed
255
322
  const imageData = ctx.getImageData(0, 0, 1, 1)
256
323
  const alpha = imageData.data[3]
@@ -260,8 +327,8 @@ describe('Effects', function () {
260
327
 
261
328
  describe('GaussianBlurHorizontal', function () {
262
329
  it('should blur with 5-pixel radius', function (done) {
263
- const effect = new etro.effect.GaussianBlurHorizontal(5)
264
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
330
+ const effect = new etro.effect.GaussianBlurHorizontal({ radius: 5 })
331
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
265
332
  const path = 'gaussian-blur-horizontal.png'
266
333
  whenOriginalLoaded(original =>
267
334
  compareImageData(original, effect, path).then(done))
@@ -270,8 +337,8 @@ describe('Effects', function () {
270
337
 
271
338
  describe('GaussianBlurVertical', function () {
272
339
  it('should blur with 5-pixel radius', function (done) {
273
- const effect = new etro.effect.GaussianBlurVertical(5)
274
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
340
+ const effect = new etro.effect.GaussianBlurVertical({ radius: 5 })
341
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
275
342
  const path = 'gaussian-blur-vertical.png'
276
343
  whenOriginalLoaded(original =>
277
344
  compareImageData(original, effect, path).then(done))
@@ -280,8 +347,8 @@ describe('Effects', function () {
280
347
 
281
348
  describe('Pixelate', function () {
282
349
  it('should decimate to 3-pixel texels', function (done) {
283
- const effect = new etro.effect.Pixelate(3)
284
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
350
+ const effect = new etro.effect.Pixelate({ pixelSize: 3 })
351
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
285
352
  const path = 'pixelate.png'
286
353
  whenOriginalLoaded(original =>
287
354
  compareImageData(original, effect, path).then(done))
@@ -290,60 +357,62 @@ describe('Effects', function () {
290
357
 
291
358
  describe('Transform', function () {
292
359
  it('should translate', function (done) {
293
- const effect = new etro.effect.Transform(
294
- new etro.effect.Transform.Matrix().translate(-3, 5)
295
- )
296
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
360
+ const effect = new etro.effect.Transform({
361
+ matrix: new etro.effect.Transform.Matrix().translate(-3, 5)
362
+ })
363
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
297
364
  const path = 'transform/translate.png'
298
365
  whenOriginalLoaded(original =>
299
366
  compareImageData(original, effect, path).then(done))
300
367
  })
301
368
 
302
369
  it('should translate by non-integers', function (done) {
303
- const effect = new etro.effect.Transform(
304
- new etro.effect.Transform.Matrix().translate(0.5, 0.5)
305
- )
306
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
370
+ const effect = new etro.effect.Transform({
371
+ matrix: new etro.effect.Transform.Matrix().translate(0.5, 0.5)
372
+ })
373
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
307
374
  const path = 'transform/translate-fraction.png'
308
375
  whenOriginalLoaded(original =>
309
376
  compareImageData(original, effect, path).then(done))
310
377
  })
311
378
 
312
379
  it('should scale', function (done) {
313
- const effect = new etro.effect.Transform(
314
- new etro.effect.Transform.Matrix().scale(2, 2)
315
- )
316
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
380
+ const effect = new etro.effect.Transform({
381
+ matrix: new etro.effect.Transform.Matrix().scale(2, 2)
382
+ })
383
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
317
384
  const path = 'transform/scale.png'
318
385
  whenOriginalLoaded(original =>
319
386
  compareImageData(original, effect, path).then(done))
320
387
  })
321
388
 
322
389
  it('should scale by non-integers', function (done) {
323
- const effect = new etro.effect.Transform(
324
- new etro.effect.Transform.Matrix().scale(0.5, 0.5)
325
- )
326
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
390
+ const effect = new etro.effect.Transform({
391
+ matrix: new etro.effect.Transform.Matrix().scale(0.5, 0.5)
392
+ })
393
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
327
394
  const path = 'transform/scale-fraction.png'
328
395
  whenOriginalLoaded(original =>
329
396
  compareImageData(original, effect, path).then(done))
330
397
  })
331
398
 
332
399
  it('should rotate', function (done) {
333
- const effect = new etro.effect.Transform(
334
- new etro.effect.Transform.Matrix().rotate(Math.PI / 6)
335
- )
336
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
400
+ const effect = new etro.effect.Transform({
401
+ matrix: new etro.effect.Transform.Matrix().rotate(Math.PI / 6)
402
+ })
403
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
337
404
  const path = 'transform/rotate.png'
338
405
  whenOriginalLoaded(original =>
339
406
  compareImageData(original, effect, path).then(done))
340
407
  })
341
408
 
342
409
  it('should multiply together', function (done) {
343
- const effect = new etro.effect.Transform(
344
- new etro.effect.Transform.Matrix().scale(2, 2)
345
- .multiply(new etro.effect.Transform.Matrix().translate(-3, 5)))
346
- effect._target = new etro.Movie(dummyCanvas) // so val doesn't break because it can't cache (it requires a movie)
410
+ const effect = new etro.effect.Transform({
411
+ matrix: new etro.effect.Transform.Matrix()
412
+ .scale(2, 2)
413
+ .multiply(new etro.effect.Transform.Matrix().translate(-3, 5))
414
+ })
415
+ effect._target = new etro.Movie({ canvas: dummyCanvas }) // so val doesn't break because it can't cache (it requires a movie)
347
416
  const path = 'transform/multiply.png'
348
417
  whenOriginalLoaded(original =>
349
418
  compareImageData(original, effect, path).then(done))
@@ -22,4 +22,18 @@ describe('Events', function () {
22
22
  etro.event.publish(o, 'foo', {})
23
23
  expect(notified).toEqual(types.slice(2))
24
24
  })
25
+
26
+ it('unsubscribe removes event listeners', function () {
27
+ const o = {}
28
+ let listenerCalled = false
29
+ const listener = () => {
30
+ listenerCalled = true
31
+ }
32
+
33
+ etro.event.subscribe(o, 'test', listener)
34
+ etro.event.unsubscribe(o, listener)
35
+ etro.event.publish(o, 'test', {})
36
+
37
+ expect(listenerCalled).toBe(false)
38
+ })
25
39
  })
@@ -3,7 +3,7 @@ describe('Layers', function () {
3
3
  let layer
4
4
 
5
5
  beforeEach(function () {
6
- layer = new etro.layer.Base(0, 4)
6
+ layer = new etro.layer.Base({ startTime: 0, duration: 4 })
7
7
  })
8
8
 
9
9
  it("should be of type 'layer'", function () {
@@ -37,11 +37,35 @@ describe('Layers', function () {
37
37
  let layer
38
38
 
39
39
  beforeEach(function () {
40
- layer = new etro.layer.Visual(0, 4, { background: 'blue' })
41
- layer.attach(
42
- { width: 400, height: 400, currentTime: 0, movie: {}, propertyFilters: {} }
43
- )
40
+ layer = new etro.layer.Visual({ startTime: 0, duration: 4, background: 'blue' })
41
+ const movie = { width: 400, height: 400, currentTime: 0, propertyFilters: {} }
42
+ movie.movie = movie
43
+ layer.attach(movie)
44
44
  layer.render(0)
45
+ // Clear cache populated by render()
46
+ etro.clearCachedValues(movie)
47
+ })
48
+
49
+ it("should use the movie's width when no layer width is given", function () {
50
+ const width = etro.val(layer, 'width', 0)
51
+ expect(width).toBe(layer.movie.width)
52
+ })
53
+
54
+ it("should use the movie's height when no layer height is given", function () {
55
+ const height = etro.val(layer, 'height', 0)
56
+ expect(height).toBe(layer.movie.height)
57
+ })
58
+
59
+ it('should use the width if provided', function () {
60
+ layer.width = 4
61
+ const width = etro.val(layer, 'width', 0)
62
+ expect(width).toBe(4)
63
+ })
64
+
65
+ it('should use the height if provided', function () {
66
+ layer.height = 4
67
+ const height = etro.val(layer, 'height', 0)
68
+ expect(height).toBe(4)
45
69
  })
46
70
 
47
71
  it('should render the background', function () {
@@ -56,36 +80,96 @@ describe('Layers', function () {
56
80
  }
57
81
  expect(allBlue).toBe(true)
58
82
  })
83
+
84
+ it('should call `attach` when an effect is added', function () {
85
+ const effect = new etro.effect.Base()
86
+ spyOn(effect, 'attach')
87
+ layer.effects.push(effect)
88
+ expect(effect.attach).toHaveBeenCalled()
89
+ })
90
+
91
+ it('should call `detach` when an effect is removed', function () {
92
+ const effect = new etro.effect.Base()
93
+ layer.effects.push(effect)
94
+ spyOn(effect, 'detach')
95
+ layer.effects.pop()
96
+ expect(effect.detach).toHaveBeenCalled()
97
+ })
98
+
99
+ it('should call `detach` when an effect is replaced', function () {
100
+ const effect = new etro.effect.Base()
101
+ layer.effects.push(effect)
102
+ spyOn(effect, 'detach')
103
+ layer.effects[0] = new etro.effect.Base()
104
+ expect(effect.detach).toHaveBeenCalled()
105
+ })
59
106
  })
60
107
 
61
- describe('Image', function () {
108
+ describe('VisualSource', function () {
109
+ const CustomVisualSource = etro.layer.VisualSourceMixin(etro.layer.Visual)
62
110
  let layer
63
111
 
64
112
  beforeEach(function (done) {
65
113
  const image = new Image()
66
114
  image.src = '/base/spec/assets/layer/image.jpg'
67
115
  image.onload = () => {
68
- layer = new etro.layer.Image(0, 4, image)
116
+ layer = new CustomVisualSource({ startTime: 0, duration: 4, source: image })
69
117
  // Simulate attach to movie
70
- layer.attach(
71
- { width: image.width, height: image.height, currentTime: 0 }
72
- )
118
+ const movie = { width: image.width, height: image.height, currentTime: 0, propertyFilters: [] }
119
+ movie.movie = movie
120
+ layer.attach(movie)
73
121
  done()
74
122
  }
75
123
  })
76
124
 
125
+ it("should use the image source's width when no sourceWidth is provided", function () {
126
+ const sourceWidth = etro.val(layer, 'sourceWidth', 0)
127
+ expect(sourceWidth).toBe(layer.source.width)
128
+ })
129
+
130
+ it("should use the image source's height when no sourceHeight is provided", function () {
131
+ const sourceHeight = etro.val(layer, 'sourceHeight', 0)
132
+ expect(sourceHeight).toBe(layer.source.height)
133
+ })
134
+
135
+ it('should use sourceWidth when no destWidth is provided', function () {
136
+ const destWidth = etro.val(layer, 'destWidth', 0)
137
+ const sourceWidth = etro.val(layer, 'sourceWidth', 0)
138
+ expect(destWidth).toBe(sourceWidth)
139
+ })
140
+
141
+ it('should use sourceHeight when no destHeight is provided', function () {
142
+ const destHeight = etro.val(layer, 'destHeight', 0)
143
+ const sourceHeight = etro.val(layer, 'sourceHeight', 0)
144
+ expect(destHeight).toBe(sourceHeight)
145
+ })
146
+
147
+ it('should use destWidth when no width is provided', function () {
148
+ const width = etro.val(layer, 'width', 0)
149
+ const destWidth = etro.val(layer, 'destWidth', 0)
150
+ expect(width).toBe(destWidth)
151
+ })
152
+
153
+ it('should use destHeight when no height is provided', function () {
154
+ const height = etro.val(layer, 'height', 0)
155
+ const destHeight = etro.val(layer, 'destHeight', 0)
156
+ expect(height).toBe(destHeight)
157
+ })
158
+
77
159
  it('should render', function () {
78
160
  // Render layer (actual outcome)
79
161
  layer.render(0)
80
- const imageData = layer.cctx.getImageData(0, 0, layer.width, layer.height)
162
+ const width = etro.val(layer, 'width', 0)
163
+ const height = etro.val(layer, 'height', 0)
164
+ const imageData = layer.cctx.getImageData(0, 0, width, height)
81
165
 
82
166
  // Draw image (expected outcome)
83
167
  const testCanv = document.createElement('canvas')
84
- testCanv.width = layer.width
85
- testCanv.height = layer.height
168
+ testCanv.width = width
169
+ testCanv.height = height
86
170
  const testCtx = testCanv.getContext('2d')
87
- testCtx.drawImage(layer.image, 0, 0, layer.width, layer.height)
88
- const testImageData = testCtx.getImageData(0, 0, layer.width, layer.height)
171
+ testCtx.drawImage(layer.source, 0, 0, width, height)
172
+ const testImageData = testCtx.getImageData(0, 0, width, height)
89
173
 
90
174
  // Compare expected outcome with actual outcome
91
175
  let equal = true
@@ -94,12 +178,75 @@ describe('Layers', function () {
94
178
  }
95
179
  expect(equal).toBe(true)
96
180
  })
181
+
182
+ it('should scale with `imageWidth` and `imageHeight`', function () {
183
+ const resizedLayer = new etro.layer.Image({
184
+ startTime: 0,
185
+ duration: 1,
186
+ source: layer.source,
187
+ destWidth: 100,
188
+ destHeight: 100
189
+ })
190
+
191
+ // Render layer (actual outcome)
192
+ const movie = {}
193
+ resizedLayer.attach(movie)
194
+ resizedLayer.render(0)
195
+ const imageData = resizedLayer.cctx.getImageData(0, 0, resizedLayer.destWidth, resizedLayer.destHeight)
196
+
197
+ // Draw image (expected outcome)
198
+ const testCanv = document.createElement('canvas')
199
+ testCanv.width = resizedLayer.destWidth
200
+ testCanv.height = resizedLayer.destHeight
201
+ const testCtx = testCanv.getContext('2d')
202
+ testCtx.drawImage(layer.source, 0, 0, resizedLayer.destWidth, resizedLayer.destHeight)
203
+ const testImageData = testCtx.getImageData(0, 0, resizedLayer.destWidth, resizedLayer.destHeight)
204
+
205
+ // Compare expected outcome with actual outcome
206
+ expect(imageData.data).toEqual(testImageData.data)
207
+ })
208
+
209
+ it('should be cropped to the clip with and height', function () {
210
+ const newLayer = new etro.layer.Image({
211
+ startTime: 0,
212
+ duration: 1,
213
+ source: layer.source,
214
+ sourceWidth: 2,
215
+ sourceHeight: 3
216
+ })
217
+
218
+ // Render layer (actual outcome)
219
+ const movie = {}
220
+ newLayer.attach(movie)
221
+ newLayer.render(0)
222
+ const imageData = newLayer.cctx.getImageData(
223
+ 0, 0, newLayer.sourceWidth, newLayer.sourceHeight
224
+ )
225
+
226
+ // Draw image (expected outcome)
227
+ // testCanv will contain the part of the layer with the image.
228
+ const testCanv = document.createElement('canvas')
229
+ testCanv.width = newLayer.sourceWidth
230
+ testCanv.height = newLayer.sourceHeight
231
+ const testCtx = testCanv.getContext('2d')
232
+ testCtx.drawImage(
233
+ layer.source,
234
+ 0, 0,
235
+ newLayer.sourceWidth, newLayer.sourceHeight,
236
+ 0, 0,
237
+ newLayer.sourceWidth, newLayer.sourceHeight
238
+ )
239
+ const testImageData = testCtx.getImageData(0, 0, newLayer.sourceWidth, newLayer.sourceHeight)
240
+
241
+ // Compare expected image data with actual image data
242
+ expect(imageData.data).toEqual(testImageData.data)
243
+ })
97
244
  })
98
245
 
99
246
  describe('Media', function () {
100
247
  let layer
101
248
  // Media is an abstract mixin, so make a concrete subclass here.
102
- const CustomMedia = etro.layer.MediaMixin(etro.layer.Base)
249
+ const CustomMedia = etro.layer.AudioSourceMixin(etro.layer.Base)
103
250
  const source = new Audio()
104
251
 
105
252
  beforeAll(function (done) {
@@ -108,7 +255,17 @@ describe('Layers', function () {
108
255
  })
109
256
 
110
257
  beforeEach(function () {
111
- layer = new CustomMedia(0, source)
258
+ layer = new CustomMedia({ startTime: 0, source })
259
+ })
260
+
261
+ it('should update its currentTime when the movie seeks', function () {
262
+ const movie = {
263
+ actx: new AudioContext(),
264
+ currentTime: 2 // not 0
265
+ }
266
+ layer.attach(movie)
267
+ etro.event.publish(movie, 'movie.seek', {})
268
+ expect(layer.currentTime).toBe(2)
112
269
  })
113
270
 
114
271
  it('should have its duration depend on its playbackRate', function () {
@@ -137,7 +294,7 @@ describe('Layers', function () {
137
294
 
138
295
  it('should play', function () {
139
296
  let timesPlayed = 0
140
- layer.media.addEventListener('play', () => {
297
+ layer.source.addEventListener('play', () => {
141
298
  timesPlayed++
142
299
  })
143
300
  for (let i = 0; i < 3; i++) {