basic ui, some filters

This commit is contained in:
2019-01-21 20:39:56 +01:00
parent e76aafc35a
commit 6394f2a869
30 changed files with 974 additions and 111 deletions

View File

@@ -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',

View File

@@ -2,7 +2,7 @@ import Node from './Node';
export default class ImageIdentity extends Node {
constructor() {
super({
super('ImageIdentity', {
image: 'Image',
}, {
image: 'Image',

View File

@@ -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]);
}
}
}
};

View File

@@ -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);

View File

@@ -4,7 +4,7 @@ import {createCanvas} from './canvas';
export default class ToBlob extends Node {
constructor(options) {
super({
super('ToBlob', {
image: 'Image',
}, {
image: 'Image',

View File

@@ -4,7 +4,7 @@ import WebGL from './WebGl';
export default class BrightnessContrast extends WebGL {
constructor() {
super({
super('BrightnessContrast', {
brightness: 'Number',
contrast: 'Number',
})

View 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);
}
}

View File

@@ -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',

View File

@@ -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',
})
}

View File

@@ -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
View 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();
}
}

View File

@@ -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';

View File

@@ -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;
}
}