etro 0.7.0 → 0.8.2

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