This commit is contained in:
2019-01-02 19:16:16 +01:00
commit e76aafc35a
34 changed files with 13596 additions and 0 deletions

0
src/index.js Normal file
View File

33
src/index.spec.js Normal file
View 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
View 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;
}
}

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

11
src/nodes/types.d.ts vendored Normal file
View 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
View File