🎉
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
11021
package-lock.json
generated
Normal file
11021
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "image-filter-pipeline",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "webpack.config.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "webpack-dev-server --quiet --progress --watch --mode development",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "standa.fifik@gmail.com",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.2.2",
|
||||||
|
"@babel/preset-env": "^7.2.3",
|
||||||
|
"@types/jest": "^23.3.10",
|
||||||
|
"@types/webpack": "^4.4.22",
|
||||||
|
"babel-jest": "^23.6.0",
|
||||||
|
"canvas": "^2.2.0",
|
||||||
|
"jest": "^23.6.0",
|
||||||
|
"webpack": "^4.28.3",
|
||||||
|
"webpack-cli": "^3.1.2",
|
||||||
|
"webpack-dev-server": "^3.1.14",
|
||||||
|
"babel-loader": "^8.0.4",
|
||||||
|
"babel-polyfill": "^6.26.0",
|
||||||
|
"css-loader": "^1.0.1",
|
||||||
|
"file-loader": "^2.0.0",
|
||||||
|
"friendly-errors-webpack-plugin": "^1.7.0",
|
||||||
|
"html-webpack-plugin": "^3.2.0",
|
||||||
|
"vue-loader": "^15.4.2",
|
||||||
|
"vue-template-compiler": "^2.5.17"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^2.5.21",
|
||||||
|
"webgl-utils": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/index.js
Normal file
0
src/index.js
Normal file
33
src/index.spec.js
Normal file
33
src/index.spec.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as nodes from './nodes';
|
||||||
|
import {createCanvas} from './nodes/canvas';
|
||||||
|
|
||||||
|
const createImage = (width, height) => {
|
||||||
|
const ctx = createCanvas(width, height).getContext('2d')
|
||||||
|
ctx.fillStyle = 'silver';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
return ctx.getImageData(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image = createImage(1920, 1080);
|
||||||
|
|
||||||
|
describe('basic usage', () => {
|
||||||
|
it('should emit image', (cb) => {
|
||||||
|
|
||||||
|
const resize = new nodes.Resize({width: 100, height: 100, method: 'cover'});
|
||||||
|
const output = new nodes.ToBlob({type: 'image/jpeg', quality: 100});
|
||||||
|
|
||||||
|
output.in.image.connect(resize.out.image);
|
||||||
|
|
||||||
|
resize.out.image.onchange((value) => {
|
||||||
|
expect(value.width).toBe(100);
|
||||||
|
expect(value.height).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
output.out.image.onchange((/** @type {Blob} */blob) => {
|
||||||
|
expect(blob.size).toBeGreaterThan(0);
|
||||||
|
cb();
|
||||||
|
})
|
||||||
|
|
||||||
|
resize.in.image.value = Image;
|
||||||
|
});
|
||||||
|
})
|
||||||
108
src/nodes/Compose.js
Normal file
108
src/nodes/Compose.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import Node from './Node';
|
||||||
|
import {createCanvas, mediaSize, paintToCanvas} from './canvas';
|
||||||
|
|
||||||
|
export default class Resize extends Node {
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
fg: 'Image',
|
||||||
|
fgX: 'Number',
|
||||||
|
fgY: 'Number',
|
||||||
|
bg: 'Image',
|
||||||
|
bgX: 'Number',
|
||||||
|
bgY: 'Number',
|
||||||
|
width: 'Number',
|
||||||
|
height: 'Number',
|
||||||
|
mode: [
|
||||||
|
'source-over',
|
||||||
|
'source-in',
|
||||||
|
'source-out',
|
||||||
|
'source-atop',
|
||||||
|
'destination-over',
|
||||||
|
'destination-in',
|
||||||
|
'destination-out',
|
||||||
|
'destination-atop',
|
||||||
|
'lighter',
|
||||||
|
'copy',
|
||||||
|
'xor',
|
||||||
|
'multiply',
|
||||||
|
'screen',
|
||||||
|
'overlay',
|
||||||
|
'darken',
|
||||||
|
'lighten',
|
||||||
|
'color-dodge',
|
||||||
|
'color-burn',
|
||||||
|
'hard-light',
|
||||||
|
'soft-light',
|
||||||
|
'difference',
|
||||||
|
'exclusion',
|
||||||
|
'hue',
|
||||||
|
'saturation',
|
||||||
|
'color',
|
||||||
|
'luminosity',
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
image: 'Image'
|
||||||
|
});
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
return this.__in.width.value || this.options.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
return this.__in.height.value || this.options.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fg() {
|
||||||
|
if (!this.__in.fg.value) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
const {width, height} = this.__in.fg.value;
|
||||||
|
return {
|
||||||
|
image: this.__in.fg.value,
|
||||||
|
top: this.__in.fgY.value || 0,
|
||||||
|
left: this.__in.fgX.value || 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get bg() {
|
||||||
|
if (!this.__in.bg.value) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
const {width, height} = this.__in.bg.value;
|
||||||
|
return {
|
||||||
|
image: this.__in.bg.value,
|
||||||
|
top: this.__in.bgY.value || 0,
|
||||||
|
left: this.__in.bgX.value || 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.__in.mode.value || 'source-over';
|
||||||
|
}
|
||||||
|
|
||||||
|
__update() {
|
||||||
|
const canvas = createCanvas(this.width, this.height);
|
||||||
|
|
||||||
|
if (this.bg) {
|
||||||
|
paintToCanvas(canvas, this.bg.image, this.bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.globalCompositeOperation = this.mode;
|
||||||
|
|
||||||
|
if (this.fg) {
|
||||||
|
paintToCanvas(canvas, this.fg.image, this.fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__out.image.value = canvas;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/nodes/ImageIdentity.js
Normal file
15
src/nodes/ImageIdentity.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import Node from './Node';
|
||||||
|
|
||||||
|
export default class ImageIdentity extends Node {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
image: 'Image',
|
||||||
|
}, {
|
||||||
|
image: 'Image',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
__update() {
|
||||||
|
this.__out.image.value = this.__in.image.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/nodes/Node.js
Normal file
29
src/nodes/Node.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {Inputs, Outputs} from './io.js';
|
||||||
|
|
||||||
|
export default class Node {
|
||||||
|
|
||||||
|
constructor(inputNames, outputNames) {
|
||||||
|
this.__in = new Inputs(inputNames);
|
||||||
|
this.__out = new Outputs(outputNames);
|
||||||
|
this.__in.update = (name) => this.__update([name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
__update(changes) {
|
||||||
|
throw new Error('__update method not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input getters
|
||||||
|
*/
|
||||||
|
get in() {
|
||||||
|
return this.__in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output getters
|
||||||
|
*/
|
||||||
|
get out() {
|
||||||
|
return this.__out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
48
src/nodes/Resize.js
Normal file
48
src/nodes/Resize.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import Node from './Node';
|
||||||
|
import {createCanvas, mediaSize, paintToCanvas} from './canvas';
|
||||||
|
|
||||||
|
export default class Resize extends Node {
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
image: 'Image',
|
||||||
|
width: 'Number',
|
||||||
|
height: 'Number',
|
||||||
|
}, {
|
||||||
|
image: 'Image'
|
||||||
|
});
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
return this.__in.width.value || this.options.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
return this.__in.height.value || this.options.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
__update() {
|
||||||
|
const media = this.__in.image.value;
|
||||||
|
const canvas = createCanvas(this.width, this.height);
|
||||||
|
|
||||||
|
const {width: srcWidth, height: srcHeight} = mediaSize(media);
|
||||||
|
const {width: destWidth, height: destHeight} = mediaSize(canvas);
|
||||||
|
const srcAspectRatio = (srcWidth / srcHeight);
|
||||||
|
const destAspectRatio = (destWidth / destHeight);
|
||||||
|
const newImage = {};
|
||||||
|
if (srcAspectRatio > destAspectRatio) {
|
||||||
|
newImage.width = destHeight * srcAspectRatio;
|
||||||
|
newImage.height = destHeight;
|
||||||
|
} else {
|
||||||
|
newImage.width = destWidth;
|
||||||
|
newImage.height = destWidth / srcAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
newImage.top = (destHeight - newImage.height) / 2;
|
||||||
|
newImage.left = (destWidth - newImage.width) / 2;
|
||||||
|
|
||||||
|
paintToCanvas(canvas, media, newImage);
|
||||||
|
this.__out.image.value = canvas;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/nodes/ToBlob.js
Normal file
36
src/nodes/ToBlob.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import Node from './Node';
|
||||||
|
import {createCanvas} from './canvas';
|
||||||
|
|
||||||
|
export default class ToBlob extends Node {
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
image: 'Image',
|
||||||
|
}, {
|
||||||
|
image: 'Image',
|
||||||
|
});
|
||||||
|
this.options = Object.assign({
|
||||||
|
quality: 100,
|
||||||
|
mimetype: 'image/jpeg',
|
||||||
|
}, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
get mimetype() {
|
||||||
|
return this.options.mimetype;
|
||||||
|
}
|
||||||
|
|
||||||
|
get quality() {
|
||||||
|
return this.options.quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
__update() {
|
||||||
|
const {width, height} = this.__in.image.value;
|
||||||
|
const canvas = createCanvas(width, height);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.putImageData(this.__in.image.value, 0, 0);
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
this.__out.image.value = blob;
|
||||||
|
}, this.mimetype, this.quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
src/nodes/WebGL/BrightnessContrast.js
Normal file
48
src/nodes/WebGL/BrightnessContrast.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import WebGL from './WebGl';
|
||||||
|
|
||||||
|
|
||||||
|
export default class BrightnessContrast extends WebGL {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
brightness: 'Number',
|
||||||
|
contrast: 'Number',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get frag() {
|
||||||
|
return `
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
// our texture
|
||||||
|
uniform sampler2D u_image;
|
||||||
|
|
||||||
|
// the texCoords passed in from the vertex shader.
|
||||||
|
varying vec2 v_texCoord;
|
||||||
|
|
||||||
|
uniform vec2 u_brightnessContrast;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 c = texture2D(u_image, v_texCoord).rgb * u_brightnessContrast[0];
|
||||||
|
gl_FragColor = vec4(
|
||||||
|
clamp(
|
||||||
|
(c - 0.5) * u_brightnessContrast[1] + 0.5,
|
||||||
|
0.0, 1.0),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
get contrast() {
|
||||||
|
return this.in.contrast.value || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get brightness() {
|
||||||
|
return this.in.brightness.value || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setParams(gl, program) {
|
||||||
|
gl.uniform2f(gl.getUniformLocation(program, "u_brightnessContrast"), this.brightness, this.contrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/nodes/WebGL/HSV.js
Normal file
67
src/nodes/WebGL/HSV.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import WebGL from './WebGl';
|
||||||
|
|
||||||
|
|
||||||
|
export default class BrightnessContrast extends WebGL {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
hue: 'Number',
|
||||||
|
saturation: 'Number',
|
||||||
|
value: 'Number',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get frag() {
|
||||||
|
return `
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
// our texture
|
||||||
|
uniform sampler2D u_image;
|
||||||
|
|
||||||
|
// the texCoords passed in from the vertex shader.
|
||||||
|
varying vec2 v_texCoord;
|
||||||
|
|
||||||
|
uniform vec3 u_shift;
|
||||||
|
|
||||||
|
vec3 rgb2hsv(vec3 c) {
|
||||||
|
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
||||||
|
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
||||||
|
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
||||||
|
|
||||||
|
float d = q.x - min(q.w, q.y);
|
||||||
|
float e = 1.0e-10;
|
||||||
|
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hsv2rgb(vec3 c) {
|
||||||
|
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||||
|
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||||
|
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 c = rgb2hsv(texture2D(u_image, v_texCoord).rgb);
|
||||||
|
gl_FragColor = vec4(
|
||||||
|
hsv2rgb(vec3(c.x + u_shift.x, c.y * u_shift.y, c.z * u_shift.z)),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
get hue() {
|
||||||
|
return ((this.in.hue.value || 0) % 360) / 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
get saturation() {
|
||||||
|
return this.in.saturation.value || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.in.value.value || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setParams(gl, program) {
|
||||||
|
gl.uniform3f(gl.getUniformLocation(program, "u_shift"), this.hue, this.saturation, this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/nodes/WebGL/Sepia.js
Normal file
51
src/nodes/WebGL/Sepia.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import WebGL from './WebGl';
|
||||||
|
|
||||||
|
|
||||||
|
export default class BrightnessContrast extends WebGL {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
amount: 'Number',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get frag() {
|
||||||
|
return `
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
// our texture
|
||||||
|
uniform sampler2D u_image;
|
||||||
|
|
||||||
|
// the texCoords passed in from the vertex shader.
|
||||||
|
varying vec2 v_texCoord;
|
||||||
|
|
||||||
|
uniform float amount;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 color = texture2D(u_image, v_texCoord).rgb;
|
||||||
|
float r = color.r;
|
||||||
|
float g = color.g;
|
||||||
|
float b = color.b;
|
||||||
|
|
||||||
|
color.r = min(1.0, (r * (1.0 - (0.607 * amount))) + (g * (0.769 * amount)) + (b * (0.189 * amount)));
|
||||||
|
color.g = min(1.0, (r * 0.349 * amount) + (g * (1.0 - (0.314 * amount))) + (b * 0.168 * amount));
|
||||||
|
color.b = min(1.0, (r * 0.272 * amount) + (g * 0.534 * amount) + (b * (1.0 - (0.869 * amount))));
|
||||||
|
|
||||||
|
gl_FragColor = vec4(color, 1);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
get amount() {
|
||||||
|
return this.in.amount.value || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {WebGLRenderingContext} gl
|
||||||
|
* @param {*} program
|
||||||
|
*/
|
||||||
|
_setParams(gl, program) {
|
||||||
|
gl.uniform1f(gl.getUniformLocation(program, "amount"), this.amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
220
src/nodes/WebGL/WebGl.js
Normal file
220
src/nodes/WebGL/WebGl.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import Node from '../Node';
|
||||||
|
import webglUtils from '../lib/webgl-utils';
|
||||||
|
import {createCanvas, mediaSize, paintToCanvas} from '../canvas';
|
||||||
|
|
||||||
|
console.log(webglUtils);
|
||||||
|
|
||||||
|
export default class WebGL extends Node {
|
||||||
|
|
||||||
|
constructor(inputs={}) {
|
||||||
|
super(Object.assign({
|
||||||
|
image: 'Image',
|
||||||
|
}, inputs), {
|
||||||
|
image: 'Image'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get vert() {
|
||||||
|
return `
|
||||||
|
attribute vec2 a_position;
|
||||||
|
attribute vec2 a_texCoord;
|
||||||
|
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
|
||||||
|
varying vec2 v_texCoord;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// convert the rectangle from pixels to 0.0 to 1.0
|
||||||
|
vec2 zeroToOne = a_position / u_resolution;
|
||||||
|
|
||||||
|
// convert from 0->1 to 0->2
|
||||||
|
vec2 zeroToTwo = zeroToOne * 2.0;
|
||||||
|
|
||||||
|
// convert from 0->2 to -1->+1 (clipspace)
|
||||||
|
vec2 clipSpace = zeroToTwo - 1.0;
|
||||||
|
|
||||||
|
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
|
||||||
|
|
||||||
|
// pass the texCoord to the fragment shader
|
||||||
|
// The GPU will interpolate this value between points.
|
||||||
|
v_texCoord = a_texCoord;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get frag() {
|
||||||
|
return `
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
// our texture
|
||||||
|
uniform sampler2D u_image;
|
||||||
|
|
||||||
|
// the texCoords passed in from the vertex shader.
|
||||||
|
varying vec2 v_texCoord;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
fragShader(gl) {
|
||||||
|
var shader = gl.createShader(gl.FRAGMENT_SHADER);
|
||||||
|
gl.shaderSource(shader, this.frag);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
|
||||||
|
if ( !gl.getShaderParameter(shader, gl.COMPILE_STATUS) ) {
|
||||||
|
var info = gl.getShaderInfoLog(shader);
|
||||||
|
throw 'Could not compile WebGL program. \n\n' + info;
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
vertShader(gl) {
|
||||||
|
var shader = gl.createShader(gl.VERTEX_SHADER);
|
||||||
|
gl.shaderSource(shader, this.vert);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
|
||||||
|
if ( !gl.getShaderParameter(shader, gl.COMPILE_STATUS) ) {
|
||||||
|
var info = gl.getShaderInfoLog(shader);
|
||||||
|
throw 'Could not compile WebGL program. \n\n' + info;
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {WebGLRenderingContext} gl
|
||||||
|
*/
|
||||||
|
program(gl) {
|
||||||
|
const program = gl.createProgram();
|
||||||
|
|
||||||
|
gl.attachShader(program, this.vertShader(gl));
|
||||||
|
gl.attachShader(program, this.fragShader(gl));
|
||||||
|
gl.linkProgram(program);
|
||||||
|
|
||||||
|
if ( !gl.getProgramParameter( program, gl.LINK_STATUS) ) {
|
||||||
|
var info = gl.getProgramInfoLog(program);
|
||||||
|
throw 'Could not compile WebGL program. \n\n' + info;
|
||||||
|
}
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
get image() {
|
||||||
|
return this.in.image.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
createTexture(gl) {
|
||||||
|
const texture = gl.createTexture();
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
|
|
||||||
|
// Set the parameters so we can render any size image.
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
|
if (this.image) {
|
||||||
|
// Upload the image into the texture.
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.image);
|
||||||
|
}
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setParams(gl, program) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
__update() {
|
||||||
|
const image = this.in.image.value;
|
||||||
|
if (!image) return;
|
||||||
|
const {width, height} = mediaSize(image);
|
||||||
|
const canvas = createCanvas(width, height);
|
||||||
|
const gl = canvas.getContext('webgl');
|
||||||
|
const program = this.program(gl);
|
||||||
|
|
||||||
|
const positionLocation = gl.getAttribLocation(program, "a_position");
|
||||||
|
const texcoordLocation = gl.getAttribLocation(program, "a_texCoord");
|
||||||
|
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
|
||||||
|
|
||||||
|
const positionBuffer = gl.createBuffer();
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||||
|
this.setRectangle(gl, 0, 0, width, height);
|
||||||
|
const texcoordBuffer = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||||
|
0.0, 0.0,
|
||||||
|
1.0, 0.0,
|
||||||
|
0.0, 1.0,
|
||||||
|
0.0, 1.0,
|
||||||
|
1.0, 0.0,
|
||||||
|
1.0, 1.0,
|
||||||
|
]), gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
this.createTexture(gl);
|
||||||
|
|
||||||
|
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||||
|
gl.clearColor(0, 0, 0, 0);
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
gl.useProgram(program);
|
||||||
|
|
||||||
|
gl.enableVertexAttribArray(positionLocation);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||||
|
|
||||||
|
// Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
|
||||||
|
var size = 2; // 2 components per iteration
|
||||||
|
var type = gl.FLOAT; // the data is 32bit floats
|
||||||
|
var normalize = false; // don't normalize the data
|
||||||
|
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
|
||||||
|
var offset = 0; // start at the beginning of the buffer
|
||||||
|
gl.vertexAttribPointer(
|
||||||
|
positionLocation, size, type, normalize, stride, offset);
|
||||||
|
|
||||||
|
// Turn on the teccord attribute
|
||||||
|
gl.enableVertexAttribArray(texcoordLocation);
|
||||||
|
|
||||||
|
// Bind the position buffer.
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
|
||||||
|
|
||||||
|
// Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
|
||||||
|
var size = 2; // 2 components per iteration
|
||||||
|
var type = gl.FLOAT; // the data is 32bit floats
|
||||||
|
var normalize = false; // don't normalize the data
|
||||||
|
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
|
||||||
|
var offset = 0; // start at the beginning of the buffer
|
||||||
|
gl.vertexAttribPointer(
|
||||||
|
texcoordLocation, size, type, normalize, stride, offset);
|
||||||
|
|
||||||
|
// set the resolution
|
||||||
|
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||||
|
|
||||||
|
this._setParams(gl, program);
|
||||||
|
|
||||||
|
// Draw the rectangle.
|
||||||
|
var primitiveType = gl.TRIANGLES;
|
||||||
|
var offset = 0;
|
||||||
|
var count = 6;
|
||||||
|
gl.drawArrays(primitiveType, offset, count);
|
||||||
|
|
||||||
|
this.out.image.value = canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRectangle(gl, x, y, width, height) {
|
||||||
|
var x1 = x;
|
||||||
|
var x2 = x + width;
|
||||||
|
var y1 = y;
|
||||||
|
var y2 = y + height;
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||||
|
x1, y1,
|
||||||
|
x2, y1,
|
||||||
|
x1, y2,
|
||||||
|
x1, y2,
|
||||||
|
x2, y1,
|
||||||
|
x2, y2,
|
||||||
|
]), gl.STATIC_DRAW);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/nodes/canvas.js
Normal file
55
src/nodes/canvas.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Number} width
|
||||||
|
* @param {Number} height
|
||||||
|
* @returns {HTMLCanvasElement}
|
||||||
|
*/
|
||||||
|
export const createCanvas = (width, height) => {
|
||||||
|
if (window.name !== 'nodejs') {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
} else {
|
||||||
|
const {createCanvas} = require('canvas');
|
||||||
|
const canvas = createCanvas(width, height);
|
||||||
|
canvas.toBlob = (callback, type, quality) => {
|
||||||
|
canvas.toBuffer((err, data) => {
|
||||||
|
callback(new Blob([data]));
|
||||||
|
}, type)
|
||||||
|
canvas
|
||||||
|
}
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {HTMLImageElement|HTMLVideoElement} PaintableElement
|
||||||
|
* @typedef {{top: Number, left: Number, width: Number, height: Number}} Bounds
|
||||||
|
* @typedef {function(HTMLCanvasElement, PaintableElement, Bounds): HTMLCanvasElement} PaintToCanvasFunction
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {PaintableElement|{width: Number, height: Number}} media
|
||||||
|
* @returns {{width: Number, height: Number}}
|
||||||
|
*/
|
||||||
|
export const mediaSize = (media) =>
|
||||||
|
(media instanceof HTMLVideoElement) ? {width: media.videoWidth, height: media.videoHeight} :
|
||||||
|
(media instanceof HTMLImageElement) ? {width: media.naturalWidth, height: media.naturalHeight} :
|
||||||
|
{width: media.width, height: media.height};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
* @param {PaintableElement} media
|
||||||
|
* @param {Bounds} param2
|
||||||
|
*/
|
||||||
|
export const paintToCanvas = (canvas, media, {top, left, width, height}) => {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (canvas.width > 0 && canvas.height > 0 && width > 0 && height > 0) {
|
||||||
|
ctx.drawImage(media, left, top, width, height);
|
||||||
|
}
|
||||||
|
return canvas;
|
||||||
|
};
|
||||||
8
src/nodes/index.js
Normal file
8
src/nodes/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export {default as Resize} from './Resize';
|
||||||
|
export {default as ImageIdentity} from './ImageIdentity';
|
||||||
|
export {default as ToBlob} from './ToBlob';
|
||||||
|
export {default as Compose} from './Compose';
|
||||||
|
export {default as WebGL} from './WebGL/WebGl';
|
||||||
|
export {default as BrightnessContrast} from './WebGL/BrightnessContrast';
|
||||||
|
export {default as HSV} from './WebGL/HSV';
|
||||||
|
export {default as Sepia} from './WebGL/Sepia';
|
||||||
12
src/nodes/io.d.ts
vendored
Normal file
12
src/nodes/io.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as types from './types'
|
||||||
|
|
||||||
|
|
||||||
|
export class Inputs<T extends string> {
|
||||||
|
constructor(variables: T[]);
|
||||||
|
[k: string]: types.Input<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Outputs<T extends string> {
|
||||||
|
constructor(variables: T[]);
|
||||||
|
[k: string]: types.Input<any>
|
||||||
|
}
|
||||||
130
src/nodes/io.js
Normal file
130
src/nodes/io.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import * as types from './types';
|
||||||
|
|
||||||
|
|
||||||
|
export class Input {
|
||||||
|
|
||||||
|
constructor(name, owner) {
|
||||||
|
this.__output = null;
|
||||||
|
this.__value = null;
|
||||||
|
this.__name = name;
|
||||||
|
this.__owner = owner
|
||||||
|
this.__listeners = [];
|
||||||
|
this.__onchangelistener = (value) => {
|
||||||
|
this.value = value;
|
||||||
|
this.__notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.__name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.__value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(value) {
|
||||||
|
this.__value = value;
|
||||||
|
this.__owner.update(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(output) {
|
||||||
|
this.__output = output
|
||||||
|
this.__output.onchange(this.__onchangelistener);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(output) {
|
||||||
|
this.__output.offchage(this.__onchangelistener);
|
||||||
|
}
|
||||||
|
|
||||||
|
__notifyListeners() {
|
||||||
|
for (let listener of this.__listeners) {
|
||||||
|
listener(this.value, this.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onchange(listener) {
|
||||||
|
this.__listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
offchange(listener) {
|
||||||
|
this.__listeners = this.__listeners
|
||||||
|
.filter((l) => l !== listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Output {
|
||||||
|
|
||||||
|
constructor(name, owner) {
|
||||||
|
this.__value = null;
|
||||||
|
this.__name = name;
|
||||||
|
this.__owner = owner
|
||||||
|
this.__listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.__name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.__value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(value) {
|
||||||
|
this.__value = value;
|
||||||
|
this.__notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
__notifyListeners() {
|
||||||
|
for (let listener of this.__listeners) {
|
||||||
|
listener(this.value, this.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onchange(listener) {
|
||||||
|
this.__listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
offchange(listener) {
|
||||||
|
this.__listeners = this.__listeners
|
||||||
|
.filter((l) => l !== listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Inputs {
|
||||||
|
|
||||||
|
constructor(variables) {
|
||||||
|
this.__values = {};
|
||||||
|
this.variables = variables;
|
||||||
|
|
||||||
|
for (let name of Object.keys(this.variables)) {
|
||||||
|
this.__values[name] = new Input(name, this);
|
||||||
|
Object.defineProperty(this, name, {
|
||||||
|
get() {
|
||||||
|
return this.__values[name];
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(changed) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Outputs {
|
||||||
|
constructor(variables) {
|
||||||
|
this.__values = {};
|
||||||
|
this.variables = variables;
|
||||||
|
|
||||||
|
for (let name of Object.keys(this.variables)) {
|
||||||
|
this.__values[name] = new Output(name, this);
|
||||||
|
Object.defineProperty(this, name, {
|
||||||
|
get() {
|
||||||
|
return this.__values[name];
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1293
src/nodes/lib/webgl-utils.js
Normal file
1293
src/nodes/lib/webgl-utils.js
Normal file
File diff suppressed because it is too large
Load Diff
11
src/nodes/types.d.ts
vendored
Normal file
11
src/nodes/types.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface Output<T> {
|
||||||
|
readonly name: string
|
||||||
|
value: T,
|
||||||
|
connect(input: Input<T>):void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Input<T> {
|
||||||
|
readonly name: string
|
||||||
|
value: T,
|
||||||
|
connect(output: Output<T>): void
|
||||||
|
}
|
||||||
0
src/nodes/types.js
Normal file
0
src/nodes/types.js
Normal file
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"module": "es2015",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"target": "esnext"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
ui/Components/Graph.vue
Normal file
24
ui/Components/Graph.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Node
|
||||||
|
v-for="(node, index) in graph"
|
||||||
|
:key="index"
|
||||||
|
:node="node"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Node from './Node.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Node,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
graph: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
46
ui/Components/Node.vue
Normal file
46
ui/Components/Node.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
border: 2px solid black;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 2px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div style="flex: 1">
|
||||||
|
<strong>Inputs:</strong>
|
||||||
|
<div v-for="(type, name) in node.in.variables" :key="name">
|
||||||
|
<div>{{ name }} <i>{{typeName(type)}}</i></div>
|
||||||
|
<Value :type="type" :value="node.in[name]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1">
|
||||||
|
<strong>Outputs:</strong>
|
||||||
|
<div v-for="(type, name) in node.out.variables" :key="name">
|
||||||
|
<div>{{ name }} <i>{{typeName(type)}}</i></div>
|
||||||
|
<Value :type="type" :value="node.out[name]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Value from './Value';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Value,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
typeName(type) {
|
||||||
|
return Array.isArray(type) ? 'Option' : type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
63
ui/Components/Value/Image.vue
Normal file
63
ui/Components/Value/Image.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<canvas
|
||||||
|
style="
|
||||||
|
width: 150px;
|
||||||
|
box-shadow: 0 0 6px;
|
||||||
|
"
|
||||||
|
@click="isRunning = !isRunning"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import {paintToCanvas, mediaSize} from '../../../src/nodes/canvas.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: null,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(value) {
|
||||||
|
this.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isRunning: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.redraw();
|
||||||
|
this.value.onchange(this.redraw);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
redraw() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
const value = this.value.value;
|
||||||
|
/** @type {HTMLCanvasElement} */
|
||||||
|
const canvas = this.$el;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!value) {
|
||||||
|
canvas.width = 1;
|
||||||
|
canvas.height = 1;
|
||||||
|
ctx.clearRect(0, 0, 1, 1);
|
||||||
|
} else {
|
||||||
|
const {width, height} = mediaSize(value);
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
paintToCanvas(canvas, value, {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width, height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
23
ui/Components/Value/Select.vue
Normal file
23
ui/Components/Value/Select.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<select v-model="value.value">
|
||||||
|
<option
|
||||||
|
v-for="t in type"
|
||||||
|
:value="t"
|
||||||
|
:key="t"
|
||||||
|
>{{ t }}</option>
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: null,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
13
ui/Components/Value/String.vue
Normal file
13
ui/Components/Value/String.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<input v-model="value.value">
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: null,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
24
ui/Components/Value/index.js
Normal file
24
ui/Components/Value/index.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import StringValue from './String.vue';
|
||||||
|
import ImageValue from './Image.vue';
|
||||||
|
import SelectValue from './Select.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: null,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render(h) {
|
||||||
|
return h(
|
||||||
|
this.type === 'Image' ? ImageValue :
|
||||||
|
this.type === 'Number' ? StringValue :
|
||||||
|
Array.isArray(this.type) ? SelectValue :
|
||||||
|
null,
|
||||||
|
{props: {value: this.value, type: this.type}})
|
||||||
|
}
|
||||||
|
};
|
||||||
36
ui/graph.js
Normal file
36
ui/graph.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as nodes from '../src/nodes';
|
||||||
|
import {sampleImage, getStream} from './helpers';
|
||||||
|
|
||||||
|
const resize = new nodes.Resize({
|
||||||
|
width: 1000,
|
||||||
|
height: 1000,
|
||||||
|
method: 'cover'
|
||||||
|
});
|
||||||
|
|
||||||
|
const brightnessContrast = new nodes.BrightnessContrast({})
|
||||||
|
const hsv = new nodes.HSV({})
|
||||||
|
const sepia = new nodes.Sepia({})
|
||||||
|
|
||||||
|
const compose = new nodes.Compose({
|
||||||
|
width: 1200,
|
||||||
|
height: 1800,
|
||||||
|
});
|
||||||
|
|
||||||
|
compose.in.fgX.value = 100;
|
||||||
|
compose.in.fgY.value = 100;
|
||||||
|
compose.in.bg.value = sampleImage(1200, 1800);
|
||||||
|
|
||||||
|
brightnessContrast.in.image.connect(resize.out.image);
|
||||||
|
hsv.in.image.connect(brightnessContrast.out.image);
|
||||||
|
sepia.in.image.connect(hsv.out.image);
|
||||||
|
compose.in.fg.connect(sepia.out.image);
|
||||||
|
|
||||||
|
getStream((video) => resize.in.image.value = video);
|
||||||
|
|
||||||
|
export default [
|
||||||
|
resize,
|
||||||
|
brightnessContrast,
|
||||||
|
hsv,
|
||||||
|
sepia,
|
||||||
|
compose,
|
||||||
|
];
|
||||||
35
ui/helpers.js
Normal file
35
ui/helpers.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export const sampleImage = (width, height) => {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = width;
|
||||||
|
c.height = height;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
const grd = ctx.createLinearGradient(0, 0, width, height);
|
||||||
|
grd.addColorStop(0, "black");
|
||||||
|
grd.addColorStop(1, "white");
|
||||||
|
|
||||||
|
ctx.fillStyle = grd;
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getStream = (callback) => {
|
||||||
|
navigator.getUserMedia({video: true}, (stream) => {
|
||||||
|
console.log(stream)
|
||||||
|
/** @type {HTMLVideoElement} */
|
||||||
|
const video = document.getElementById('Input');
|
||||||
|
video.srcObject = stream;
|
||||||
|
video.play();
|
||||||
|
let lastFrameTime = null;
|
||||||
|
const feedLoop = () => {
|
||||||
|
if (lastFrameTime !== video.currentTime) {
|
||||||
|
lastFrameTime = video.currentTime;
|
||||||
|
callback(video);
|
||||||
|
|
||||||
|
}
|
||||||
|
requestAnimationFrame(feedLoop);
|
||||||
|
};
|
||||||
|
feedLoop();
|
||||||
|
}, console.error.bind(console))
|
||||||
|
}
|
||||||
13
ui/index.html
Normal file
13
ui/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>UI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<video id="Input" style="width: 100px"></video>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
ui/index.js
Normal file
11
ui/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import Graph from './Components/Graph.vue';
|
||||||
|
|
||||||
|
import graph from './graph';
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
render(h) {
|
||||||
|
return h(Graph, {props: {graph}})
|
||||||
|
}
|
||||||
|
})
|
||||||
72
webpack.config.js
Normal file
72
webpack.config.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
|
||||||
|
|
||||||
|
function resolve(dir) {
|
||||||
|
return path.join(__dirname, '..', dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('webpack').Configuration}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
entry: {
|
||||||
|
main: './ui/index.js',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'dist'),
|
||||||
|
publicPath: '/',
|
||||||
|
filename: 'js/[name].js',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [
|
||||||
|
'.css',
|
||||||
|
'.js',
|
||||||
|
'.vue',
|
||||||
|
'.ts',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.vue$/,
|
||||||
|
loader: 'vue-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
include: [
|
||||||
|
resolve('src'),
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
presets: [
|
||||||
|
["@babel/preset-env"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
{loader: 'style-loader'},
|
||||||
|
{loader: 'css-loader'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new VueLoaderPlugin(),
|
||||||
|
new FriendlyErrorsWebpackPlugin(),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: 'ui/index.html',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
devServer: {
|
||||||
|
contentBase: path.resolve(__dirname, 'dist/'),
|
||||||
|
disableHostCheck: true,
|
||||||
|
historyApiFallback: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
Reference in New Issue
Block a user