♻️ switching to typescript

This commit is contained in:
2019-12-06 20:33:06 +01:00
parent 08e1cb221f
commit 38d8357ecc
40 changed files with 1048 additions and 764 deletions

View File

@@ -1,5 +1,13 @@
type PoolCanvas<T extends (OffscreenCanvas | HTMLCanvasElement)> = T & {
acquire(): void
release(): void
}
class CanvasPool {
class CanvasPool<T extends (OffscreenCanvas | HTMLCanvasElement)> {
__ctx: any
__pool: PoolCanvas<T>[]
__pointerCounter: WeakMap<PoolCanvas<T>, number>
constructor(ctx) {
this.__ctx = ctx;
@@ -9,26 +17,28 @@ class CanvasPool {
__createNewCanvas() {
const canvas = (this.__ctx !== 'webgl') ? new OffscreenCanvas(1,1) : document.createElement('canvas');
// @ts-ignore
canvas.acquire = this.acquireCanvas.bind(this, canvas);
// @ts-ignore
canvas.release = this.releaseCanvas.bind(this, canvas);
canvas.getContext(this.__ctx);
this.__pool.push(canvas);
this.__pointerCounter.set(canvas, 0);
this.__pool.push(<PoolCanvas<T>>canvas);
this.__pointerCounter.set(<PoolCanvas<T>>canvas, 0);
}
createCanvas() {
createCanvas():PoolCanvas<T> {
if (this.__pool.length === 0) {
this.__createNewCanvas();
}
return this.__pool.pop();
}
acquireCanvas(canvas) {
acquireCanvas(canvas: PoolCanvas<T>) {
const inUse = this.__pointerCounter.get(canvas) + 1;
this.__pointerCounter.set(canvas, inUse);
}
releaseCanvas(canvas) {
releaseCanvas(canvas: PoolCanvas<T>) {
const inUse = this.__pointerCounter.get(canvas) - 1;
this.__pointerCounter.set(canvas, inUse);
if (inUse === 0) {
@@ -38,5 +48,5 @@ class CanvasPool {
}
export const canvasPool2D = new CanvasPool('2d');
export const canvasPoolWebGL = new CanvasPool('webgl');
export const canvasPool2D = new CanvasPool<OffscreenCanvas>('2d');
export const canvasPoolWebGL = new CanvasPool<HTMLCanvasElement>('webgl');

View File

@@ -1,14 +1,34 @@
import * as nodes from './nodes';
import * as nodes from './nodes/index';
import Node from './nodes/Node'
import {MultiSubject} from './helpers/listener';
interface GraphNode {
node: Node<any, any>,
x?: number,
y?: number
}
export default class Graph {
constructor() {
this.nodes = [];
nodes: GraphNode[]
__nodesActive: number
__isRunningSet: WeakSet<any>
subject: MultiSubject
constructor(nodes=[]) {
this.__isRunningSet = new WeakSet();
this.nodes = nodes;
this.__nodesActive = 0;
this.__nodesActiveDebounce = null;
this.subject = new MultiSubject(['running', 'stopped']);
this.__setupRunningListeners();
}
get isRunning() {
return this.__nodesActive > 0;
}
get isStopped() {
return this.__nodesActive === 0;
}
serialize() {
@@ -18,28 +38,38 @@ export default class Graph {
}));
}
static async deserialize(nodes) {
const graph = new Graph();
const isRunningSet = new WeakSet();
graph.nodes = await createUnconnectedNodes(nodes);
for (const {node} of graph.nodes) {
node.subject.on('running', (isRunning) => {
if (!isRunning && isRunningSet.has(node)) {
isRunningSet.delete(node);
graph.__nodesActive -= 1;
} else if (isRunning && !isRunningSet.has(node)) {
isRunningSet.add(node);
graph.__nodesActive += 1;
}
if (graph.__nodesActive > 0) {
graph.subject.next('running');
} else {
console.info('[Graphfx] graph stopped');
graph.subject.next('stopped');
}
});
addNode(node) {
this.nodes.push({node});
this.__setupNodeRunningListener(node);
}
__setupNodeRunningListener(node) {
node.subject.on('running', (isRunning) => {
if (!isRunning && this.__isRunningSet.has(node)) {
this.__isRunningSet.delete(node);
this.__nodesActive -= 1;
} else if (isRunning && !this.__isRunningSet.has(node)) {
this.__isRunningSet.add(node);
this.__nodesActive += 1;
}
graph.nodes = await reconnectNodes(graph.nodes);
if (this.__nodesActive > 0) {
this.subject.next('running');
} else {
console.info('[Graphfx] graph stopped');
this.subject.next('stopped');
}
});
}
__setupRunningListeners() {
for (const {node} of this.nodes) {
this.__setupNodeRunningListener(node);
}
}
static async deserialize(nodes) {
const graph = new Graph(await createUnconnectedNodes(nodes));
await reconnectNodes(graph.nodes);
return graph;
}
@@ -71,17 +101,20 @@ export default class Graph {
export const createNode = async ({name, options, id}) => {
export async function createNode<T extends keyof typeof nodes>(
{name, options, id}:{name: T, options: any, id: string}):Promise<typeof nodes[T]> {
try {
const node = new nodes[name](options);
await node.deserialize({id, options});
// @ts-ignore
return node;
} catch (err) {
console.error('Failed to create node', name, err)
}
}
export const createUnconnectedNodes = async (nodes) => {
export const createUnconnectedNodes = async (nodes):Promise<GraphNode[]> => {
// @ts-ignore
return (await Promise.all(nodes.map(async ({node, x, y}) => {
const nodeInstance = await createNode(node);
return ({
@@ -113,7 +146,8 @@ export const reconnectNodes = async (nodes) => {
}))
}
export const createNodes = async (nodes) => {
nodes = await createUnconnectedNodes(nodes);
return reconnectNodes(nodes);
export const createNodes = async (nodes):Promise<GraphNode[]> => {
const unconnectedNodes = await createUnconnectedNodes(nodes);
// @ts-ignore
return reconnectNodes(unconnectedNodes);
}

View File

@@ -1,26 +1,38 @@
import Node from '../Node';
import {waitForMedia} from '../../utils';
import {waitForMedia, merge} from '../../utils';
import {canvasPool2D} from '../../canvas/CanvasPool';
import {
Variables,
ImageVar,
BooleanVar,
NumberVar
} from '../io/AbstractIOSet';
export default class Canvas2d extends Node {
constructor(name, inputDefinition, outputDefiniton, options) {
const inputs = {
image: {
type: 'Image',
} as ImageVar,
};
const outputs = {
image: {
type: 'Image',
} as ImageVar,
width: {
type: 'Number',
} as NumberVar,
height: {
type: 'Number',
} as NumberVar
};
export default class Canvas2d<I extends Variables, O extends Variables> extends Node<I & (typeof inputs), O & (typeof outputs)> {
constructor(name, inputDefinition: I, outputDefiniton: O, options) {
super(name,
Object.assign({
image: {
type: 'Image',
},
}, inputDefinition),
Object.assign({
image: {
type: 'Image',
},
width: {
type: 'Number',
},
height: {
type: 'Number',
}
}, outputDefiniton),
merge(inputs, inputDefinition),
merge(outputs, outputDefiniton),
options
);
}

View File

@@ -1,154 +0,0 @@
import Canvas2d from './Canvas2d';
import {createCanvas, mediaSize, paintToCanvas} from '../canvas';
export default class Compose extends Canvas2d {
constructor(options) {
super('Compose', {
image: null,
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: {
type: 'Image'
}
}, options);
}
get width() {
return this.__in.width.value;
}
get height() {
return this.__in.height.value;
}
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,
};
}
}
/**
* @param {any} values
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} ctx
*/
async render({width, height, mode}, canvas, ctx) {
if (!width || !height) {
return;
}
if (!this.bg && !this.fg) {
return;
}
canvas.width = width;
canvas.height = height;
ctx.globalCompositeOperation = 'source-over';
if (this.bg) {
paintToCanvas(canvas, this.bg.image, this.bg);
}
ctx.globalCompositeOperation = mode;
if (this.fg) {
paintToCanvas(canvas, this.fg.image, this.fg);
}
return canvas;
}
}

View File

@@ -0,0 +1,163 @@
import Canvas2d from './Canvas2d';
import {createCanvas, mediaSize, paintToCanvas} from '../canvas';
import {
ImageVar,
StringVar,
NumberVar
} from '../io/AbstractIOSet';
const inputs = {
image: null,
fg: {
type: 'Image'
} as ImageVar,
fgX: {
type: 'Number',
default: 0,
step: 1,
} as NumberVar,
fgY: {
type: 'Number',
default: 0,
step: 1,
} as NumberVar,
bg: {
type: 'Image',
} as ImageVar,
bgX: {
type: 'Number',
default: 0,
step: 1,
} as NumberVar,
bgY: {
type: 'Number',
default: 0,
step: 1,
} as NumberVar,
width: {
type: 'Number',
default: 100,
step: 1,
min: 1
} as NumberVar,
height: {
type: 'Number',
default: 100,
step: 1,
min: 1,
} as NumberVar,
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',
]
} as StringVar
}
const outputs = {
image: {
type: 'Image'
} as ImageVar
}
export default class Compose extends Canvas2d<typeof inputs, typeof outputs> {
constructor(options={}) {
super('Compose', inputs, outputs, options);
}
get width() {
return this.__in.width.value;
}
get height() {
return this.__in.height.value;
}
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,
};
}
}
/**
* @param {any} values
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} ctx
*/
async render({width, height, mode}, canvas, ctx) {
if (!width || !height) {
return;
}
if (!this.bg && !this.fg) {
return;
}
canvas.width = width;
canvas.height = height;
ctx.globalCompositeOperation = 'source-over';
if (this.bg) {
paintToCanvas(canvas, this.bg.image, this.bg);
}
ctx.globalCompositeOperation = mode;
if (this.fg) {
paintToCanvas(canvas, this.fg.image, this.fg);
}
return canvas;
}
}

View File

@@ -1,30 +1,35 @@
import Canvas2d from './Canvas2d';
import {createCanvas, mediaSize, paintToCanvas} from '../canvas';
import {
NumberVar
} from '../io/AbstractIOSet';
export default class EmptySpace extends Canvas2d {
const inputs = {};
const outputs = {
w: {
type: 'Number',
} as NumberVar,
h: {
type: 'Number',
} as NumberVar,
top: {
type: 'Number',
} as NumberVar,
right: {
type: 'Number',
} as NumberVar,
bottom: {
type: 'Number',
} as NumberVar,
left: {
type: 'Number',
} as NumberVar
};
export default class EmptySpace extends Canvas2d<typeof inputs, typeof outputs> {
constructor() {
super('EmptySpace', {
}, {
w: {
type: 'Number',
},
h: {
type: 'Number',
},
top: {
type: 'Number',
},
right: {
type: 'Number',
},
bottom: {
type: 'Number',
},
left: {
type: 'Number',
}
});
super('EmptySpace', inputs, outputs, {});
this._update();
}
@@ -42,22 +47,23 @@ export default class EmptySpace extends Canvas2d {
canvas.height = height;
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, width, height)
for (let offset=0; offset<imageData.data.length; offset+=4) {
const x = (offset/4) % width;
const y = Math.floor((offset/4) / width)
const [r,g,b,a] = imageData.data.slice(offset, offset+4);
if (a < 1) {
let x = 0, y = 0, a, offset;
let length = imageData.data.length/4;
for (offset=0; offset<length; offset+=1) {
x = (offset) % width;
y = Math.floor(offset / width)
a = imageData.data[offset * 4 + 3];
if (a < 254) {
top = Math.min(top, y);
right = Math.max(right, x);
bottom = Math.max(bottom, y);
left = Math.min(left, x);
}
imageData.data[offset] = a;
imageData.data[offset + 1] = a;
imageData.data[offset + 2] = a;
imageData.data[offset + 3] = a;
if (x === left && y == bottom && x < right) {
offset += (right - left) -1;
}
}
ctx.putImageData(imageData, 0, 0);
this.out.top.value = top;
this.out.right.value = right;
this.out.bottom.value = bottom;

View File

@@ -1,33 +0,0 @@
import Canvas2d from './Canvas2d';
export default class Fill extends Canvas2d {
constructor() {
super('Fill', {
image: null,
width: {
type: 'Number',
default: 100,
min: 1,
},
height: {
type: 'Number',
default: 100,
min: 1,
},
color: {
type: 'Color',
default: '#FFFFFF'
}
}, {});
this._update();
}
async render({color, width, height}, canvas, ctx) {
canvas.width = width;
canvas.height = height;
ctx.fillStyle = color;
ctx.fillRect(0, 0, width, height);
return canvas;
}
}

View File

@@ -0,0 +1,40 @@
import Canvas2d from './Canvas2d';
import {
NumberVar,
ColorVar
} from '../io/AbstractIOSet';
const inputs = {
image: null,
width: {
type: 'Number',
default: 100,
min: 1,
} as NumberVar,
height: {
type: 'Number',
default: 100,
min: 1,
} as NumberVar,
color: {
type: 'Color',
default: '#FFFFFF'
} as ColorVar
};
const outputs = {};
export default class Fill extends Canvas2d<typeof inputs, typeof outputs> {
constructor() {
super('Fill', inputs, outputs);
this._update();
}
async render({color, width, height}, canvas, ctx) {
canvas.width = width;
canvas.height = height;
ctx.fillStyle = color;
ctx.fillRect(0, 0, width, height);
return canvas;
}
}

View File

@@ -1,19 +1,26 @@
import Canvas2d from './Canvas2d';
import {createCanvas, mediaSize, paintToCanvas} from '../canvas';
import {
BooleanVar
} from '../io/AbstractIOSet';
export default class Flip extends Canvas2d {
const inputs = {
horizontal: {
type: 'Boolean',
default: false,
} as BooleanVar,
vertical: {
type: 'Boolean',
default: false,
} as BooleanVar,
}
const outputs = {};
export default class Flip extends Canvas2d<typeof inputs, typeof outputs> {
constructor() {
super('Flip', {
horizontal: {
type: 'Boolean',
default: false,
},
vertical: {
type: 'Boolean',
default: false,
},
}, {});
super('Flip', inputs, outputs, {});
this._update();
}

View File

@@ -1,22 +1,33 @@
import Canvas2d from './Canvas2d';
import {waitForMedia} from '../../utils';
import {createCanvas, mediaSize, paintToCanvas} from '../canvas';
import {
ImageVar,
StringVar,
NumberVar
} from '../io/AbstractIOSet';
export default class Resize extends Canvas2d {
const inputs = {
width: {
type: 'Number',
default: 100,
min: 1,
} as NumberVar,
height: {
type: 'Number',
default: 100,
min: 1,
} as NumberVar,
};
const outputs = {};
export default class Resize extends Canvas2d<typeof inputs, typeof outputs> {
constructor() {
super('Resize', {
width: {
type: 'Number',
default: 100,
min: 1,
},
height: {
type: 'Number',
default: 100,
min: 1,
},
}, {});
super('Resize', inputs, outputs, {});
}
/**

View File

@@ -1,91 +0,0 @@
import Canvas2d from './Canvas2d';
import {waitForMedia} from '../../utils';
import {createCanvas, mediaSize, paintToCanvas} from '../canvas';
const isEmpty = (x) => !x || Object.keys(x).length === 0;
export default class Resize extends Canvas2d {
constructor() {
super('Text', {
image: null,
text: {
type: 'String',
default: '',
},
fontSize: {
type: 'Number',
min: 1,
default: 50,
},
fontStyle: {
type: 'String',
default: 'normal',
enum: [
'normal',
'bold',
'italic',
'bold italic',
]
},
font: {
type: 'Font', // {name, url, fontface}
default: 'Arial',
},
width: {
type: 'Number',
default: 100,
min: 1,
},
height: {
type: 'Number',
default: 100,
min: 1,
},
color: {
type: 'Color',
default: '#FFFFFF'
},
textAlign: {
type: 'String',
default: 'center',
enum: [
'left',
'center',
'right',
]
}
}, {});
this._update();
}
/**
*
* @param {{name: string, url: string, fontface: FontFace, type: 'font'}} font
*/
async loadFont(font) {
if (typeof font === 'string') {
return font;
} else if (!font) {
return;
} else {
return font.name;
}
}
async render({font, fontSize, text, color, width, height, textAlign, fontStyle}, canvas, ctx) {
canvas.width = width;
canvas.height = height;
ctx.font = `${fontStyle} ${fontSize}px "${(await this.loadFont(font)) || ''}"`;
// ctx.font = `${fontStyle} ${fontSize}px "Arial"`;
ctx.fillStyle = color;
ctx.textAlign = textAlign;
const x = textAlign === 'center' ? canvas.width /2 :
textAlign === 'left' ? 0 : canvas.width;
text.split('\\n').forEach((line, index) => {
ctx.fillText(line, x, fontSize * (1 + index));
});
return canvas;
}
}

101
src/nodes/Canvas2d/Text.ts Normal file
View File

@@ -0,0 +1,101 @@
import Canvas2d from './Canvas2d';
import {waitForMedia} from '../../utils';
import {createCanvas, mediaSize, paintToCanvas} from '../canvas';
import {
NumberVar,
StringVar,
FontVar,
ColorVar,
} from '../io/AbstractIOSet';
const isEmpty = (x) => !x || Object.keys(x).length === 0;
const inputs = {
image: null,
text: {
type: 'String',
default: '',
} as StringVar,
fontSize: {
type: 'Number',
min: 1,
default: 50,
} as NumberVar,
fontStyle: {
type: 'String',
default: 'normal',
enum: [
'normal',
'bold',
'italic',
'bold italic',
]
} as StringVar,
font: {
type: 'Font', // {name, url, fontface}
default: 'Arial',
} as FontVar,
width: {
type: 'Number',
default: 100,
min: 1,
} as NumberVar,
height: {
type: 'Number',
default: 100,
min: 1,
} as NumberVar,
color: {
type: 'Color',
default: '#FFFFFF'
} as ColorVar,
textAlign: {
type: 'String',
default: 'center',
enum: [
'left',
'center',
'right',
]
} as StringVar
};
const outputs = {};
export default class Resize extends Canvas2d<typeof inputs, typeof outputs> {
constructor() {
super('Text', inputs, outputs, {});
this._update();
}
/**
*
* @param {{name: string, url: string, fontface: FontFace, type: 'font'}} font
*/
async loadFont(font) {
if (typeof font === 'string') {
return font;
} else if (!font) {
return;
} else {
return font.name;
}
}
async render({font, fontSize, text, color, width, height, textAlign, fontStyle}, canvas, ctx) {
canvas.width = width;
canvas.height = height;
ctx.font = `${fontStyle} ${fontSize}px "${(await this.loadFont(font)) || ''}"`;
// ctx.font = `${fontStyle} ${fontSize}px "Arial"`;
ctx.fillStyle = color;
ctx.textAlign = textAlign;
const x = textAlign === 'center' ? canvas.width /2 :
textAlign === 'left' ? 0 : canvas.width;
text.split('\\n').forEach((line, index) => {
ctx.fillText(line, x, fontSize * (1 + index));
});
return canvas;
}
}

View File

@@ -1,26 +1,32 @@
import Node from './Node';
import {ImageVar, BooleanVar, NumberVar} from './io/AbstractIOSet';
export default class Disable extends Node {
const inputs = {
image: {
type: 'Image',
} as ImageVar,
disabled: {
type: 'Boolean',
default: false,
} as BooleanVar,
};
const outputs = {
image: {
type: 'Image',
} as ImageVar,
width: {
type: 'Number',
} as NumberVar,
height: {
type: 'Number',
} as NumberVar,
};
export default class Disable extends Node<typeof inputs, typeof outputs> {
constructor() {
super('Disable', {
image: {
type: 'Image',
},
disabled: {
type: 'Boolean',
default: false,
},
}, {
image: {
type: 'Image',
},
width: {
type: 'Number',
},
height: {
type: 'Number',
},
}, {});
super('Disable', inputs, outputs, {});
}
async _update() {

View File

@@ -1,42 +0,0 @@
import Node from './Node';
export default class Font extends Node {
constructor() {
super('Font', {
fontSize: {
type: 'Number',
min: 1,
default: 50,
},
fontStyle: {
type: 'String',
default: 'normal',
enum: [
'normal',
'bold',
'italic',
'bold italic',
]
},
font: {
type: 'Font',
},
}, {
fontSize: {
type: 'Number',
},
fontStyle: {
type: 'String'
},
font: {
type: 'Font',
},
}, {});
}
async _update() {
this.out.font.value = this.in.font.value;
this.out.fontStyle.value = this.in.fontStyle.value;
}
}

47
src/nodes/Font.ts Normal file
View File

@@ -0,0 +1,47 @@
import Node from './Node';
import {FontVar, StringVar, NumberVar} from './io/AbstractIOSet';
const inputs = {
fontSize: {
type: 'Number',
min: 1,
default: 50,
} as NumberVar,
fontStyle: {
type: 'String',
default: 'normal',
enum: [
'normal',
'bold',
'italic',
'bold italic',
]
} as StringVar,
font: {
type: 'Font',
} as FontVar,
}
const outputs = {
fontSize: {
type: 'Number',
} as NumberVar,
fontStyle: {
type: 'String'
} as StringVar,
font: {
type: 'Font',
} as FontVar,
}
export default class Font extends Node<typeof inputs, typeof outputs> {
constructor() {
super('Font', inputs, outputs, {});
}
async _update() {
this.out.font.value = this.in.font.value;
this.out.fontStyle.value = this.in.fontStyle.value;
}
}

View File

@@ -1,56 +0,0 @@
import Node from '../Node';
export default class NumberBinaryOperation extends Node {
constructor() {
super('NumberBinaryOperation', {
x: {
type: 'Number',
default: 0,
},
y: {
type: 'Number',
default: 0,
},
operation: {
type: 'String',
default: '+',
enum: [
'+',
'-',
'⨉',
'÷',
'mod',
'max',
'min',
]
}
}, {
result: {
type: 'Number'
},
}, {});
this.__canvas = document.createElement('canvas');
}
async _update() {
const x = this.in.x.value;
const y = this.in.y.value;
const op = this.in.operation.value;
const value =
op === '+' ? x + y :
op === '-' ? x - y :
op === '⨉' ? x * y :
op === '÷' ?
y === 0 ? NaN :
x / y :
op === 'mod' ? x % y :
op === 'max' ? Math.max(x, y) :
op === 'min' ? Math.min(x, y) :
NaN;
if (!isNaN(value)) {
this.out.result.value = value;
}
}
}

View File

@@ -0,0 +1,63 @@
import Node from '../Node';
import {
NumberVar,
StringVar,
} from '../io/AbstractIOSet';
const inputs = {
x: {
type: 'Number',
default: 0,
} as NumberVar,
y: {
type: 'Number',
default: 0,
} as NumberVar,
operation: {
type: 'String',
default: '+',
enum: [
'+',
'-',
'⨉',
'÷',
'mod',
'max',
'min',
]
} as StringVar
};
const outputs = {
result: {
type: 'Number'
} as NumberVar,
}
export default class NumberBinaryOperation extends Node<typeof inputs, typeof outputs> {
constructor() {
super('NumberBinaryOperation', inputs, outputs, {});
}
async _update() {
const x = this.in.x.value;
const y = this.in.y.value;
const op = this.in.operation.value;
const value =
op === '+' ? x + y :
op === '-' ? x - y :
op === '⨉' ? x * y :
op === '÷' ?
y === 0 ? NaN :
x / y :
op === 'mod' ? x % y :
op === 'max' ? Math.max(x, y) :
op === 'min' ? Math.min(x, y) :
NaN;
if (!isNaN(value)) {
this.out.result.value = value;
}
}
}

View File

@@ -1,41 +0,0 @@
import Node from '../Node';
export default class Numbers extends Node {
constructor() {
super('Numbers', {
x: {
type: 'Number',
default: 0,
},
y: {
type: 'Number',
default: 0,
},
z: {
type: 'Number',
default: 0,
},
}, {
x: {
type: 'Number',
default: 0,
},
y: {
type: 'Number',
default: 0,
},
z: {
type: 'Number',
default: 0,
},
}, {});
this.__canvas = document.createElement('canvas');
}
async _update() {
this.out.x.value = this.in.x.value;
this.out.y.value = this.in.y.value;
this.out.z.value = this.in.z.value;
}
}

49
src/nodes/Math/Numbers.ts Normal file
View File

@@ -0,0 +1,49 @@
import Node from '../Node';
import {
NumberVar
} from '../io/AbstractIOSet';
const inputs = {
x: {
type: 'Number',
default: 0,
} as NumberVar,
y: {
type: 'Number',
default: 0,
} as NumberVar,
z: {
type: 'Number',
default: 0,
} as NumberVar,
};
const outputs = {
x: {
type: 'Number',
default: 0,
} as NumberVar,
y: {
type: 'Number',
default: 0,
} as NumberVar,
z: {
type: 'Number',
default: 0,
} as NumberVar,
}
export default class Numbers extends Node<typeof inputs, typeof outputs> {
constructor() {
super('Numbers', inputs, outputs, {});
}
async _update() {
this.out.x.value = this.in.x.value;
this.out.y.value = this.in.y.value;
this.out.z.value = this.in.z.value;
}
}

View File

@@ -1,15 +1,27 @@
import {Inputs, Outputs} from './io';
import {Inputs, Outputs} from './io/index';
import uuidv4 from 'uuid/v4';
import {Variables, InputProperties, OutputProperties} from './io/AbstractIOSet';
import {MultiSubject} from '../helpers/listener';
export default class Node {
export default class Node<I extends Variables, O extends Variables> {
constructor(name, inputDefinition, outputDefiniton, options) {
name: string
uid: string
id: string
__in: Inputs<I> & InputProperties<I>
__out: Outputs<O> & OutputProperties<O>
__scheduledUpdate: boolean
__currentUpdate: Promise<any>
__running: boolean
_stopped: boolean
subject: MultiSubject
constructor(name: string, inputDefinition: I, outputDefiniton: O, options) {
this.name = name;
this.uid = uuidv4();
this.id = uuidv4();
this.__in = new Inputs(inputDefinition, this);
this.__out = new Outputs(outputDefiniton, this);
this.__in = new Inputs(inputDefinition, this) as Inputs<I> & InputProperties<I>;
this.__out = new Outputs(outputDefiniton, this) as Outputs<O> & OutputProperties<O>;
this.__in.update = (name) => this.__update([name]);
this.__scheduledUpdate = false;
this.__currentUpdate = Promise.resolve();

View File

@@ -1,19 +1,24 @@
import WebGL from './WebGl';
import {
NumberVar,
} from '../io/AbstractIOSet';
const inputs = {
brightness: {
type: 'Number',
default: 1,
} as NumberVar,
contrast: {
type: 'Number',
default: 1,
} as NumberVar,
};
export default class BrightnessContrast extends WebGL {
export default class BrightnessContrast extends WebGL<typeof inputs> {
constructor() {
super('BrightnessContrast', {
brightness: {
type: 'Number',
default: 1,
},
contrast: {
type: 'Number',
default: 1,
},
})
super('BrightnessContrast', inputs)
}
get frag() {

View File

@@ -1,39 +1,43 @@
import WebGL from './WebGl';
import {
NumberVar
} from '../io/AbstractIOSet';
const inputs = {
red: {
type: 'Number',
default: 1,
min: 0,
max: 1,
step: 0.1,
} as NumberVar,
green: {
type: 'Number',
default: 1,
min: 0,
max: 1,
step: 0.1,
} as NumberVar,
blue: {
type: 'Number',
default: 1,
min: 0,
max: 1,
step: 0.1,
} as NumberVar,
amount: {
type: 'Number',
default: 0,
min: 0,
max: 1,
step: 0.1,
} as NumberVar
};
export default class Channels extends WebGL {
export default class Channels extends WebGL<typeof inputs> {
constructor() {
super('Channels', {
red: {
type: 'Number',
default: 1,
min: 0,
max: 1,
step: 0.1,
},
green: {
type: 'Number',
default: 1,
min: 0,
max: 1,
step: 0.1,
},
blue: {
type: 'Number',
default: 1,
min: 0,
max: 1,
step: 0.1,
},
amount: {
type: 'Number',
default: 0,
min: 0,
max: 1,
step: 0.1,
}
})
super('Channels', inputs)
}
get frag() {

View File

@@ -1,4 +1,8 @@
import WebGL from './WebGl';
import {
NumberVar,
ColorVar
} from '../io/AbstractIOSet';
const hexColorTOvec3 = (val) => {
const match = val.match(/#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/);
@@ -9,35 +13,37 @@ const hexColorTOvec3 = (val) => {
}
};
export default class GreenScreen extends WebGL {
const inputs = {
balance: {
type: 'Number',
default: 0.5,
step: 0.1,
} as NumberVar,
screen: {
type: 'Color',
default: '#2CD6A4',
} as ColorVar,
screenWeight: {
type: 'Number',
default: 1,
min: 0,
max: 1,
step: 0.1,
} as NumberVar,
clipBlack: {
type: 'Number',
default: 0,
} as NumberVar,
clipWhite: {
type: 'Number',
default: 1,
} as NumberVar,
};
export default class GreenScreen extends WebGL<typeof inputs> {
constructor() {
super('GreenScreen', {
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,
},
})
super('GreenScreen', inputs)
}
get frag() {

View File

@@ -1,23 +1,28 @@
import WebGL from './WebGl';
import {
NumberVar
} from '../io/AbstractIOSet';
const inputs = {
hue: {
type: 'Number',
default: 0,
} as NumberVar,
saturation: {
type: 'Number',
default: 1,
} as NumberVar,
value: {
type: 'Number',
default: 1,
} as NumberVar
};
export default class HSV extends WebGL {
export default class HSV extends WebGL<typeof inputs> {
constructor() {
super('HSV', {
hue: {
type: 'Number',
default: 0,
},
saturation: {
type: 'Number',
default: 1,
},
value: {
type: 'Number',
default: 1,
}
})
super('HSV', inputs)
}
get frag() {

View File

@@ -1,15 +1,19 @@
import WebGL from './WebGl';
import {
NumberVar
} from '../io/AbstractIOSet';
const inputs = {
amount: {
type: 'Number',
default: 0,
} as NumberVar,
};
export default class Sepia extends WebGL {
export default class Sepia extends WebGL<typeof inputs> {
constructor() {
super('Sepia', {
amount: {
type: 'Number',
default: 0,
},
})
super('Sepia', inputs)
}
get frag() {

View File

@@ -1,25 +1,43 @@
import Node from '../Node';
import {createCanvas, mediaSize, paintToCanvas} from '../canvas';
import {merge} from '../../utils';
import {canvasPool2D} from '../../canvas/CanvasPool';
import {
Variables,
ImageVar,
NumberVar
} from '../io/AbstractIOSet';
export default class WebGL extends Node {
const inputs = {
image: {
type: 'Image',
} as ImageVar,
};
constructor(name, inputs={}) {
super(name, Object.assign({
image: {
type: 'Image',
},
}, inputs), {
image: {
type: 'Image',
},
width: {
type: 'Number',
},
height: {
type: 'Number',
}
});
const outputs = {
image: {
type: 'Image',
} as ImageVar,
width: {
type: 'Number',
} as NumberVar,
height: {
type: 'Number',
} as NumberVar
}
export default class WebGL<I extends Variables> extends Node<I & (typeof inputs), (typeof outputs)> {
canvas: HTMLCanvasElement
program: WebGLProgram
constructor(name, inputDefinition: I) {
super(
name,
merge(inputs, inputDefinition),
outputs,
{}
)
}
destroy() {
@@ -75,7 +93,7 @@ export default class WebGL extends Node {
`
}
fragShader(gl) {
fragShader(gl: WebGLRenderingContext) {
var shader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shader, this.frag);
gl.compileShader(shader);
@@ -87,7 +105,7 @@ export default class WebGL extends Node {
return shader;
}
vertShader(gl) {
vertShader(gl: WebGLRenderingContext) {
var shader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shader, this.vert);
gl.compileShader(shader);
@@ -99,11 +117,7 @@ export default class WebGL extends Node {
return shader;
}
/**
*
* @param {WebGLRenderingContext} gl
*/
compileProgram(gl) {
compileProgram(gl: WebGLRenderingContext) {
const program = gl.createProgram();
gl.attachShader(program, this.vertShader(gl));
@@ -122,7 +136,7 @@ export default class WebGL extends Node {
return this.in.image.value;
}
createTexture(gl) {
createTexture(gl: WebGLRenderingContext) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
@@ -136,7 +150,7 @@ export default class WebGL extends Node {
return texture;
}
_createFrameBuffer(gl, {width, height}) {
_createFrameBuffer(gl: WebGLRenderingContext, {width, height}) {
const texture = this.createTexture(gl);
const frameBuffer = gl.createFramebuffer();
@@ -152,7 +166,7 @@ export default class WebGL extends Node {
_passes() {
return [
(gl, program, image) => {
(gl: WebGLRenderingContext, program: WebGLProgram, image) => {
this._setParams(gl, program);
},
]

View File

@@ -1,10 +1,4 @@
/**
*
* @param {Number} width
* @param {Number} height
* @returns {HTMLCanvasElement}
*/
export const createCanvas = (width, height) => {
export const createCanvas = (width: number, height: number):HTMLCanvasElement => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
@@ -12,19 +6,11 @@ export const createCanvas = (width, height) => {
return canvas;
}
/**
* @typedef {HTMLImageElement|HTMLVideoElement} PaintableElement
* @typedef {{top: Number, left: Number, width: Number, height: Number}} Bounds
* @typedef {function(HTMLCanvasElement, PaintableElement, Bounds): HTMLCanvasElement} PaintToCanvasFunction
*/
type Canvas = HTMLCanvasElement | OffscreenCanvas;
export type Bounds = {top: number, left: number, width: number, height: number};
/**
*
* @param {PaintableElement|{width: Number, height: Number}} media
* @returns {{width: Number, height: Number}}
*/
export const mediaSize = (media) =>
export const mediaSize = (media: HTMLImageElement|HTMLVideoElement|{width: number, height: number}) =>
(media === null) ? {width: null, height: null} :
(media instanceof HTMLVideoElement) ? {width: media.videoWidth, height: media.videoHeight} :
(media instanceof HTMLImageElement) ? {width: media.naturalWidth, height: media.naturalHeight} :
@@ -35,7 +21,11 @@ export const mediaSize = (media) =>
* @param {PaintableElement} media
* @param {Bounds} param2
*/
export const paintToCanvas = (canvas, media, {top, left, width, height}) => {
export const paintToCanvas = (
canvas: Canvas,
media: CanvasImageSource,
{top, left, width, height}: Bounds
) => {
const ctx = canvas.getContext('2d');
if (canvas.width > 0 && canvas.height > 0 && width > 0 && height > 0) {
ctx.drawImage(media, left, top, width, height);

View File

@@ -13,7 +13,7 @@ export {default as HSV} from './WebGL/HSV';
export {default as Sepia} from './WebGL/Sepia';
export {default as GreenScreen} from './WebGL/Greenscreen';
export {default as Channels} from './WebGL/Channels';
export {default as Blur} from './WebGL/Blur';
// export {default as Blur} from './WebGL/Blur';
// Math
export {default as Numbers} from './Math/Numbers';
export {default as NumberBinaryOperation} from './Math/NumberBinaryOperation';

View File

@@ -1,3 +1,8 @@
import {
default as AbstractIOSet,
Variable,
VariableValueType,
} from './AbstractIOSet';
import {isNil} from '../../utils';
export const defaultConstrainSatisfied = (value) => {
@@ -32,9 +37,16 @@ export const valueConstrainsSatisfied = (definition, value) => {
)
};
export default class AbstractIO {
export default class AbstractIO<V extends Variable> {
constructor(name, definition, owner) {
__name: string
__owner: AbstractIOSet<any>
__definition: V
__value: VariableValueType<V>
label: string
__listeners: Function[]
constructor(name, definition: V, owner: AbstractIOSet<any>) {
this.__name = name;
this.__owner = owner
this.__definition = definition;
@@ -64,18 +76,18 @@ export default class AbstractIO {
}
get value() {
return this.__getValue;
return this.__getValue();
}
set value(val) {
this.__setValue(val);
}
__getValue() {
return (valueConstrainsSatisfied(this.__definition, this.__value)) ? this.__value : this.__defaultValue;
__getValue(): VariableValueType<V> {
return (valueConstrainsSatisfied(this.__definition, this.__value)) ? this.__value : this.__defaultValue as VariableValueType<V>;
}
__setValue(value) {
__setValue(value: VariableValueType<V>) {
if (valueConstrainsSatisfied(this.__definition, value)) {
this.__value = value;
}

View File

@@ -1,38 +0,0 @@
export default class AbstractIOSet {
constructor(variables, owner) {
this.__values = {};
this.__owner = owner;
this.variables = variables;
for (let name of Object.keys(this.variables)) {
if (variables[name] === null) {
delete this.variables[name];
continue;
}
this.__values[name] = this.__createProperty(name, variables[name]);
Object.defineProperty(this, name, {
get() {
return this.__values[name];
},
})
}
}
*[Symbol.iterator]() {
for (let name of Object.keys(this.variables)) {
yield this.__values[name];
}
}
__createProperty(name, definition) {
}
update(changed) {
}
}

View File

@@ -0,0 +1,122 @@
import Node from '../Node';
import Input from './Input';
import Output from './Output';
import AbstractIO from './AbstractIO';
type ImageType = HTMLCanvasElement
| OffscreenCanvas
| HTMLImageElement
| HTMLVideoElement
| ImageData
| ImageBitmap;
export interface NumberVar {
type: 'Number',
min?: number
max?: number
step?: number
default?: VariableValueType<NumberVar>
}
export interface StringVar {
type: 'String',
default?: VariableValueType<StringVar>
enum?: string[]
}
export interface BooleanVar {
type: 'Boolean',
default?: VariableValueType<BooleanVar>
}
export interface ColorVar {
type: 'Color',
default?: VariableValueType<ColorVar>
}
export interface FontVar {
type: 'Font',
default?: VariableValueType<FontVar>
}
export interface ImageVar {
type: 'Image'
default?: VariableValueType<ImageVar>
}
export type Variable = NumberVar
| StringVar
| BooleanVar
| ColorVar
| FontVar
| ImageVar
export interface Variables {
[key: string]: Variable
}
export type VariablesType<V extends Variables> = {
[K in keyof V]: V[K] extends Variable ? V[K] : never
};
export type VariableValueType<T> = T extends NumberVar ? number :
T extends BooleanVar ? boolean :
T extends ImageVar ? ImageType :
string
export type IOProperties<V extends Variables> = {
[K in keyof V]: AbstractIO<V[K]>
}
export type InputProperties<V extends Variables> = {
[K in keyof V]: Input<V[K]>
}
export type OutputProperties<V extends Variables> = {
[K in keyof V]: Output<V[K]>
}
export default class AbstractIOSet<V extends Variables> {
__values: any
__owner: Node<any, any>
variables: V
constructor(variables: V, owner: Node<any, any>) {
this.__values = {};
this.__owner = owner;
this.variables = variables;
for (let name of Object.keys(this.variables)) {
if (variables[name] === null) {
delete this.variables[name];
continue;
}
this.__values[name] = this.__createProperty(name, variables[name]);
Object.defineProperty(this, name, {
get() {
return this.__values[name];
},
})
}
}
get id() {
return `${this.__owner.id}-set`;
}
*[Symbol.iterator]() {
for (let name of Object.keys(this.variables)) {
yield this.__values[name];
}
}
__createProperty(name, definition) {
}
update(changed) {
}
}

View File

@@ -1,7 +1,15 @@
import AbstractIO from './AbstractIO';
import Output from './Output';
import {
Variable,
VariableValueType,
} from './AbstractIOSet';
import {serialize, deserialize} from './serializer';
export default class Input extends AbstractIO {
export default class Input<V extends Variable> extends AbstractIO<V> {
__output: Output<any>
__onchangelistener: Function
constructor(name, definition, owner) {
super(name, definition, owner);
@@ -13,11 +21,11 @@ export default class Input extends AbstractIO {
}
}
get value() {
get value(): VariableValueType<V> {
return this.__getValue();
}
set value(value) {
set value(value: VariableValueType<V>) {
this.__setValue(value);
this.__owner.update(this.name);
this.__notifyListeners();
@@ -27,7 +35,7 @@ export default class Input extends AbstractIO {
return this.__output;
}
connect(output) {
connect(output: Output<any>) {
if (output && output.type === this.type) {
this.disconnect();
this.__output = output
@@ -36,7 +44,7 @@ export default class Input extends AbstractIO {
}
}
disconnect(output) {
disconnect(output?) {
if (this.__output) {
this.__output.offchange(this.__onchangelistener);
this.__output = null;

View File

@@ -1,9 +1,10 @@
import Input from './Input';
import AbstractIOSet from './AbstractIOSet';
import Node from '../Node';
import {default as AbstractIOSet, Variables} from './AbstractIOSet';
export default class Inputs extends AbstractIOSet {
export default class Inputs<V extends Variables> extends AbstractIOSet<V> {
constructor(variables, owner) {
constructor(variables: V, owner: Node<any, any>) {
super(variables, owner);
}

View File

@@ -1,6 +1,9 @@
import AbstractIO from './AbstractIO';
import {
Variable,
} from './AbstractIOSet';
export default class Output extends AbstractIO {
export default class Output<V extends Variable> extends AbstractIO<V> {
constructor(name, definition, owner) {
super(name, definition, owner);

View File

@@ -1,8 +1,9 @@
import Node from '../Node';
import Output from './Output';
import AbstractIOSet from './AbstractIOSet';
import {default as AbstractIOSet, Variables} from './AbstractIOSet';
export default class Outputs extends AbstractIOSet {
constructor(variables, owner) {
export default class Outputs<V extends Variables> extends AbstractIOSet<V> {
constructor(variables: V, owner: Node<any, any>) {
super(variables, owner);
}

View File

@@ -1,13 +0,0 @@
export const isNil = (val) => val === null || val === undefined;
const waitForImage = (img) => new Promise((resolve) => img.addEventListener('load', resolve, {once: true}));
export const waitForMedia = async (media) => {
if (media instanceof Image && !media.complete) {
await waitForImage(media);
}
return media;
}
export const setImmediate = (fn) => Promise.resolve().then(fn);

16
src/utils.ts Normal file
View File

@@ -0,0 +1,16 @@
export const isNil = (val) => val === null || val === undefined;
const waitForImage = (img:HTMLImageElement): Promise<HTMLImageElement> => new Promise((resolve) => img.addEventListener('load', () => resolve(img), {once: true}));
export const waitForMedia = async <T>(media:T):Promise<T> => {
if (media instanceof Image && !media.complete) {
await waitForImage(media);
}
return media;
}
export const setImmediate = <T extends (...args: any) => any>(fn:T):Promise<ReturnType<T>> => Promise.resolve().then(fn);
export const merge = (...objects) => {
return objects.reduce((acc, nxt) => Object.assign(acc, nxt), {});
}