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.
Files changed (103) hide show
  1. package/.env +2 -0
  2. package/.github/workflows/nodejs.yml +27 -0
  3. package/CHANGELOG.md +109 -0
  4. package/CODE_OF_CONDUCT.md +77 -0
  5. package/CONTRIBUTING.md +155 -0
  6. package/LICENSE +674 -0
  7. package/README.md +57 -0
  8. package/dist/etro.js +3397 -0
  9. package/docs/effect.js.html +1215 -0
  10. package/docs/event.js.html +145 -0
  11. package/docs/index.html +81 -0
  12. package/docs/index.js.html +92 -0
  13. package/docs/layer.js.html +888 -0
  14. package/docs/module-effect-GaussianBlurComponent.html +345 -0
  15. package/docs/module-effect.Brightness.html +339 -0
  16. package/docs/module-effect.Channels.html +319 -0
  17. package/docs/module-effect.ChromaKey.html +611 -0
  18. package/docs/module-effect.Contrast.html +339 -0
  19. package/docs/module-effect.EllipticalMask.html +200 -0
  20. package/docs/module-effect.GaussianBlur.html +202 -0
  21. package/docs/module-effect.GaussianBlurHorizontal.html +242 -0
  22. package/docs/module-effect.GaussianBlurVertical.html +242 -0
  23. package/docs/module-effect.Pixelate.html +330 -0
  24. package/docs/module-effect.Shader.html +1227 -0
  25. package/docs/module-effect.Stack.html +406 -0
  26. package/docs/module-effect.Transform.Matrix.html +193 -0
  27. package/docs/module-effect.Transform.html +1174 -0
  28. package/docs/module-effect.html +148 -0
  29. package/docs/module-event.html +473 -0
  30. package/docs/module-index.html +186 -0
  31. package/docs/module-layer-Media.html +1116 -0
  32. package/docs/module-layer-MediaMixin.html +164 -0
  33. package/docs/module-layer.Audio.html +1188 -0
  34. package/docs/module-layer.Base.html +629 -0
  35. package/docs/module-layer.Image.html +1421 -0
  36. package/docs/module-layer.Text.html +1731 -0
  37. package/docs/module-layer.Video.html +1938 -0
  38. package/docs/module-layer.Visual.html +1698 -0
  39. package/docs/module-layer.html +137 -0
  40. package/docs/module-movie.html +3118 -0
  41. package/docs/module-util.Color.html +702 -0
  42. package/docs/module-util.Font.html +395 -0
  43. package/docs/module-util.html +845 -0
  44. package/docs/movie.js.html +689 -0
  45. package/docs/scripts/collapse.js +20 -0
  46. package/docs/scripts/linenumber.js +25 -0
  47. package/docs/scripts/nav.js +12 -0
  48. package/docs/scripts/polyfill.js +4 -0
  49. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  50. package/docs/scripts/prettify/lang-css.js +2 -0
  51. package/docs/scripts/prettify/prettify.js +28 -0
  52. package/docs/scripts/search.js +83 -0
  53. package/docs/styles/jsdoc.css +671 -0
  54. package/docs/styles/prettify.css +79 -0
  55. package/docs/util.js.html +503 -0
  56. package/eslint.conf.js +28 -0
  57. package/eslint.test-conf.js +4 -0
  58. package/examples/application/readme-screenshot.html +86 -0
  59. package/examples/application/video-player.html +131 -0
  60. package/examples/application/webcam.html +28 -0
  61. package/examples/introduction/audio.html +52 -0
  62. package/examples/introduction/effects.html +56 -0
  63. package/examples/introduction/export.html +70 -0
  64. package/examples/introduction/functions.html +35 -0
  65. package/examples/introduction/hello-world1.html +33 -0
  66. package/examples/introduction/hello-world2.html +32 -0
  67. package/examples/introduction/keyframes.html +67 -0
  68. package/examples/introduction/media.html +55 -0
  69. package/examples/introduction/text.html +27 -0
  70. package/jsdoc.conf.json +3 -0
  71. package/karma.conf.js +60 -0
  72. package/package.json +63 -0
  73. package/private-todo.txt +70 -0
  74. package/rename-file.sh +18 -0
  75. package/rename-versions.sh +14 -0
  76. package/rename.sh +22 -0
  77. package/rollup.config.js +31 -0
  78. package/screenshots/2019-08-17_0.png +0 -0
  79. package/scripts/gen-effect-samples.html +99 -0
  80. package/scripts/save-effect-samples.js +43 -0
  81. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  82. package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
  83. package/spec/assets/effect/original.png +0 -0
  84. package/spec/assets/effect/pixelate.png +0 -0
  85. package/spec/assets/effect/transform/multiply.png +0 -0
  86. package/spec/assets/effect/transform/rotate.png +0 -0
  87. package/spec/assets/effect/transform/scale-fraction.png +0 -0
  88. package/spec/assets/effect/transform/scale.png +0 -0
  89. package/spec/assets/effect/transform/translate-fraction.png +0 -0
  90. package/spec/assets/effect/transform/translate.png +0 -0
  91. package/spec/assets/layer/audio.wav +0 -0
  92. package/spec/assets/layer/image.jpg +0 -0
  93. package/spec/effect.spec.js +352 -0
  94. package/spec/event.spec.js +25 -0
  95. package/spec/layer.spec.js +128 -0
  96. package/spec/movie.spec.js +154 -0
  97. package/spec/util.spec.js +285 -0
  98. package/src/effect.js +1265 -0
  99. package/src/event.js +78 -0
  100. package/src/index.js +23 -0
  101. package/src/layer.js +875 -0
  102. package/src/movie.js +636 -0
  103. package/src/util.js +487 -0
