♻️ switching to typescript
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "graphfx",
|
||||
"version": "0.0.15",
|
||||
"version": "0.1.0",
|
||||
"description": "Graph image processing pipeline",
|
||||
"main": "src/index.js",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --quiet --progress --watch --mode development",
|
||||
"build": "webpack --mode production --quiet --progress",
|
||||
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
addNode(node) {
|
||||
this.nodes.push({node});
|
||||
this.__setupNodeRunningListener(node);
|
||||
}
|
||||
if (graph.__nodesActive > 0) {
|
||||
graph.subject.next('running');
|
||||
|
||||
__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;
|
||||
}
|
||||
if (this.__nodesActive > 0) {
|
||||
this.subject.next('running');
|
||||
} else {
|
||||
console.info('[Graphfx] graph stopped');
|
||||
graph.subject.next('stopped');
|
||||
this.subject.next('stopped');
|
||||
}
|
||||
});
|
||||
}
|
||||
graph.nodes = await reconnectNodes(graph.nodes);
|
||||
|
||||
__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);
|
||||
}
|
||||
@@ -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) {
|
||||
super(name,
|
||||
Object.assign({
|
||||
const inputs = {
|
||||
image: {
|
||||
type: 'Image',
|
||||
},
|
||||
}, inputDefinition),
|
||||
Object.assign({
|
||||
} as ImageVar,
|
||||
};
|
||||
|
||||
const outputs = {
|
||||
image: {
|
||||
type: 'Image',
|
||||
},
|
||||
} as ImageVar,
|
||||
width: {
|
||||
type: 'Number',
|
||||
},
|
||||
} as NumberVar,
|
||||
height: {
|
||||
type: 'Number',
|
||||
}
|
||||
}, outputDefiniton),
|
||||
} 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,
|
||||
merge(inputs, inputDefinition),
|
||||
merge(outputs, outputDefiniton),
|
||||
options
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
163
src/nodes/Canvas2d/Compose.ts
Normal file
163
src/nodes/Canvas2d/Compose.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
constructor() {
|
||||
super('EmptySpace', {
|
||||
}, {
|
||||
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', 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
40
src/nodes/Canvas2d/Fill.ts
Normal file
40
src/nodes/Canvas2d/Fill.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
constructor() {
|
||||
super('Flip', {
|
||||
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', inputs, outputs, {});
|
||||
this._update();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
constructor() {
|
||||
super('Resize', {
|
||||
|
||||
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', inputs, outputs, {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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
101
src/nodes/Canvas2d/Text.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,32 @@
|
||||
import Node from './Node';
|
||||
import {ImageVar, BooleanVar, NumberVar} from './io/AbstractIOSet';
|
||||
|
||||
export default class Disable extends Node {
|
||||
constructor() {
|
||||
super('Disable', {
|
||||
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', inputs, outputs, {});
|
||||
}
|
||||
|
||||
async _update() {
|
||||
@@ -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
47
src/nodes/Font.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
63
src/nodes/Math/NumberBinaryOperation.ts
Normal file
63
src/nodes/Math/NumberBinaryOperation.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
49
src/nodes/Math/Numbers.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
@@ -1,19 +1,24 @@
|
||||
import WebGL from './WebGl';
|
||||
import {
|
||||
NumberVar,
|
||||
} from '../io/AbstractIOSet';
|
||||
|
||||
|
||||
export default class BrightnessContrast extends WebGL {
|
||||
|
||||
constructor() {
|
||||
super('BrightnessContrast', {
|
||||
const inputs = {
|
||||
brightness: {
|
||||
type: 'Number',
|
||||
default: 1,
|
||||
},
|
||||
} as NumberVar,
|
||||
contrast: {
|
||||
type: 'Number',
|
||||
default: 1,
|
||||
},
|
||||
})
|
||||
} as NumberVar,
|
||||
};
|
||||
|
||||
|
||||
export default class BrightnessContrast extends WebGL<typeof inputs> {
|
||||
|
||||
constructor() {
|
||||
super('BrightnessContrast', inputs)
|
||||
}
|
||||
|
||||
get frag() {
|
||||
@@ -1,39 +1,43 @@
|
||||
import WebGL from './WebGl';
|
||||
import {
|
||||
NumberVar
|
||||
} from '../io/AbstractIOSet';
|
||||
|
||||
|
||||
export default class Channels extends WebGL {
|
||||
|
||||
constructor() {
|
||||
super('Channels', {
|
||||
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<typeof inputs> {
|
||||
|
||||
constructor() {
|
||||
super('Channels', inputs)
|
||||
}
|
||||
|
||||
get frag() {
|
||||
@@ -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 {
|
||||
|
||||
constructor() {
|
||||
super('GreenScreen', {
|
||||
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', inputs)
|
||||
}
|
||||
|
||||
get frag() {
|
||||
@@ -1,23 +1,28 @@
|
||||
import WebGL from './WebGl';
|
||||
import {
|
||||
NumberVar
|
||||
} from '../io/AbstractIOSet';
|
||||
|
||||
|
||||
export default class HSV extends WebGL {
|
||||
|
||||
constructor() {
|
||||
super('HSV', {
|
||||
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<typeof inputs> {
|
||||
|
||||
constructor() {
|
||||
super('HSV', inputs)
|
||||
}
|
||||
|
||||
get frag() {
|
||||
@@ -1,15 +1,19 @@
|
||||
import WebGL from './WebGl';
|
||||
import {
|
||||
NumberVar
|
||||
} from '../io/AbstractIOSet';
|
||||
|
||||
|
||||
export default class Sepia extends WebGL {
|
||||
|
||||
constructor() {
|
||||
super('Sepia', {
|
||||
const inputs = {
|
||||
amount: {
|
||||
type: 'Number',
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
} as NumberVar,
|
||||
};
|
||||
|
||||
export default class Sepia extends WebGL<typeof inputs> {
|
||||
|
||||
constructor() {
|
||||
super('Sepia', inputs)
|
||||
}
|
||||
|
||||
get frag() {
|
||||
@@ -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 {
|
||||
|
||||
constructor(name, inputs={}) {
|
||||
super(name, Object.assign({
|
||||
const inputs = {
|
||||
image: {
|
||||
type: 'Image',
|
||||
},
|
||||
}, inputs), {
|
||||
} as ImageVar,
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
]
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
122
src/nodes/io/AbstractIOSet.ts
Normal file
122
src/nodes/io/AbstractIOSet.ts
Normal 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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
13
src/utils.js
13
src/utils.js
@@ -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
16
src/utils.ts
Normal 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), {});
|
||||
}
|
||||
Reference in New Issue
Block a user