etro 0.6.0 → 0.8.1

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 (157) hide show
  1. package/.github/workflows/nodejs.yml +4 -2
  2. package/CHANGELOG.md +85 -4
  3. package/CODE_OF_CONDUCT.md +5 -5
  4. package/CONTRIBUTING.md +33 -79
  5. package/README.md +93 -26
  6. package/dist/effect/base.d.ts +51 -0
  7. package/dist/effect/brightness.d.ts +16 -0
  8. package/dist/effect/channels.d.ts +23 -0
  9. package/dist/effect/chroma-key.d.ts +23 -0
  10. package/dist/effect/contrast.d.ts +15 -0
  11. package/dist/effect/elliptical-mask.d.ts +31 -0
  12. package/dist/effect/gaussian-blur.d.ts +60 -0
  13. package/dist/effect/grayscale.d.ts +7 -0
  14. package/dist/effect/index.d.ts +15 -0
  15. package/dist/effect/pixelate.d.ts +18 -0
  16. package/dist/effect/shader.d.ts +99 -0
  17. package/dist/effect/stack.d.ts +23 -0
  18. package/dist/effect/transform.d.ts +73 -0
  19. package/dist/etro-cjs.js +9387 -0
  20. package/dist/etro-iife.js +9390 -0
  21. package/dist/etro.d.ts +7 -0
  22. package/dist/event.d.ts +35 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/layer/audio-source.d.ts +24 -0
  25. package/dist/layer/audio.d.ts +14 -0
  26. package/dist/layer/base.d.ts +82 -0
  27. package/dist/layer/image.d.ts +6 -0
  28. package/dist/layer/index.d.ts +11 -0
  29. package/dist/layer/text.d.ts +60 -0
  30. package/dist/layer/video.d.ts +11 -0
  31. package/dist/layer/visual-source.d.ts +32 -0
  32. package/dist/layer/visual.d.ts +58 -0
  33. package/dist/movie.d.ts +192 -0
  34. package/dist/object.d.ts +12 -0
  35. package/dist/util.d.ts +125 -0
  36. package/eslint.conf.js +2 -9
  37. package/eslint.example-conf.js +9 -0
  38. package/eslint.test-conf.js +1 -0
  39. package/eslint.typescript-conf.js +5 -0
  40. package/examples/application/readme-screenshot.html +16 -17
  41. package/examples/application/video-player.html +10 -11
  42. package/examples/application/webcam.html +6 -6
  43. package/examples/introduction/audio.html +30 -18
  44. package/examples/introduction/effects.html +37 -14
  45. package/examples/introduction/export.html +40 -27
  46. package/examples/introduction/functions.html +6 -4
  47. package/examples/introduction/hello-world1.html +9 -5
  48. package/examples/introduction/hello-world2.html +5 -5
  49. package/examples/introduction/keyframes.html +35 -23
  50. package/examples/introduction/media.html +26 -18
  51. package/examples/introduction/text.html +9 -5
  52. package/karma.conf.js +6 -4
  53. package/package.json +34 -13
  54. package/rollup.config.js +19 -3
  55. package/scripts/gen-effect-samples.html +27 -26
  56. package/scripts/save-effect-samples.js +14 -15
  57. package/src/effect/base.ts +115 -0
  58. package/src/effect/brightness.ts +43 -0
  59. package/src/effect/channels.ts +50 -0
  60. package/src/effect/chroma-key.ts +82 -0
  61. package/src/effect/contrast.ts +42 -0
  62. package/src/effect/elliptical-mask.ts +75 -0
  63. package/src/effect/gaussian-blur.ts +232 -0
  64. package/src/effect/grayscale.ts +34 -0
  65. package/src/effect/index.ts +22 -0
  66. package/src/effect/pixelate.ts +58 -0
  67. package/src/effect/shader.ts +557 -0
  68. package/src/effect/stack.ts +77 -0
  69. package/src/effect/transform.ts +193 -0
  70. package/src/etro.ts +26 -0
  71. package/src/event.ts +112 -0
  72. package/src/index.ts +8 -0
  73. package/src/layer/audio-source.ts +219 -0
  74. package/src/layer/audio.ts +34 -0
  75. package/src/layer/base.ts +175 -0
  76. package/src/layer/image.ts +8 -0
  77. package/src/layer/index.ts +13 -0
  78. package/src/layer/text.ts +138 -0
  79. package/src/layer/video.ts +15 -0
  80. package/src/layer/visual-source.ts +150 -0
  81. package/src/layer/visual.ts +197 -0
  82. package/src/movie.ts +701 -0
  83. package/src/object.ts +14 -0
  84. package/src/util.ts +466 -0
  85. package/tsconfig.json +8 -0
  86. package/dist/etro.js +0 -3397
  87. package/docs/effect.js.html +0 -1215
  88. package/docs/event.js.html +0 -145
  89. package/docs/index.html +0 -81
  90. package/docs/index.js.html +0 -92
  91. package/docs/layer.js.html +0 -888
  92. package/docs/module-effect-GaussianBlurComponent.html +0 -345
  93. package/docs/module-effect.Brightness.html +0 -339
  94. package/docs/module-effect.Channels.html +0 -319
  95. package/docs/module-effect.ChromaKey.html +0 -611
  96. package/docs/module-effect.Contrast.html +0 -339
  97. package/docs/module-effect.EllipticalMask.html +0 -200
  98. package/docs/module-effect.GaussianBlur.html +0 -202
  99. package/docs/module-effect.GaussianBlurHorizontal.html +0 -242
  100. package/docs/module-effect.GaussianBlurVertical.html +0 -242
  101. package/docs/module-effect.Pixelate.html +0 -330
  102. package/docs/module-effect.Shader.html +0 -1227
  103. package/docs/module-effect.Stack.html +0 -406
  104. package/docs/module-effect.Transform.Matrix.html +0 -193
  105. package/docs/module-effect.Transform.html +0 -1174
  106. package/docs/module-effect.html +0 -148
  107. package/docs/module-event.html +0 -473
  108. package/docs/module-index.html +0 -186
  109. package/docs/module-layer-Media.html +0 -1116
  110. package/docs/module-layer-MediaMixin.html +0 -164
  111. package/docs/module-layer.Audio.html +0 -1188
  112. package/docs/module-layer.Base.html +0 -629
  113. package/docs/module-layer.Image.html +0 -1421
  114. package/docs/module-layer.Text.html +0 -1731
  115. package/docs/module-layer.Video.html +0 -1938
  116. package/docs/module-layer.Visual.html +0 -1698
  117. package/docs/module-layer.html +0 -137
  118. package/docs/module-movie.html +0 -3118
  119. package/docs/module-util.Color.html +0 -702
  120. package/docs/module-util.Font.html +0 -395
  121. package/docs/module-util.html +0 -845
  122. package/docs/movie.js.html +0 -689
  123. package/docs/scripts/collapse.js +0 -20
  124. package/docs/scripts/linenumber.js +0 -25
  125. package/docs/scripts/nav.js +0 -12
  126. package/docs/scripts/polyfill.js +0 -4
  127. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
  128. package/docs/scripts/prettify/lang-css.js +0 -2
  129. package/docs/scripts/prettify/prettify.js +0 -28
  130. package/docs/scripts/search.js +0 -83
  131. package/docs/styles/jsdoc.css +0 -671
  132. package/docs/styles/prettify.css +0 -79
  133. package/docs/util.js.html +0 -503
  134. package/screenshots/2019-08-17_0.png +0 -0
  135. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  136. package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
  137. package/spec/assets/effect/original.png +0 -0
  138. package/spec/assets/effect/pixelate.png +0 -0
  139. package/spec/assets/effect/transform/multiply.png +0 -0
  140. package/spec/assets/effect/transform/rotate.png +0 -0
  141. package/spec/assets/effect/transform/scale-fraction.png +0 -0
  142. package/spec/assets/effect/transform/scale.png +0 -0
  143. package/spec/assets/effect/transform/translate-fraction.png +0 -0
  144. package/spec/assets/effect/transform/translate.png +0 -0
  145. package/spec/assets/layer/audio.wav +0 -0
  146. package/spec/assets/layer/image.jpg +0 -0
  147. package/spec/effect.spec.js +0 -352
  148. package/spec/event.spec.js +0 -25
  149. package/spec/layer.spec.js +0 -128
  150. package/spec/movie.spec.js +0 -154
  151. package/spec/util.spec.js +0 -285
  152. package/src/effect.js +0 -1265
  153. package/src/event.js +0 -78
  154. package/src/index.js +0 -23
  155. package/src/layer.js +0 -875
  156. package/src/movie.js +0 -636
  157. package/src/util.js +0 -487
