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.
- package/.github/workflows/nodejs.yml +4 -2
- package/.github/workflows/shipjs-trigger.yml +29 -0
- package/CHANGELOG.md +73 -12
- package/CODE_OF_CONDUCT.md +5 -5
- package/CONTRIBUTING.md +31 -77
- package/README.md +81 -26
- package/dist/effect/base.d.ts +51 -0
- package/dist/effect/brightness.d.ts +16 -0
- package/dist/effect/channels.d.ts +23 -0
- package/dist/effect/chroma-key.d.ts +23 -0
- package/dist/effect/contrast.d.ts +15 -0
- package/dist/effect/elliptical-mask.d.ts +31 -0
- package/dist/effect/gaussian-blur.d.ts +60 -0
- package/dist/effect/grayscale.d.ts +7 -0
- package/dist/effect/index.d.ts +15 -0
- package/dist/effect/pixelate.d.ts +18 -0
- package/dist/effect/shader.d.ts +99 -0
- package/dist/effect/stack.d.ts +23 -0
- package/dist/effect/transform.d.ts +73 -0
- package/dist/etro-cjs.js +9287 -3331
- package/dist/etro-iife.js +9229 -3273
- package/dist/etro.d.ts +7 -0
- package/dist/event.d.ts +35 -0
- package/dist/index.d.ts +6 -0
- package/dist/layer/audio-source.d.ts +24 -0
- package/dist/layer/audio.d.ts +14 -0
- package/dist/layer/base.d.ts +82 -0
- package/dist/layer/image.d.ts +6 -0
- package/dist/layer/index.d.ts +11 -0
- package/dist/layer/text.d.ts +60 -0
- package/dist/layer/video.d.ts +11 -0
- package/dist/layer/visual-source.d.ts +32 -0
- package/dist/layer/visual.d.ts +58 -0
- package/dist/movie.d.ts +192 -0
- package/dist/object.d.ts +12 -0
- package/dist/util.d.ts +125 -0
- package/eslint.conf.js +2 -9
- package/eslint.example-conf.js +9 -0
- package/eslint.test-conf.js +1 -0
- package/eslint.typescript-conf.js +5 -0
- package/examples/application/readme-screenshot.html +16 -17
- package/examples/application/video-player.html +10 -11
- package/examples/application/webcam.html +6 -6
- package/examples/introduction/audio.html +30 -18
- package/examples/introduction/effects.html +37 -14
- package/examples/introduction/export.html +32 -25
- package/examples/introduction/functions.html +6 -4
- package/examples/introduction/hello-world1.html +9 -5
- package/examples/introduction/hello-world2.html +5 -5
- package/examples/introduction/keyframes.html +35 -23
- package/examples/introduction/media.html +26 -18
- package/examples/introduction/text.html +9 -5
- package/karma.conf.js +5 -3
- package/package.json +36 -14
- package/rollup.config.js +15 -4
- package/scripts/gen-effect-samples.html +26 -25
- package/scripts/save-effect-samples.js +14 -15
- package/ship.config.js +80 -0
- package/src/effect/base.ts +115 -0
- package/src/effect/brightness.ts +43 -0
- package/src/effect/channels.ts +50 -0
- package/src/effect/chroma-key.ts +82 -0
- package/src/effect/contrast.ts +42 -0
- package/src/effect/elliptical-mask.ts +75 -0
- package/src/effect/gaussian-blur.ts +232 -0
- package/src/effect/grayscale.ts +34 -0
- package/src/effect/index.ts +22 -0
- package/src/effect/pixelate.ts +58 -0
- package/src/effect/shader.ts +557 -0
- package/src/effect/stack.ts +78 -0
- package/src/effect/transform.ts +193 -0
- package/src/etro.ts +26 -0
- package/src/event.ts +112 -0
- package/src/index.ts +8 -0
- package/src/layer/audio-source.ts +219 -0
- package/src/layer/audio.ts +34 -0
- package/src/layer/base.ts +175 -0
- package/src/layer/image.ts +8 -0
- package/src/layer/index.ts +13 -0
- package/src/layer/text.ts +138 -0
- package/src/layer/video.ts +15 -0
- package/src/layer/visual-source.ts +150 -0
- package/src/layer/visual.ts +197 -0
- package/src/movie.ts +707 -0
- package/src/object.ts +14 -0
- package/src/util.ts +466 -0
- package/tsconfig.json +8 -0
- package/docs/effect.js.html +0 -1215
- package/docs/event.js.html +0 -145
- package/docs/index.html +0 -81
- package/docs/index.js.html +0 -92
- package/docs/layer.js.html +0 -888
- package/docs/module-effect-GaussianBlurComponent.html +0 -345
- package/docs/module-effect.Brightness.html +0 -339
- package/docs/module-effect.Channels.html +0 -319
- package/docs/module-effect.ChromaKey.html +0 -611
- package/docs/module-effect.Contrast.html +0 -339
- package/docs/module-effect.EllipticalMask.html +0 -200
- package/docs/module-effect.GaussianBlur.html +0 -202
- package/docs/module-effect.GaussianBlurHorizontal.html +0 -242
- package/docs/module-effect.GaussianBlurVertical.html +0 -242
- package/docs/module-effect.Pixelate.html +0 -330
- package/docs/module-effect.Shader.html +0 -1227
- package/docs/module-effect.Stack.html +0 -406
- package/docs/module-effect.Transform.Matrix.html +0 -193
- package/docs/module-effect.Transform.html +0 -1174
- package/docs/module-effect.html +0 -148
- package/docs/module-event.html +0 -473
- package/docs/module-index.html +0 -186
- package/docs/module-layer-Media.html +0 -1116
- package/docs/module-layer-MediaMixin.html +0 -164
- package/docs/module-layer.Audio.html +0 -1188
- package/docs/module-layer.Base.html +0 -629
- package/docs/module-layer.Image.html +0 -1421
- package/docs/module-layer.Text.html +0 -1731
- package/docs/module-layer.Video.html +0 -1938
- package/docs/module-layer.Visual.html +0 -1698
- package/docs/module-layer.html +0 -137
- package/docs/module-movie.html +0 -3118
- package/docs/module-util.Color.html +0 -702
- package/docs/module-util.Font.html +0 -395
- package/docs/module-util.html +0 -845
- package/docs/movie.js.html +0 -689
- package/docs/scripts/collapse.js +0 -20
- package/docs/scripts/linenumber.js +0 -25
- package/docs/scripts/nav.js +0 -12
- package/docs/scripts/polyfill.js +0 -4
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
- package/docs/scripts/prettify/lang-css.js +0 -2
- package/docs/scripts/prettify/prettify.js +0 -28
- package/docs/scripts/search.js +0 -83
- package/docs/styles/jsdoc.css +0 -671
- package/docs/styles/prettify.css +0 -79
- package/docs/util.js.html +0 -503
- package/screenshots/2019-08-17_0.png +0 -0
- package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
- package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
- package/spec/assets/effect/original.png +0 -0
- package/spec/assets/effect/pixelate.png +0 -0
- package/spec/assets/effect/transform/multiply.png +0 -0
- package/spec/assets/effect/transform/rotate.png +0 -0
- package/spec/assets/effect/transform/scale-fraction.png +0 -0
- package/spec/assets/effect/transform/scale.png +0 -0
- package/spec/assets/effect/transform/translate-fraction.png +0 -0
- package/spec/assets/effect/transform/translate.png +0 -0
- package/spec/assets/layer/audio.wav +0 -0
- package/spec/assets/layer/image.jpg +0 -0
- package/spec/effect.spec.js +0 -352
- package/spec/event.spec.js +0 -25
- package/spec/layer.spec.js +0 -150
- package/spec/movie.spec.js +0 -162
- package/spec/util.spec.js +0 -285
- package/src/effect.js +0 -1268
- package/src/event.js +0 -78
- package/src/index.js +0 -23
- package/src/layer.js +0 -897
- package/src/movie.js +0 -637
- package/src/util.js +0 -505
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Dynamic } from '../util'
|
|
2
|
+
import { Shader } from './shader'
|
|
3
|
+
|
|
4
|
+
export interface BrightnessOptions {
|
|
5
|
+
brightness?: Dynamic<number>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Changes the brightness
|
|
10
|
+
*/
|
|
11
|
+
export class Brightness extends Shader {
|
|
12
|
+
brightness: Dynamic<number>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param [brightness=0] - the value to add to each pixel's color
|
|
16
|
+
* channels (between -255 and 255)
|
|
17
|
+
*/
|
|
18
|
+
constructor (options: BrightnessOptions = {}) {
|
|
19
|
+
super({
|
|
20
|
+
fragmentSource: `
|
|
21
|
+
precision mediump float;
|
|
22
|
+
|
|
23
|
+
uniform sampler2D u_Source;
|
|
24
|
+
uniform float u_Brightness;
|
|
25
|
+
|
|
26
|
+
varying highp vec2 v_TextureCoord;
|
|
27
|
+
|
|
28
|
+
void main() {
|
|
29
|
+
vec4 color = texture2D(u_Source, v_TextureCoord);
|
|
30
|
+
vec3 rgb = clamp(color.rgb + u_Brightness / 255.0, 0.0, 1.0);
|
|
31
|
+
gl_FragColor = vec4(rgb, color.a);
|
|
32
|
+
}
|
|
33
|
+
`,
|
|
34
|
+
uniforms: {
|
|
35
|
+
brightness: '1f'
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
/**
|
|
39
|
+
* The value to add to each pixel's color channels (between -255 and 255)
|
|
40
|
+
*/
|
|
41
|
+
this.brightness = options.brightness || 0
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -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
|
+
}
|