♻️ better types system

This commit is contained in:
2019-01-22 12:52:22 +01:00
parent 6394f2a869
commit abd29d2999
27 changed files with 635 additions and 300 deletions

View File

@@ -5,54 +5,89 @@ export default class Compose extends Node {
constructor(options) {
super('Compose', {
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',
]
fg: {
type: 'Image'
},
fgX: {
type: 'Number',
default: 0,
step: 1,
},
fgY: {
type: 'Number',
default: 0,
step: 1,
},
bg: {
type: 'Image',
},
bgX: {
type: 'Number',
default: 0,
step: 1,
},
bgY: {
type: 'Number',
default: 0,
step: 1,
},
width: {
type: 'Number',
default: 100,
step: 1,
min: 1
},
height: {
type: 'Number',
default: 100,
step: 1,
min: 1,
},
mode: {
type: 'String',
default: 'source-over',
enum: [
'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;
image: {
type: 'Image'
}
}, options);
}
get width() {
return this.__in.width.value || this.options.width;
return this.__in.width.value;
}
get height() {
return this.__in.height.value || this.options.height;
return this.__in.height.value;
}
get fg() {
@@ -85,11 +120,10 @@ export default class Compose extends Node {
}
}
get mode() {
return this.__in.mode.value || 'source-over';
}
__update() {
if (!this.width || !this.height) {
return;
}
const canvas = createCanvas(this.width, this.height);
if (this.bg) {

View File

@@ -1,13 +1,13 @@
import {Inputs, Outputs} from './io.js';
import {Inputs, Outputs} from './io';
import uuidv4 from 'uuid/v4';
export default class Node {
constructor(name, inputNames, outputNames) {
constructor(name, inputDefinition, outputDefiniton, options) {
this.name = name;
this.id = uuidv4();
this.__in = new Inputs(inputNames, this);
this.__out = new Outputs(outputNames, this);
this.__in = new Inputs(inputDefinition, this);
this.__out = new Outputs(outputDefiniton, this);
this.__in.update = (name) => this.__update([name]);
}

View File

@@ -5,21 +5,33 @@ export default class Resize extends Node {
constructor(options) {
super('Resize', {
image: 'Image',
width: 'Number',
height: 'Number',
image: {
type: 'Image',
},
width: {
type: 'Number',
default: 100,
min: 1,
},
height: {
type: 'Number',
default: 100,
min: 1,
},
}, {
image: 'Image'
image: {
type: 'Image',
}
});
this.options = options;
}
get width() {
return this.__in.width.value || this.options.width;
return this.__in.width.value;
}
get height() {
return this.__in.height.value || this.options.height;
return this.__in.height.value;
}
__update() {

View File

@@ -5,8 +5,14 @@ export default class BrightnessContrast extends WebGL {
constructor() {
super('BrightnessContrast', {
brightness: 'Number',
contrast: 'Number',
brightness: {
type: 'Number',
default: 1,
},
contrast: {
type: 'Number',
default: 1,
},
})
}
@@ -23,23 +29,24 @@ export default class BrightnessContrast extends WebGL {
uniform vec2 u_brightnessContrast;
void main() {
vec3 c = texture2D(u_image, v_texCoord).rgb * u_brightnessContrast[0];
vec4 pixelColor = texture2D(u_image, v_texCoord);
vec3 c = pixelColor.rgb * u_brightnessContrast[0];
gl_FragColor = vec4(
clamp(
(c - 0.5) * u_brightnessContrast[1] + 0.5,
0.0, 1.0),
1
pixelColor.a
);
}
`
}
get contrast() {
return this.in.contrast.value || 1;
return this.in.contrast.value
}
get brightness() {
return this.in.brightness.value || 1;
return this.in.brightness.value;
}
_setParams(gl, program) {

View File

@@ -13,11 +13,30 @@ export default class GreenScreen extends WebGL {
constructor() {
super('GreenScreen', {
balance: 'Number',
screen: 'Color',
screenWeight: 'Number',
clipBlack: 'Number',
clipWhite: 'Number',
balance: {
type: 'Number',
default: 0.5,
step: 0.1,
},
screen: {
type: 'Color',
default: '#2CD6A4',
},
screenWeight: {
type: 'Number',
default: 1,
min: 0,
max: 1,
step: 0.1,
},
clipBlack: {
type: 'Number',
default: 0,
},
clipWhite: {
type: 'Number',
default: 1,
},
})
}
@@ -52,13 +71,13 @@ export default class GreenScreen extends WebGL {
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);
float screenSat = screenFmax - mix(screenSecondaryComponents - screenFmin, screenSecondaryComponents / 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);
float solid = step(1.0, pixelSat + step(fmax, 0.1) + diffPrimary);
/*
Semi-transparent pixel if the primary component matches but if saturation is less
@@ -77,26 +96,27 @@ export default class GreenScreen extends WebGL {
}
get clipBlack() {
return (this.in.clipBlack.value || 0);
return this.in.clipBlack.value;
}
get clipWhite() {
return (this.in.clipWhite.value || 0);
return this.in.clipWhite.value;
}
get screenWeight() {
return (this.in.screenWeight.value || 1);
return this.in.screenWeight.value;
}
get screen() {
return hexColorTOvec3(this.in.screen.value || '#00FF00');
return hexColorTOvec3(this.in.screen.value);
}
get balance() {
return (this.in.balance.value || 0.5);
return this.in.balance.value;
}
_setParams(gl, program) {
console.log(this.balance, this.clipBlack, this.clipWhite, this.screenWeight)
gl.uniform3f(gl.getUniformLocation(program, 'screen'), ...this.screen);
gl.uniform1f(gl.getUniformLocation(program, 'balance'), this.balance);
gl.uniform1f(gl.getUniformLocation(program, 'clipBlack'), this.clipBlack);

View File

@@ -5,9 +5,18 @@ export default class HSV extends WebGL {
constructor() {
super('HSV', {
hue: 'Number',
saturation: 'Number',
value: 'Number',
hue: {
type: 'Number',
default: 0,
},
saturation: {
type: 'Number',
default: 1,
},
value: {
type: 'Number',
default: 1,
}
})
}
@@ -50,15 +59,15 @@ export default class HSV extends WebGL {
}
get hue() {
return ((this.in.hue.value || 0) % 360) / 360;
return ((this.in.hue.value) % 360) / 360;
}
get saturation() {
return this.in.saturation.value || 1;
return this.in.saturation.value;
}
get value() {
return this.in.value.value || 1;
return this.in.value.value;
}
_setParams(gl, program) {

View File

@@ -5,7 +5,10 @@ export default class Sepia extends WebGL {
constructor() {
super('Sepia', {
amount: 'Number',
amount: {
type: 'Number',
default: 0,
},
})
}
@@ -22,7 +25,7 @@ export default class Sepia extends WebGL {
uniform float amount;
void main() {
vec3 color = texture2D(u_image, v_texCoord).rgb;
vec4 color = texture2D(u_image, v_texCoord);
float r = color.r;
float g = color.g;
float b = color.b;
@@ -31,13 +34,13 @@ export default class Sepia extends WebGL {
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);
gl_FragColor = color;
}
`
}
get amount() {
return this.in.amount.value || 0;
return this.in.amount.value;
}
/**

View File

@@ -8,9 +8,13 @@ export default class WebGL extends Node {
constructor(name, inputs={}) {
super(name, Object.assign({
image: 'Image',
image: {
type: 'Image',
},
}, inputs), {
image: 'Image'
image: {
type: 'Image',
},
});
}

View File

@@ -4,7 +4,9 @@ export default class ToBlob extends Node {
constructor(options) {
super('Webcam', {}, {
image: 'Image',
image: {
type: 'Image'
},
});
this.start();
}
@@ -16,8 +18,8 @@ export default class ToBlob extends Node {
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 },
width: { min: 1024, ideal: 1920, max: 1920 },
height: { min: 776, ideal: 1080, max: 1080 },
deviceId: last(devices).deviceId
},
});

12
src/nodes/io.d.ts vendored
View File

@@ -1,12 +0,0 @@
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>
}

View File

@@ -1,175 +0,0 @@
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 id() {
return `${this.__owner.id}-${this.__name}`;
}
get name() {
return this.__name;
}
get value() {
return this.__value;
}
set value(value) {
this.__value = value;
this.__owner.update(this.name);
}
connect(output) {
this.disconnect();
this.__output = output
this.__output.onchange(this.__onchangelistener);
}
disconnect(output) {
if (this.__output) {
this.__output.offchange(this.__onchangelistener);
this.__output = null;
}
}
get output() {
return this.__output;
}
__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);
}
serialize() {
return {
value: this.__output ? null : this.__value,
output: this.__output ? this.__output.id: null,
}
}
}
export class Output {
constructor(name, owner) {
this.__value = null;
this.__name = name;
this.__owner = owner
this.__listeners = [];
}
get id() {
return `${this.__owner.id}-${this.__name}`;
}
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, owner) {
this.__values = {};
this.__owner = owner;
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];
},
})
}
}
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, owner) {
this.__values = {};
this.__owner = owner;
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];
},
})
}
}
get id() {
return `${this.__owner.id}-out`;
}
serialize() {
return null;
}
}

View File

@@ -0,0 +1,97 @@
const isNil = (val) => val === null || val === undefined;
export const defaultConstrainSatisfied = (value) => {
return !isNil(value);
}
export const imageConstrainSatisfied = (value) => {
return value instanceof HTMLCanvasElement ||
value instanceof Image ||
value instanceof HTMLVideoElement ||
value instanceof ImageData ||
value instanceof ImageBitmap;
};
export const colorConstrainSatisfied = (value) => {
return /#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/.test(value);
};
export const numberConstrainsSatisfied = (definition, value) => {
return typeof(value) === 'number' && !isNaN(value) &&
(isNil(definition.min) || (value >= definition.min)) &&
(isNil(definition.max) || (value <= definition.max))
}
export const valueConstrainsSatisfied = (definition, value) => {
return defaultConstrainSatisfied(value) && (
definition.type === 'Number' ? numberConstrainsSatisfied(definition, value) :
definition.type === 'Color' ? colorConstrainSatisfied(value) :
definition.type === 'Image' ? imageConstrainSatisfied(value) :
true
)
};
export default class AbstractIO {
constructor(name, definition, owner) {
this.__name = name;
this.__owner = owner
this.__definition = definition;
this.__value = null;
this.__listeners = [];
}
get definition() {
return this.__definition;
}
get __defaultValue() {
return this.__definition.default;
}
get id() {
return `${this.__owner.id}-${this.__name}`;
}
get name() {
return this.__name;
}
get type() {
return this.__definition.type;
}
get value() {
return this.__getValue;
}
set value(val) {
this.__setValue(val);
}
__getValue() {
return (valueConstrainsSatisfied(this.__definition, this.__value)) ? this.__value : this.__defaultValue;
}
__setValue(value) {
if (valueConstrainsSatisfied(this.__definition, value)) {
this.__value = value;
}
}
__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);
}
}

View File

@@ -0,0 +1,28 @@
export default class AbstractIOSet {
constructor(variables, owner) {
this.__values = {};
this.__owner = owner;
this.variables = variables;
for (let name of Object.keys(this.variables)) {
this.__values[name] = this.__createProperty(name, variables[name]);
Object.defineProperty(this, name, {
get() {
return this.__values[name];
},
})
}
}
__createProperty(name, definition) {
}
update(changed) {
}
}

49
src/nodes/io/Input.js Normal file
View File

@@ -0,0 +1,49 @@
import AbstractIO from './AbstractIO';
export default class Input extends AbstractIO {
constructor(name, definition, owner) {
super(name, definition, owner);
this.__output = null;
this.__onchangelistener = (value) => {
this.value = value;
this.__notifyListeners();
}
}
get value() {
return this.__getValue();
}
set value(value) {
this.__setValue(value);
this.__owner.update(this.name);
}
get output() {
return this.__output;
}
connect(output) {
if (output && output.type === this.type) {
this.disconnect();
this.__output = output
this.__output.onchange(this.__onchangelistener);
}
}
disconnect(output) {
if (this.__output) {
this.__output.offchange(this.__onchangelistener);
this.__output = null;
}
}
serialize() {
return {
value: this.__output ? null : this.__value,
output: this.__output ? this.__output.id: null,
}
}
}

25
src/nodes/io/Inputs.js Normal file
View File

@@ -0,0 +1,25 @@
import Input from './Input';
import AbstractIOSet from './AbstractIOSet';
export default class Inputs extends AbstractIOSet {
constructor(variables, owner) {
super(variables, owner);
}
__createProperty(name, definition) {
return new Input(name, definition, this);
}
get id() {
return `${this.__owner.id}-in`;
}
serialize() {
const res = {};
for (let name of Object.keys(this.variables)) {
res[name] = this.__values[name].serialize();
}
return res;
}
}

View File

@@ -0,0 +1,36 @@
import Inputs from './Inputs';
describe('Inputs', () => {
it('should create with default values', () => {
const owner = {
id: 'owner',
update(name) {
}
};
const inputs = new Inputs({
x: {
type: 'Number',
min: 0,
max: 1,
default: 0,
},
y: {
type: 'Number',
default: 1
},
foo: {
type: 'String',
default: 'foo'
},
}, owner)
expect(inputs.x.type).toBe('Number');
expect(inputs.x.value).toBe(0);
expect(inputs.y.value).toBe(1);
inputs.x.value = 1;
expect(inputs.x.value).toBe(1);
});
});

40
src/nodes/io/Output.js Normal file
View File

@@ -0,0 +1,40 @@
import AbstractIO from './AbstractIO';
export default class Output extends AbstractIO {
constructor(name, definition, owner) {
super(name, definition, owner);
}
get id() {
return `${this.__owner.id}-${this.__name}`;
}
get name() {
return this.__name;
}
get value() {
return this.__getValue();
}
set value(value) {
this.__setValue(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);
}
}

20
src/nodes/io/Outputs.js Normal file
View File

@@ -0,0 +1,20 @@
import Output from './Output';
import AbstractIOSet from './AbstractIOSet';
export default class Outputs extends AbstractIOSet {
constructor(variables, owner) {
super(variables, owner);
}
__createProperty(name, definition) {
return new Output(name, definition, this);
}
get id() {
return `${this.__owner.id}-out`;
}
serialize() {
return null;
}
}

View File

@@ -0,0 +1,30 @@
import Outputs from './Outputs';
describe('Outputs', () => {
it('should create with default values', () => {
const owner = {
id: 'owner',
};
const inputs = new Outputs({
x: {
type: 'Number',
min: 0,
max: 1,
default: 0,
},
y: {
type: 'Number',
default: 1
},
foo: {
type: 'String',
default: 'foo'
},
}, owner)
expect(inputs.x.type).toBe('Number');
expect(inputs.x.value).toBe(0);
expect(inputs.y.value).toBe(1);
});
});

4
src/nodes/io/index.js Normal file
View File

@@ -0,0 +1,4 @@
export {default as Input} from './Input';
export {default as Inputs} from './Inputs';
export {default as Output} from './Output';
export {default as Outputs} from './Outputs';