package/src/effect.js ADDED
@@ -0,0 +1,1265 @@
1
+ /**
2
+ * @module effect
3
+ *
4
+ * @todo Investigate why an effect might run once in the beginning even if its layer isn't at the beginning
5
+ * @todo Add audio effect support
6
+ * @todo Move shader source to external files
7
+ */
8
+
9
+ import { publish, subscribe } from './event.js'
10
+ import { val, watchPublic } from './util.js'
11
+
12
+ /**
13
+ * Any effect that modifies the visual contents of a layer.
14
+ *
15
+ * <em>Note: At this time, simply use the <code>actx</code> property of the movie to add audio nodes to a
16
+ * layer's media. TODO: add more audio support, including more types of audio nodes, probably in a
17
+ * different module.</em>
18
+ */
19
+ export class Base {
20
+ constructor () {
21
+ const newThis = watchPublic(this) // proxy that will be returned by constructor
22
+
23
+ newThis.enabled = true
24
+ newThis._target = null
25
+
26
+ // Propogate up to target
27
+ subscribe(newThis, 'effect.change.modify', event => {
28
+ if (!newThis._target) {
29
+ return
30
+ }
31
+ const type = `${newThis._target.type}.change.effect.modify`
32
+ publish(newThis._target, type, { ...event, target: newThis._target, source: newThis, type })
33
+ })
34
+
35
+ return newThis
36
+ }
37
+
38
+ attach (target) {
39
+ this._target = target
40
+ }
41
+
42
+ detach () {
43
+ this._target = null
44
+ }
45
+
46
+ // subclasses must implement apply
47
+ /**
48
+ * Apply this effect to a target at the given time
49
+ *
50
+ * @param {module:movie|module:layer.Base} target
51
+ * @param {number} reltime - the movie's current time relative to the layer (will soon be replaced with an instance getter)
52
+ * @abstract
53
+ */
54
+ apply (target, reltime) {
55
+ throw new Error('No overriding method found or super.apply was called')
56
+ }
57
+
58
+ /**
59
+ * The current time of the target
60
+ * @type number
61
+ */
62
+ get currentTime () {
63
+ return this._target ? this._target.currentTime : undefined
64
+ }
65
+
66
+ get parent () {
67
+ return this._target
68
+ }
69
+
70
+ get movie () {
71
+ return this._target ? this._target.movie : undefined
72
+ }
73
+ }
74
+ // id for events (independent of instance, but easy to access when on prototype chain)
75
+ Base.prototype.type = 'effect'
76
+ Base.prototype.publicExcludes = []
77
+ Base.prototype.propertyFilters = {}
78
+
79
+ /**
80
+ * A sequence of effects to apply, treated as one effect. This can be useful for defining reused effect sequences as one effect.
81
+ */
82
+ export class Stack extends Base {
83
+ constructor (effects) {
84
+ super()
85
+
86
+ this._effectsBack = []
87
+ this._effects = new Proxy(this._effectsBack, {
88
+ apply: function (target, thisArg, argumentsList) {
89
+ return thisArg[target].apply(this, argumentsList)
90
+ },
91
+ deleteProperty: function (target, property) {
92
+ const value = target[property]
93
+ publish(value, 'effect.detach', { effectTarget: this._target })
94
+ delete target[property]
95
+ return true
96
+ },
97
+ set: function (target, property, value) {
98
+ if (!isNaN(property)) { // if property is an number (index)
99
+ if (target[property]) {
100
+ delete target[property] // call deleteProperty
101
+ }
102
+ publish(value, 'effect.attach', { effectTarget: this._target }) // Attach effect to movie (first)
103
+ }
104
+ target[property] = value
105
+ return true
106
+ }
107
+ })
108
+ effects.forEach(effect => this.effects.push(effect))
109
+ }
110
+
111
+ attach (movie) {
112
+ super.attach(movie)
113
+ this.effects.forEach(effect => {
114
+ effect.detach()
115
+ effect.attach(movie)
116
+ })
117
+ }
118
+
119
+ detach () {
120
+ super.detach()
121
+ this.effects.forEach(effect => {
122
+ effect.detach()
123
+ })
124
+ }
125
+
126
+ apply (target, reltime) {
127
+ for (let i = 0; i < this.effects.length; i++) {
128
+ const effect = this.effects[i]
129
+ effect.apply(target, reltime)
130
+ }
131
+ }
132
+
133
+ /**
134
+ * @type module:effect.Base[]
135
+ */
136
+ get effects () {
137
+ return this._effects
138
+ }
139
+
140
+ /**
141
+ * Convenience method for chaining
142
+ * @param {module:effect.Base} effect - the effect to append
143
+ */
144
+ addEffect (effect) {
145
+ this.effects.push(effect)
146
+ return this
147
+ }
148
+ }
149
+
150
+ /**
151
+ * A hardware-accelerated pixel mapping
152
+ * @todo can `v_TextureCoord` be replaced by `gl_FragUV`
153
+ */
154
+ export class Shader extends Base {
155
+ /**
156
+ * @param {string} fragmentSrc
157
+ * @param {object} [userUniforms={}]
158
+ * @param {object[]} [userTextures=[]]
159
+ * @param {object} [sourceTextureOptions={}]
160
+ */
161
+ constructor (fragmentSrc = Shader._IDENTITY_FRAGMENT_SOURCE, userUniforms = {}, userTextures = [], sourceTextureOptions = {}) {
162
+ super()
163
+ // TODO: split up into multiple methods
164
+
165
+ const gl = this._initGl()
166
+ this._program = Shader._initShaderProgram(gl, Shader._VERTEX_SOURCE, fragmentSrc)
167
+ this._buffers = Shader._initRectBuffers(gl)
168
+
169
+ this._initTextures(userUniforms, userTextures, sourceTextureOptions)
170
+ this._initAttribs()
171
+ this._initUniforms(userUniforms)
172
+ }
173
+
174
+ _initGl () {
175
+ this._canvas = document.createElement('canvas')
176
+ const gl = this._canvas.getContext('webgl')
177
+ if (gl === null) {
178
+ throw new Error('Unable to initialize WebGL. Your browser or machine may not support it.')
179
+ }
180
+ this._gl = gl
181
+ return gl
182
+ }
183
+
184
+ _initTextures (userUniforms, userTextures, sourceTextureOptions) {
185
+ const gl = this._gl
186
+ const maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)
187
+ if (userTextures.length > maxTextures) {
188
+ console.warn('Too many textures!')
189
+ }
190
+ this._userTextures = {}
191
+ for (const name in userTextures) {
192
+ const userOptions = userTextures[name]
193
+ // Apply default options.
194
+ const options = { ...Shader._DEFAULT_TEXTURE_OPTIONS, ...userOptions }
195
+
196
+ if (options.createUniform) {
197
+ // Automatically, create a uniform with the same name as this texture, that points to it.
198
+ // This is an easy way for the user to use custom textures, without having to define multiple properties in the effect object.
199
+ if (userUniforms[name]) {
200
+ throw new Error(`Texture - uniform naming conflict: ${name}!`)
201
+ }
202
+ // Add this as a "user uniform".
203
+ userUniforms[name] = '1i' // texture pointer
204
+ }
205
+ this._userTextures[name] = options
206
+ }
207
+ this._sourceTextureOptions = { ...Shader._DEFAULT_TEXTURE_OPTIONS, ...sourceTextureOptions }
208
+ }
209
+
210
+ _initAttribs () {
211
+ const gl = this._gl
212
+ this._attribLocations = {
213
+ textureCoord: gl.getAttribLocation(this._program, 'a_TextureCoord')
214
+ // a_VertexPosition ?? somehow it works without it though...
215
+ }
216
+ }
217
+
218
+ _initUniforms (userUniforms) {
219
+ const gl = this._gl
220
+ this._uniformLocations = {
221
+ // modelViewMatrix: gl.getUniformLocation(this._program, "u_ModelViewMatrix"),
222
+ source: gl.getUniformLocation(this._program, 'u_Source'),
223
+ size: gl.getUniformLocation(this._program, 'u_Size')
224
+ }
225
+ // The options value can just be a string equal to the type of the variable, for syntactic sugar.
226
+ // If this is the case, convert it to a real options object.
227
+ this._userUniforms = {}
228
+ for (const name in userUniforms) {
229
+ const val = userUniforms[name]
230
+ this._userUniforms[name] = typeof val === 'string' ? { type: val } : val
231
+ }
232
+ for (const unprefixed in userUniforms) {
233
+ // property => u_Property
234
+ const prefixed = 'u_' + unprefixed.charAt(0).toUpperCase() + (unprefixed.length > 1 ? unprefixed.slice(1) : '')
235
+ this._uniformLocations[unprefixed] = gl.getUniformLocation(this._program, prefixed)
236
+ }
237
+ }
238
+
239
+ // Not needed, right?
240
+ /* watchWebGLOptions() {
241
+ const pubChange = () => {
242
+ this.publish("change", {});
243
+ };
244
+ for (let name in this._userTextures) {
245
+ watch(this, name, pubChange);
246
+ }
247
+ for (let name in this._userUniforms) {
248
+ watch(this, name, pubChange);
249
+ }
250
+ } */
251
+
252
+ apply (target, reltime) {
253
+ const gl = this._gl
254
+ this._checkDimensions(target)
255
+ this._refreshGl()
256
+
257
+ this._enablePositionAttrib()
258
+ this._enableTexCoordAttrib()
259
+ this._prepareTextures(target, reltime)
260
+
261
+ gl.useProgram(this._program)
262
+
263
+ this._prepareUniforms(target, reltime)
264
+
265
+ this._draw(target)
266
+ }
267
+
268
+ _checkDimensions (target) {
269
+ const gl = this._gl
270
+ // TODO: Change target.canvas.width => target.width and see if it breaks anything.
271
+ if (this._canvas.width !== target.canvas.width || this._canvas.height !== target.canvas.height) { // (optimization)
272
+ this._canvas.width = target.canvas.width
273
+ this._canvas.height = target.canvas.height
274
+
275
+ gl.viewport(0, 0, target.canvas.width, target.canvas.height)
276
+ }
277
+ }
278
+
279
+ _refreshGl () {
280
+ const gl = this._gl
281
+ gl.clearColor(0, 0, 0, 1) // clear to black; fragments can be made transparent with the blendfunc below
282
+ // gl.clearDepth(1.0); // clear everything
283
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.SRC_ALPHA, gl.ONE, gl.ZERO) // idk why I can't multiply rgb by zero
284
+ gl.enable(gl.BLEND)
285
+ gl.disable(gl.DEPTH_TEST) // gl.depthFunc(gl.LEQUAL);
286
+
287
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
288
+ }
289
+
290
+ _enablePositionAttrib () {
291
+ const gl = this._gl
292
+ // Tell WebGL how to pull out the positions from buffer
293
+ const numComponents = 2
294
+ const type = gl.FLOAT // the data in the buffer is 32bit floats
295
+ const normalize = false // don't normalize
296
+ const stride = 0 // how many bytes to get from one set of values to the next
297
+ // 0 = use type and numComponents above
298
+ const offset = 0 // how many bytes inside the buffer to start from
299
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._buffers.position)
300
+ gl.vertexAttribPointer(
301
+ this._attribLocations.vertexPosition,
302
+ numComponents,
303
+ type,
304
+ normalize,
305
+ stride,
306
+ offset)
307
+ gl.enableVertexAttribArray(
308
+ this._attribLocations.vertexPosition)
309
+ }
310
+
311
+ _enableTexCoordAttrib () {
312
+ const gl = this._gl
313
+ // tell webgl how to pull out the texture coordinates from buffer
314
+ const numComponents = 2 // every coordinate composed of 2 values (uv)
315
+ const type = gl.FLOAT // the data in the buffer is 32 bit float
316
+ const normalize = false // don't normalize
317
+ const stride = 0 // how many bytes to get from one set to the next
318
+ const offset = 0 // how many bytes inside the buffer to start from
319
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._buffers.textureCoord)
320
+ gl.vertexAttribPointer(this._attribLocations.textureCoord, numComponents, type, normalize, stride, offset)
321
+ gl.enableVertexAttribArray(this._attribLocations.textureCoord)
322
+ }
323
+
324
+ _prepareTextures (target, reltime) {
325
+ const gl = this._gl
326
+ // TODO: figure out which properties should be private / public
327
+
328
+ // Tell WebGL we want to affect texture unit 0
329
+ // Call `activeTexture` before `_loadTexture` so it won't be bound to the last active texture.
330
+ gl.activeTexture(gl.TEXTURE0)
331
+ this._inputTexture = Shader._loadTexture(gl, target.canvas, this._sourceTextureOptions)
332
+ // Bind the texture to texture unit 0
333
+ gl.bindTexture(gl.TEXTURE_2D, this._inputTexture)
334
+
335
+ let i = 0
336
+ for (const name in this._userTextures) {
337
+ const options = this._userTextures[name]
338
+ // Call `activeTexture` before `_loadTexture` so it won't be bound to the last active texture.
339
+ // TODO: investigate better implementation of `_loadTexture`
340
+ gl.activeTexture(gl.TEXTURE0 + (Shader.INTERNAL_TEXTURE_UNITS + i)) // use the fact that TEXTURE0, TEXTURE1, ... are continuous
341
+ const preparedTex = Shader._loadTexture(gl, val(this, name, reltime), options) // do it every frame to keep updated (I think you need to)
342
+ gl.bindTexture(gl[options.target], preparedTex)
343
+ i++
344
+ }
345
+ }
346
+
347
+ _prepareUniforms (target, reltime) {
348
+ const gl = this._gl
349
+ // Set the shader uniforms
350
+
351
+ // Tell the shader we bound the texture to texture unit 0
352
+ // All base (Shader class) uniforms are optional
353
+ if (this._uniformLocations.source) {
354
+ gl.uniform1i(this._uniformLocations.source, 0)
355
+ }
356
+
357
+ // All base (Shader class) uniforms are optional
358
+ if (this._uniformLocations.size) {
359
+ gl.uniform2iv(this._uniformLocations.size, [target.canvas.width, target.canvas.height])
360
+ }
361
+
362
+ for (const unprefixed in this._userUniforms) {
363
+ const options = this._userUniforms[unprefixed]
364
+ const value = val(this, unprefixed, reltime)
365
+ const preparedValue = this._prepareValue(value, options.type, reltime, options)
366
+ const location = this._uniformLocations[unprefixed]
367
+ gl['uniform' + options.type](location, preparedValue) // haHA JavaScript (`options.type` is "1f", for instance)
368
+ }
369
+ gl.uniform1i(this._uniformLocations.test, 0)
370
+ }
371
+
372
+ _draw (target) {
373
+ const gl = this._gl
374
+
375
+ const offset = 0
376
+ const vertexCount = 4
377
+ gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount)
378
+
379
+ // clear the target, in case the effect outputs transparent pixels
380
+ target.cctx.clearRect(0, 0, target.canvas.width, target.canvas.height)
381
+ // copy internal image state onto target
382
+ target.cctx.drawImage(this._canvas, 0, 0)
383
+ }
384
+
385
+ /**
386
+ * Converts a value of a standard type for javascript to a standard type for GLSL
387
+ * @param value - the raw value to prepare
388
+ * @param {string} outputType - the WebGL type of |value|; example: <code>1f</code> for a float
389
+ * @param {number} reltime - current time, relative to the target
390
+ * @param {object} [options] - Optional config
391
+ */
392
+ _prepareValue (value, outputType, reltime, options = {}) {
393
+ const def = options.defaultFloatComponent || 0
394
+ if (outputType === '1i') {
395
+ /*
396
+ * Textures are passed to the shader by both providing the texture (with texImage2D)
397
+ * and setting the |sampler| uniform equal to the index of the texture.
398
+ * In etro shader effects, the subclass passes the names of all the textures ot this base class,
399
+ * along with all the names of uniforms. By default, corresponding uniforms (with the same name) are
400
+ * created for each texture for ease of use. You can also define different texture properties in the
401
+ * javascript effect by setting it identical to the property with the passed texture name.
402
+ * In WebGL, it will be set to the same integer texture unit.
403
+ *
404
+ * To do this, test if |value| is identical to a texture.
405
+ * If so, set it to the texture's index, so the shader can use it.
406
+ */
407
+ let i = 0
408
+ for (const name in this._userTextures) {
409
+ const testValue = val(this, name, reltime)
410
+ if (value === testValue) {
411
+ value = Shader.INTERNAL_TEXTURE_UNITS + i // after the internal texture units
412
+ }
413
+ i++
414
+ }
415
+ }
416
+
417
+ if (outputType === '3fv') {
418
+ // allow 4-component vectors; TODO: why?
419
+ if (Array.isArray(value) && (value.length === 3 || value.length === 4)) {
420
+ return value
421
+ }
422
+ // kind of loose so this can be changed if needed
423
+ if (typeof value === 'object') {
424
+ return [
425
+ value.r !== undefined ? value.r : def,
426
+ value.g !== undefined ? value.g : def,
427
+ value.b !== undefined ? value.b : def
428
+ ]
429
+ }
430
+
431
+ throw new Error(`Invalid type: ${outputType} or value: ${value}`)
432
+ }
433
+
434
+ if (outputType === '4fv') {
435
+ if (Array.isArray(value) && value.length === 4) {
436
+ return value
437
+ }
438
+ // kind of loose so this can be changed if needed
439
+ if (typeof value === 'object') {
440
+ return [
441
+ value.r !== undefined ? value.r : def,
442
+ value.g !== undefined ? value.g : def,
443
+ value.b !== undefined ? value.b : def,
444
+ value.a !== undefined ? value.a : def
445
+ ]
446
+ }
447
+
448
+ throw new Error(`Invalid type: ${outputType} or value: ${value}`)
449
+ }
450
+
451
+ return value
452
+ }
453
+ }
454
+ // Shader.prototype.getpublicExcludes = () =>
455
+ Shader._initRectBuffers = gl => {
456
+ const position = [
457
+ // the screen/canvas (output)
458
+ -1.0, 1.0,
459
+ 1.0, 1.0,
460
+ -1.0, -1.0,
461
+ 1.0, -1.0
462
+ ]
463
+ const textureCoord = [
464
+ // the texture/canvas (input)
465
+ 0.0, 0.0,
466
+ 1.0, 0.0,
467
+ 0.0, 1.0,
468
+ 1.0, 1.0
469
+ ]
470
+
471
+ return {
472
+ position: Shader._initBuffer(gl, position),
473
+ textureCoord: Shader._initBuffer(gl, textureCoord)
474
+ }
475
+ }
476
+ /**
477
+ * Creates the quad covering the screen
478
+ */
479
+ Shader._initBuffer = (gl, data) => {
480
+ const buffer = gl.createBuffer()
481
+
482
+ // Select the buffer as the one to apply buffer operations to from here out.
483
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
484
+
485
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW)
486
+
487
+ return buffer
488
+ }
489
+ /**
490
+ * Creates a webgl texture from the source.
491
+ * @param {object} [options] - optional WebGL config for texture
492
+ * @param {number} [options.target=gl.TEXTURE_2D]
493
+ * @param {number} [options.level=0]
494
+ * @param {number} [options.internalFormat=gl.RGBA]
495
+ * @param {number} [options.srcFormat=gl.RGBA]
496
+ * @param {number} [options.srcType=gl.UNSIGNED_BYTE]
497
+ * @param {number} [options.wrapS=gl.CLAMP_TO_EDGE]
498
+ * @param {number} [options.wrapT=gl.CLAMP_TO_EDGE]
499
+ * @param {number} [options.minFilter=gl.LINEAR]
500
+ * @param {number} [options.magFilter=gl.LINEAR]
501
+ */
502
+ Shader._loadTexture = (gl, source, options = {}) => {
503
+ options = { ...Shader._DEFAULT_TEXTURE_OPTIONS, ...options } // Apply default options, just in case.
504
+ const target = gl[options.target] // When creating the option, the user can't access `gl` so access it here.
505
+ const level = options.level
506
+ const internalFormat = gl[options.internalFormat]
507
+ const srcFormat = gl[options.srcFormat]
508
+ const srcType = gl[options.srcType]
509
+ const wrapS = gl[options.wrapS]
510
+ const wrapT = gl[options.wrapT]
511
+ const minFilter = gl[options.minFilter]
512
+ const magFilter = gl[options.magFilter]
513
+ // TODO: figure out how wrap-s and wrap-t interact with mipmaps
514
+ // (for legacy support)
515
+ // let wrapS = options.wrapS ? options.wrapS : gl.CLAMP_TO_EDGE,
516
+ // wrapT = options.wrapT ? options.wrapT : gl.CLAMP_TO_EDGE;
517
+
518
+ const tex = gl.createTexture()
519
+ gl.bindTexture(target, tex)
520
+
521
+ // gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true) // premultiply alpha
522
+
523
+ // TODO: figure out how this works with layer width/height
524
+
525
+ // TODO: support 3d textures (change texImage2D)
526
+ // set to `source`
527
+ gl.texImage2D(target, level, internalFormat, srcFormat, srcType, source)
528
+
529
+ // WebGL1 has different requirements for power of 2 images
530
+ // vs non power of 2 images so check if the image is a
531
+ // power of 2 in both dimensions.
532
+ // Get dimensions by using the fact that all valid inputs for
533
+ // texImage2D must have `width` and `height` properties except
534
+ // videos, which have `videoWidth` and `videoHeight` instead
535
+ // and `ArrayBufferView`, which is one dimensional (so don't
536
+ // worry about mipmaps)
537
+ const w = target instanceof HTMLVideoElement ? target.videoWidth : target.width
538
+ const h = target instanceof HTMLVideoElement ? target.videoHeight : target.height
539
+ if ((w && isPowerOf2(w)) && (h && isPowerOf2(h))) {
540
+ // Yes, it's a power of 2. All wrap modes are valid. Generate mips.
541
+ gl.texParameteri(target, gl.TEXTURE_WRAP_S, wrapS)
542
+ gl.texParameteri(target, gl.TEXTURE_WRAP_T, wrapT)
543
+ gl.generateMipmap(target)
544
+ } else {
545
+ // No, it's not a power of 2. Turn off mips and set
546
+ // wrapping to clamp to edge
547
+ if (wrapS !== gl.CLAMP_TO_EDGE || wrapT !== gl.CLAMP_TO_EDGE) {
548
+ console.warn('Wrap mode is not CLAMP_TO_EDGE for a non-power-of-two texture. Defaulting to CLAMP_TO_EDGE')
549
+ }
550
+ gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
551
+ gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
552
+ gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, minFilter)
553
+ gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, magFilter)
554
+ }
555
+
556
+ return tex
557
+ }
558
+ const isPowerOf2 = value => (value && (value - 1)) === 0
559
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Adding_2D_content_to_a_WebGL_context
560
+ Shader._initShaderProgram = (gl, vertexSrc, fragmentSrc) => {
561
+ const vertexShader = Shader._loadShader(gl, gl.VERTEX_SHADER, vertexSrc)
562
+ const fragmentShader = Shader._loadShader(gl, gl.FRAGMENT_SHADER, fragmentSrc)
563
+
564
+ const shaderProgram = gl.createProgram()
565
+ gl.attachShader(shaderProgram, vertexShader)
566
+ gl.attachShader(shaderProgram, fragmentShader)
567
+ gl.linkProgram(shaderProgram)
568
+
569
+ // check program creation status
570
+ if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
571
+ console.warn('Unable to link shader program: ' + gl.getProgramInfoLog(shaderProgram))
572
+ return null
573
+ }
574
+
575
+ return shaderProgram
576
+ }
577
+ Shader._loadShader = (gl, type, source) => {
578
+ const shader = gl.createShader(type)
579
+ gl.shaderSource(shader, source)
580
+ gl.compileShader(shader)
581
+
582
+ // check compile status
583
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
584
+ console.warn('An error occured compiling shader: ' + gl.getShaderInfoLog(shader))
585
+ gl.deleteShader(shader)
586
+ return null
587
+ }
588
+
589
+ return shader
590
+ }
591
+ /**
592
+ * WebGL texture units consumed by <code>Shader</code>
593
+ */
594
+ Shader.INTERNAL_TEXTURE_UNITS = 1
595
+ Shader._DEFAULT_TEXTURE_OPTIONS = {
596
+ createUniform: true,
597
+ target: 'TEXTURE_2D',
598
+ level: 0,
599
+ internalFormat: 'RGBA',
600
+ srcFormat: 'RGBA',
601
+ srcType: 'UNSIGNED_BYTE',
602
+ minFilter: 'LINEAR',
603
+ magFilter: 'LINEAR',
604
+ wrapS: 'CLAMP_TO_EDGE',
605
+ wrapT: 'CLAMP_TO_EDGE'
606
+ }
607
+ Shader._VERTEX_SOURCE = `
608
+ attribute vec4 a_VertexPosition;
609
+ attribute vec2 a_TextureCoord;
610
+
611
+ varying highp vec2 v_TextureCoord;
612
+
613
+ void main() {
614
+ // no need for projection or model-view matrices, since we're just rendering a rectangle
615
+ // that fills the screen (see position values)
616
+ gl_Position = a_VertexPosition;
617
+ v_TextureCoord = a_TextureCoord;
618
+ }
619
+ `
620
+ Shader._IDENTITY_FRAGMENT_SOURCE = `
621
+ precision mediump float;
622
+
623
+ uniform sampler2D u_Source;
624
+
625
+ varying highp vec2 v_TextureCoord;
626
+
627
+ void main() {
628
+ gl_FragColor = texture2D(u_Source, v_TextureCoord);
629
+ }
630
+ `
631
+
632
+ /* COLOR & TRANSPARENCY */
633
+ // TODO: move shader source code to external .js files (with exports)
634
+
635
+ /**
636
+ * Changes the brightness
637
+ */
638
+ export class Brightness extends Shader {
639
+ /**
640
+ * @param {number} [brightness=0] - the value to add to each pixel's channels [-255, 255]
641
+ */
642
+ constructor (brightness = 0.0) {
643
+ super(`
644
+ precision mediump float;
645
+
646
+ uniform sampler2D u_Source;
647
+ uniform float u_Brightness;
648
+
649
+ varying highp vec2 v_TextureCoord;
650
+
651
+ void main() {
652
+ vec4 color = texture2D(u_Source, v_TextureCoord);
653
+ vec3 rgb = clamp(color.rgb + u_Brightness / 255.0, 0.0, 1.0);
654
+ gl_FragColor = vec4(rgb, color.a);
655
+ }
656
+ `, {
657
+ brightness: '1f'
658
+ })
659
+ /**
660
+ * The value to add to each pixel's channels [-255, 255]
661
+ * @type number
662
+ */
663
+ this.brightness = brightness
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Changes the contrast
669
+ */
670
+ export class Contrast extends Shader {
671
+ /**
672
+ * @param {number} [contrast=1] - the contrast multiplier
673
+ */
674
+ constructor (contrast = 1.0) {
675
+ super(`
676
+ precision mediump float;
677
+
678
+ uniform sampler2D u_Source;
679
+ uniform float u_Contrast;
680
+
681
+ varying highp vec2 v_TextureCoord;
682
+
683
+ void main() {
684
+ vec4 color = texture2D(u_Source, v_TextureCoord);
685
+ vec3 rgb = clamp(u_Contrast * (color.rgb - 0.5) + 0.5, 0.0, 1.0);
686
+ gl_FragColor = vec4(rgb, color.a);
687
+ }
688
+ `, {
689
+ contrast: '1f'
690
+ })
691
+ /**
692
+ * The contrast multiplier
693
+ * @type number
694
+ */
695
+ this.contrast = contrast
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Multiplies each channel by a different number
701
+ */
702
+ export class Channels extends Shader {
703
+ /**
704
+ * @param {module:util.Color} factors - channel factors, each defaulting to 1
705
+ */
706
+ constructor (factors = {}) {
707
+ super(`
708
+ precision mediump float;
709
+
710
+ uniform sampler2D u_Source;
711
+ uniform vec4 u_Factors;
712
+
713
+ varying highp vec2 v_TextureCoord;
714
+
715
+ void main() {
716
+ vec4 color = texture2D(u_Source, v_TextureCoord);
717
+ gl_FragColor = clamp(u_Factors * color, 0.0, 1.0);
718
+ }
719
+ `, {
720
+ factors: { type: '4fv', defaultFloatComponent: 1 }
721
+ })
722
+
723
+ /**
724
+ * Channel factors, each defaulting to 1
725
+ * @type module:util.Color
726
+ */
727
+ this.factors = factors
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Reduces alpha for pixels which are close to a specified target color
733
+ */
734
+ export class ChromaKey extends Shader {
735
+ /**
736
+ * @param {module:util.Color} [target={r: 0, g: 0, b: 0}] - the color to remove
737
+ * @param {number} [threshold=0] - how much error is allowed
738
+ * @param {boolean} [interpolate=false] - true value to interpolate the alpha channel,
739
+ * or false value for no smoothing (i.e. 255 or 0 alpha)
740
+ * @param {number} [smoothingSharpness=0] - a modifier to lessen the smoothing range, if applicable
741
+ * @todo Use <code>smoothingSharpness</code>
742
+ */
743
+ constructor (target = { r: 0, g: 0, b: 0 }, threshold = 0, interpolate = false/*, smoothingSharpness=0 */) {
744
+ super(`
745
+ precision mediump float;
746
+
747
+ uniform sampler2D u_Source;
748
+ uniform vec3 u_Target;
749
+ uniform float u_Threshold;
750
+ uniform bool u_Interpolate;
751
+
752
+ varying highp vec2 v_TextureCoord;
753
+
754
+ void main() {
755
+ vec4 color = texture2D(u_Source, v_TextureCoord);
756
+ float alpha = color.a;
757
+ vec3 dist = abs(color.rgb - u_Target / 255.0);
758
+ if (!u_Interpolate) {
759
+ // Standard way that most video editors probably use (all-or-nothing method)
760
+ float thresh = u_Threshold / 255.0;
761
+ bool transparent = dist.r <= thresh && dist.g <= thresh && dist.b <= thresh;
762
+ if (transparent)
763
+ alpha = 0.0;
764
+ } else {
765
+ /*
766
+ better way IMHO:
767
+ Take the average of the absolute differences between the pixel and the target for each channel
768
+ */
769
+ float transparency = (dist.r + dist.g + dist.b) / 3.0;
770
+ // TODO: custom or variety of interpolation methods
771
+ alpha = transparency;
772
+ }
773
+ gl_FragColor = vec4(color.rgb, alpha);
774
+ }
775
+ `, {
776
+ target: '3fv',
777
+ threshold: '1f',
778
+ interpolate: '1i'
779
+ })
780
+ /**
781
+ * The color to remove
782
+ * @type module:util.Color
783
+ */
784
+ this.target = target
785
+ /**
786
+ * How much error is alloed
787
+ * @type number
788
+ */
789
+ this.threshold = threshold
790
+ /**
791
+ * True value to interpolate the alpha channel,
792
+ * or false value for no smoothing (i.e. 255 or 0 alpha)
793
+ * @type boolean
794
+ */
795
+ this.interpolate = interpolate
796
+ // this.smoothingSharpness = smoothingSharpness;
797
+ }
798
+ }
799
+
800
+ /* BLUR */
801
+
802
+ /**
803
+ * Applies a Gaussian blur
804
+ *
805
+ * @todo Improve performance
806
+ * @todo Make sure this is truly gaussian even though it doens't require a standard deviation
807
+ */
808
+ export class GaussianBlur extends Stack {
809
+ constructor (radius) {
810
+ // Divide into two shader effects (use the fact that gaussian blurring can be split into components for performance benefits)
811
+ super([
812
+ new GaussianBlurHorizontal(radius),
813
+ new GaussianBlurVertical(radius)
814
+ ])
815
+ }
816
+ }
817
+
818
+ /**
819
+ * Shared class for both horizontal and vertical gaussian blur classes.
820
+ * @todo If radius == 0, don't affect the image (right now, the image goes black).
821
+ */
822
+ class GaussianBlurComponent extends Shader {
823
+ /**
824
+ * @param {string} src - fragment src code specific to which component (horizontal or vertical)
825
+ * @param {number} radius
826
+ */
827
+ constructor (src, radius) {
828
+ super(src, {
829
+ radius: '1i'
830
+ }, {
831
+ shape: { minFilter: 'NEAREST', magFilter: 'NEAREST' }
832
+ })
833
+ /**
834
+ * @type number
835
+ */
836
+ this.radius = radius
837
+ this._radiusCache = undefined
838
+ }
839
+
840
+ apply (target, reltime) {
841
+ const radiusVal = val(this, 'radius', reltime)
842
+ if (radiusVal !== this._radiusCache) {
843
+ // Regenerate gaussian distribution.
844
+ this.shape = GaussianBlurComponent.render1DKernel(
845
+ GaussianBlurComponent.gen1DKernel(radiusVal)
846
+ ) // distribution canvas
847
+ }
848
+ this._radiusCache = radiusVal
849
+
850
+ super.apply(target, reltime)
851
+ }
852
+ }
853
+ GaussianBlurComponent.prototype.publicExcludes = Shader.prototype.publicExcludes.concat(['shape'])
854
+ /**
855
+ * Render Gaussian kernel to a canvas for use in shader.
856
+ * @param {number[]} kernel
857
+ * @private
858
+ *
859
+ * @return {HTMLCanvasElement}
860
+ */
861
+ GaussianBlurComponent.render1DKernel = kernel => {
862
+ // TODO: Use Float32Array instead of canvas.
863
+ // init canvas
864
+ const canvas = document.createElement('canvas')
865
+ canvas.width = kernel.length
866
+ canvas.height = 1 // 1-dimensional
867
+ const ctx = canvas.getContext('2d')
868
+
869
+ // draw to canvas
870
+ const imageData = ctx.createImageData(canvas.width, canvas.height)
871
+ for (let i = 0; i < kernel.length; i++) {
872
+ imageData.data[4 * i + 0] = 255 * kernel[i] // Use red channel to store distribution weights.
873
+ imageData.data[4 * i + 1] = 0 // Clear all other channels.
874
+ imageData.data[4 * i + 2] = 0
875
+ imageData.data[4 * i + 3] = 255
876
+ }
877
+ ctx.putImageData(imageData, 0, 0)
878
+
879
+ return canvas
880
+ }
881
+ GaussianBlurComponent.gen1DKernel = radius => {
882
+ const pascal = GaussianBlurComponent.genPascalRow(2 * radius + 1)
883
+ // don't use `reduce` and `map` (overhead?)
884
+ let sum = 0
885
+ for (let i = 0; i < pascal.length; i++) {
886
+ sum += pascal[i]
887
+ }
888
+ for (let i = 0; i < pascal.length; i++) {
889
+ pascal[i] /= sum
890
+ }
891
+ return pascal
892
+ }
893
+ GaussianBlurComponent.genPascalRow = index => {
894
+ if (index < 0) {
895
+ throw new Error(`Invalid index ${index}`)
896
+ }
897
+ let currRow = [1]
898
+ for (let i = 1; i < index; i++) {
899
+ const nextRow = []
900
+ nextRow.length = currRow.length + 1
901
+ // edges are always 1's
902
+ nextRow[0] = nextRow[nextRow.length - 1] = 1
903
+ for (let j = 1; j < nextRow.length - 1; j++) {
904
+ nextRow[j] = currRow[j - 1] + currRow[j]
905
+ }
906
+ currRow = nextRow
907
+ }
908
+ return currRow
909
+ }
910
+
911
+ /**
912
+ * Horizontal component of gaussian blur
913
+ */
914
+ export class GaussianBlurHorizontal extends GaussianBlurComponent {
915
+ /**
916
+ * @param {number} radius
917
+ */
918
+ constructor (radius) {
919
+ super(`
920
+ #define MAX_RADIUS 250
921
+
922
+ precision mediump float;
923
+
924
+ uniform sampler2D u_Source;
925
+ uniform ivec2 u_Size; // pixel dimensions of input and output
926
+ uniform sampler2D u_Shape; // pseudo one-dimension of blur distribution (would be 1D but webgl doesn't support it)
927
+ uniform int u_Radius; // TODO: support floating-point radii
928
+
929
+ varying highp vec2 v_TextureCoord;
930
+
931
+ void main() {
932
+ /*
933
+ * Ideally, totalWeight should end up being 1, but due to rounding errors, it sometimes ends up less than 1
934
+ * (I believe JS canvas stores values as integers, which rounds down for the majority of the Gaussian curve)
935
+ * So, normalize by accumulating all the weights and dividing by that.
936
+ */
937
+ float totalWeight = 0.0;
938
+ vec4 avg = vec4(0.0);
939
+ // GLSL can only use constants in for-loop declaration, so start at zero, and stop before 2 * u_Radius + 1,
940
+ // opposed to starting at -u_Radius and stopping _at_ +u_Radius.
941
+ for (int i = 0; i < 2 * MAX_RADIUS + 1; i++) {
942
+ if (i >= 2 * u_Radius + 1)
943
+ break; // GLSL can only use constants in for-loop declaration, so we break here.
944
+ // (2 * u_Radius + 1) is the width of u_Shape, by definition
945
+ float weight = texture2D(u_Shape, vec2(float(i) / float(2 * u_Radius + 1), 0.5)).r; // TODO: use single-channel format
946
+ totalWeight += weight;
947
+ vec4 sample = texture2D(u_Source, v_TextureCoord + vec2(i - u_Radius, 0.0) / vec2(u_Size));
948
+ avg += weight * sample;
949
+ }
950
+ gl_FragColor = avg / totalWeight;
951
+ }
952
+ `, radius)
953
+ }
954
+ }
955
+
956
+ /**
957
+ * Vertical component of gaussian blur
958
+ */
959
+ export class GaussianBlurVertical extends GaussianBlurComponent {
960
+ /**
961
+ * @param {number} radius
962
+ */
963
+ constructor (radius) {
964
+ super(`
965
+ #define MAX_RADIUS 250
966
+
967
+ precision mediump float;
968
+
969
+ uniform sampler2D u_Source;
970
+ uniform ivec2 u_Size; // pixel dimensions of input and output
971
+ uniform sampler2D u_Shape; // pseudo one-dimension of blur distribution (would be 1D but webgl doesn't support it)
972
+ uniform int u_Radius; // TODO: support floating-point radii
973
+
974
+ varying highp vec2 v_TextureCoord;
975
+
976
+ void main() {
977
+ /*
978
+ * Ideally, totalWeight should end up being 1, but due to rounding errors, it sometimes ends up less than 1
979
+ * (I believe JS canvas stores values as integers, which rounds down for the majority of the Gaussian curve)
980
+ * So, normalize by accumulating all the weights and dividing by that.
981
+ */
982
+ float totalWeight = 0.0;
983
+ vec4 avg = vec4(0.0);
984
+ // GLSL can only use constants in for-loop declaration, so start at zero, and stop before 2 * u_Radius + 1,
985
+ // opposed to starting at -u_Radius and stopping _at_ +u_Radius.
986
+ for (int i = 0; i < 2 * MAX_RADIUS + 1; i++) {
987
+ if (i >= 2 * u_Radius + 1)
988
+ break; // GLSL can only use constants in for-loop declaration, so we break here.
989
+ // (2 * u_Radius + 1) is the width of u_Shape, by definition
990
+ float weight = texture2D(u_Shape, vec2(float(i) / float(2 * u_Radius + 1), 0.5)).r; // TODO: use single-channel format
991
+ totalWeight += weight;
992
+ vec4 sample = texture2D(u_Source, v_TextureCoord + vec2(0.0, i - u_Radius) / vec2(u_Size));
993
+ avg += weight * sample;
994
+ }
995
+ gl_FragColor = avg / totalWeight;
996
+ }
997
+ `, radius)
998
+ }
999
+ }
1000
+
1001
+ /**
1002
+ * Makes the target look pixelated
1003
+ * @todo just resample with NEAREST interpolation? but how?
1004
+ */
1005
+ export class Pixelate extends Shader {
1006
+ /**
1007
+ * @param {number} pixelSize
1008
+ */
1009
+ constructor (pixelSize = 1) {
1010
+ super(`
1011
+ precision mediump float;
1012
+
1013
+ uniform sampler2D u_Source;
1014
+ uniform ivec2 u_Size;
1015
+ uniform int u_PixelSize;
1016
+
1017
+ varying highp vec2 v_TextureCoord;
1018
+
1019
+ void main() {
1020
+ int ps = u_PixelSize;
1021
+
1022
+ // Snap to nearest block's center
1023
+ vec2 loc = vec2(u_Size) * v_TextureCoord; // pixel-space
1024
+ vec2 snappedLoc = float(ps) * floor(loc / float(ps));
1025
+ vec2 centeredLoc = snappedLoc + vec2(float(u_PixelSize) / 2.0 + 0.5);
1026
+ vec2 clampedLoc = clamp(centeredLoc, vec2(0.0), vec2(u_Size));
1027
+ gl_FragColor = texture2D(u_Source, clampedLoc / vec2(u_Size));
1028
+ }
1029
+ `, {
1030
+ pixelSize: '1i'
1031
+ })
1032
+ /**
1033
+ * @type number
1034
+ */
1035
+ this.pixelSize = pixelSize
1036
+ }
1037
+
1038
+ apply (target, reltime) {
1039
+ const ps = val(this, 'pixelSize', reltime)
1040
+ if (ps % 1 !== 0 || ps < 0) {
1041
+ throw new Error('Pixel size must be a nonnegative integer')
1042
+ }
1043
+
1044
+ super.apply(target, reltime)
1045
+ }
1046
+ }
1047
+
1048
+ // TODO: implement directional blur
1049
+ // TODO: implement radial blur
1050
+ // TODO: implement zoom blur
1051
+
1052
+ /* DISTORTION */
1053
+ /**
1054
+ * Transforms a layer or movie using a transformation matrix. Use {@link Transform.Matrix}
1055
+ * to either A) calculate those values based on a series of translations, scalings and rotations)
1056
+ * or B) input the matrix values directly, using the optional argument in the constructor.
1057
+ */
1058
+ export class Transform extends Base {
1059
+ /**
1060
+ * @param {module:effect.Transform.Matrix} matrix - how to transform the target
1061
+ */
1062
+ constructor (matrix) {
1063
+ super()
1064
+ /**
1065
+ * How to transform the target
1066
+ * @type module:effect.Transform.Matrix
1067
+ */
1068
+ this.matrix = matrix
1069
+ this._tmpMatrix = new Transform.Matrix()
1070
+ this._tmpCanvas = document.createElement('canvas')
1071
+ this._tmpCtx = this._tmpCanvas.getContext('2d')
1072
+ }
1073
+
1074
+ apply (target, reltime) {
1075
+ if (target.canvas.width !== this._tmpCanvas.width) {
1076
+ this._tmpCanvas.width = target.canvas.width
1077
+ }
1078
+ if (target.canvas.height !== this._tmpCanvas.height) {
1079
+ this._tmpCanvas.height = target.canvas.height
1080
+ }
1081
+ this._tmpMatrix.data = val(this, 'matrix.data', reltime) // use data, since that's the underlying storage
1082
+
1083
+ this._tmpCtx.setTransform(
1084
+ this._tmpMatrix.a, this._tmpMatrix.b, this._tmpMatrix.c,
1085
+ this._tmpMatrix.d, this._tmpMatrix.e, this._tmpMatrix.f
1086
+ )
1087
+ this._tmpCtx.drawImage(target.canvas, 0, 0)
1088
+ // Assume it was identity for now
1089
+ this._tmpCtx.setTransform(1, 0, 0, 0, 1, 0, 0, 0, 1)
1090
+ target.cctx.clearRect(0, 0, target.canvas.width, target.canvas.height)
1091
+ target.cctx.drawImage(this._tmpCanvas, 0, 0)
1092
+ }
1093
+ }
1094
+ /**
1095
+ * @class
1096
+ * A 3x3 matrix for storing 2d transformations
1097
+ */
1098
+ Transform.Matrix = class Matrix {
1099
+ constructor (data) {
1100
+ this.data = data || [
1101
+ 1, 0, 0,
1102
+ 0, 1, 0,
1103
+ 0, 0, 1
1104
+ ]
1105
+ }
1106
+
1107
+ identity () {
1108
+ for (let i = 0; i < this.data.length; i++) {
1109
+ this.data[i] = Transform.Matrix.IDENTITY.data[i]
1110
+ }
1111
+
1112
+ return this
1113
+ }
1114
+
1115
+ /**
1116
+ * @param {number} x
1117
+ * @param {number} y
1118
+ * @param {number} [val]
1119
+ */
1120
+ cell (x, y, val) {
1121
+ if (val !== undefined) {
1122
+ this.data[3 * y + x] = val
1123
+ }
1124
+ return this.data[3 * y + x]
1125
+ }
1126
+
1127
+ /* For canvas context setTransform */
1128
+ get a () {
1129
+ return this.data[0]
1130
+ }
1131
+
1132
+ get b () {
1133
+ return this.data[3]
1134
+ }
1135
+
1136
+ get c () {
1137
+ return this.data[1]
1138
+ }
1139
+
1140
+ get d () {
1141
+ return this.data[4]
1142
+ }
1143
+
1144
+ get e () {
1145
+ return this.data[2]
1146
+ }
1147
+
1148
+ get f () {
1149
+ return this.data[5]
1150
+ }
1151
+
1152
+ /**
1153
+ * Combines <code>this</code> with another matrix <code>other</code>
1154
+ * @param other
1155
+ */
1156
+ multiply (other) {
1157
+ // copy to temporary matrix to avoid modifying `this` while reading from it
1158
+ // http://www.informit.com/articles/article.aspx?p=98117&seqNum=4
1159
+ for (let x = 0; x < 3; x++) {
1160
+ for (let y = 0; y < 3; y++) {
1161
+ let sum = 0
1162
+ for (let i = 0; i < 3; i++) {
1163
+ sum += this.cell(x, i) * other.cell(i, y)
1164
+ }
1165
+ TMP_MATRIX.cell(x, y, sum)
1166
+ }
1167
+ }
1168
+ // copy data from TMP_MATRIX to this
1169
+ for (let i = 0; i < TMP_MATRIX.data.length; i++) {
1170
+ this.data[i] = TMP_MATRIX.data[i]
1171
+ }
1172
+ return this
1173
+ }
1174
+
1175
+ /**
1176
+ * @param {number} x
1177
+ * @param {number} y
1178
+ */
1179
+ translate (x, y) {
1180
+ this.multiply(new Transform.Matrix([
1181
+ 1, 0, x,
1182
+ 0, 1, y,
1183
+ 0, 0, 1
1184
+ ]))
1185
+
1186
+ return this
1187
+ }
1188
+
1189
+ /**
1190
+ * @param {number} x
1191
+ * @param {number} y
1192
+ */
1193
+ scale (x, y) {
1194
+ this.multiply(new Transform.Matrix([
1195
+ x, 0, 0,
1196
+ 0, y, 0,
1197
+ 0, 0, 1
1198
+ ]))
1199
+
1200
+ return this
1201
+ }
1202
+
1203
+ /**
1204
+ * @param {number} a - the angle or rotation in radians
1205
+ */
1206
+ rotate (a) {
1207
+ const c = Math.cos(a); const s = Math.sin(a)
1208
+ this.multiply(new Transform.Matrix([
1209
+ c, s, 0,
1210
+ -s, c, 0,
1211
+ 0, 0, 1
1212
+ ]))
1213
+
1214
+ return this
1215
+ }
1216
+ }
1217
+ /**
1218
+ * The identity matrix
1219
+ */
1220
+ Transform.Matrix.IDENTITY = new Transform.Matrix()
1221
+ const TMP_MATRIX = new Transform.Matrix()
1222
+
1223
+ /**
1224
+ * Preserves an ellipse of the layer and clears the rest
1225
+ * @todo Parent layer mask effects will make more complex masks easier
1226
+ */
1227
+ export class EllipticalMask extends Base {
1228
+ constructor (x, y, radiusX, radiusY, rotation = 0, startAngle = 0, endAngle = 2 * Math.PI, anticlockwise = false) {
1229
+ super()
1230
+ this.x = x
1231
+ this.y = y
1232
+ this.radiusX = radiusX
1233
+ this.radiusY = radiusY
1234
+ this.rotation = rotation
1235
+ this.startAngle = startAngle
1236
+ this.endAngle = endAngle
1237
+ this.anticlockwise = anticlockwise
1238
+ // for saving image data before clearing
1239
+ this._tmpCanvas = document.createElement('canvas')
1240
+ this._tmpCtx = this._tmpCanvas.getContext('2d')
1241
+ }
1242
+
1243
+ apply (target, reltime) {
1244
+ const ctx = target.cctx; const canvas = target.canvas
1245
+ const x = val(this, 'x', reltime); const y = val(this.y, target, reltime)
1246
+ const radiusX = val(this, 'radiusX', reltime); const radiusY = val(this.radiusY, target, reltime)
1247
+ const rotation = val(this, 'rotation', reltime)
1248
+ const startAngle = val(this, 'startAngle', reltime); const endAngle = val(this.endAngle, target, reltime)
1249
+ const anticlockwise = val(this, 'anticlockwise', reltime)
1250
+ this._tmpCanvas.width = target.canvas.width
1251
+ this._tmpCanvas.height = target.canvas.height
1252
+ this._tmpCtx.drawImage(canvas, 0, 0)
1253
+
1254
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
1255
+ ctx.save() // idk how to preserve clipping state without save/restore
1256
+ // create elliptical path and clip
1257
+ ctx.beginPath()
1258
+ ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
1259
+ ctx.closePath()
1260
+ ctx.clip()
1261
+ // render image with clipping state
1262
+ ctx.drawImage(this._tmpCanvas, 0, 0)
1263
+ ctx.restore()
1264
+ }
1265
+ }