etro 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +2 -0
- package/.github/workflows/nodejs.yml +27 -0
- package/CHANGELOG.md +109 -0
- package/CODE_OF_CONDUCT.md +77 -0
- package/CONTRIBUTING.md +155 -0
- package/LICENSE +674 -0
- package/README.md +57 -0
- package/dist/etro.js +3397 -0
- package/docs/effect.js.html +1215 -0
- package/docs/event.js.html +145 -0
- package/docs/index.html +81 -0
- package/docs/index.js.html +92 -0
- package/docs/layer.js.html +888 -0
- package/docs/module-effect-GaussianBlurComponent.html +345 -0
- package/docs/module-effect.Brightness.html +339 -0
- package/docs/module-effect.Channels.html +319 -0
- package/docs/module-effect.ChromaKey.html +611 -0
- package/docs/module-effect.Contrast.html +339 -0
- package/docs/module-effect.EllipticalMask.html +200 -0
- package/docs/module-effect.GaussianBlur.html +202 -0
- package/docs/module-effect.GaussianBlurHorizontal.html +242 -0
- package/docs/module-effect.GaussianBlurVertical.html +242 -0
- package/docs/module-effect.Pixelate.html +330 -0
- package/docs/module-effect.Shader.html +1227 -0
- package/docs/module-effect.Stack.html +406 -0
- package/docs/module-effect.Transform.Matrix.html +193 -0
- package/docs/module-effect.Transform.html +1174 -0
- package/docs/module-effect.html +148 -0
- package/docs/module-event.html +473 -0
- package/docs/module-index.html +186 -0
- package/docs/module-layer-Media.html +1116 -0
- package/docs/module-layer-MediaMixin.html +164 -0
- package/docs/module-layer.Audio.html +1188 -0
- package/docs/module-layer.Base.html +629 -0
- package/docs/module-layer.Image.html +1421 -0
- package/docs/module-layer.Text.html +1731 -0
- package/docs/module-layer.Video.html +1938 -0
- package/docs/module-layer.Visual.html +1698 -0
- package/docs/module-layer.html +137 -0
- package/docs/module-movie.html +3118 -0
- package/docs/module-util.Color.html +702 -0
- package/docs/module-util.Font.html +395 -0
- package/docs/module-util.html +845 -0
- package/docs/movie.js.html +689 -0
- package/docs/scripts/collapse.js +20 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +83 -0
- package/docs/styles/jsdoc.css +671 -0
- package/docs/styles/prettify.css +79 -0
- package/docs/util.js.html +503 -0
- package/eslint.conf.js +28 -0
- package/eslint.test-conf.js +4 -0
- package/examples/application/readme-screenshot.html +86 -0
- package/examples/application/video-player.html +131 -0
- package/examples/application/webcam.html +28 -0
- package/examples/introduction/audio.html +52 -0
- package/examples/introduction/effects.html +56 -0
- package/examples/introduction/export.html +70 -0
- package/examples/introduction/functions.html +35 -0
- package/examples/introduction/hello-world1.html +33 -0
- package/examples/introduction/hello-world2.html +32 -0
- package/examples/introduction/keyframes.html +67 -0
- package/examples/introduction/media.html +55 -0
- package/examples/introduction/text.html +27 -0
- package/jsdoc.conf.json +3 -0
- package/karma.conf.js +60 -0
- package/package.json +63 -0
- package/private-todo.txt +70 -0
- package/rename-file.sh +18 -0
- package/rename-versions.sh +14 -0
- package/rename.sh +22 -0
- package/rollup.config.js +31 -0
- package/screenshots/2019-08-17_0.png +0 -0
- package/scripts/gen-effect-samples.html +99 -0
- package/scripts/save-effect-samples.js +43 -0
- package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
- package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
- package/spec/assets/effect/original.png +0 -0
- package/spec/assets/effect/pixelate.png +0 -0
- package/spec/assets/effect/transform/multiply.png +0 -0
- package/spec/assets/effect/transform/rotate.png +0 -0
- package/spec/assets/effect/transform/scale-fraction.png +0 -0
- package/spec/assets/effect/transform/scale.png +0 -0
- package/spec/assets/effect/transform/translate-fraction.png +0 -0
- package/spec/assets/effect/transform/translate.png +0 -0
- package/spec/assets/layer/audio.wav +0 -0
- package/spec/assets/layer/image.jpg +0 -0
- package/spec/effect.spec.js +352 -0
- package/spec/event.spec.js +25 -0
- package/spec/layer.spec.js +128 -0
- package/spec/movie.spec.js +154 -0
- package/spec/util.spec.js +285 -0
- package/src/effect.js +1265 -0
- package/src/event.js +78 -0
- package/src/index.js +23 -0
- package/src/layer.js +875 -0
- package/src/movie.js +636 -0
- package/src/util.js +487 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
const RED = new Uint8ClampedArray([255, 0, 0, 255])
|
|
2
|
+
const BLUE = new Uint8ClampedArray([0, 0, 255, 255])
|
|
3
|
+
|
|
4
|
+
const clamp = (x, a, b) => Math.max(Math.min(x, b), a)
|
|
5
|
+
|
|
6
|
+
function createPixel (colorData) {
|
|
7
|
+
// Create 1x1 canvas and set pixel to red
|
|
8
|
+
const ctx = document.createElement('canvas')
|
|
9
|
+
.getContext('2d')
|
|
10
|
+
ctx.canvas.width = ctx.canvas.height = 1
|
|
11
|
+
const imageData = ctx.createImageData(1, 1)
|
|
12
|
+
for (let i = 0; i < imageData.data.length; i++) {
|
|
13
|
+
imageData.data[i] = colorData[i]
|
|
14
|
+
}
|
|
15
|
+
ctx.putImageData(imageData, 0, 0)
|
|
16
|
+
|
|
17
|
+
return ctx
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a square canvas with random opaque noise
|
|
22
|
+
* @param {number} size the width and height
|
|
23
|
+
* @return {TestCanvas}
|
|
24
|
+
*
|
|
25
|
+
* @typedef {Object} TestCanvas
|
|
26
|
+
* @property {CanvasRenderingContext2D} ctx
|
|
27
|
+
* @property {ImageData} imageData
|
|
28
|
+
*/
|
|
29
|
+
function createRandomCanvas (size) {
|
|
30
|
+
const ctx = document.createElement('canvas')
|
|
31
|
+
.getContext('2d')
|
|
32
|
+
ctx.canvas.width = ctx.canvas.height = size
|
|
33
|
+
// Create a grid of random colors
|
|
34
|
+
const imageData = ctx.createImageData(ctx.canvas.width, ctx.canvas.height)
|
|
35
|
+
// opaque so premultiplied alpha won't mess up the rgb comparisons
|
|
36
|
+
const data = imageData.data.map((_, i) => i % 4 === 3 ? 255 : Math.floor(256 * Math.random()))
|
|
37
|
+
for (let i = 0; i < data.length; i++) {
|
|
38
|
+
imageData.data[i] = data[i]
|
|
39
|
+
}
|
|
40
|
+
ctx.putImageData(imageData, 0, 0)
|
|
41
|
+
|
|
42
|
+
return { ctx, imageData }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getImageData (path, targetCanvas = undefined) {
|
|
46
|
+
return new Promise(resolve => {
|
|
47
|
+
targetCanvas = targetCanvas || document.createElement('canvas')
|
|
48
|
+
const img = new Image()
|
|
49
|
+
img.onload = () => {
|
|
50
|
+
targetCanvas.width = img.width
|
|
51
|
+
targetCanvas.height = img.height
|
|
52
|
+
const ctx = targetCanvas.getContext('2d')
|
|
53
|
+
ctx.drawImage(img, 0, 0)
|
|
54
|
+
resolve(ctx.getImageData(0, 0, img.width, img.height))
|
|
55
|
+
}
|
|
56
|
+
img.src = 'base/spec/assets/effect/' + path
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function copyCanvas (source) {
|
|
61
|
+
const dest = document.createElement('canvas')
|
|
62
|
+
dest.width = source.width
|
|
63
|
+
dest.height = source.height
|
|
64
|
+
dest.getContext('2d')
|
|
65
|
+
.drawImage(source, 0, 0)
|
|
66
|
+
return dest
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compareImageData (original, effect, path) {
|
|
70
|
+
return new Promise(resolve => {
|
|
71
|
+
const result = copyCanvas(original)
|
|
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!
|
|
74
|
+
const actual = ctx.getImageData(0, 0, result.width, result.height)
|
|
75
|
+
|
|
76
|
+
getImageData(path).then(expected => {
|
|
77
|
+
expect(actual).toEqual(expected)
|
|
78
|
+
resolve()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
* Don't reload the original image for each test, just once;
|
|
85
|
+
* However, Jasmine will exit if we don't start the tests synchronously
|
|
86
|
+
* So, start them, and then wait for the original image to load in the
|
|
87
|
+
* test
|
|
88
|
+
*/
|
|
89
|
+
const whenOriginalLoaded = (() => {
|
|
90
|
+
const original = document.createElement('canvas')
|
|
91
|
+
const loadedCallbacks = []
|
|
92
|
+
let loaded = false
|
|
93
|
+
getImageData('original.png', original).then(data => {
|
|
94
|
+
loaded = true
|
|
95
|
+
loadedCallbacks.forEach(callback => callback(original))
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
function whenOriginalLoaded (callback) {
|
|
99
|
+
if (!loaded) {
|
|
100
|
+
loadedCallbacks.push(callback)
|
|
101
|
+
} else {
|
|
102
|
+
callback(original)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return whenOriginalLoaded
|
|
106
|
+
})()
|
|
107
|
+
|
|
108
|
+
const dummyCanvas = document.createElement('canvas')
|
|
109
|
+
|
|
110
|
+
/* TESTS */
|
|
111
|
+
|
|
112
|
+
describe('Effects', function () {
|
|
113
|
+
describe('Base', function () {
|
|
114
|
+
let effect
|
|
115
|
+
|
|
116
|
+
beforeEach(function () {
|
|
117
|
+
effect = new etro.effect.Base(0, 3)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("should be of type 'effect'", function () {
|
|
121
|
+
expect(effect.type).toBe('effect')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should set _target when attached', function () {
|
|
125
|
+
const movie = {}
|
|
126
|
+
effect.attach(movie)
|
|
127
|
+
expect(effect._target).toBe(movie)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('Stack', function () {
|
|
132
|
+
let effects, stack
|
|
133
|
+
|
|
134
|
+
beforeEach(function () {
|
|
135
|
+
effects = [
|
|
136
|
+
new etro.effect.Brightness(10),
|
|
137
|
+
new etro.effect.Contrast(1.5)
|
|
138
|
+
]
|
|
139
|
+
stack = new etro.effect.Stack(effects)
|
|
140
|
+
stack.attach(new etro.Movie(dummyCanvas))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should attach its children to the target when attached', function () {
|
|
144
|
+
expect(effects.every(child => child._target === stack._target)).toBe(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should be the same as applying individual effects', function () {
|
|
148
|
+
const original = createRandomCanvas(4).ctx.canvas
|
|
149
|
+
const result = copyCanvas(original)
|
|
150
|
+
const resultCtx = result.getContext('2d')
|
|
151
|
+
|
|
152
|
+
effects.forEach(effect => effect.apply({
|
|
153
|
+
canvas: result, cctx: resultCtx, movie: new etro.Movie(dummyCanvas)
|
|
154
|
+
}))
|
|
155
|
+
const expected = resultCtx.getImageData(0, 0, result.width, result.height)
|
|
156
|
+
|
|
157
|
+
resultCtx.drawImage(original, 0, 0) // reset
|
|
158
|
+
stack.apply({
|
|
159
|
+
canvas: result, cctx: resultCtx, movie: new etro.Movie(dummyCanvas)
|
|
160
|
+
})
|
|
161
|
+
const actual = resultCtx.getImageData(0, 0, result.width, result.height)
|
|
162
|
+
expect(actual).toEqual(expected)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('Shader', function () {
|
|
167
|
+
let effect
|
|
168
|
+
|
|
169
|
+
beforeEach(function () {
|
|
170
|
+
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)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should construct', function () {})
|
|
175
|
+
|
|
176
|
+
it('should not change the target if no arguments are passed', function () {
|
|
177
|
+
const { ctx, imageData: originalData } = createRandomCanvas(2)
|
|
178
|
+
// apply effect to a fake layer containing `ctx`
|
|
179
|
+
effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
|
|
180
|
+
// Verify no change
|
|
181
|
+
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
|
|
182
|
+
expect(imageData).toEqual(originalData)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('Brightness', function () {
|
|
187
|
+
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)
|
|
190
|
+
const ctx = createPixel(RED)
|
|
191
|
+
// Apply effect to a fake layer containing `ctx`
|
|
192
|
+
effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
|
|
193
|
+
// Verify brightness changed
|
|
194
|
+
const imageData = ctx.getImageData(0, 0, 1, 1)
|
|
195
|
+
expect(imageData.data).toEqual(RED.map((c, i) => c % 4 === 3 ? c
|
|
196
|
+
: clamp(c + effect.brightness, 0, 255)))
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('Contrast', function () {
|
|
201
|
+
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)
|
|
204
|
+
const ctx = createPixel(RED)
|
|
205
|
+
// Apply effect to a fake layer containing `ctx`
|
|
206
|
+
effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
|
|
207
|
+
// Verify brightness changed
|
|
208
|
+
const imageData = ctx.getImageData(0, 0, 1, 1)
|
|
209
|
+
expect(imageData.data).toEqual(RED.map((c, i) => c % 4 === 3 ? c
|
|
210
|
+
: Math.round(clamp(effect.contrast * (c - 255 / 2), 0, 255))))
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('Channels', function () {
|
|
215
|
+
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)
|
|
218
|
+
const ctx = createPixel(RED)
|
|
219
|
+
// Apply effect to a fake layer containing `ctx`
|
|
220
|
+
effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
|
|
221
|
+
// Verify brightness changed
|
|
222
|
+
const imageData = ctx.getImageData(0, 0, 1, 1)
|
|
223
|
+
expect(imageData.data).toEqual(new Uint8ClampedArray([
|
|
224
|
+
Math.floor((effect.factors.r || 1) * RED[0]),
|
|
225
|
+
Math.floor((effect.factors.g || 1) * RED[1]),
|
|
226
|
+
Math.floor((effect.factors.b || 1) * RED[2]),
|
|
227
|
+
Math.floor((effect.factors.a || 1) * RED[3])
|
|
228
|
+
]))
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('ChromaKey', function () {
|
|
233
|
+
let effect
|
|
234
|
+
|
|
235
|
+
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)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('should make the target color transparent', function () {
|
|
241
|
+
const ctx = createPixel(RED)
|
|
242
|
+
// Apply effect to a fake layer containing `ctx`
|
|
243
|
+
effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
|
|
244
|
+
// Verify brightness changed
|
|
245
|
+
const imageData = ctx.getImageData(0, 0, 1, 1)
|
|
246
|
+
const alpha = imageData.data[3]
|
|
247
|
+
expect(alpha).toBe(0)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should not make other colors transparent', function () {
|
|
251
|
+
const ctx = createPixel(BLUE)
|
|
252
|
+
// Apply effect to a fake layer containing `ctx`
|
|
253
|
+
effect.apply({ canvas: ctx.canvas, cctx: ctx, movie: new etro.Movie(dummyCanvas) })
|
|
254
|
+
// Verify brightness changed
|
|
255
|
+
const imageData = ctx.getImageData(0, 0, 1, 1)
|
|
256
|
+
const alpha = imageData.data[3]
|
|
257
|
+
expect(alpha).toBe(255)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('GaussianBlurHorizontal', function () {
|
|
262
|
+
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)
|
|
265
|
+
const path = 'gaussian-blur-horizontal.png'
|
|
266
|
+
whenOriginalLoaded(original =>
|
|
267
|
+
compareImageData(original, effect, path).then(done))
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('GaussianBlurVertical', function () {
|
|
272
|
+
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)
|
|
275
|
+
const path = 'gaussian-blur-vertical.png'
|
|
276
|
+
whenOriginalLoaded(original =>
|
|
277
|
+
compareImageData(original, effect, path).then(done))
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
describe('Pixelate', function () {
|
|
282
|
+
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)
|
|
285
|
+
const path = 'pixelate.png'
|
|
286
|
+
whenOriginalLoaded(original =>
|
|
287
|
+
compareImageData(original, effect, path).then(done))
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe('Transform', function () {
|
|
292
|
+
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)
|
|
297
|
+
const path = 'transform/translate.png'
|
|
298
|
+
whenOriginalLoaded(original =>
|
|
299
|
+
compareImageData(original, effect, path).then(done))
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
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)
|
|
307
|
+
const path = 'transform/translate-fraction.png'
|
|
308
|
+
whenOriginalLoaded(original =>
|
|
309
|
+
compareImageData(original, effect, path).then(done))
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
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)
|
|
317
|
+
const path = 'transform/scale.png'
|
|
318
|
+
whenOriginalLoaded(original =>
|
|
319
|
+
compareImageData(original, effect, path).then(done))
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
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)
|
|
327
|
+
const path = 'transform/scale-fraction.png'
|
|
328
|
+
whenOriginalLoaded(original =>
|
|
329
|
+
compareImageData(original, effect, path).then(done))
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
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)
|
|
337
|
+
const path = 'transform/rotate.png'
|
|
338
|
+
whenOriginalLoaded(original =>
|
|
339
|
+
compareImageData(original, effect, path).then(done))
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
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)
|
|
347
|
+
const path = 'transform/multiply.png'
|
|
348
|
+
whenOriginalLoaded(original =>
|
|
349
|
+
compareImageData(original, effect, path).then(done))
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
describe('Events', function () {
|
|
2
|
+
it('should trigger subscribers', function () {
|
|
3
|
+
const o = {}
|
|
4
|
+
|
|
5
|
+
const types = ['foo.bar.test', 'foo.bar', 'foo']
|
|
6
|
+
types.forEach(type => {
|
|
7
|
+
etro.event.subscribe(o, type, event => {
|
|
8
|
+
expect(event.target).toEqual(o)
|
|
9
|
+
notified.push(type)
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
let notified = []
|
|
14
|
+
etro.event.publish(o, 'foo.bar.test', {})
|
|
15
|
+
expect(notified).toEqual(types)
|
|
16
|
+
|
|
17
|
+
notified = []
|
|
18
|
+
etro.event.publish(o, 'foo.bar', {})
|
|
19
|
+
expect(notified).toEqual(types.slice(1))
|
|
20
|
+
|
|
21
|
+
notified = []
|
|
22
|
+
etro.event.publish(o, 'foo', {})
|
|
23
|
+
expect(notified).toEqual(types.slice(2))
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
describe('Layers', function () {
|
|
2
|
+
describe('Base', function () {
|
|
3
|
+
let layer
|
|
4
|
+
|
|
5
|
+
beforeEach(function () {
|
|
6
|
+
layer = new etro.layer.Base(0, 4)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("should be of type 'layer'", function () {
|
|
10
|
+
expect(layer.type).toBe('layer')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should attach to movie', function () {
|
|
14
|
+
const movie = {}
|
|
15
|
+
// Simulate attach to movie
|
|
16
|
+
layer.attach(movie)
|
|
17
|
+
expect(layer._movie).toEqual(movie)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should propogate changes up', function () {
|
|
21
|
+
// Connect to movie to publish event to
|
|
22
|
+
const movie = {}
|
|
23
|
+
layer.attach(movie)
|
|
24
|
+
|
|
25
|
+
// Listen for event called on moive
|
|
26
|
+
let timesFired = 0
|
|
27
|
+
etro.event.subscribe(movie, 'movie.change.layer', () => {
|
|
28
|
+
timesFired++
|
|
29
|
+
})
|
|
30
|
+
// Modify layer
|
|
31
|
+
layer.startTime = 1
|
|
32
|
+
expect(timesFired).toBe(1)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('Visual', function () {
|
|
37
|
+
let layer
|
|
38
|
+
|
|
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
|
+
)
|
|
44
|
+
layer.render(0)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should render the background', function () {
|
|
48
|
+
const imageData = layer.cctx.getImageData(0, 0, 400, 400)
|
|
49
|
+
let allBlue = true
|
|
50
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
51
|
+
allBlue = allBlue &&
|
|
52
|
+
imageData.data[i + 0] === 0 &&
|
|
53
|
+
imageData.data[i + 1] === 0 &&
|
|
54
|
+
imageData.data[i + 2] === 255 &&
|
|
55
|
+
imageData.data[i + 3] === 255
|
|
56
|
+
}
|
|
57
|
+
expect(allBlue).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('Image', function () {
|
|
62
|
+
let layer
|
|
63
|
+
|
|
64
|
+
beforeEach(function (done) {
|
|
65
|
+
const image = new Image()
|
|
66
|
+
image.src = '/base/spec/assets/layer/image.jpg'
|
|
67
|
+
image.onload = () => {
|
|
68
|
+
layer = new etro.layer.Image(0, 4, image)
|
|
69
|
+
// Simulate attach to movie
|
|
70
|
+
layer.attach(
|
|
71
|
+
{ width: image.width, height: image.height, currentTime: 0 }
|
|
72
|
+
)
|
|
73
|
+
done()
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should render', function () {
|
|
78
|
+
// Render layer (actual outcome)
|
|
79
|
+
layer.render(0)
|
|
80
|
+
const imageData = layer.cctx.getImageData(0, 0, layer.width, layer.height)
|
|
81
|
+
|
|
82
|
+
// Draw image (expected outcome)
|
|
83
|
+
const testCanv = document.createElement('canvas')
|
|
84
|
+
testCanv.width = layer.width
|
|
85
|
+
testCanv.height = layer.height
|
|
86
|
+
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)
|
|
89
|
+
|
|
90
|
+
// Compare expected outcome with actual outcome
|
|
91
|
+
let equal = true
|
|
92
|
+
for (let i = 0; i < imageData.data.length; i++) {
|
|
93
|
+
equal = equal && imageData.data[i] === testImageData.data[i]
|
|
94
|
+
}
|
|
95
|
+
expect(equal).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// I suspect this doesn't work becuase of autoplay restrictions
|
|
100
|
+
/* describe('Audio', function () {
|
|
101
|
+
let layer
|
|
102
|
+
|
|
103
|
+
beforeEach(function (done) {
|
|
104
|
+
const audio = new Audio()
|
|
105
|
+
audio.src = '/base/spec/assets/layer/audio.wav'
|
|
106
|
+
// audio.muted = true // until we figure out how to allow autoplay in headless chrome
|
|
107
|
+
audio.addEventListener('loadedmetadata', () => {
|
|
108
|
+
layer = new etro.layer.Audio(0, audio)
|
|
109
|
+
layer.attach(
|
|
110
|
+
{ actx: new AudioContext(), currentTime: 0 }
|
|
111
|
+
)
|
|
112
|
+
done()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should play', function () {
|
|
117
|
+
let timesPlayed = 0
|
|
118
|
+
layer.media.addEventListener('play', () => {
|
|
119
|
+
timesPlayed++
|
|
120
|
+
})
|
|
121
|
+
for (let i = 0; i < 3; i++) {
|
|
122
|
+
layer.start(0) // reltime = 0s
|
|
123
|
+
layer.stop(0) // reltime = 0s
|
|
124
|
+
}
|
|
125
|
+
expect(timesPlayed).toBe(3)
|
|
126
|
+
})
|
|
127
|
+
}) */
|
|
128
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
describe('Movie', function () {
|
|
2
|
+
let movie, canvas
|
|
3
|
+
|
|
4
|
+
beforeEach(function () {
|
|
5
|
+
if (canvas) {
|
|
6
|
+
document.body.removeChild(canvas)
|
|
7
|
+
}
|
|
8
|
+
canvas = document.createElement('canvas')
|
|
9
|
+
document.body.appendChild(canvas)
|
|
10
|
+
movie = new etro.Movie(canvas)
|
|
11
|
+
movie.addLayer(new etro.layer.Visual(0, 0.5))
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('identity ->', function () {
|
|
15
|
+
it("should be of type 'movie'", function () {
|
|
16
|
+
expect(movie.type).toBe('movie')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('operations ->', function () {
|
|
21
|
+
it('should not be paused after playing', function () {
|
|
22
|
+
movie.play()
|
|
23
|
+
expect(movie.paused).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should be paused after pausing', function () {
|
|
27
|
+
movie.play()
|
|
28
|
+
movie.pause()
|
|
29
|
+
// No promise returned by `pause`, because code is async in implementation.
|
|
30
|
+
expect(movie.paused).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should be paused after stopping', function () {
|
|
34
|
+
movie.play()
|
|
35
|
+
movie.stop()
|
|
36
|
+
expect(movie.paused).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should be reset to beginning after stopping', function () {
|
|
40
|
+
movie.play()
|
|
41
|
+
movie.stop()
|
|
42
|
+
expect(movie.currentTime).toBe(0)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should be `recording` when recording', function () {
|
|
46
|
+
movie.record(10)
|
|
47
|
+
expect(movie.recording).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should not be paused when recording', function () {
|
|
51
|
+
movie.record(10)
|
|
52
|
+
expect(movie.paused).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should return blob after recording', function (done) {
|
|
56
|
+
movie.record(60)
|
|
57
|
+
.then(video => {
|
|
58
|
+
expect(video.size).toBeGreaterThan(0)
|
|
59
|
+
done()
|
|
60
|
+
})
|
|
61
|
+
.catch(e => {
|
|
62
|
+
throw e
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('events ->', function () {
|
|
68
|
+
it("should fire 'movie.play' once", function () {
|
|
69
|
+
let timesFired = 0
|
|
70
|
+
etro.event.subscribe(movie, 'movie.play', function () {
|
|
71
|
+
timesFired++
|
|
72
|
+
})
|
|
73
|
+
movie.play().then(function () {
|
|
74
|
+
expect(timesFired).toBe(1)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("should fire 'movie.pause' once", function () {
|
|
79
|
+
let timesFired = 0
|
|
80
|
+
etro.event.subscribe(movie, 'movie.pause', function () {
|
|
81
|
+
timesFired++
|
|
82
|
+
})
|
|
83
|
+
// play, pause and check if event was fired
|
|
84
|
+
movie.play().then(function () {
|
|
85
|
+
movie.pause()
|
|
86
|
+
expect(timesFired).toBe(1)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("should fire 'movie.record' once", function () {
|
|
91
|
+
let timesFired = 0
|
|
92
|
+
etro.event.subscribe(movie, 'movie.record', function () {
|
|
93
|
+
timesFired++
|
|
94
|
+
})
|
|
95
|
+
movie.record().then(function () {
|
|
96
|
+
expect(timesFired).toBe(1)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("should fire 'movie.record' with correct options", function () {
|
|
101
|
+
const options = {
|
|
102
|
+
video: true, // even default values should be passed (exactly what user provides)
|
|
103
|
+
audio: false
|
|
104
|
+
}
|
|
105
|
+
etro.event.subscribe(movie, 'movie.record', function (event) {
|
|
106
|
+
expect(event.options).toEqual(options)
|
|
107
|
+
})
|
|
108
|
+
movie.record(options)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("should fire 'movie.ended'", function () {
|
|
112
|
+
let timesFired = 0
|
|
113
|
+
etro.event.subscribe(movie, 'movie.ended', function () {
|
|
114
|
+
timesFired++
|
|
115
|
+
})
|
|
116
|
+
movie.play().then(function () {
|
|
117
|
+
expect(timesFired).toBe(1)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("should fire 'movie.loadeddata'", function () {
|
|
122
|
+
/*
|
|
123
|
+
* 'loadeddata' gets timesFired when when the frame is fully loaded
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
let firedOnce = false
|
|
127
|
+
etro.event.subscribe(movie, 'movie.loadeddata', () => {
|
|
128
|
+
firedOnce = true
|
|
129
|
+
})
|
|
130
|
+
movie.refresh().then(() => {
|
|
131
|
+
expect(firedOnce).toBe(true)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it("should fire 'movie.seek'", function () {
|
|
136
|
+
let timesFired = 0
|
|
137
|
+
etro.event.subscribe(movie, 'movie.seek', function () {
|
|
138
|
+
timesFired++
|
|
139
|
+
})
|
|
140
|
+
movie.currentTime = movie.duration / 2
|
|
141
|
+
expect(timesFired).toBe(1)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("should fire 'movie.timeupdate'", function () {
|
|
145
|
+
let firedOnce = false
|
|
146
|
+
etro.event.subscribe(movie, 'movie.timeupdate', function () {
|
|
147
|
+
firedOnce = true
|
|
148
|
+
})
|
|
149
|
+
movie.play().then(function () {
|
|
150
|
+
expect(firedOnce).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|