✨ basic ui, some filters
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import Node from './Node';
|
||||
import {createCanvas, mediaSize, paintToCanvas} from './canvas';
|
||||
|
||||
export default class Resize extends Node {
|
||||
export default class Compose extends Node {
|
||||
|
||||
constructor(options) {
|
||||
super({
|
||||
super('Compose', {
|
||||
fg: 'Image',
|
||||
fgX: 'Number',
|
||||
fgY: 'Number',
|
||||
|
||||
@@ -2,7 +2,7 @@ import Node from './Node';
|
||||
|
||||
export default class ImageIdentity extends Node {
|
||||
constructor() {
|
||||
super({
|
||||
super('ImageIdentity', {
|
||||
image: 'Image',
|
||||
}, {
|
||||
image: 'Image',
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {Inputs, Outputs} from './io.js';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
|
||||
export default class Node {
|
||||
|
||||
constructor(inputNames, outputNames) {
|
||||
this.__in = new Inputs(inputNames);
|
||||
this.__out = new Outputs(outputNames);
|
||||
constructor(name, inputNames, outputNames) {
|
||||
this.name = name;
|
||||
this.id = uuidv4();
|
||||
this.__in = new Inputs(inputNames, this);
|
||||
this.__out = new Outputs(outputNames, this);
|
||||
this.__in.update = (name) => this.__update([name]);
|
||||
}
|
||||
|
||||
@@ -25,5 +28,33 @@ export default class Node {
|
||||
get out() {
|
||||
return this.__out;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
options: {
|
||||
in: this.in.serialize(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
deserialize({id, options}) {
|
||||
this.id = id || uuidv4();
|
||||
}
|
||||
|
||||
reconnect({options}, outputs) {
|
||||
console.log('outputs', outputs)
|
||||
for (let name of Object.keys(options.in)) {
|
||||
const {value, output} = options.in[name];
|
||||
if (value) {
|
||||
this.in[name].value = value;
|
||||
}
|
||||
if (output) {
|
||||
console.log('connecting', this.in[name].id, outputs[output])
|
||||
this.in[name].connect(outputs[output]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {createCanvas, mediaSize, paintToCanvas} from './canvas';
|
||||
export default class Resize extends Node {
|
||||
|
||||
constructor(options) {
|
||||
super({
|
||||
super('Resize', {
|
||||
image: 'Image',
|
||||
width: 'Number',
|
||||
height: 'Number',
|
||||
@@ -24,6 +24,9 @@ export default class Resize extends Node {
|
||||
|
||||
__update() {
|
||||
const media = this.__in.image.value;
|
||||
if (!media) {
|
||||
return;
|
||||
}
|
||||
const canvas = createCanvas(this.width, this.height);
|
||||
|
||||
const {width: srcWidth, height: srcHeight} = mediaSize(media);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {createCanvas} from './canvas';
|
||||
export default class ToBlob extends Node {
|
||||
|
||||
constructor(options) {
|
||||
super({
|
||||
super('ToBlob', {
|
||||
image: 'Image',
|
||||
}, {
|
||||
image: 'Image',
|
||||
|
||||
@@ -4,7 +4,7 @@ import WebGL from './WebGl';
|
||||
export default class BrightnessContrast extends WebGL {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
super('BrightnessContrast', {
|
||||
brightness: 'Number',
|
||||
contrast: 'Number',
|
||||
})
|
||||
|
||||
106
src/nodes/WebGL/Greenscreen.js
Normal file
106
src/nodes/WebGL/Greenscreen.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import WebGL from './WebGl';
|
||||
|
||||
const hexColorTOvec3 = (val) => {
|
||||
const match = val.match(/#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/);
|
||||
if (match) {
|
||||
return [parseInt(match[1], 16) / 255, parseInt(match[2], 16) / 255, parseInt(match[3], 16) / 255];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default class GreenScreen extends WebGL {
|
||||
|
||||
constructor() {
|
||||
super('GreenScreen', {
|
||||
balance: 'Number',
|
||||
screen: 'Color',
|
||||
screenWeight: 'Number',
|
||||
clipBlack: 'Number',
|
||||
clipWhite: '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 balance;
|
||||
|
||||
uniform vec3 screen;
|
||||
|
||||
uniform float clipBlack;
|
||||
uniform float clipWhite;
|
||||
uniform float screenWeight;
|
||||
|
||||
void main() {
|
||||
float pixelSat, secondaryComponents;
|
||||
vec4 sourcePixel = texture2D(u_image, v_texCoord);
|
||||
float fmin = min(min(sourcePixel.r, sourcePixel.g), sourcePixel.b);
|
||||
float fmax = max(max(sourcePixel.r, sourcePixel.g), sourcePixel.b);
|
||||
vec3 pixelPrimary = step(fmax, sourcePixel.rgb);
|
||||
secondaryComponents = dot(1.0 - pixelPrimary, sourcePixel.rgb);
|
||||
|
||||
// luminance = fmax
|
||||
float screenFmin = min(min(screen.r, screen.g), screen.b); //Min. value of RGB
|
||||
float screenFmax = max(max(screen.r, screen.g), screen.b); //Max. value of RGB
|
||||
vec3 screenPrimary = step(screenFmax, screen.rgb);
|
||||
float screenSecondaryComponents = dot(1.0 - screenPrimary, screen.rgb);
|
||||
float screenSat = fmax - mix(secondaryComponents - fmin, secondaryComponents / 2.0, balance);
|
||||
|
||||
pixelSat = fmax - mix(secondaryComponents - fmin, secondaryComponents / 2.0, balance);
|
||||
|
||||
// solid pixel if primary color component is not the same as the screen color
|
||||
float diffPrimary = dot(abs(pixelPrimary - screenPrimary), vec3(1.0));
|
||||
float solid = step(1.0, step(pixelSat, 0.1) + step(fmax, 0.1) + diffPrimary);
|
||||
|
||||
/*
|
||||
Semi-transparent pixel if the primary component matches but if saturation is less
|
||||
than that of screen color. Otherwise totally transparent
|
||||
*/
|
||||
float alpha = max(0.0, 1.0 - pixelSat / screenSat);
|
||||
alpha = smoothstep(clipBlack, clipWhite, alpha);
|
||||
vec4 semiTransparentPixel = vec4((sourcePixel.rgb - (1.0 - alpha) * screen.rgb * screenWeight) / max(0.00001, alpha), alpha);
|
||||
vec4 pixel = mix(semiTransparentPixel, sourcePixel, solid);
|
||||
// vec4 pixel = vec4(sourcePixel.rgb, (1.0 - alpha));
|
||||
|
||||
gl_FragColor = vec4(pixel.rgb * pixel.a, pixel.a);
|
||||
//gl_FragColor = vec4(min(1.0, max(pixel.r, 0.0)), min(1.0, max(pixel.g, 0.0)), min(1.0, max(pixel.b, 0.0)), min(1.0, max(pixel.a, 0.0)));
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
get clipBlack() {
|
||||
return (this.in.clipBlack.value || 0);
|
||||
}
|
||||
|
||||
get clipWhite() {
|
||||
return (this.in.clipWhite.value || 0);
|
||||
}
|
||||
|
||||
get screenWeight() {
|
||||
return (this.in.screenWeight.value || 1);
|
||||
}
|
||||
|
||||
get screen() {
|
||||
return hexColorTOvec3(this.in.screen.value || '#00FF00');
|
||||
}
|
||||
|
||||
get balance() {
|
||||
return (this.in.balance.value || 0.5);
|
||||
}
|
||||
|
||||
_setParams(gl, program) {
|
||||
gl.uniform3f(gl.getUniformLocation(program, 'screen'), ...this.screen);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'balance'), this.balance);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'clipBlack'), this.clipBlack);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'clipWhite'), this.clipWhite);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'screenWeight'), this.screenWeight);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import WebGL from './WebGl';
|
||||
|
||||
|
||||
export default class BrightnessContrast extends WebGL {
|
||||
export default class HSV extends WebGL {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
super('HSV', {
|
||||
hue: 'Number',
|
||||
saturation: 'Number',
|
||||
value: 'Number',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import WebGL from './WebGl';
|
||||
|
||||
|
||||
export default class BrightnessContrast extends WebGL {
|
||||
export default class Sepia extends WebGL {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
super('Sepia', {
|
||||
amount: 'Number',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ console.log(webglUtils);
|
||||
|
||||
export default class WebGL extends Node {
|
||||
|
||||
constructor(inputs={}) {
|
||||
super(Object.assign({
|
||||
constructor(name, inputs={}) {
|
||||
super(name, Object.assign({
|
||||
image: 'Image',
|
||||
}, inputs), {
|
||||
image: 'Image'
|
||||
@@ -86,7 +86,7 @@ export default class WebGL extends Node {
|
||||
*
|
||||
* @param {WebGLRenderingContext} gl
|
||||
*/
|
||||
program(gl) {
|
||||
compileProgram(gl) {
|
||||
const program = gl.createProgram();
|
||||
|
||||
gl.attachShader(program, this.vertShader(gl));
|
||||
@@ -115,10 +115,7 @@ export default class WebGL extends Node {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -126,22 +123,46 @@ export default class WebGL extends Node {
|
||||
|
||||
}
|
||||
|
||||
setup() {
|
||||
if (!this.canvas) {
|
||||
this.program = null;
|
||||
this.canvas = createCanvas(0, 0);
|
||||
}
|
||||
const gl = this.canvas.getContext('webgl');
|
||||
if (!this.program) {
|
||||
this.program = this.compileProgram(gl);
|
||||
gl.useProgram(this.program);
|
||||
this.createTexture(gl);
|
||||
}
|
||||
}
|
||||
|
||||
__update() {
|
||||
const image = this.in.image.value;
|
||||
if (!image) return;
|
||||
const {width, height} = mediaSize(image);
|
||||
const canvas = createCanvas(width, height);
|
||||
this.setup();
|
||||
const canvas = this.canvas;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const gl = canvas.getContext('webgl');
|
||||
const program = this.program(gl);
|
||||
const program = this.program;
|
||||
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
|
||||
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();
|
||||
if (this.image) {
|
||||
// Upload the image into the texture.
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.image);
|
||||
}
|
||||
|
||||
|
||||
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([
|
||||
@@ -153,17 +174,12 @@ export default class WebGL extends Node {
|
||||
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);
|
||||
const positionBuffer = gl.createBuffer();
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
this.setRectangle(gl, 0, 0, width, height);
|
||||
|
||||
// Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
|
||||
var size = 2; // 2 components per iteration
|
||||
@@ -195,10 +211,7 @@ export default class WebGL extends Node {
|
||||
this._setParams(gl, program);
|
||||
|
||||
// Draw the rectangle.
|
||||
var primitiveType = gl.TRIANGLES;
|
||||
var offset = 0;
|
||||
var count = 6;
|
||||
gl.drawArrays(primitiveType, offset, count);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
|
||||
this.out.image.value = canvas;
|
||||
}
|
||||
|
||||
41
src/nodes/Webcam.js
Normal file
41
src/nodes/Webcam.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import Node from './Node';
|
||||
|
||||
export default class ToBlob extends Node {
|
||||
|
||||
constructor(options) {
|
||||
super('Webcam', {}, {
|
||||
image: 'Image',
|
||||
});
|
||||
this.start();
|
||||
}
|
||||
|
||||
|
||||
async start() {
|
||||
const devices = (await navigator.mediaDevices.enumerateDevices()).filter((d) => d.kind === 'videoinput')
|
||||
const last = (ary) => ary[ary.length - 1];
|
||||
console.log(last(devices));
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
width: { min: 1024, ideal: 1280, max: 1920 },
|
||||
height: { min: 776, ideal: 720, max: 1080 },
|
||||
deviceId: last(devices).deviceId
|
||||
},
|
||||
});
|
||||
console.log(stream)
|
||||
/** @type {HTMLVideoElement} */
|
||||
|
||||
const video = document.createElement('video');
|
||||
video.playsinline = true;
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
let lastFrameTime = null;
|
||||
const feedLoop = () => {
|
||||
if (lastFrameTime !== video.currentTime) {
|
||||
lastFrameTime = video.currentTime;
|
||||
this.out.image.value = video;
|
||||
}
|
||||
requestAnimationFrame(feedLoop);
|
||||
};
|
||||
feedLoop();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export {default as Webcam} from './Webcam';
|
||||
export {default as Resize} from './Resize';
|
||||
export {default as ImageIdentity} from './ImageIdentity';
|
||||
export {default as ToBlob} from './ToBlob';
|
||||
@@ -5,4 +6,5 @@ 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';
|
||||
export {default as Sepia} from './WebGL/Sepia';
|
||||
export {default as GreenScreen} from './WebGL/Greenscreen';
|
||||
@@ -15,6 +15,10 @@ export class Input {
|
||||
}
|
||||
}
|
||||
|
||||
get id() {
|
||||
return `${this.__owner.id}-${this.__name}`;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.__name;
|
||||
}
|
||||
@@ -29,12 +33,20 @@ export class Input {
|
||||
}
|
||||
|
||||
connect(output) {
|
||||
this.disconnect();
|
||||
this.__output = output
|
||||
this.__output.onchange(this.__onchangelistener);
|
||||
}
|
||||
|
||||
disconnect(output) {
|
||||
this.__output.offchage(this.__onchangelistener);
|
||||
if (this.__output) {
|
||||
this.__output.offchange(this.__onchangelistener);
|
||||
this.__output = null;
|
||||
}
|
||||
}
|
||||
|
||||
get output() {
|
||||
return this.__output;
|
||||
}
|
||||
|
||||
__notifyListeners() {
|
||||
@@ -51,6 +63,13 @@ export class Input {
|
||||
this.__listeners = this.__listeners
|
||||
.filter((l) => l !== listener);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
value: this.__output ? null : this.__value,
|
||||
output: this.__output ? this.__output.id: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Output {
|
||||
@@ -62,6 +81,10 @@ export class Output {
|
||||
this.__listeners = [];
|
||||
}
|
||||
|
||||
get id() {
|
||||
return `${this.__owner.id}-${this.__name}`;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.__name;
|
||||
}
|
||||
@@ -94,8 +117,9 @@ export class Output {
|
||||
|
||||
export class Inputs {
|
||||
|
||||
constructor(variables) {
|
||||
constructor(variables, owner) {
|
||||
this.__values = {};
|
||||
this.__owner = owner;
|
||||
this.variables = variables;
|
||||
|
||||
for (let name of Object.keys(this.variables)) {
|
||||
@@ -108,14 +132,27 @@ export class Inputs {
|
||||
}
|
||||
}
|
||||
|
||||
get id() {
|
||||
return `${this.__owner.id}-in`;
|
||||
}
|
||||
|
||||
update(changed) {
|
||||
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const res = {};
|
||||
for (let name of Object.keys(this.variables)) {
|
||||
res[name] = this.__values[name].serialize();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export class Outputs {
|
||||
constructor(variables) {
|
||||
constructor(variables, owner) {
|
||||
this.__values = {};
|
||||
this.__owner = owner;
|
||||
this.variables = variables;
|
||||
|
||||
for (let name of Object.keys(this.variables)) {
|
||||
@@ -127,4 +164,12 @@ export class Outputs {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get id() {
|
||||
return `${this.__owner.id}-out`;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user