etro 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/nodejs.yml +1 -1
- package/CHANGELOG.md +45 -1
- package/CONTRIBUTING.md +23 -19
- package/README.md +81 -26
- package/dist/effect/base.d.ts +38 -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 +9337 -3331
- package/dist/etro-iife.js +9279 -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 +69 -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 +0 -8
- package/eslint.example-conf.js +9 -0
- package/eslint.typescript-conf.js +5 -0
- package/examples/application/readme-screenshot.html +12 -9
- package/examples/application/video-player.html +7 -7
- package/examples/application/webcam.html +6 -6
- package/examples/introduction/audio.html +30 -18
- package/examples/introduction/effects.html +14 -10
- 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 +1 -1
- package/package.json +29 -13
- package/rollup.config.js +15 -4
- package/scripts/gen-effect-samples.html +29 -25
- package/scripts/save-effect-samples.js +14 -15
- package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
- package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
- package/spec/assets/effect/grayscale.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/effect.spec.js +126 -57
- package/spec/event.spec.js +14 -0
- package/spec/layer.spec.js +175 -18
- package/spec/movie.spec.js +191 -7
- package/spec/util.spec.js +14 -5
- package/src/effect/base.ts +96 -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 +59 -0
- package/src/effect/shader.ts +561 -0
- package/src/effect/stack.ts +74 -0
- package/src/effect/transform.ts +194 -0
- package/src/etro.ts +26 -0
- package/src/event.ts +118 -0
- package/src/index.ts +8 -0
- package/src/layer/audio-source.ts +217 -0
- package/src/layer/audio.ts +35 -0
- package/src/layer/base.ts +156 -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 +198 -0
- package/src/movie.ts +709 -0
- package/src/object.ts +14 -0
- package/src/util.ts +473 -0
- package/tsconfig.json +8 -0
- package/screenshots/2019-08-17_0.png +0 -0
- 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,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,59 @@
|
|
|
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
|
+
|
|
57
|
+
super.apply(target, reltime)
|
|
58
|
+
}
|
|
59
|
+
}
|