@@ -0,0 +1,50 @@
1
+ import { Dynamic } from '../util'
2
+ import { Shader } from './shader'
3
+
4
+ export interface ChannelsOptions {
5
+ factors?: Dynamic<{
6
+ r?: number,
7
+ g?: number,
8
+ b?: number
9
+ }>
10
+ }
11
+
12
+ /**
13
+ * Multiplies each channel by a different factor
14
+ */
15
+ export class Channels extends Shader {
16
+ factors: Dynamic<{
17
+ r?: number,
18
+ b?: number,
19
+ g?: number
20
+ }>
21
+
22
+ /**
23
+ * @param factors - channel factors, each defaulting to 1
24
+ */
25
+ constructor (options: ChannelsOptions = {}) {
26
+ super({
27
+ fragmentSource: `
28
+ precision mediump float;
29
+
30
+ uniform sampler2D u_Source;
31
+ uniform vec4 u_Factors;
32
+
33
+ varying highp vec2 v_TextureCoord;
34
+
35
+ void main() {
36
+ vec4 color = texture2D(u_Source, v_TextureCoord);
37
+ gl_FragColor = clamp(u_Factors * color, 0.0, 1.0);
38
+ }
39
+ `,
40
+ uniforms: {
41
+ factors: { type: '4fv', defaultFloatComponent: 1 }
42
+ }
43
+ })
44
+
45
+ /**
46
+ * Channel factors, each defaulting to 1
47
+ */
48
+ this.factors = options.factors || {}
49
+ }
50
+ }
@@ -0,0 +1,82 @@
1
+ import { Dynamic, Color } from '../util'
2
+ import { Shader } from './shader'
3
+
4
+ export interface ChromaKeyOptions {
5
+ target?: Dynamic<Color>
6
+ threshold?: Dynamic<number>
7
+ interpolate?: Dynamic<boolean>
8
+ // smoothingSharpness
9
+ }
10
+
11
+ /**
12
+ * Reduces alpha for pixels which are close to a specified target color
13
+ */
14
+ export class ChromaKey extends Shader {
15
+ target: Dynamic<Color>
16
+ threshold: Dynamic<number>
17
+ interpolate: Dynamic<boolean>
18
+
19
+ /**
20
+ * @param [target={r: 0, g: 0, b: 0, a: 1}] - the color to remove
21
+ * @param [threshold=0] - how much error to allow
22
+ * @param [interpolate=false] - <code>true</code> to interpolate
23
+ * the alpha channel, or <code>false</code> value for no smoothing (i.e. an
24
+ * alpha of either 0 or 255)
25
+ */
26
+ // TODO: Use <code>smoothingSharpness</code>
27
+ constructor (options: ChromaKeyOptions = {}) {
28
+ super({
29
+ fragmentSource: `
30
+ precision mediump float;
31
+
32
+ uniform sampler2D u_Source;
33
+ uniform vec3 u_Target;
34
+ uniform float u_Threshold;
35
+ uniform bool u_Interpolate;
36
+
37
+ varying highp vec2 v_TextureCoord;
38
+
39
+ void main() {
40
+ vec4 color = texture2D(u_Source, v_TextureCoord);
41
+ float alpha = color.a;
42
+ vec3 dist = abs(color.rgb - u_Target / 255.0);
43
+ if (!u_Interpolate) {
44
+ // Standard way that most video editors probably use (all-or-nothing method)
45
+ float thresh = u_Threshold / 255.0;
46
+ bool transparent = dist.r <= thresh && dist.g <= thresh && dist.b <= thresh;
47
+ if (transparent)
48
+ alpha = 0.0;
49
+ } else {
50
+ /*
51
+ better way IMHO:
52
+ Take the average of the absolute differences between the pixel and the target for each channel
53
+ */
54
+ float transparency = (dist.r + dist.g + dist.b) / 3.0;
55
+ // TODO: custom or variety of interpolation methods
56
+ alpha = transparency;
57
+ }
58
+ gl_FragColor = vec4(color.rgb, alpha);
59
+ }
60
+ `,
61
+ uniforms: {
62
+ target: '3fv',
63
+ threshold: '1f',
64
+ interpolate: '1i'
65
+ }
66
+ })
67
+ /**
68
+ * The color to remove
69
+ */
70
+ this.target = options.target || new Color(0, 0, 0)
71
+ /**
72
+ * How much error to allow
73
+ */
74
+ this.threshold = options.threshold || 0
75
+ /**
76
+ * <code>true<code> to interpolate the alpha channel, or <code>false<code>
77
+ * for no smoothing (i.e. 255 or 0 alpha)
78
+ */
79
+ this.interpolate = options.interpolate || false
80
+ // this.smoothingSharpness = smoothingSharpness;
81
+ }
82
+ }
@@ -0,0 +1,42 @@
1
+ import { Dynamic } from '../util'
2
+ import { Shader } from './shader'
3
+
4
+ export interface ContrastOptions {
5
+ contrast?: Dynamic<number>
6
+ }
7
+
8
+ /**
9
+ * Changes the contrast by multiplying the RGB channels by a constant
10
+ */
11
+ export class Contrast extends Shader {
12
+ contrast: Dynamic<number>
13
+
14
+ /**
15
+ * @param [contrast=1] - the contrast multiplier
16
+ */
17
+ constructor (options: ContrastOptions = {}) {
18
+ super({
19
+ fragmentSource: `
20
+ precision mediump float;
21
+
22
+ uniform sampler2D u_Source;
23
+ uniform float u_Contrast;
24
+
25
+ varying highp vec2 v_TextureCoord;
26
+
27
+ void main() {
28
+ vec4 color = texture2D(u_Source, v_TextureCoord);
29
+ vec3 rgb = clamp(u_Contrast * (color.rgb - 0.5) + 0.5, 0.0, 1.0);
30
+ gl_FragColor = vec4(rgb, color.a);
31
+ }
32
+ `,
33
+ uniforms: {
34
+ contrast: '1f'
35
+ }
36
+ })
37
+ /**
38
+ * The contrast multiplier
39
+ */
40
+ this.contrast = options.contrast || 1
41
+ }
42
+ }
@@ -0,0 +1,75 @@
1
+ import { Movie } from '../movie'
2
+ import { Dynamic, val } from '../util'
3
+ import { Base } from './base'
4
+ import { Visual } from '../layer/index'
5
+
6
+ export class EllipticalMaskOptions {
7
+ x: Dynamic<number>
8
+ y: Dynamic<number>
9
+ radiusX: Dynamic<number>
10
+ radiusY: Dynamic<number>
11
+ rotation?: Dynamic<number>
12
+ startAngle?: Dynamic<number>
13
+ endAngle?: Dynamic<number>
14
+ anticlockwise?: Dynamic<boolean>
15
+ }
16
+
17
+ /**
18
+ * Preserves an ellipse of the layer and clears the rest
19
+ */
20
+ // TODO: Parent layer mask effects will make more complex masks easier
21
+ export class EllipticalMask extends Base {
22
+ x: Dynamic<number>
23
+ y: Dynamic<number>
24
+ radiusX: Dynamic<number>
25
+ radiusY: Dynamic<number>
26
+ rotation: Dynamic<number>
27
+ startAngle: Dynamic<number>
28
+ endAngle: Dynamic<number>
29
+ anticlockwise: Dynamic<boolean>
30
+
31
+ private _tmpCanvas
32
+ private _tmpCtx
33
+
34
+ constructor (options: EllipticalMaskOptions) {
35
+ super()
36
+ this.x = options.x
37
+ this.y = options.y
38
+ this.radiusX = options.radiusX
39
+ this.radiusY = options.radiusY
40
+ this.rotation = options.rotation || 0
41
+ this.startAngle = options.startAngle || 0
42
+ this.endAngle = options.endAngle !== undefined ? options.endAngle : 2 * Math.PI
43
+ this.anticlockwise = options.anticlockwise || false
44
+ // for saving image data before clearing
45
+ this._tmpCanvas = document.createElement('canvas')
46
+ this._tmpCtx = this._tmpCanvas.getContext('2d')
47
+ }
48
+
49
+ apply (target: Movie | Visual, reltime: number): void {
50
+ const ctx = target.cctx
51
+ const canvas = target.canvas
52
+ const x = val(this, 'x', reltime)
53
+ const y = val(this, 'y', reltime)
54
+ const radiusX = val(this, 'radiusX', reltime)
55
+ const radiusY = val(this, 'radiusY', reltime)
56
+ const rotation = val(this, 'rotation', reltime)
57
+ const startAngle = val(this, 'startAngle', reltime)
58
+ const endAngle = val(this, 'endAngle', reltime)
59
+ const anticlockwise = val(this, 'anticlockwise', reltime)
60
+ this._tmpCanvas.width = target.canvas.width
61
+ this._tmpCanvas.height = target.canvas.height
62
+ this._tmpCtx.drawImage(canvas, 0, 0)
63
+
64
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
65
+ ctx.save() // idk how to preserve clipping state without save/restore
66
+ // create elliptical path and clip
67
+ ctx.beginPath()
68
+ ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
69
+ ctx.closePath()
70
+ ctx.clip()
71
+ // render image with clipping state
72
+ ctx.drawImage(this._tmpCanvas, 0, 0)
73
+ ctx.restore()
74
+ }
75
+ }
@@ -0,0 +1,232 @@
1
+ import { val } from '../util'
2
+ import { Stack } from './stack'
3
+ import { Shader } from './shader'
4
+ import { Movie } from '../movie'
5
+ import { Visual } from '../layer'
6
+
7
+ export interface GaussianBlurOptions {
8
+ radius: number
9
+ }
10
+
11
+ /**
12
+ * Applies a Gaussian blur
13
+ */
14
+ // TODO: Improve performance
15
+ // TODO: Make sure this is truly gaussian even though it doens't require a
16
+ // standard deviation
17
+ export class GaussianBlur extends Stack {
18
+ constructor (options: GaussianBlurOptions) {
19
+ // Divide into two shader effects (use the fact that gaussian blurring can
20
+ // be split into components for performance benefits)
21
+ super({
22
+ effects: [
23
+ new GaussianBlurHorizontal(options),
24
+ new GaussianBlurVertical(options)
25
+ ]
26
+ })
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Shared class for both horizontal and vertical gaussian blur classes.
32
+ */
33
+ // TODO: If radius == 0, don't affect the image (right now, the image goes black).
34
+ class GaussianBlurComponent extends Shader {
35
+ radius: number
36
+ shape: HTMLCanvasElement
37
+
38
+ private _radiusCache: number
39
+
40
+ /**
41
+ * @param src - fragment source code (specific to which component -
42
+ * horizontal or vertical)
43
+ * @param radius - only integers are currently supported
44
+ */
45
+ constructor (options: {
46
+ fragmentSource: string,
47
+ radius: number
48
+ }) {
49
+ super({
50
+ fragmentSource: options.fragmentSource,
51
+ uniforms: {
52
+ radius: '1i'
53
+ },
54
+ textures: {
55
+ shape: { minFilter: 'NEAREST', magFilter: 'NEAREST' }
56
+ }
57
+ })
58
+ /**
59
+ */
60
+ this.radius = options.radius
61
+ this._radiusCache = undefined
62
+ }
63
+
64
+ apply (target: Movie | Visual, reltime: number): void {
65
+ const radiusVal = val(this, 'radius', reltime)
66
+ if (radiusVal !== this._radiusCache)
67
+ // Regenerate gaussian distribution canvas.
68
+ this.shape = GaussianBlurComponent._render1DKernel(
69
+ GaussianBlurComponent._gen1DKernel(radiusVal)
70
+ )
71
+
72
+ this._radiusCache = radiusVal
73
+
74
+ super.apply(target, reltime)
75
+ }
76
+
77
+ /**
78
+ * Render Gaussian kernel to a canvas for use in shader.
79
+ * @param kernel
80
+ * @private
81
+ *
82
+ * @return
83
+ */
84
+ private static _render1DKernel (kernel: number[]): HTMLCanvasElement {
85
+ // TODO: Use Float32Array instead of canvas.
86
+ // init canvas
87
+ const canvas = document.createElement('canvas')
88
+ canvas.width = kernel.length
89
+ canvas.height = 1 // 1-dimensional
90
+ const ctx = canvas.getContext('2d')
91
+
92
+ // draw to canvas
93
+ const imageData = ctx.createImageData(canvas.width, canvas.height)
94
+ for (let i = 0; i < kernel.length; i++) {
95
+ imageData.data[4 * i + 0] = 255 * kernel[i] // Use red channel to store distribution weights.
96
+ imageData.data[4 * i + 1] = 0 // Clear all other channels.
97
+ imageData.data[4 * i + 2] = 0
98
+ imageData.data[4 * i + 3] = 255
99
+ }
100
+ ctx.putImageData(imageData, 0, 0)
101
+
102
+ return canvas
103
+ }
104
+
105
+ private static _gen1DKernel (radius: number): number[] {
106
+ const pascal = GaussianBlurComponent._genPascalRow(2 * radius + 1)
107
+ // don't use `reduce` and `map` (overhead?)
108
+ let sum = 0
109
+ for (let i = 0; i < pascal.length; i++)
110
+ sum += pascal[i]
111
+
112
+ for (let i = 0; i < pascal.length; i++)
113
+ pascal[i] /= sum
114
+
115
+ return pascal
116
+ }
117
+
118
+ private static _genPascalRow (index: number): number[] {
119
+ if (index < 0)
120
+ throw new Error(`Invalid index ${index}`)
121
+
122
+ let currRow = [1]
123
+ for (let i = 1; i < index; i++) {
124
+ const nextRow = []
125
+ nextRow.length = currRow.length + 1
126
+ // edges are always 1's
127
+ nextRow[0] = nextRow[nextRow.length - 1] = 1
128
+ for (let j = 1; j < nextRow.length - 1; j++)
129
+ nextRow[j] = currRow[j - 1] + currRow[j]
130
+
131
+ currRow = nextRow
132
+ }
133
+ return currRow
134
+ }
135
+ }
136
+ GaussianBlurComponent.prototype.publicExcludes = Shader.prototype.publicExcludes.concat(['shape'])
137
+
138
+ /**
139
+ * Horizontal component of gaussian blur
140
+ */
141
+ export class GaussianBlurHorizontal extends GaussianBlurComponent {
142
+ /**
143
+ * @param radius
144
+ */
145
+ constructor (options: GaussianBlurOptions) {
146
+ super({
147
+ fragmentSource: `
148
+ #define MAX_RADIUS 250
149
+
150
+ precision mediump float;
151
+
152
+ uniform sampler2D u_Source;
153
+ uniform ivec2 u_Size; // pixel dimensions of input and output
154
+ uniform sampler2D u_Shape; // pseudo one-dimension of blur distribution (would be 1D but webgl doesn't support it)
155
+ uniform int u_Radius; // TODO: support floating-point radii
156
+
157
+ varying highp vec2 v_TextureCoord;
158
+
159
+ void main() {
160
+ /*
161
+ * Ideally, totalWeight should end up being 1, but due to rounding errors, it sometimes ends up less than 1
162
+ * (I believe JS canvas stores values as integers, which rounds down for the majority of the Gaussian curve)
163
+ * So, normalize by accumulating all the weights and dividing by that.
164
+ */
165
+ float totalWeight = 0.0;
166
+ vec4 avg = vec4(0.0);
167
+ // GLSL can only use constants in for-loop declaration, so start at zero, and stop before 2 * u_Radius + 1,
168
+ // opposed to starting at -u_Radius and stopping _at_ +u_Radius.
169
+ for (int i = 0; i < 2 * MAX_RADIUS + 1; i++) {
170
+ if (i >= 2 * u_Radius + 1)
171
+ break; // GLSL can only use constants in for-loop declaration, so we break here.
172
+ // (2 * u_Radius + 1) is the width of u_Shape, by definition
173
+ float weight = texture2D(u_Shape, vec2(float(i) / float(2 * u_Radius + 1), 0.5)).r; // TODO: use single-channel format
174
+ totalWeight += weight;
175
+ vec4 sample = texture2D(u_Source, v_TextureCoord + vec2(i - u_Radius, 0.0) / vec2(u_Size));
176
+ avg += weight * sample;
177
+ }
178
+ gl_FragColor = avg / totalWeight;
179
+ }
180
+ `,
181
+ radius: options.radius
182
+ })
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Vertical component of gaussian blur
188
+ */
189
+ export class GaussianBlurVertical extends GaussianBlurComponent {
190
+ /**
191
+ * @param radius
192
+ */
193
+ constructor (options: GaussianBlurOptions) {
194
+ super({
195
+ fragmentSource: `
196
+ #define MAX_RADIUS 250
197
+
198
+ precision mediump float;
199
+
200
+ uniform sampler2D u_Source;
201
+ uniform ivec2 u_Size; // pixel dimensions of input and output
202
+ uniform sampler2D u_Shape; // pseudo one-dimension of blur distribution (would be 1D but webgl doesn't support it)
203
+ uniform int u_Radius; // TODO: support floating-point radii
204
+
205
+ varying highp vec2 v_TextureCoord;
206
+
207
+ void main() {
208
+ /*
209
+ * Ideally, totalWeight should end up being 1, but due to rounding errors, it sometimes ends up less than 1
210
+ * (I believe JS canvas stores values as integers, which rounds down for the majority of the Gaussian curve)
211
+ * So, normalize by accumulating all the weights and dividing by that.
212
+ */
213
+ float totalWeight = 0.0;
214
+ vec4 avg = vec4(0.0);
215
+ // GLSL can only use constants in for-loop declaration, so start at zero, and stop before 2 * u_Radius + 1,
216
+ // opposed to starting at -u_Radius and stopping _at_ +u_Radius.
217
+ for (int i = 0; i < 2 * MAX_RADIUS + 1; i++) {
218
+ if (i >= 2 * u_Radius + 1)
219
+ break; // GLSL can only use constants in for-loop declaration, so we break here.
220
+ // (2 * u_Radius + 1) is the width of u_Shape, by definition
221
+ float weight = texture2D(u_Shape, vec2(float(i) / float(2 * u_Radius + 1), 0.5)).r; // TODO: use single-channel format
222
+ totalWeight += weight;
223
+ vec4 sample = texture2D(u_Source, v_TextureCoord + vec2(0.0, i - u_Radius) / vec2(u_Size));
224
+ avg += weight * sample;
225
+ }
226
+ gl_FragColor = avg / totalWeight;
227
+ }
228
+ `,
229
+ radius: options.radius
230
+ })
231
+ }
232
+ }
@@ -0,0 +1,34 @@
1
+ import { Shader } from './shader'
2
+
3
+ /**
4
+ * Converts the target to a grayscale image
5
+ */
6
+ export class Grayscale extends Shader {
7
+ constructor () {
8
+ super({
9
+ fragmentSource: `
10
+ precision mediump float;
11
+
12
+ uniform sampler2D u_Source;
13
+ uniform vec4 u_Factors;
14
+
15
+ varying highp vec2 v_TextureCoord;
16
+
17
+ float max3(float x, float y, float z) {
18
+ return max(x, max(y, z));
19
+ }
20
+
21
+ float min3(float x, float y, float z) {
22
+ return min(x, min(y, z));
23
+ }
24
+
25
+ void main() {
26
+ vec4 color = texture2D(u_Source, v_TextureCoord);
27
+ // Desaturate
28
+ float value = (max3(color.r, color.g, color.b) + min3(color.r, color.g, color.b)) / 2.0;
29
+ gl_FragColor = vec4(value, value, value, color.a);
30
+ }
31
+ `
32
+ })
33
+ }
34
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @module effect
3
+ */
4
+
5
+ // TODO: Investigate why an effect might run once in the beginning even if its layer isn't at the beginning
6
+ // TODO: Add audio effect support
7
+ // TODO: implement directional blur
8
+ // TODO: implement radial blur
9
+ // TODO: implement zoom blur
10
+
11
+ export * from './base'
12
+ export * from './brightness'
13
+ export * from './channels'
14
+ export * from './chroma-key'
15
+ export * from './contrast'
16
+ export * from './elliptical-mask'
17
+ export * from './gaussian-blur'
18
+ export * from './grayscale'
19
+ export * from './pixelate'
20
+ export * from './shader'
21
+ export * from './stack'
22
+ export * from './transform'
@@ -0,0 +1,58 @@
1
+ import { Visual } from '../layer'
2
+ import { Movie } from '../movie'
3
+ import { val, Dynamic } from '../util'
4
+ import { Shader } from './shader'
5
+
6
+ export interface PixelateOptions {
7
+ pixelSize?: Dynamic<number>
8
+ }
9
+
10
+ /**
11
+ * Breaks the target up into squares of `pixelSize` by `pixelSize`
12
+ */
13
+ // TODO: just resample with NEAREST interpolation? but how?
14
+ export class Pixelate extends Shader {
15
+ pixelSize: Dynamic<number>
16
+
17
+ /**
18
+ * @param pixelSize
19
+ */
20
+ constructor (options: PixelateOptions = {}) {
21
+ super({
22
+ fragmentSource: `
23
+ precision mediump float;
24
+
25
+ uniform sampler2D u_Source;
26
+ uniform ivec2 u_Size;
27
+ uniform int u_PixelSize;
28
+
29
+ varying highp vec2 v_TextureCoord;
30
+
31
+ void main() {
32
+ int ps = u_PixelSize;
33
+
34
+ // Snap to nearest block's center
35
+ vec2 loc = vec2(u_Size) * v_TextureCoord; // pixel-space
36
+ vec2 snappedLoc = float(ps) * floor(loc / float(ps));
37
+ vec2 centeredLoc = snappedLoc + vec2(float(u_PixelSize) / 2.0 + 0.5);
38
+ vec2 clampedLoc = clamp(centeredLoc, vec2(0.0), vec2(u_Size));
39
+ gl_FragColor = texture2D(u_Source, clampedLoc / vec2(u_Size));
40
+ }
41
+ `,
42
+ uniforms: {
43
+ pixelSize: '1i'
44
+ }
45
+ })
46
+ /**
47
+ */
48
+ this.pixelSize = options.pixelSize || 1
49
+ }
50
+
51
+ apply (target: Movie | Visual, reltime: number): void {
52
+ const ps = val(this, 'pixelSize', reltime)
53
+ if (ps % 1 !== 0 || ps < 0)
54
+ throw new Error('Pixel size must be a nonnegative integer')
55
+
56
+ super.apply(target, reltime)
57
+ }
58
+ }