Files
ComfyUI_frontend/build/litegraph.js
2023-10-05 10:54:37 +02:00

34281 lines
1.0 MiB

//packer version
(function(global) {
// *************************************************************
// LiteGraph CLASS *******
// *************************************************************
/**
* The Global Scope. It contains all the registered node classes.
*
* @class LiteGraph
* @constructor
*/
var LiteGraph = (global.LiteGraph = {
VERSION: 0.4,
CANVAS_GRID_SIZE: 10,
NODE_TITLE_HEIGHT: 30,
NODE_TITLE_TEXT_Y: 20,
NODE_SLOT_HEIGHT: 20,
NODE_WIDGET_HEIGHT: 20,
NODE_WIDTH: 140,
NODE_MIN_WIDTH: 50,
NODE_COLLAPSED_RADIUS: 10,
NODE_COLLAPSED_WIDTH: 80,
NODE_TITLE_COLOR: "#999",
NODE_SELECTED_TITLE_COLOR: "#FFF",
NODE_TEXT_SIZE: 14,
NODE_TEXT_COLOR: "#AAA",
NODE_SUBTEXT_SIZE: 12,
NODE_DEFAULT_COLOR: "#333",
NODE_DEFAULT_BGCOLOR: "#353535",
NODE_DEFAULT_BOXCOLOR: "#666",
NODE_DEFAULT_SHAPE: "box",
NODE_BOX_OUTLINE_COLOR: "#FFF",
DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)",
DEFAULT_GROUP_FONT: 24,
WIDGET_BGCOLOR: "#222",
WIDGET_OUTLINE_COLOR: "#666",
WIDGET_TEXT_COLOR: "#DDD",
WIDGET_SECONDARY_TEXT_COLOR: "#999",
LINK_COLOR: "#9A9",
EVENT_LINK_COLOR: "#A86",
CONNECTING_LINK_COLOR: "#AFA",
MAX_NUMBER_OF_NODES: 1000, //avoid infinite loops
DEFAULT_POSITION: [100, 100], //default node position
VALID_SHAPES: ["default", "box", "round", "card"], //,"circle"
//shapes are used for nodes but also for slots
BOX_SHAPE: 1,
ROUND_SHAPE: 2,
CIRCLE_SHAPE: 3,
CARD_SHAPE: 4,
ARROW_SHAPE: 5,
GRID_SHAPE: 6, // intended for slot arrays
//enums
INPUT: 1,
OUTPUT: 2,
EVENT: -1, //for outputs
ACTION: -1, //for inputs
NODE_MODES: ["Always", "On Event", "Never", "On Trigger"], // helper, will add "On Request" and more in the future
NODE_MODES_COLORS:["#666","#422","#333","#224","#626"], // use with node_box_coloured_by_mode
ALWAYS: 0,
ON_EVENT: 1,
NEVER: 2,
ON_TRIGGER: 3,
UP: 1,
DOWN: 2,
LEFT: 3,
RIGHT: 4,
CENTER: 5,
LINK_RENDER_MODES: ["Straight", "Linear", "Spline"], // helper
STRAIGHT_LINK: 0,
LINEAR_LINK: 1,
SPLINE_LINK: 2,
NORMAL_TITLE: 0,
NO_TITLE: 1,
TRANSPARENT_TITLE: 2,
AUTOHIDE_TITLE: 3,
VERTICAL_LAYOUT: "vertical", // arrange nodes vertically
proxy: null, //used to redirect calls
node_images_path: "",
debug: false,
catch_exceptions: true,
throw_errors: true,
allow_scripts: false, //if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits
registered_node_types: {}, //nodetypes by string
node_types_by_file_extension: {}, //used for dropping files in the canvas
Nodes: {}, //node types by classname
Globals: {}, //used to store vars between graphs
searchbox_extras: {}, //used to add extra features to the search box
auto_sort_node_types: false, // [true!] If set to true, will automatically sort node types / categories in the context menus
node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback
node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback
dialog_close_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
dialog_close_on_mouse_leave_delay: 500,
shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys
click_do_break_link_to: false, // [false!]prefer false, way too easy to break links
search_hide_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
search_filter_enabled: false, // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out]
search_show_all_on_open: true, // [true!] opens the results list when opening the search widget
auto_load_slot_types: false, // [if want false, use true, run, get vars values to be statically set, than disable] nodes types and nodeclass association with node types need to be calculated, if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out]
// set these values if not using auto_load_slot_types
registered_slot_in_types: {}, // slot types for nodeclass
registered_slot_out_types: {}, // slot types for nodeclass
slot_types_in: [], // slot types IN
slot_types_out: [], // slot types OUT
slot_types_default_in: [], // specify for each IN slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
slot_types_default_out: [], // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node
do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this
allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentially, one by one
middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel)
release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults
pointerevents_method: "mouse", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now)
// TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary)
ctrl_shift_v_paste_connect_unselected_outputs: false, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes
// if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers.
// use this if you must have node IDs that are unique across all graphs and subgraphs.
use_uuids: false,
/**
* Register a node class so it can be listed when the user wants to create a new one
* @method registerNodeType
* @param {String} type name of the node and path
* @param {Class} base_class class containing the structure of a node
*/
registerNodeType: function(type, base_class) {
if (!base_class.prototype) {
throw "Cannot register a simple object, it must be a class with a prototype";
}
base_class.type = type;
if (LiteGraph.debug) {
console.log("Node registered: " + type);
}
const classname = base_class.name;
const pos = type.lastIndexOf("/");
base_class.category = type.substring(0, pos);
if (!base_class.title) {
base_class.title = classname;
}
//extend class
for (var i in LGraphNode.prototype) {
if (!base_class.prototype[i]) {
base_class.prototype[i] = LGraphNode.prototype[i];
}
}
const prev = this.registered_node_types[type];
if(prev) {
console.log("replacing node type: " + type);
}
if( !Object.prototype.hasOwnProperty.call( base_class.prototype, "shape") ) {
Object.defineProperty(base_class.prototype, "shape", {
set: function(v) {
switch (v) {
case "default":
delete this._shape;
break;
case "box":
this._shape = LiteGraph.BOX_SHAPE;
break;
case "round":
this._shape = LiteGraph.ROUND_SHAPE;
break;
case "circle":
this._shape = LiteGraph.CIRCLE_SHAPE;
break;
case "card":
this._shape = LiteGraph.CARD_SHAPE;
break;
default:
this._shape = v;
}
},
get: function() {
return this._shape;
},
enumerable: true,
configurable: true
});
//used to know which nodes to create when dragging files to the canvas
if (base_class.supported_extensions) {
for (let i in base_class.supported_extensions) {
const ext = base_class.supported_extensions[i];
if(ext && ext.constructor === String) {
this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class;
}
}
}
}
this.registered_node_types[type] = base_class;
if (base_class.constructor.name) {
this.Nodes[classname] = base_class;
}
if (LiteGraph.onNodeTypeRegistered) {
LiteGraph.onNodeTypeRegistered(type, base_class);
}
if (prev && LiteGraph.onNodeTypeReplaced) {
LiteGraph.onNodeTypeReplaced(type, base_class, prev);
}
//warnings
if (base_class.prototype.onPropertyChange) {
console.warn(
"LiteGraph node class " +
type +
" has onPropertyChange method, it must be called onPropertyChanged with d at the end"
);
}
// TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
if (this.auto_load_slot_types) {
new base_class(base_class.title || "tmpnode");
}
},
/**
* removes a node type from the system
* @method unregisterNodeType
* @param {String|Object} type name of the node or the node constructor itself
*/
unregisterNodeType: function(type) {
const base_class =
type.constructor === String
? this.registered_node_types[type]
: type;
if (!base_class) {
throw "node type not found: " + type;
}
delete this.registered_node_types[base_class.type];
if (base_class.constructor.name) {
delete this.Nodes[base_class.constructor.name];
}
},
/**
* Save a slot type and his node
* @method registerSlotType
* @param {String|Object} type name of the node or the node constructor itself
* @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, ..
*/
registerNodeAndSlotType: function(type, slot_type, out){
out = out || false;
const base_class =
type.constructor === String &&
this.registered_node_types[type] !== "anonymous"
? this.registered_node_types[type]
: type;
const class_type = base_class.constructor.type;
let allTypes = [];
if (typeof slot_type === "string") {
allTypes = slot_type.split(",");
} else if (slot_type == this.EVENT || slot_type == this.ACTION) {
allTypes = ["_event_"];
} else {
allTypes = ["*"];
}
for (let i = 0; i < allTypes.length; ++i) {
let slotType = allTypes[i];
if (slotType === "") {
slotType = "*";
}
const registerTo = out
? "registered_slot_out_types"
: "registered_slot_in_types";
if (this[registerTo][slotType] === undefined) {
this[registerTo][slotType] = { nodes: [] };
}
if (!this[registerTo][slotType].nodes.includes(class_type)) {
this[registerTo][slotType].nodes.push(class_type);
}
// check if is a new type
if (!out) {
if (!this.slot_types_in.includes(slotType.toLowerCase())) {
this.slot_types_in.push(slotType.toLowerCase());
this.slot_types_in.sort();
}
} else {
if (!this.slot_types_out.includes(slotType.toLowerCase())) {
this.slot_types_out.push(slotType.toLowerCase());
this.slot_types_out.sort();
}
}
}
},
/**
* Create a new nodetype by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function.
* Useful to wrap simple methods that do not require properties, and that only process some input to generate an output.
* @method wrapFunctionAsNode
* @param {String} name node name with namespace (p.e.: 'math/sum')
* @param {Function} func
* @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type
* @param {String} return_type [optional] string with the return type, otherwise it will be generic
* @param {Object} properties [optional] properties to be configurable
*/
wrapFunctionAsNode: function(
name,
func,
param_types,
return_type,
properties
) {
var params = Array(func.length);
var code = "";
var names = LiteGraph.getParameterNames(func);
for (var i = 0; i < names.length; ++i) {
code +=
"this.addInput('" +
names[i] +
"'," +
(param_types && param_types[i]
? "'" + param_types[i] + "'"
: "0") +
");\n";
}
code +=
"this.addOutput('out'," +
(return_type ? "'" + return_type + "'" : 0) +
");\n";
if (properties) {
code +=
"this.properties = " + JSON.stringify(properties) + ";\n";
}
var classobj = Function(code);
classobj.title = name.split("/").pop();
classobj.desc = "Generated from " + func.name;
classobj.prototype.onExecute = function onExecute() {
for (var i = 0; i < params.length; ++i) {
params[i] = this.getInputData(i);
}
var r = func.apply(this, params);
this.setOutputData(0, r);
};
this.registerNodeType(name, classobj);
},
/**
* Removes all previously registered node's types
*/
clearRegisteredTypes: function() {
this.registered_node_types = {};
this.node_types_by_file_extension = {};
this.Nodes = {};
this.searchbox_extras = {};
},
/**
* Adds this method to all nodetypes, existing and to be created
* (You can add it to LGraphNode.prototype but then existing node types wont have it)
* @method addNodeMethod
* @param {Function} func
*/
addNodeMethod: function(name, func) {
LGraphNode.prototype[name] = func;
for (var i in this.registered_node_types) {
var type = this.registered_node_types[i];
if (type.prototype[name]) {
type.prototype["_" + name] = type.prototype[name];
} //keep old in case of replacing
type.prototype[name] = func;
}
},
/**
* Create a node of a given type with a name. The node is not attached to any graph yet.
* @method createNode
* @param {String} type full name of the node class. p.e. "math/sin"
* @param {String} name a name to distinguish from other nodes
* @param {Object} options to set options
*/
createNode: function(type, title, options) {
var base_class = this.registered_node_types[type];
if (!base_class) {
if (LiteGraph.debug) {
console.log(
'GraphNode type "' + type + '" not registered.'
);
}
return null;
}
var prototype = base_class.prototype || base_class;
title = title || base_class.title || type;
var node = null;
if (LiteGraph.catch_exceptions) {
try {
node = new base_class(title);
} catch (err) {
console.error(err);
return null;
}
} else {
node = new base_class(title);
}
node.type = type;
if (!node.title && title) {
node.title = title;
}
if (!node.properties) {
node.properties = {};
}
if (!node.properties_info) {
node.properties_info = [];
}
if (!node.flags) {
node.flags = {};
}
if (!node.size) {
node.size = node.computeSize();
//call onresize?
}
if (!node.pos) {
node.pos = LiteGraph.DEFAULT_POSITION.concat();
}
if (!node.mode) {
node.mode = LiteGraph.ALWAYS;
}
//extra options
if (options) {
for (var i in options) {
node[i] = options[i];
}
}
// callback
if ( node.onNodeCreated ) {
node.onNodeCreated();
}
return node;
},
/**
* Returns a registered node type with a given name
* @method getNodeType
* @param {String} type full name of the node class. p.e. "math/sin"
* @return {Class} the node class
*/
getNodeType: function(type) {
return this.registered_node_types[type];
},
/**
* Returns a list of node types matching one category
* @method getNodeType
* @param {String} category category name
* @return {Array} array with all the node classes
*/
getNodeTypesInCategory: function(category, filter) {
var r = [];
for (var i in this.registered_node_types) {
var type = this.registered_node_types[i];
if (type.filter != filter) {
continue;
}
if (category == "") {
if (type.category == null) {
r.push(type);
}
} else if (type.category == category) {
r.push(type);
}
}
if (this.auto_sort_node_types) {
r.sort(function(a,b){return a.title.localeCompare(b.title)});
}
return r;
},
/**
* Returns a list with all the node type categories
* @method getNodeTypesCategories
* @param {String} filter only nodes with ctor.filter equal can be shown
* @return {Array} array with all the names of the categories
*/
getNodeTypesCategories: function( filter ) {
var categories = { "": 1 };
for (var i in this.registered_node_types) {
var type = this.registered_node_types[i];
if ( type.category && !type.skip_list )
{
if(type.filter != filter)
continue;
categories[type.category] = 1;
}
}
var result = [];
for (var i in categories) {
result.push(i);
}
return this.auto_sort_node_types ? result.sort() : result;
},
//debug purposes: reloads all the js scripts that matches a wildcard
reloadNodes: function(folder_wildcard) {
var tmp = document.getElementsByTagName("script");
//weird, this array changes by its own, so we use a copy
var script_files = [];
for (var i=0; i < tmp.length; i++) {
script_files.push(tmp[i]);
}
var docHeadObj = document.getElementsByTagName("head")[0];
folder_wildcard = document.location.href + folder_wildcard;
for (var i=0; i < script_files.length; i++) {
var src = script_files[i].src;
if (
!src ||
src.substr(0, folder_wildcard.length) != folder_wildcard
) {
continue;
}
try {
if (LiteGraph.debug) {
console.log("Reloading: " + src);
}
var dynamicScript = document.createElement("script");
dynamicScript.type = "text/javascript";
dynamicScript.src = src;
docHeadObj.appendChild(dynamicScript);
docHeadObj.removeChild(script_files[i]);
} catch (err) {
if (LiteGraph.throw_errors) {
throw err;
}
if (LiteGraph.debug) {
console.log("Error while reloading " + src);
}
}
}
if (LiteGraph.debug) {
console.log("Nodes reloaded");
}
},
//separated just to improve if it doesn't work
cloneObject: function(obj, target) {
if (obj == null) {
return null;
}
var r = JSON.parse(JSON.stringify(obj));
if (!target) {
return r;
}
for (var i in r) {
target[i] = r[i];
}
return target;
},
/*
* https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670
*/
uuidv4: function() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,a=>(a^Math.random()*16>>a/4).toString(16));
},
/**
* Returns if the types of two slots are compatible (taking into account wildcards, etc)
* @method isValidConnection
* @param {String} type_a
* @param {String} type_b
* @return {Boolean} true if they can be connected
*/
isValidConnection: function(type_a, type_b) {
if (type_a=="" || type_a==="*") type_a = 0;
if (type_b=="" || type_b==="*") type_b = 0;
if (
!type_a //generic output
|| !type_b // generic input
|| type_a == type_b //same type (is valid for triggers)
|| (type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION)
) {
return true;
}
// Enforce string type to handle toLowerCase call (-1 number not ok)
type_a = String(type_a);
type_b = String(type_b);
type_a = type_a.toLowerCase();
type_b = type_b.toLowerCase();
// For nodes supporting multiple connection types
if (type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1) {
return type_a == type_b;
}
// Check all permutations to see if one is valid
var supported_types_a = type_a.split(",");
var supported_types_b = type_b.split(",");
for (var i = 0; i < supported_types_a.length; ++i) {
for (var j = 0; j < supported_types_b.length; ++j) {
if(this.isValidConnection(supported_types_a[i],supported_types_b[j])){
//if (supported_types_a[i] == supported_types_b[j]) {
return true;
}
}
}
return false;
},
/**
* Register a string in the search box so when the user types it it will recommend this node
* @method registerSearchboxExtra
* @param {String} node_type the node recommended
* @param {String} description text to show next to it
* @param {Object} data it could contain info of how the node should be configured
* @return {Boolean} true if they can be connected
*/
registerSearchboxExtra: function(node_type, description, data) {
this.searchbox_extras[description.toLowerCase()] = {
type: node_type,
desc: description,
data: data
};
},
/**
* Wrapper to load files (from url using fetch or from file using FileReader)
* @method fetchFile
* @param {String|File|Blob} url the url of the file (or the file itself)
* @param {String} type an string to know how to fetch it: "text","arraybuffer","json","blob"
* @param {Function} on_complete callback(data)
* @param {Function} on_error in case of an error
* @return {FileReader|Promise} returns the object used to
*/
fetchFile: function( url, type, on_complete, on_error ) {
var that = this;
if(!url)
return null;
type = type || "text";
if( url.constructor === String )
{
if (url.substr(0, 4) == "http" && LiteGraph.proxy) {
url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3);
}
return fetch(url)
.then(function(response) {
if(!response.ok)
throw new Error("File not found"); //it will be catch below
if(type == "arraybuffer")
return response.arrayBuffer();
else if(type == "text" || type == "string")
return response.text();
else if(type == "json")
return response.json();
else if(type == "blob")
return response.blob();
})
.then(function(data) {
if(on_complete)
on_complete(data);
})
.catch(function(error) {
console.error("error fetching file:",url);
if(on_error)
on_error(error);
});
}
else if( url.constructor === File || url.constructor === Blob)
{
var reader = new FileReader();
reader.onload = function(e)
{
var v = e.target.result;
if( type == "json" )
v = JSON.parse(v);
if(on_complete)
on_complete(v);
}
if(type == "arraybuffer")
return reader.readAsArrayBuffer(url);
else if(type == "text" || type == "json")
return reader.readAsText(url);
else if(type == "blob")
return reader.readAsBinaryString(url);
}
return null;
}
});
//timer that works everywhere
if (typeof performance != "undefined") {
LiteGraph.getTime = performance.now.bind(performance);
} else if (typeof Date != "undefined" && Date.now) {
LiteGraph.getTime = Date.now.bind(Date);
} else if (typeof process != "undefined") {
LiteGraph.getTime = function() {
var t = process.hrtime();
return t[0] * 0.001 + t[1] * 1e-6;
};
} else {
LiteGraph.getTime = function getTime() {
return new Date().getTime();
};
}
//*********************************************************************************
// LGraph CLASS
//*********************************************************************************
/**
* LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop.
* supported callbacks:
+ onNodeAdded: when a new node is added to the graph
+ onNodeRemoved: when a node inside this graph is removed
+ onNodeConnectionChange: some connection has changed in the graph (connected or disconnected)
*
* @class LGraph
* @constructor
* @param {Object} o data from previous serialization [optional]
*/
function LGraph(o) {
if (LiteGraph.debug) {
console.log("Graph created");
}
this.list_of_graphcanvas = null;
this.clear();
if (o) {
this.configure(o);
}
}
global.LGraph = LiteGraph.LGraph = LGraph;
//default supported types
LGraph.supported_types = ["number", "string", "boolean"];
//used to know which types of connections support this graph (some graphs do not allow certain types)
LGraph.prototype.getSupportedTypes = function() {
return this.supported_types || LGraph.supported_types;
};
LGraph.STATUS_STOPPED = 1;
LGraph.STATUS_RUNNING = 2;
/**
* Removes all nodes from this graph
* @method clear
*/
LGraph.prototype.clear = function() {
this.stop();
this.status = LGraph.STATUS_STOPPED;
this.last_node_id = 0;
this.last_link_id = 0;
this._version = -1; //used to detect changes
//safe clear
if (this._nodes) {
for (var i = 0; i < this._nodes.length; ++i) {
var node = this._nodes[i];
if (node.onRemoved) {
node.onRemoved();
}
}
}
//nodes
this._nodes = [];
this._nodes_by_id = {};
this._nodes_in_order = []; //nodes sorted in execution order
this._nodes_executable = null; //nodes that contain onExecute sorted in execution order
//other scene stuff
this._groups = [];
//links
this.links = {}; //container with all the links
//iterations
this.iteration = 0;
//custom data
this.config = {};
this.vars = {};
this.extra = {}; //to store custom data
//timing
this.globaltime = 0;
this.runningtime = 0;
this.fixedtime = 0;
this.fixedtime_lapse = 0.01;
this.elapsed_time = 0.01;
this.last_update_time = 0;
this.starttime = 0;
this.catch_errors = true;
this.nodes_executing = [];
this.nodes_actioning = [];
this.nodes_executedAction = [];
//subgraph_data
this.inputs = {};
this.outputs = {};
//notify canvas to redraw
this.change();
this.sendActionToCanvas("clear");
};
/**
* Attach Canvas to this graph
* @method attachCanvas
* @param {GraphCanvas} graph_canvas
*/
LGraph.prototype.attachCanvas = function(graphcanvas) {
if (graphcanvas.constructor != LGraphCanvas) {
throw "attachCanvas expects a LGraphCanvas instance";
}
if (graphcanvas.graph && graphcanvas.graph != this) {
graphcanvas.graph.detachCanvas(graphcanvas);
}
graphcanvas.graph = this;
if (!this.list_of_graphcanvas) {
this.list_of_graphcanvas = [];
}
this.list_of_graphcanvas.push(graphcanvas);
};
/**
* Detach Canvas from this graph
* @method detachCanvas
* @param {GraphCanvas} graph_canvas
*/
LGraph.prototype.detachCanvas = function(graphcanvas) {
if (!this.list_of_graphcanvas) {
return;
}
var pos = this.list_of_graphcanvas.indexOf(graphcanvas);
if (pos == -1) {
return;
}
graphcanvas.graph = null;
this.list_of_graphcanvas.splice(pos, 1);
};
/**
* Starts running this graph every interval milliseconds.
* @method start
* @param {number} interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate
*/
LGraph.prototype.start = function(interval) {
if (this.status == LGraph.STATUS_RUNNING) {
return;
}
this.status = LGraph.STATUS_RUNNING;
if (this.onPlayEvent) {
this.onPlayEvent();
}
this.sendEventToAllNodes("onStart");
//launch
this.starttime = LiteGraph.getTime();
this.last_update_time = this.starttime;
interval = interval || 0;
var that = this;
//execute once per frame
if ( interval == 0 && typeof window != "undefined" && window.requestAnimationFrame ) {
function on_frame() {
if (that.execution_timer_id != -1) {
return;
}
window.requestAnimationFrame(on_frame);
if(that.onBeforeStep)
that.onBeforeStep();
that.runStep(1, !that.catch_errors);
if(that.onAfterStep)
that.onAfterStep();
}
this.execution_timer_id = -1;
on_frame();
} else { //execute every 'interval' ms
this.execution_timer_id = setInterval(function() {
//execute
if(that.onBeforeStep)
that.onBeforeStep();
that.runStep(1, !that.catch_errors);
if(that.onAfterStep)
that.onAfterStep();
}, interval);
}
};
/**
* Stops the execution loop of the graph
* @method stop execution
*/
LGraph.prototype.stop = function() {
if (this.status == LGraph.STATUS_STOPPED) {
return;
}
this.status = LGraph.STATUS_STOPPED;
if (this.onStopEvent) {
this.onStopEvent();
}
if (this.execution_timer_id != null) {
if (this.execution_timer_id != -1) {
clearInterval(this.execution_timer_id);
}
this.execution_timer_id = null;
}
this.sendEventToAllNodes("onStop");
};
/**
* Run N steps (cycles) of the graph
* @method runStep
* @param {number} num number of steps to run, default is 1
* @param {Boolean} do_not_catch_errors [optional] if you want to try/catch errors
* @param {number} limit max number of nodes to execute (used to execute from start to a node)
*/
LGraph.prototype.runStep = function(num, do_not_catch_errors, limit ) {
num = num || 1;
var start = LiteGraph.getTime();
this.globaltime = 0.001 * (start - this.starttime);
var nodes = this._nodes_executable
? this._nodes_executable
: this._nodes;
if (!nodes) {
return;
}
limit = limit || nodes.length;
if (do_not_catch_errors) {
//iterations
for (var i = 0; i < num; i++) {
for (var j = 0; j < limit; ++j) {
var node = nodes[j];
if (node.mode == LiteGraph.ALWAYS && node.onExecute) {
//wrap node.onExecute();
node.doExecute();
}
}
this.fixedtime += this.fixedtime_lapse;
if (this.onExecuteStep) {
this.onExecuteStep();
}
}
if (this.onAfterExecute) {
this.onAfterExecute();
}
} else {
try {
//iterations
for (var i = 0; i < num; i++) {
for (var j = 0; j < limit; ++j) {
var node = nodes[j];
if (node.mode == LiteGraph.ALWAYS && node.onExecute) {
node.onExecute();
}
}
this.fixedtime += this.fixedtime_lapse;
if (this.onExecuteStep) {
this.onExecuteStep();
}
}
if (this.onAfterExecute) {
this.onAfterExecute();
}
this.errors_in_execution = false;
} catch (err) {
this.errors_in_execution = true;
if (LiteGraph.throw_errors) {
throw err;
}
if (LiteGraph.debug) {
console.log("Error during execution: " + err);
}
this.stop();
}
}
var now = LiteGraph.getTime();
var elapsed = now - start;
if (elapsed == 0) {
elapsed = 1;
}
this.execution_time = 0.001 * elapsed;
this.globaltime += 0.001 * elapsed;
this.iteration += 1;
this.elapsed_time = (now - this.last_update_time) * 0.001;
this.last_update_time = now;
this.nodes_executing = [];
this.nodes_actioning = [];
this.nodes_executedAction = [];
};
/**
* Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than
* nodes with only inputs.
* @method updateExecutionOrder
*/
LGraph.prototype.updateExecutionOrder = function() {
this._nodes_in_order = this.computeExecutionOrder(false);
this._nodes_executable = [];
for (var i = 0; i < this._nodes_in_order.length; ++i) {
if (this._nodes_in_order[i].onExecute) {
this._nodes_executable.push(this._nodes_in_order[i]);
}
}
};
//This is more internal, it computes the executable nodes in order and returns it
LGraph.prototype.computeExecutionOrder = function(
only_onExecute,
set_level
) {
var L = [];
var S = [];
var M = {};
var visited_links = {}; //to avoid repeating links
var remaining_links = {}; //to a
//search for the nodes without inputs (starting nodes)
for (var i = 0, l = this._nodes.length; i < l; ++i) {
var node = this._nodes[i];
if (only_onExecute && !node.onExecute) {
continue;
}
M[node.id] = node; //add to pending nodes
var num = 0; //num of input connections
if (node.inputs) {
for (var j = 0, l2 = node.inputs.length; j < l2; j++) {
if (node.inputs[j] && node.inputs[j].link != null) {
num += 1;
}
}
}
if (num == 0) {
//is a starting node
S.push(node);
if (set_level) {
node._level = 1;
}
} //num of input links
else {
if (set_level) {
node._level = 0;
}
remaining_links[node.id] = num;
}
}
while (true) {
if (S.length == 0) {
break;
}
//get an starting node
var node = S.shift();
L.push(node); //add to ordered list
delete M[node.id]; //remove from the pending nodes
if (!node.outputs) {
continue;
}
//for every output
for (var i = 0; i < node.outputs.length; i++) {
var output = node.outputs[i];
//not connected
if (
output == null ||
output.links == null ||
output.links.length == 0
) {
continue;
}
//for every connection
for (var j = 0; j < output.links.length; j++) {
var link_id = output.links[j];
var link = this.links[link_id];
if (!link) {
continue;
}
//already visited link (ignore it)
if (visited_links[link.id]) {
continue;
}
var target_node = this.getNodeById(link.target_id);
if (target_node == null) {
visited_links[link.id] = true;
continue;
}
if (
set_level &&
(!target_node._level ||
target_node._level <= node._level)
) {
target_node._level = node._level + 1;
}
visited_links[link.id] = true; //mark as visited
remaining_links[target_node.id] -= 1; //reduce the number of links remaining
if (remaining_links[target_node.id] == 0) {
S.push(target_node);
} //if no more links, then add to starters array
}
}
}
//the remaining ones (loops)
for (var i in M) {
L.push(M[i]);
}
if (L.length != this._nodes.length && LiteGraph.debug) {
console.warn("something went wrong, nodes missing");
}
var l = L.length;
//save order number in the node
for (var i = 0; i < l; ++i) {
L[i].order = i;
}
//sort now by priority
L = L.sort(function(A, B) {
var Ap = A.constructor.priority || A.priority || 0;
var Bp = B.constructor.priority || B.priority || 0;
if (Ap == Bp) {
//if same priority, sort by order
return A.order - B.order;
}
return Ap - Bp; //sort by priority
});
//save order number in the node, again...
for (var i = 0; i < l; ++i) {
L[i].order = i;
}
return L;
};
/**
* Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively.
* It doesn't include the node itself
* @method getAncestors
* @return {Array} an array with all the LGraphNodes that affect this node, in order of execution
*/
LGraph.prototype.getAncestors = function(node) {
var ancestors = [];
var pending = [node];
var visited = {};
while (pending.length) {
var current = pending.shift();
if (!current.inputs) {
continue;
}
if (!visited[current.id] && current != node) {
visited[current.id] = true;
ancestors.push(current);
}
for (var i = 0; i < current.inputs.length; ++i) {
var input = current.getInputNode(i);
if (input && ancestors.indexOf(input) == -1) {
pending.push(input);
}
}
}
ancestors.sort(function(a, b) {
return a.order - b.order;
});
return ancestors;
};
/**
* Positions every node in a more readable manner
* @method arrange
*/
LGraph.prototype.arrange = function (margin, layout) {
margin = margin || 100;
const nodes = this.computeExecutionOrder(false, true);
const columns = [];
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
const col = node._level || 1;
if (!columns[col]) {
columns[col] = [];
}
columns[col].push(node);
}
let x = margin;
for (let i = 0; i < columns.length; ++i) {
const column = columns[i];
if (!column) {
continue;
}
let max_size = 100;
let y = margin + LiteGraph.NODE_TITLE_HEIGHT;
for (let j = 0; j < column.length; ++j) {
const node = column[j];
node.pos[0] = (layout == LiteGraph.VERTICAL_LAYOUT) ? y : x;
node.pos[1] = (layout == LiteGraph.VERTICAL_LAYOUT) ? x : y;
const max_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 1 : 0;
if (node.size[max_size_index] > max_size) {
max_size = node.size[max_size_index];
}
const node_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 0 : 1;
y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT;
}
x += max_size + margin;
}
this.setDirtyCanvas(true, true);
};
/**
* Returns the amount of time the graph has been running in milliseconds
* @method getTime
* @return {number} number of milliseconds the graph has been running
*/
LGraph.prototype.getTime = function() {
return this.globaltime;
};
/**
* Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant
* @method getFixedTime
* @return {number} number of milliseconds the graph has been running
*/
LGraph.prototype.getFixedTime = function() {
return this.fixedtime;
};
/**
* Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct
* if the nodes are using graphical actions
* @method getElapsedTime
* @return {number} number of milliseconds it took the last cycle
*/
LGraph.prototype.getElapsedTime = function() {
return this.elapsed_time;
};
/**
* Sends an event to all the nodes, useful to trigger stuff
* @method sendEventToAllNodes
* @param {String} eventname the name of the event (function to be called)
* @param {Array} params parameters in array format
*/
LGraph.prototype.sendEventToAllNodes = function(eventname, params, mode) {
mode = mode || LiteGraph.ALWAYS;
var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes;
if (!nodes) {
return;
}
for (var j = 0, l = nodes.length; j < l; ++j) {
var node = nodes[j];
if (
node.constructor === LiteGraph.Subgraph &&
eventname != "onExecute"
) {
if (node.mode == mode) {
node.sendEventToAllNodes(eventname, params, mode);
}
continue;
}
if (!node[eventname] || node.mode != mode) {
continue;
}
if (params === undefined) {
node[eventname]();
} else if (params && params.constructor === Array) {
node[eventname].apply(node, params);
} else {
node[eventname](params);
}
}
};
LGraph.prototype.sendActionToCanvas = function(action, params) {
if (!this.list_of_graphcanvas) {
return;
}
for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
var c = this.list_of_graphcanvas[i];
if (c[action]) {
c[action].apply(c, params);
}
}
};
/**
* Adds a new node instance to this graph
* @method add
* @param {LGraphNode} node the instance of the node
*/
LGraph.prototype.add = function(node, skip_compute_order) {
if (!node) {
return;
}
//groups
if (node.constructor === LGraphGroup) {
this._groups.push(node);
this.setDirtyCanvas(true);
this.change();
node.graph = this;
this._version++;
return;
}
//nodes
if (node.id != -1 && this._nodes_by_id[node.id] != null) {
console.warn(
"LiteGraph: there is already a node with this ID, changing it"
);
if (LiteGraph.use_uuids) {
node.id = LiteGraph.uuidv4();
}
else {
node.id = ++this.last_node_id;
}
}
if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
throw "LiteGraph: max number of nodes in a graph reached";
}
//give him an id
if (LiteGraph.use_uuids) {
if (node.id == null || node.id == -1)
node.id = LiteGraph.uuidv4();
}
else {
if (node.id == null || node.id == -1) {
node.id = ++this.last_node_id;
} else if (this.last_node_id < node.id) {
this.last_node_id = node.id;
}
}
node.graph = this;
this._version++;
this._nodes.push(node);
this._nodes_by_id[node.id] = node;
if (node.onAdded) {
node.onAdded(this);
}
if (this.config.align_to_grid) {
node.alignToGrid();
}
if (!skip_compute_order) {
this.updateExecutionOrder();
}
if (this.onNodeAdded) {
this.onNodeAdded(node);
}
this.setDirtyCanvas(true);
this.change();
return node; //to chain actions
};
/**
* Removes a node from the graph
* @method remove
* @param {LGraphNode} node the instance of the node
*/
LGraph.prototype.remove = function(node) {
if (node.constructor === LiteGraph.LGraphGroup) {
var index = this._groups.indexOf(node);
if (index != -1) {
this._groups.splice(index, 1);
}
node.graph = null;
this._version++;
this.setDirtyCanvas(true, true);
this.change();
return;
}
if (this._nodes_by_id[node.id] == null) {
return;
} //not found
if (node.ignore_remove) {
return;
} //cannot be removed
this.beforeChange(); //sure? - almost sure is wrong
//disconnect inputs
if (node.inputs) {
for (var i = 0; i < node.inputs.length; i++) {
var slot = node.inputs[i];
if (slot.link != null) {
node.disconnectInput(i);
}
}
}
//disconnect outputs
if (node.outputs) {
for (var i = 0; i < node.outputs.length; i++) {
var slot = node.outputs[i];
if (slot.links != null && slot.links.length) {
node.disconnectOutput(i);
}
}
}
//node.id = -1; //why?
//callback
if (node.onRemoved) {
node.onRemoved();
}
node.graph = null;
this._version++;
//remove from canvas render
if (this.list_of_graphcanvas) {
for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
var canvas = this.list_of_graphcanvas[i];
if (canvas.selected_nodes[node.id]) {
delete canvas.selected_nodes[node.id];
}
if (canvas.node_dragged == node) {
canvas.node_dragged = null;
}
}
}
//remove from containers
var pos = this._nodes.indexOf(node);
if (pos != -1) {
this._nodes.splice(pos, 1);
}
delete this._nodes_by_id[node.id];
if (this.onNodeRemoved) {
this.onNodeRemoved(node);
}
//close panels
this.sendActionToCanvas("checkPanels");
this.setDirtyCanvas(true, true);
this.afterChange(); //sure? - almost sure is wrong
this.change();
this.updateExecutionOrder();
};
/**
* Returns a node by its id.
* @method getNodeById
* @param {Number} id
*/
LGraph.prototype.getNodeById = function(id) {
if (id == null) {
return null;
}
return this._nodes_by_id[id];
};
/**
* Returns a list of nodes that matches a class
* @method findNodesByClass
* @param {Class} classObject the class itself (not an string)
* @return {Array} a list with all the nodes of this type
*/
LGraph.prototype.findNodesByClass = function(classObject, result) {
result = result || [];
result.length = 0;
for (var i = 0, l = this._nodes.length; i < l; ++i) {
if (this._nodes[i].constructor === classObject) {
result.push(this._nodes[i]);
}
}
return result;
};
/**
* Returns a list of nodes that matches a type
* @method findNodesByType
* @param {String} type the name of the node type
* @return {Array} a list with all the nodes of this type
*/
LGraph.prototype.findNodesByType = function(type, result) {
var type = type.toLowerCase();
result = result || [];
result.length = 0;
for (var i = 0, l = this._nodes.length; i < l; ++i) {
if (this._nodes[i].type.toLowerCase() == type) {
result.push(this._nodes[i]);
}
}
return result;
};
/**
* Returns the first node that matches a name in its title
* @method findNodeByTitle
* @param {String} name the name of the node to search
* @return {Node} the node or null
*/
LGraph.prototype.findNodeByTitle = function(title) {
for (var i = 0, l = this._nodes.length; i < l; ++i) {
if (this._nodes[i].title == title) {
return this._nodes[i];
}
}
return null;
};
/**
* Returns a list of nodes that matches a name
* @method findNodesByTitle
* @param {String} name the name of the node to search
* @return {Array} a list with all the nodes with this name
*/
LGraph.prototype.findNodesByTitle = function(title) {
var result = [];
for (var i = 0, l = this._nodes.length; i < l; ++i) {
if (this._nodes[i].title == title) {
result.push(this._nodes[i]);
}
}
return result;
};
/**
* Returns the top-most node in this position of the canvas
* @method getNodeOnPos
* @param {number} x the x coordinate in canvas space
* @param {number} y the y coordinate in canvas space
* @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph
* @return {LGraphNode} the node at this position or null
*/
LGraph.prototype.getNodeOnPos = function(x, y, nodes_list, margin) {
nodes_list = nodes_list || this._nodes;
var nRet = null;
for (var i = nodes_list.length - 1; i >= 0; i--) {
var n = nodes_list[i];
if (n.isPointInside(x, y, margin)) {
// check for lesser interest nodes (TODO check for overlapping, use the top)
/*if (typeof n == "LGraphGroup"){
nRet = n;
}else{*/
return n;
/*}*/
}
}
return nRet;
};
/**
* Returns the top-most group in that position
* @method getGroupOnPos
* @param {number} x the x coordinate in canvas space
* @param {number} y the y coordinate in canvas space
* @return {LGraphGroup} the group or null
*/
LGraph.prototype.getGroupOnPos = function(x, y) {
for (var i = this._groups.length - 1; i >= 0; i--) {
var g = this._groups[i];
if (g.isPointInside(x, y, 2, true)) {
return g;
}
}
return null;
};
/**
* Checks that the node type matches the node type registered, used when replacing a nodetype by a newer version during execution
* this replaces the ones using the old version with the new version
* @method checkNodeTypes
*/
LGraph.prototype.checkNodeTypes = function() {
var changes = false;
for (var i = 0; i < this._nodes.length; i++) {
var node = this._nodes[i];
var ctor = LiteGraph.registered_node_types[node.type];
if (node.constructor == ctor) {
continue;
}
console.log("node being replaced by newer version: " + node.type);
var newnode = LiteGraph.createNode(node.type);
changes = true;
this._nodes[i] = newnode;
newnode.configure(node.serialize());
newnode.graph = this;
this._nodes_by_id[newnode.id] = newnode;
if (node.inputs) {
newnode.inputs = node.inputs.concat();
}
if (node.outputs) {
newnode.outputs = node.outputs.concat();
}
}
this.updateExecutionOrder();
};
// ********** GLOBALS *****************
LGraph.prototype.onAction = function(action, param, options) {
this._input_nodes = this.findNodesByClass(
LiteGraph.GraphInput,
this._input_nodes
);
for (var i = 0; i < this._input_nodes.length; ++i) {
var node = this._input_nodes[i];
if (node.properties.name != action) {
continue;
}
//wrap node.onAction(action, param);
node.actionDo(action, param, options);
break;
}
};
LGraph.prototype.trigger = function(action, param) {
if (this.onTrigger) {
this.onTrigger(action, param);
}
};
/**
* Tell this graph it has a global graph input of this type
* @method addGlobalInput
* @param {String} name
* @param {String} type
* @param {*} value [optional]
*/
LGraph.prototype.addInput = function(name, type, value) {
var input = this.inputs[name];
if (input) {
//already exist
return;
}
this.beforeChange();
this.inputs[name] = { name: name, type: type, value: value };
this._version++;
this.afterChange();
if (this.onInputAdded) {
this.onInputAdded(name, type);
}
if (this.onInputsOutputsChange) {
this.onInputsOutputsChange();
}
};
/**
* Assign a data to the global graph input
* @method setGlobalInputData
* @param {String} name
* @param {*} data
*/
LGraph.prototype.setInputData = function(name, data) {
var input = this.inputs[name];
if (!input) {
return;
}
input.value = data;
};
/**
* Returns the current value of a global graph input
* @method getInputData
* @param {String} name
* @return {*} the data
*/
LGraph.prototype.getInputData = function(name) {
var input = this.inputs[name];
if (!input) {
return null;
}
return input.value;
};
/**
* Changes the name of a global graph input
* @method renameInput
* @param {String} old_name
* @param {String} new_name
*/
LGraph.prototype.renameInput = function(old_name, name) {
if (name == old_name) {
return;
}
if (!this.inputs[old_name]) {
return false;
}
if (this.inputs[name]) {
console.error("there is already one input with that name");
return false;
}
this.inputs[name] = this.inputs[old_name];
delete this.inputs[old_name];
this._version++;
if (this.onInputRenamed) {
this.onInputRenamed(old_name, name);
}
if (this.onInputsOutputsChange) {
this.onInputsOutputsChange();
}
};
/**
* Changes the type of a global graph input
* @method changeInputType
* @param {String} name
* @param {String} type
*/
LGraph.prototype.changeInputType = function(name, type) {
if (!this.inputs[name]) {
return false;
}
if (
this.inputs[name].type &&
String(this.inputs[name].type).toLowerCase() ==
String(type).toLowerCase()
) {
return;
}
this.inputs[name].type = type;
this._version++;
if (this.onInputTypeChanged) {
this.onInputTypeChanged(name, type);
}
};
/**
* Removes a global graph input
* @method removeInput
* @param {String} name
* @param {String} type
*/
LGraph.prototype.removeInput = function(name) {
if (!this.inputs[name]) {
return false;
}
delete this.inputs[name];
this._version++;
if (this.onInputRemoved) {
this.onInputRemoved(name);
}
if (this.onInputsOutputsChange) {
this.onInputsOutputsChange();
}
return true;
};
/**
* Creates a global graph output
* @method addOutput
* @param {String} name
* @param {String} type
* @param {*} value
*/
LGraph.prototype.addOutput = function(name, type, value) {
this.outputs[name] = { name: name, type: type, value: value };
this._version++;
if (this.onOutputAdded) {
this.onOutputAdded(name, type);
}
if (this.onInputsOutputsChange) {
this.onInputsOutputsChange();
}
};
/**
* Assign a data to the global output
* @method setOutputData
* @param {String} name
* @param {String} value
*/
LGraph.prototype.setOutputData = function(name, value) {
var output = this.outputs[name];
if (!output) {
return;
}
output.value = value;
};
/**
* Returns the current value of a global graph output
* @method getOutputData
* @param {String} name
* @return {*} the data
*/
LGraph.prototype.getOutputData = function(name) {
var output = this.outputs[name];
if (!output) {
return null;
}
return output.value;
};
/**
* Renames a global graph output
* @method renameOutput
* @param {String} old_name
* @param {String} new_name
*/
LGraph.prototype.renameOutput = function(old_name, name) {
if (!this.outputs[old_name]) {
return false;
}
if (this.outputs[name]) {
console.error("there is already one output with that name");
return false;
}
this.outputs[name] = this.outputs[old_name];
delete this.outputs[old_name];
this._version++;
if (this.onOutputRenamed) {
this.onOutputRenamed(old_name, name);
}
if (this.onInputsOutputsChange) {
this.onInputsOutputsChange();
}
};
/**
* Changes the type of a global graph output
* @method changeOutputType
* @param {String} name
* @param {String} type
*/
LGraph.prototype.changeOutputType = function(name, type) {
if (!this.outputs[name]) {
return false;
}
if (
this.outputs[name].type &&
String(this.outputs[name].type).toLowerCase() ==
String(type).toLowerCase()
) {
return;
}
this.outputs[name].type = type;
this._version++;
if (this.onOutputTypeChanged) {
this.onOutputTypeChanged(name, type);
}
};
/**
* Removes a global graph output
* @method removeOutput
* @param {String} name
*/
LGraph.prototype.removeOutput = function(name) {
if (!this.outputs[name]) {
return false;
}
delete this.outputs[name];
this._version++;
if (this.onOutputRemoved) {
this.onOutputRemoved(name);
}
if (this.onInputsOutputsChange) {
this.onInputsOutputsChange();
}
return true;
};
LGraph.prototype.triggerInput = function(name, value) {
var nodes = this.findNodesByTitle(name);
for (var i = 0; i < nodes.length; ++i) {
nodes[i].onTrigger(value);
}
};
LGraph.prototype.setCallback = function(name, func) {
var nodes = this.findNodesByTitle(name);
for (var i = 0; i < nodes.length; ++i) {
nodes[i].setTrigger(func);
}
};
//used for undo, called before any change is made to the graph
LGraph.prototype.beforeChange = function(info) {
if (this.onBeforeChange) {
this.onBeforeChange(this,info);
}
this.sendActionToCanvas("onBeforeChange", this);
};
//used to resend actions, called after any change is made to the graph
LGraph.prototype.afterChange = function(info) {
if (this.onAfterChange) {
this.onAfterChange(this,info);
}
this.sendActionToCanvas("onAfterChange", this);
};
LGraph.prototype.connectionChange = function(node, link_info) {
this.updateExecutionOrder();
if (this.onConnectionChange) {
this.onConnectionChange(node);
}
this._version++;
this.sendActionToCanvas("onConnectionChange");
};
/**
* returns if the graph is in live mode
* @method isLive
*/
LGraph.prototype.isLive = function() {
if (!this.list_of_graphcanvas) {
return false;
}
for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
var c = this.list_of_graphcanvas[i];
if (c.live_mode) {
return true;
}
}
return false;
};
/**
* clears the triggered slot animation in all links (stop visual animation)
* @method clearTriggeredSlots
*/
LGraph.prototype.clearTriggeredSlots = function() {
for (var i in this.links) {
var link_info = this.links[i];
if (!link_info) {
continue;
}
if (link_info._last_time) {
link_info._last_time = 0;
}
}
};
/* Called when something visually changed (not the graph!) */
LGraph.prototype.change = function() {
if (LiteGraph.debug) {
console.log("Graph changed");
}
this.sendActionToCanvas("setDirty", [true, true]);
if (this.on_change) {
this.on_change(this);
}
};
LGraph.prototype.setDirtyCanvas = function(fg, bg) {
this.sendActionToCanvas("setDirty", [fg, bg]);
};
/**
* Destroys a link
* @method removeLink
* @param {Number} link_id
*/
LGraph.prototype.removeLink = function(link_id) {
var link = this.links[link_id];
if (!link) {
return;
}
var node = this.getNodeById(link.target_id);
if (node) {
node.disconnectInput(link.target_slot);
}
};
//save and recover app state ***************************************
/**
* Creates a Object containing all the info about this graph, it can be serialized
* @method serialize
* @return {Object} value of the node
*/
LGraph.prototype.serialize = function() {
var nodes_info = [];
for (var i = 0, l = this._nodes.length; i < l; ++i) {
nodes_info.push(this._nodes[i].serialize());
}
//pack link info into a non-verbose format
var links = [];
for (var i in this.links) {
//links is an OBJECT
var link = this.links[i];
if (!link.serialize) {
//weird bug I havent solved yet
console.warn(
"weird LLink bug, link info is not a LLink but a regular object"
);
var link2 = new LLink();
for (var j in link) {
link2[j] = link[j];
}
this.links[i] = link2;
link = link2;
}
links.push(link.serialize());
}
var groups_info = [];
for (var i = 0; i < this._groups.length; ++i) {
groups_info.push(this._groups[i].serialize());
}
var data = {
last_node_id: this.last_node_id,
last_link_id: this.last_link_id,
nodes: nodes_info,
links: links,
groups: groups_info,
config: this.config,
extra: this.extra,
version: LiteGraph.VERSION
};
if(this.onSerialize)
this.onSerialize(data);
return data;
};
/**
* Configure a graph from a JSON string
* @method configure
* @param {String} str configure a graph from a JSON string
* @param {Boolean} returns if there was any error parsing
*/
LGraph.prototype.configure = function(data, keep_old) {
if (!data) {
return;
}
if (!keep_old) {
this.clear();
}
var nodes = data.nodes;
//decode links info (they are very verbose)
if (data.links && data.links.constructor === Array) {
var links = [];
for (var i = 0; i < data.links.length; ++i) {
var link_data = data.links[i];
if(!link_data) //weird bug
{
console.warn("serialized graph link data contains errors, skipping.");
continue;
}
var link = new LLink();
link.configure(link_data);
links[link.id] = link;
}
data.links = links;
}
//copy all stored fields
for (var i in data) {
if(i == "nodes" || i == "groups" ) //links must be accepted
continue;
this[i] = data[i];
}
var error = false;
//create nodes
this._nodes = [];
if (nodes) {
for (var i = 0, l = nodes.length; i < l; ++i) {
var n_info = nodes[i]; //stored info
var node = LiteGraph.createNode(n_info.type, n_info.title);
if (!node) {
if (LiteGraph.debug) {
console.log(
"Node not found or has errors: " + n_info.type
);
}
//in case of error we create a replacement node to avoid losing info
node = new LGraphNode();
node.last_serialization = n_info;
node.has_errors = true;
error = true;
//continue;
}
node.id = n_info.id; //id it or it will create a new id
this.add(node, true); //add before configure, otherwise configure cannot create links
}
//configure nodes afterwards so they can reach each other
for (var i = 0, l = nodes.length; i < l; ++i) {
var n_info = nodes[i];
var node = this.getNodeById(n_info.id);
if (node) {
node.configure(n_info);
}
}
}
//groups
this._groups.length = 0;
if (data.groups) {
for (var i = 0; i < data.groups.length; ++i) {
var group = new LiteGraph.LGraphGroup();
group.configure(data.groups[i]);
this.add(group);
}
}
this.updateExecutionOrder();
this.extra = data.extra || {};
if(this.onConfigure)
this.onConfigure(data);
this._version++;
this.setDirtyCanvas(true, true);
return error;
};
LGraph.prototype.load = function(url, callback) {
var that = this;
//from file
if(url.constructor === File || url.constructor === Blob)
{
var reader = new FileReader();
reader.addEventListener('load', function(event) {
var data = JSON.parse(event.target.result);
that.configure(data);
if(callback)
callback();
});
reader.readAsText(url);
return;
}
//is a string, then an URL
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.send(null);
req.onload = function(oEvent) {
if (req.status !== 200) {
console.error("Error loading graph:", req.status, req.response);
return;
}
var data = JSON.parse( req.response );
that.configure(data);
if(callback)
callback();
};
req.onerror = function(err) {
console.error("Error loading graph:", err);
};
};
LGraph.prototype.onNodeTrace = function(node, msg, color) {
//TODO
};
//this is the class in charge of storing link information
function LLink(id, type, origin_id, origin_slot, target_id, target_slot) {
this.id = id;
this.type = type;
this.origin_id = origin_id;
this.origin_slot = origin_slot;
this.target_id = target_id;
this.target_slot = target_slot;
this._data = null;
this._pos = new Float32Array(2); //center
}
LLink.prototype.configure = function(o) {
if (o.constructor === Array) {
this.id = o[0];
this.origin_id = o[1];
this.origin_slot = o[2];
this.target_id = o[3];
this.target_slot = o[4];
this.type = o[5];
} else {
this.id = o.id;
this.type = o.type;
this.origin_id = o.origin_id;
this.origin_slot = o.origin_slot;
this.target_id = o.target_id;
this.target_slot = o.target_slot;
}
};
LLink.prototype.serialize = function() {
return [
this.id,
this.origin_id,
this.origin_slot,
this.target_id,
this.target_slot,
this.type
];
};
LiteGraph.LLink = LLink;
// *************************************************************
// Node CLASS *******
// *************************************************************
/*
title: string
pos: [x,y]
size: [x,y]
input|output: every connection
+ { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array });
general properties:
+ clip_area: if you render outside the node, it will be clipped
+ unsafe_execution: not allowed for safe execution
+ skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected
+ resizable: if set to false it wont be resizable with the mouse
+ horizontal: slots are distributed horizontally
+ widgets_start_y: widgets start at y distance from the top of the node
flags object:
+ collapsed: if it is collapsed
supported callbacks:
+ onAdded: when added to graph (warning: this is called BEFORE the node is configured when loading)
+ onRemoved: when removed from graph
+ onStart: when the graph starts playing
+ onStop: when the graph stops playing
+ onDrawForeground: render the inside widgets inside the node
+ onDrawBackground: render the background area inside the node (only in edit mode)
+ onMouseDown
+ onMouseMove
+ onMouseUp
+ onMouseEnter
+ onMouseLeave
+ onExecute: execute the node
+ onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour)
+ onGetInputs: returns an array of possible inputs
+ onGetOutputs: returns an array of possible outputs
+ onBounding: in case this node has a bigger bounding than the node itself (the callback receives the bounding as [x,y,w,h])
+ onDblClick: double clicked in the node
+ onInputDblClick: input slot double clicked (can be used to automatically create a node connected)
+ onOutputDblClick: output slot double clicked (can be used to automatically create a node connected)
+ onConfigure: called after the node has been configured
+ onSerialize: to add extra info when serializing (the callback receives the object that should be filled with the data)
+ onSelected
+ onDeselected
+ onDropItem : DOM item dropped over the node
+ onDropFile : file dropped over the node
+ onConnectInput : if returns false the incoming connection will be canceled
+ onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info )
+ onAction: action slot triggered
+ getExtraMenuOptions: to add option to context menu
*/
/**
* Base Class for all the node type classes
* @class LGraphNode
* @param {String} name a name for the node
*/
function LGraphNode(title) {
this._ctor(title);
}
global.LGraphNode = LiteGraph.LGraphNode = LGraphNode;
LGraphNode.prototype._ctor = function(title) {
this.title = title || "Unnamed";
this.size = [LiteGraph.NODE_WIDTH, 60];
this.graph = null;
this._pos = new Float32Array(10, 10);
Object.defineProperty(this, "pos", {
set: function(v) {
if (!v || v.length < 2) {
return;
}
this._pos[0] = v[0];
this._pos[1] = v[1];
},
get: function() {
return this._pos;
},
enumerable: true
});
if (LiteGraph.use_uuids) {
this.id = LiteGraph.uuidv4();
}
else {
this.id = -1; //not know till not added
}
this.type = null;
//inputs available: array of inputs
this.inputs = [];
this.outputs = [];
this.connections = [];
//local data
this.properties = {}; //for the values
this.properties_info = []; //for the info
this.flags = {};
};
/**
* configure a node from an object containing the serialized info
* @method configure
*/
LGraphNode.prototype.configure = function(info) {
if (this.graph) {
this.graph._version++;
}
for (var j in info) {
if (j == "properties") {
//i don't want to clone properties, I want to reuse the old container
for (var k in info.properties) {
this.properties[k] = info.properties[k];
if (this.onPropertyChanged) {
this.onPropertyChanged( k, info.properties[k] );
}
}
continue;
}
if (info[j] == null) {
continue;
} else if (typeof info[j] == "object") {
//object
if (this[j] && this[j].configure) {
this[j].configure(info[j]);
} else {
this[j] = LiteGraph.cloneObject(info[j], this[j]);
}
} //value
else {
this[j] = info[j];
}
}
if (!info.title) {
this.title = this.constructor.title;
}
if (this.inputs) {
for (var i = 0; i < this.inputs.length; ++i) {
var input = this.inputs[i];
var link_info = this.graph ? this.graph.links[input.link] : null;
if (this.onConnectionsChange)
this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated
if( this.onInputAdded )
this.onInputAdded(input);
}
}
if (this.outputs) {
for (var i = 0; i < this.outputs.length; ++i) {
var output = this.outputs[i];
if (!output.links) {
continue;
}
for (var j = 0; j < output.links.length; ++j) {
var link_info = this.graph ? this.graph.links[output.links[j]] : null;
if (this.onConnectionsChange)
this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated
}
if( this.onOutputAdded )
this.onOutputAdded(output);
}
}
if( this.widgets )
{
for (var i = 0; i < this.widgets.length; ++i)
{
var w = this.widgets[i];
if(!w)
continue;
if(w.options && w.options.property && this.properties[ w.options.property ])
w.value = JSON.parse( JSON.stringify( this.properties[ w.options.property ] ) );
}
if (info.widgets_values) {
for (var i = 0; i < info.widgets_values.length; ++i) {
if (this.widgets[i]) {
this.widgets[i].value = info.widgets_values[i];
}
}
}
}
if (this.onConfigure) {
this.onConfigure(info);
}
};
/**
* serialize the content
* @method serialize
*/
LGraphNode.prototype.serialize = function() {
//create serialization object
var o = {
id: this.id,
type: this.type,
pos: this.pos,
size: this.size,
flags: LiteGraph.cloneObject(this.flags),
order: this.order,
mode: this.mode
};
//special case for when there were errors
if (this.constructor === LGraphNode && this.last_serialization) {
return this.last_serialization;
}
if (this.inputs) {
o.inputs = this.inputs;
}
if (this.outputs) {
//clear outputs last data (because data in connections is never serialized but stored inside the outputs info)
for (var i = 0; i < this.outputs.length; i++) {
delete this.outputs[i]._data;
}
o.outputs = this.outputs;
}
if (this.title && this.title != this.constructor.title) {
o.title = this.title;
}
if (this.properties) {
o.properties = LiteGraph.cloneObject(this.properties);
}
if (this.widgets && this.serialize_widgets) {
o.widgets_values = [];
for (var i = 0; i < this.widgets.length; ++i) {
if(this.widgets[i])
o.widgets_values[i] = this.widgets[i].value;
else
o.widgets_values[i] = null;
}
}
if (!o.type) {
o.type = this.constructor.type;
}
if (this.color) {
o.color = this.color;
}
if (this.bgcolor) {
o.bgcolor = this.bgcolor;
}
if (this.boxcolor) {
o.boxcolor = this.boxcolor;
}
if (this.shape) {
o.shape = this.shape;
}
if (this.onSerialize) {
if (this.onSerialize(o)) {
console.warn(
"node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter"
);
}
}
return o;
};
/* Creates a clone of this node */
LGraphNode.prototype.clone = function() {
var node = LiteGraph.createNode(this.type);
if (!node) {
return null;
}
//we clone it because serialize returns shared containers
var data = LiteGraph.cloneObject(this.serialize());
//remove links
if (data.inputs) {
for (var i = 0; i < data.inputs.length; ++i) {
data.inputs[i].link = null;
}
}
if (data.outputs) {
for (var i = 0; i < data.outputs.length; ++i) {
if (data.outputs[i].links) {
data.outputs[i].links.length = 0;
}
}
}
delete data["id"];
if (LiteGraph.use_uuids) {
data["id"] = LiteGraph.uuidv4()
}
//remove links
node.configure(data);
return node;
};
/**
* serialize and stringify
* @method toString
*/
LGraphNode.prototype.toString = function() {
return JSON.stringify(this.serialize());
};
//LGraphNode.prototype.deserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph
/**
* get the title string
* @method getTitle
*/
LGraphNode.prototype.getTitle = function() {
return this.title || this.constructor.title;
};
/**
* sets the value of a property
* @method setProperty
* @param {String} name
* @param {*} value
*/
LGraphNode.prototype.setProperty = function(name, value) {
if (!this.properties) {
this.properties = {};
}
if( value === this.properties[name] )
return;
var prev_value = this.properties[name];
this.properties[name] = value;
if (this.onPropertyChanged) {
if( this.onPropertyChanged(name, value, prev_value) === false ) //abort change
this.properties[name] = prev_value;
}
if(this.widgets) //widgets could be linked to properties
for(var i = 0; i < this.widgets.length; ++i)
{
var w = this.widgets[i];
if(!w)
continue;
if(w.options.property == name)
{
w.value = value;
break;
}
}
};
// Execution *************************
/**
* sets the output data
* @method setOutputData
* @param {number} slot
* @param {*} data
*/
LGraphNode.prototype.setOutputData = function(slot, data) {
if (!this.outputs) {
return;
}
//this maybe slow and a niche case
//if(slot && slot.constructor === String)
// slot = this.findOutputSlot(slot);
if (slot == -1 || slot >= this.outputs.length) {
return;
}
var output_info = this.outputs[slot];
if (!output_info) {
return;
}
//store data in the output itself in case we want to debug
output_info._data = data;
//if there are connections, pass the data to the connections
if (this.outputs[slot].links) {
for (var i = 0; i < this.outputs[slot].links.length; i++) {
var link_id = this.outputs[slot].links[i];
var link = this.graph.links[link_id];
if(link)
link.data = data;
}
}
};
/**
* sets the output data type, useful when you want to be able to overwrite the data type
* @method setOutputDataType
* @param {number} slot
* @param {String} datatype
*/
LGraphNode.prototype.setOutputDataType = function(slot, type) {
if (!this.outputs) {
return;
}
if (slot == -1 || slot >= this.outputs.length) {
return;
}
var output_info = this.outputs[slot];
if (!output_info) {
return;
}
//store data in the output itself in case we want to debug
output_info.type = type;
//if there are connections, pass the data to the connections
if (this.outputs[slot].links) {
for (var i = 0; i < this.outputs[slot].links.length; i++) {
var link_id = this.outputs[slot].links[i];
this.graph.links[link_id].type = type;
}
}
};
/**
* Retrieves the input data (data traveling through the connection) from one slot
* @method getInputData
* @param {number} slot
* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
* @return {*} data or if it is not connected returns undefined
*/
LGraphNode.prototype.getInputData = function(slot, force_update) {
if (!this.inputs) {
return;
} //undefined;
if (slot >= this.inputs.length || this.inputs[slot].link == null) {
return;
}
var link_id = this.inputs[slot].link;
var link = this.graph.links[link_id];
if (!link) {
//bug: weird case but it happens sometimes
return null;
}
if (!force_update) {
return link.data;
}
//special case: used to extract data from the incoming connection before the graph has been executed
var node = this.graph.getNodeById(link.origin_id);
if (!node) {
return link.data;
}
if (node.updateOutputData) {
node.updateOutputData(link.origin_slot);
} else if (node.onExecute) {
node.onExecute();
}
return link.data;
};
/**
* Retrieves the input data type (in case this supports multiple input types)
* @method getInputDataType
* @param {number} slot
* @return {String} datatype in string format
*/
LGraphNode.prototype.getInputDataType = function(slot) {
if (!this.inputs) {
return null;
} //undefined;
if (slot >= this.inputs.length || this.inputs[slot].link == null) {
return null;
}
var link_id = this.inputs[slot].link;
var link = this.graph.links[link_id];
if (!link) {
//bug: weird case but it happens sometimes
return null;
}
var node = this.graph.getNodeById(link.origin_id);
if (!node) {
return link.type;
}
var output_info = node.outputs[link.origin_slot];
if (output_info) {
return output_info.type;
}
return null;
};
/**
* Retrieves the input data from one slot using its name instead of slot number
* @method getInputDataByName
* @param {String} slot_name
* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
* @return {*} data or if it is not connected returns null
*/
LGraphNode.prototype.getInputDataByName = function(
slot_name,
force_update
) {
var slot = this.findInputSlot(slot_name);
if (slot == -1) {
return null;
}
return this.getInputData(slot, force_update);
};
/**
* tells you if there is a connection in one input slot
* @method isInputConnected
* @param {number} slot
* @return {boolean}
*/
LGraphNode.prototype.isInputConnected = function(slot) {
if (!this.inputs) {
return false;
}
return slot < this.inputs.length && this.inputs[slot].link != null;
};
/**
* tells you info about an input connection (which node, type, etc)
* @method getInputInfo
* @param {number} slot
* @return {Object} object or null { link: id, name: string, type: string or 0 }
*/
LGraphNode.prototype.getInputInfo = function(slot) {
if (!this.inputs) {
return null;
}
if (slot < this.inputs.length) {
return this.inputs[slot];
}
return null;
};
/**
* Returns the link info in the connection of an input slot
* @method getInputLink
* @param {number} slot
* @return {LLink} object or null
*/
LGraphNode.prototype.getInputLink = function(slot) {
if (!this.inputs) {
return null;
}
if (slot < this.inputs.length) {
var slot_info = this.inputs[slot];
return this.graph.links[ slot_info.link ];
}
return null;
};
/**
* returns the node connected in the input slot
* @method getInputNode
* @param {number} slot
* @return {LGraphNode} node or null
*/
LGraphNode.prototype.getInputNode = function(slot) {
if (!this.inputs) {
return null;
}
if (slot >= this.inputs.length) {
return null;
}
var input = this.inputs[slot];
if (!input || input.link === null) {
return null;
}
var link_info = this.graph.links[input.link];
if (!link_info) {
return null;
}
return this.graph.getNodeById(link_info.origin_id);
};
/**
* returns the value of an input with this name, otherwise checks if there is a property with that name
* @method getInputOrProperty
* @param {string} name
* @return {*} value
*/
LGraphNode.prototype.getInputOrProperty = function(name) {
if (!this.inputs || !this.inputs.length) {
return this.properties ? this.properties[name] : null;
}
for (var i = 0, l = this.inputs.length; i < l; ++i) {
var input_info = this.inputs[i];
if (name == input_info.name && input_info.link != null) {
var link = this.graph.links[input_info.link];
if (link) {
return link.data;
}
}
}
return this.properties[name];
};
/**
* tells you the last output data that went in that slot
* @method getOutputData
* @param {number} slot
* @return {Object} object or null
*/
LGraphNode.prototype.getOutputData = function(slot) {
if (!this.outputs) {
return null;
}
if (slot >= this.outputs.length) {
return null;
}
var info = this.outputs[slot];
return info._data;
};
/**
* tells you info about an output connection (which node, type, etc)
* @method getOutputInfo
* @param {number} slot
* @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] }
*/
LGraphNode.prototype.getOutputInfo = function(slot) {
if (!this.outputs) {
return null;
}
if (slot < this.outputs.length) {
return this.outputs[slot];
}
return null;
};
/**
* tells you if there is a connection in one output slot
* @method isOutputConnected
* @param {number} slot
* @return {boolean}
*/
LGraphNode.prototype.isOutputConnected = function(slot) {
if (!this.outputs) {
return false;
}
return (
slot < this.outputs.length &&
this.outputs[slot].links &&
this.outputs[slot].links.length
);
};
/**
* tells you if there is any connection in the output slots
* @method isAnyOutputConnected
* @return {boolean}
*/
LGraphNode.prototype.isAnyOutputConnected = function() {
if (!this.outputs) {
return false;
}
for (var i = 0; i < this.outputs.length; ++i) {
if (this.outputs[i].links && this.outputs[i].links.length) {
return true;
}
}
return false;
};
/**
* retrieves all the nodes connected to this output slot
* @method getOutputNodes
* @param {number} slot
* @return {array}
*/
LGraphNode.prototype.getOutputNodes = function(slot) {
if (!this.outputs || this.outputs.length == 0) {
return null;
}
if (slot >= this.outputs.length) {
return null;
}
var output = this.outputs[slot];
if (!output.links || output.links.length == 0) {
return null;
}
var r = [];
for (var i = 0; i < output.links.length; i++) {
var link_id = output.links[i];
var link = this.graph.links[link_id];
if (link) {
var target_node = this.graph.getNodeById(link.target_id);
if (target_node) {
r.push(target_node);
}
}
}
return r;
};
LGraphNode.prototype.addOnTriggerInput = function(){
var trigS = this.findInputSlot("onTrigger");
if (trigS == -1){ //!trigS ||
var input = this.addInput("onTrigger", LiteGraph.EVENT, {optional: true, nameLocked: true});
return this.findInputSlot("onTrigger");
}
return trigS;
}
LGraphNode.prototype.addOnExecutedOutput = function(){
var trigS = this.findOutputSlot("onExecuted");
if (trigS == -1){ //!trigS ||
var output = this.addOutput("onExecuted", LiteGraph.ACTION, {optional: true, nameLocked: true});
return this.findOutputSlot("onExecuted");
}
return trigS;
}
LGraphNode.prototype.onAfterExecuteNode = function(param, options){
var trigS = this.findOutputSlot("onExecuted");
if (trigS != -1){
//console.debug(this.id+":"+this.order+" triggering slot onAfterExecute");
//console.debug(param);
//console.debug(options);
this.triggerSlot(trigS, param, null, options);
}
}
LGraphNode.prototype.changeMode = function(modeTo){
switch(modeTo){
case LiteGraph.ON_EVENT:
// this.addOnExecutedOutput();
break;
case LiteGraph.ON_TRIGGER:
this.addOnTriggerInput();
this.addOnExecutedOutput();
break;
case LiteGraph.NEVER:
break;
case LiteGraph.ALWAYS:
break;
case LiteGraph.ON_REQUEST:
break;
default:
return false;
break;
}
this.mode = modeTo;
return true;
};
/**
* Triggers the node code execution, place a boolean/counter to mark the node as being executed
* @method execute
* @param {*} param
* @param {*} options
*/
LGraphNode.prototype.doExecute = function(param, options) {
options = options || {};
if (this.onExecute){
// enable this to give the event an ID
if (!options.action_call) options.action_call = this.id+"_exec_"+Math.floor(Math.random()*9999);
this.graph.nodes_executing[this.id] = true; //.push(this.id);
this.onExecute(param, options);
this.graph.nodes_executing[this.id] = false; //.pop();
// save execution/action ref
this.exec_version = this.graph.iteration;
if(options && options.action_call){
this.action_call = options.action_call; // if (param)
this.graph.nodes_executedAction[this.id] = options.action_call;
}
}
this.execute_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event
if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); // callback
};
/**
* Triggers an action, wrapped by logics to control execution flow
* @method actionDo
* @param {String} action name
* @param {*} param
*/
LGraphNode.prototype.actionDo = function(action, param, options) {
options = options || {};
if (this.onAction){
// enable this to give the event an ID
if (!options.action_call) options.action_call = this.id+"_"+(action?action:"action")+"_"+Math.floor(Math.random()*9999);
this.graph.nodes_actioning[this.id] = (action?action:"actioning"); //.push(this.id);
this.onAction(action, param, options);
this.graph.nodes_actioning[this.id] = false; //.pop();
// save execution/action ref
if(options && options.action_call){
this.action_call = options.action_call; // if (param)
this.graph.nodes_executedAction[this.id] = options.action_call;
}
}
this.action_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event
if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options);
};
/**
* Triggers an event in this node, this will trigger any output with the same name
* @method trigger
* @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all
* @param {*} param
*/
LGraphNode.prototype.trigger = function(action, param, options) {
if (!this.outputs || !this.outputs.length) {
return;
}
if (this.graph)
this.graph._last_trigger_time = LiteGraph.getTime();
for (var i = 0; i < this.outputs.length; ++i) {
var output = this.outputs[i];
if ( !output || output.type !== LiteGraph.EVENT || (action && output.name != action) )
continue;
this.triggerSlot(i, param, null, options);
}
};
/**
* Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes
* @method triggerSlot
* @param {Number} slot the index of the output slot
* @param {*} param
* @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot
*/
LGraphNode.prototype.triggerSlot = function(slot, param, link_id, options) {
options = options || {};
if (!this.outputs) {
return;
}
if(slot == null)
{
console.error("slot must be a number");
return;
}
if(slot.constructor !== Number)
console.warn("slot must be a number, use node.trigger('name') if you want to use a string");
var output = this.outputs[slot];
if (!output) {
return;
}
var links = output.links;
if (!links || !links.length) {
return;
}
if (this.graph) {
this.graph._last_trigger_time = LiteGraph.getTime();
}
//for every link attached here
for (var k = 0; k < links.length; ++k) {
var id = links[k];
if (link_id != null && link_id != id) {
//to skip links
continue;
}
var link_info = this.graph.links[links[k]];
if (!link_info) {
//not connected
continue;
}
link_info._last_time = LiteGraph.getTime();
var node = this.graph.getNodeById(link_info.target_id);
if (!node) {
//node not found?
continue;
}
//used to mark events in graph
var target_connection = node.inputs[link_info.target_slot];
if (node.mode === LiteGraph.ON_TRIGGER)
{
// generate unique trigger ID if not present
if (!options.action_call) options.action_call = this.id+"_trigg_"+Math.floor(Math.random()*9999);
if (node.onExecute) {
// -- wrapping node.onExecute(param); --
node.doExecute(param, options);
}
}
else if (node.onAction) {
// generate unique action ID if not present
if (!options.action_call) options.action_call = this.id+"_act_"+Math.floor(Math.random()*9999);
//pass the action name
var target_connection = node.inputs[link_info.target_slot];
// wrap node.onAction(target_connection.name, param);
node.actionDo(target_connection.name, param, options);
}
}
};
/**
* clears the trigger slot animation
* @method clearTriggeredSlot
* @param {Number} slot the index of the output slot
* @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot
*/
LGraphNode.prototype.clearTriggeredSlot = function(slot, link_id) {
if (!this.outputs) {
return;
}
var output = this.outputs[slot];
if (!output) {
return;
}
var links = output.links;
if (!links || !links.length) {
return;
}
//for every link attached here
for (var k = 0; k < links.length; ++k) {
var id = links[k];
if (link_id != null && link_id != id) {
//to skip links
continue;
}
var link_info = this.graph.links[links[k]];
if (!link_info) {
//not connected
continue;
}
link_info._last_time = 0;
}
};
/**
* changes node size and triggers callback
* @method setSize
* @param {vec2} size
*/
LGraphNode.prototype.setSize = function(size)
{
this.size = size;
if(this.onResize)
this.onResize(this.size);
}
/**
* add a new property to this node
* @method addProperty
* @param {string} name
* @param {*} default_value
* @param {string} type string defining the output type ("vec3","number",...)
* @param {Object} extra_info this can be used to have special properties of the property (like values, etc)
*/
LGraphNode.prototype.addProperty = function(
name,
default_value,
type,
extra_info
) {
var o = { name: name, type: type, default_value: default_value };
if (extra_info) {
for (var i in extra_info) {
o[i] = extra_info[i];
}
}
if (!this.properties_info) {
this.properties_info = [];
}
this.properties_info.push(o);
if (!this.properties) {
this.properties = {};
}
this.properties[name] = default_value;
return o;
};
//connections
/**
* add a new output slot to use in this node
* @method addOutput
* @param {string} name
* @param {string} type string defining the output type ("vec3","number",...)
* @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc)
*/
LGraphNode.prototype.addOutput = function(name, type, extra_info) {
var output = { name: name, type: type, links: null };
if (extra_info) {
for (var i in extra_info) {
output[i] = extra_info[i];
}
}
if (!this.outputs) {
this.outputs = [];
}
this.outputs.push(output);
if (this.onOutputAdded) {
this.onOutputAdded(output);
}
if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true);
this.setSize( this.computeSize() );
this.setDirtyCanvas(true, true);
return output;
};
/**
* add a new output slot to use in this node
* @method addOutputs
* @param {Array} array of triplets like [[name,type,extra_info],[...]]
*/
LGraphNode.prototype.addOutputs = function(array) {
for (var i = 0; i < array.length; ++i) {
var info = array[i];
var o = { name: info[0], type: info[1], link: null };
if (array[2]) {
for (var j in info[2]) {
o[j] = info[2][j];
}
}
if (!this.outputs) {
this.outputs = [];
}
this.outputs.push(o);
if (this.onOutputAdded) {
this.onOutputAdded(o);
}
if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,info[1],true);
}
this.setSize( this.computeSize() );
this.setDirtyCanvas(true, true);
};
/**
* remove an existing output slot
* @method removeOutput
* @param {number} slot
*/
LGraphNode.prototype.removeOutput = function(slot) {
this.disconnectOutput(slot);
this.outputs.splice(slot, 1);
for (var i = slot; i < this.outputs.length; ++i) {
if (!this.outputs[i] || !this.outputs[i].links) {
continue;
}
var links = this.outputs[i].links;
for (var j = 0; j < links.length; ++j) {
var link = this.graph.links[links[j]];
if (!link) {
continue;
}
link.origin_slot -= 1;
}
}
this.setSize( this.computeSize() );
if (this.onOutputRemoved) {
this.onOutputRemoved(slot);
}
this.setDirtyCanvas(true, true);
};
/**
* add a new input slot to use in this node
* @method addInput
* @param {string} name
* @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0
* @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc)
*/
LGraphNode.prototype.addInput = function(name, type, extra_info) {
type = type || 0;
var input = { name: name, type: type, link: null };
if (extra_info) {
for (var i in extra_info) {
input[i] = extra_info[i];
}
}
if (!this.inputs) {
this.inputs = [];
}
this.inputs.push(input);
this.setSize( this.computeSize() );
if (this.onInputAdded) {
this.onInputAdded(input);
}
LiteGraph.registerNodeAndSlotType(this,type);
this.setDirtyCanvas(true, true);
return input;
};
/**
* add several new input slots in this node
* @method addInputs
* @param {Array} array of triplets like [[name,type,extra_info],[...]]
*/
LGraphNode.prototype.addInputs = function(array) {
for (var i = 0; i < array.length; ++i) {
var info = array[i];
var o = { name: info[0], type: info[1], link: null };
if (array[2]) {
for (var j in info[2]) {
o[j] = info[2][j];
}
}
if (!this.inputs) {
this.inputs = [];
}
this.inputs.push(o);
if (this.onInputAdded) {
this.onInputAdded(o);
}
LiteGraph.registerNodeAndSlotType(this,info[1]);
}
this.setSize( this.computeSize() );
this.setDirtyCanvas(true, true);
};
/**
* remove an existing input slot
* @method removeInput
* @param {number} slot
*/
LGraphNode.prototype.removeInput = function(slot) {
this.disconnectInput(slot);
var slot_info = this.inputs.splice(slot, 1);
for (var i = slot; i < this.inputs.length; ++i) {
if (!this.inputs[i]) {
continue;
}
var link = this.graph.links[this.inputs[i].link];
if (!link) {
continue;
}
link.target_slot -= 1;
}
this.setSize( this.computeSize() );
if (this.onInputRemoved) {
this.onInputRemoved(slot, slot_info[0] );
}
this.setDirtyCanvas(true, true);
};
/**
* add an special connection to this node (used for special kinds of graphs)
* @method addConnection
* @param {string} name
* @param {string} type string defining the input type ("vec3","number",...)
* @param {[x,y]} pos position of the connection inside the node
* @param {string} direction if is input or output
*/
LGraphNode.prototype.addConnection = function(name, type, pos, direction) {
var o = {
name: name,
type: type,
pos: pos,
direction: direction,
links: null
};
this.connections.push(o);
return o;
};
/**
* computes the minimum size of a node according to its inputs and output slots
* @method computeSize
* @param {vec2} minHeight
* @return {vec2} the total size
*/
LGraphNode.prototype.computeSize = function(out) {
if (this.constructor.size) {
return this.constructor.size.concat();
}
var rows = Math.max(
this.inputs ? this.inputs.length : 1,
this.outputs ? this.outputs.length : 1
);
var size = out || new Float32Array([0, 0]);
rows = Math.max(rows, 1);
var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size
var title_width = compute_text_size(this.title);
var input_width = 0;
var output_width = 0;
if (this.inputs) {
for (var i = 0, l = this.inputs.length; i < l; ++i) {
var input = this.inputs[i];
var text = input.label || input.name || "";
var text_width = compute_text_size(text);
if (input_width < text_width) {
input_width = text_width;
}
}
}
if (this.outputs) {
for (var i = 0, l = this.outputs.length; i < l; ++i) {
var output = this.outputs[i];
var text = output.label || output.name || "";
var text_width = compute_text_size(text);
if (output_width < text_width) {
output_width = text_width;
}
}
}
size[0] = Math.max(input_width + output_width + 10, title_width);
size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH);
if (this.widgets && this.widgets.length) {
size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5);
}
size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT;
var widgets_height = 0;
if (this.widgets && this.widgets.length) {
for (var i = 0, l = this.widgets.length; i < l; ++i) {
if (this.widgets[i].computeSize)
widgets_height += this.widgets[i].computeSize(size[0])[1] + 4;
else
widgets_height += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
widgets_height += 8;
}
//compute height using widgets height
if( this.widgets_up )
size[1] = Math.max( size[1], widgets_height );
else if( this.widgets_start_y != null )
size[1] = Math.max( size[1], widgets_height + this.widgets_start_y );
else
size[1] += widgets_height;
function compute_text_size(text) {
if (!text) {
return 0;
}
return font_size * text.length * 0.6;
}
if (
this.constructor.min_height &&
size[1] < this.constructor.min_height
) {
size[1] = this.constructor.min_height;
}
size[1] += 6; //margin
return size;
};
/**
* returns all the info available about a property of this node.
*
* @method getPropertyInfo
* @param {String} property name of the property
* @return {Object} the object with all the available info
*/
LGraphNode.prototype.getPropertyInfo = function( property )
{
var info = null;
//there are several ways to define info about a property
//legacy mode
if (this.properties_info) {
for (var i = 0; i < this.properties_info.length; ++i) {
if (this.properties_info[i].name == property) {
info = this.properties_info[i];
break;
}
}
}
//litescene mode using the constructor
if(this.constructor["@" + property])
info = this.constructor["@" + property];
if(this.constructor.widgets_info && this.constructor.widgets_info[property])
info = this.constructor.widgets_info[property];
//litescene mode using the constructor
if (!info && this.onGetPropertyInfo) {
info = this.onGetPropertyInfo(property);
}
if (!info)
info = {};
if(!info.type)
info.type = typeof this.properties[property];
if(info.widget == "combo")
info.type = "enum";
return info;
}
/**
* Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties
*
* @method addWidget
* @param {String} type the widget type (could be "number","string","combo"
* @param {String} name the text to show on the widget
* @param {String} value the default value
* @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify)
* @param {Object} options the object that contains special properties of this widget
* @return {Object} the created widget object
*/
LGraphNode.prototype.addWidget = function( type, name, value, callback, options )
{
if (!this.widgets) {
this.widgets = [];
}
if(!options && callback && callback.constructor === Object)
{
options = callback;
callback = null;
}
if(options && options.constructor === String) //options can be the property name
options = { property: options };
if(callback && callback.constructor === String) //callback can be the property name
{
if(!options)
options = {};
options.property = callback;
callback = null;
}
if(callback && callback.constructor !== Function)
{
console.warn("addWidget: callback must be a function");
callback = null;
}
var w = {
type: type.toLowerCase(),
name: name,
value: value,
callback: callback,
options: options || {}
};
if (w.options.y !== undefined) {
w.y = w.options.y;
}
if (!callback && !w.options.callback && !w.options.property) {
console.warn("LiteGraph addWidget(...) without a callback or property assigned");
}
if (type == "combo" && !w.options.values) {
throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }";
}
this.widgets.push(w);
this.setSize( this.computeSize() );
return w;
};
LGraphNode.prototype.addCustomWidget = function(custom_widget) {
if (!this.widgets) {
this.widgets = [];
}
this.widgets.push(custom_widget);
return custom_widget;
};
/**
* returns the bounding of the object, used for rendering purposes
* bounding is: [topleft_cornerx, topleft_cornery, width, height]
* @method getBounding
* @return {Float32Array[4]} the total size
*/
LGraphNode.prototype.getBounding = function(out) {
out = out || new Float32Array(4);
out[0] = this.pos[0] - 4;
out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT;
out[2] = this.size[0] + 4;
out[3] = this.flags.collapsed ? LiteGraph.NODE_TITLE_HEIGHT : this.size[1] + LiteGraph.NODE_TITLE_HEIGHT;
if (this.onBounding) {
this.onBounding(out);
}
return out;
};
/**
* checks if a point is inside the shape of a node
* @method isPointInside
* @param {number} x
* @param {number} y
* @return {boolean}
*/
LGraphNode.prototype.isPointInside = function(x, y, margin, skip_title) {
margin = margin || 0;
var margin_top = this.graph && this.graph.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT;
if (skip_title) {
margin_top = 0;
}
if (this.flags && this.flags.collapsed) {
//if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS)
if (
isInsideRectangle(
x,
y,
this.pos[0] - margin,
this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin,
(this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) +
2 * margin,
LiteGraph.NODE_TITLE_HEIGHT + 2 * margin
)
) {
return true;
}
} else if (
this.pos[0] - 4 - margin < x &&
this.pos[0] + this.size[0] + 4 + margin > x &&
this.pos[1] - margin_top - margin < y &&
this.pos[1] + this.size[1] + margin > y
) {
return true;
}
return false;
};
/**
* checks if a point is inside a node slot, and returns info about which slot
* @method getSlotInPosition
* @param {number} x
* @param {number} y
* @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] }
*/
LGraphNode.prototype.getSlotInPosition = function(x, y) {
//search for inputs
var link_pos = new Float32Array(2);
if (this.inputs) {
for (var i = 0, l = this.inputs.length; i < l; ++i) {
var input = this.inputs[i];
this.getConnectionPos(true, i, link_pos);
if (
isInsideRectangle(
x,
y,
link_pos[0] - 10,
link_pos[1] - 5,
20,
10
)
) {
return { input: input, slot: i, link_pos: link_pos };
}
}
}
if (this.outputs) {
for (var i = 0, l = this.outputs.length; i < l; ++i) {
var output = this.outputs[i];
this.getConnectionPos(false, i, link_pos);
if (
isInsideRectangle(
x,
y,
link_pos[0] - 10,
link_pos[1] - 5,
20,
10
)
) {
return { output: output, slot: i, link_pos: link_pos };
}
}
}
return null;
};
/**
* returns the input slot with a given name (used for dynamic slots), -1 if not found
* @method findInputSlot
* @param {string} name the name of the slot
* @param {boolean} returnObj if the obj itself wanted
* @return {number_or_object} the slot (-1 if not found)
*/
LGraphNode.prototype.findInputSlot = function(name, returnObj) {
if (!this.inputs) {
return -1;
}
for (var i = 0, l = this.inputs.length; i < l; ++i) {
if (name == this.inputs[i].name) {
return !returnObj ? i : this.inputs[i];
}
}
return -1;
};
/**
* returns the output slot with a given name (used for dynamic slots), -1 if not found
* @method findOutputSlot
* @param {string} name the name of the slot
* @param {boolean} returnObj if the obj itself wanted
* @return {number_or_object} the slot (-1 if not found)
*/
LGraphNode.prototype.findOutputSlot = function(name, returnObj) {
returnObj = returnObj || false;
if (!this.outputs) {
return -1;
}
for (var i = 0, l = this.outputs.length; i < l; ++i) {
if (name == this.outputs[i].name) {
return !returnObj ? i : this.outputs[i];
}
}
return -1;
};
// TODO refactor: USE SINGLE findInput/findOutput functions! :: merge options
/**
* returns the first free input slot
* @method findInputSlotFree
* @param {object} options
* @return {number_or_object} the slot (-1 if not found)
*/
LGraphNode.prototype.findInputSlotFree = function(optsIn) {
var optsIn = optsIn || {};
var optsDef = {returnObj: false
,typesNotAccepted: []
};
var opts = Object.assign(optsDef,optsIn);
if (!this.inputs) {
return -1;
}
for (var i = 0, l = this.inputs.length; i < l; ++i) {
if (this.inputs[i].link && this.inputs[i].link != null) {
continue;
}
if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.inputs[i].type)){
continue;
}
return !opts.returnObj ? i : this.inputs[i];
}
return -1;
};
/**
* returns the first output slot free
* @method findOutputSlotFree
* @param {object} options
* @return {number_or_object} the slot (-1 if not found)
*/
LGraphNode.prototype.findOutputSlotFree = function(optsIn) {
var optsIn = optsIn || {};
var optsDef = { returnObj: false
,typesNotAccepted: []
};
var opts = Object.assign(optsDef,optsIn);
if (!this.outputs) {
return -1;
}
for (var i = 0, l = this.outputs.length; i < l; ++i) {
if (this.outputs[i].links && this.outputs[i].links != null) {
continue;
}
if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.outputs[i].type)){
continue;
}
return !opts.returnObj ? i : this.outputs[i];
}
return -1;
};
/**
* findSlotByType for INPUTS
*/
LGraphNode.prototype.findInputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) {
return this.findSlotByType(true, type, returnObj, preferFreeSlot, doNotUseOccupied);
};
/**
* findSlotByType for OUTPUTS
*/
LGraphNode.prototype.findOutputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) {
return this.findSlotByType(false, type, returnObj, preferFreeSlot, doNotUseOccupied);
};
/**
* returns the output (or input) slot with a given type, -1 if not found
* @method findSlotByType
* @param {boolean} input uise inputs instead of outputs
* @param {string} type the type of the slot
* @param {boolean} returnObj if the obj itself wanted
* @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway)
* @return {number_or_object} the slot (-1 if not found)
*/
LGraphNode.prototype.findSlotByType = function(input, type, returnObj, preferFreeSlot, doNotUseOccupied) {
input = input || false;
returnObj = returnObj || false;
preferFreeSlot = preferFreeSlot || false;
doNotUseOccupied = doNotUseOccupied || false;
var aSlots = input ? this.inputs : this.outputs;
if (!aSlots) {
return -1;
}
// !! empty string type is considered 0, * !!
if (type == "" || type == "*") type = 0;
for (var i = 0, l = aSlots.length; i < l; ++i) {
var tFound = false;
var aSource = (type+"").toLowerCase().split(",");
var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type;
aDest = (aDest+"").toLowerCase().split(",");
for(var sI=0;sI<aSource.length;sI++){
for(var dI=0;dI<aDest.length;dI++){
if (aSource[sI]=="_event_") aSource[sI] = LiteGraph.EVENT;
if (aDest[sI]=="_event_") aDest[sI] = LiteGraph.EVENT;
if (aSource[sI]=="*") aSource[sI] = 0;
if (aDest[sI]=="*") aDest[sI] = 0;
if (aSource[sI] == aDest[dI]) {
if (preferFreeSlot && aSlots[i].links && aSlots[i].links !== null) continue;
return !returnObj ? i : aSlots[i];
}
}
}
}
// if didnt find some, stop checking for free slots
if (preferFreeSlot && !doNotUseOccupied){
for (var i = 0, l = aSlots.length; i < l; ++i) {
var tFound = false;
var aSource = (type+"").toLowerCase().split(",");
var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type;
aDest = (aDest+"").toLowerCase().split(",");
for(var sI=0;sI<aSource.length;sI++){
for(var dI=0;dI<aDest.length;dI++){
if (aSource[sI]=="*") aSource[sI] = 0;
if (aDest[sI]=="*") aDest[sI] = 0;
if (aSource[sI] == aDest[dI]) {
return !returnObj ? i : aSlots[i];
}
}
}
}
}
return -1;
};
/**
* connect this node output to the input of another node BY TYPE
* @method connectByType
* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
* @param {LGraphNode} node the target node
* @param {string} target_type the input slot type of the target node
* @return {Object} the link_info is created, otherwise null
*/
LGraphNode.prototype.connectByType = function(slot, target_node, target_slotType, optsIn) {
var optsIn = optsIn || {};
var optsDef = { createEventInCase: true
,firstFreeIfOutputGeneralInCase: true
,generalTypeInCase: true
};
var opts = Object.assign(optsDef,optsIn);
if (target_node && target_node.constructor === Number) {
target_node = this.graph.getNodeById(target_node);
}
var target_slot = target_node.findInputSlotByType(target_slotType, false, true);
if (target_slot >= 0 && target_slot !== null){
//console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot)
return this.connect(slot, target_node, target_slot);
}else{
//console.log("type "+target_slotType+" not found or not free?")
if (opts.createEventInCase && target_slotType == LiteGraph.EVENT){
// WILL CREATE THE onTrigger IN SLOT
//console.debug("connect WILL CREATE THE onTrigger "+target_slotType+" to "+target_node);
return this.connect(slot, target_node, -1);
}
// connect to the first general output slot if not found a specific type and
if (opts.generalTypeInCase){
var target_slot = target_node.findInputSlotByType(0, false, true, true);
//console.debug("connect TO a general type (*, 0), if not found the specific type ",target_slotType," to ",target_node,"RES_SLOT:",target_slot);
if (target_slot >= 0){
return this.connect(slot, target_node, target_slot);
}
}
// connect to the first free input slot if not found a specific type and this output is general
if (opts.firstFreeIfOutputGeneralInCase && (target_slotType == 0 || target_slotType == "*" || target_slotType == "")){
var target_slot = target_node.findInputSlotFree({typesNotAccepted: [LiteGraph.EVENT] });
//console.debug("connect TO TheFirstFREE ",target_slotType," to ",target_node,"RES_SLOT:",target_slot);
if (target_slot >= 0){
return this.connect(slot, target_node, target_slot);
}
}
console.debug("no way to connect type: ",target_slotType," to targetNODE ",target_node);
//TODO filter
return null;
}
}
/**
* connect this node input to the output of another node BY TYPE
* @method connectByType
* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
* @param {LGraphNode} node the target node
* @param {string} target_type the output slot type of the target node
* @return {Object} the link_info is created, otherwise null
*/
LGraphNode.prototype.connectByTypeOutput = function(slot, source_node, source_slotType, optsIn) {
var optsIn = optsIn || {};
var optsDef = { createEventInCase: true
,firstFreeIfInputGeneralInCase: true
,generalTypeInCase: true
};
var opts = Object.assign(optsDef,optsIn);
if (source_node && source_node.constructor === Number) {
source_node = this.graph.getNodeById(source_node);
}
var source_slot = source_node.findOutputSlotByType(source_slotType, false, true);
if (source_slot >= 0 && source_slot !== null){
//console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot)
return source_node.connect(source_slot, this, slot);
}else{
// connect to the first general output slot if not found a specific type and
if (opts.generalTypeInCase){
var source_slot = source_node.findOutputSlotByType(0, false, true, true);
if (source_slot >= 0){
return source_node.connect(source_slot, this, slot);
}
}
if (opts.createEventInCase && source_slotType == LiteGraph.EVENT){
// WILL CREATE THE onExecuted OUT SLOT
if (LiteGraph.do_add_triggers_slots){
var source_slot = source_node.addOnExecutedOutput();
return source_node.connect(source_slot, this, slot);
}
}
// connect to the first free output slot if not found a specific type and this input is general
if (opts.firstFreeIfInputGeneralInCase && (source_slotType == 0 || source_slotType == "*" || source_slotType == "")){
var source_slot = source_node.findOutputSlotFree({typesNotAccepted: [LiteGraph.EVENT] });
if (source_slot >= 0){
return source_node.connect(source_slot, this, slot);
}
}
console.debug("no way to connect byOUT type: ",source_slotType," to sourceNODE ",source_node);
//TODO filter
//console.log("type OUT! "+source_slotType+" not found or not free?")
return null;
}
}
/**
* connect this node output to the input of another node
* @method connect
* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
* @param {LGraphNode} node the target node
* @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
* @return {Object} the link_info is created, otherwise null
*/
LGraphNode.prototype.connect = function(slot, target_node, target_slot) {
target_slot = target_slot || 0;
if (!this.graph) {
//could be connected before adding it to a graph
console.log(
"Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them."
); //due to link ids being associated with graphs
return null;
}
//seek for the output slot
if (slot.constructor === String) {
slot = this.findOutputSlot(slot);
if (slot == -1) {
if (LiteGraph.debug) {
console.log("Connect: Error, no slot of name " + slot);
}
return null;
}
} else if (!this.outputs || slot >= this.outputs.length) {
if (LiteGraph.debug) {
console.log("Connect: Error, slot number not found");
}
return null;
}
if (target_node && target_node.constructor === Number) {
target_node = this.graph.getNodeById(target_node);
}
if (!target_node) {
throw "target node is null";
}
//avoid loopback
if (target_node == this) {
return null;
}
//you can specify the slot by name
if (target_slot.constructor === String) {
target_slot = target_node.findInputSlot(target_slot);
if (target_slot == -1) {
if (LiteGraph.debug) {
console.log(
"Connect: Error, no slot of name " + target_slot
);
}
return null;
}
} else if (target_slot === LiteGraph.EVENT) {
if (LiteGraph.do_add_triggers_slots){
//search for first slot with event? :: NO this is done outside
//console.log("Connect: Creating triggerEvent");
// force mode
target_node.changeMode(LiteGraph.ON_TRIGGER);
target_slot = target_node.findInputSlot("onTrigger");
}else{
return null; // -- break --
}
} else if (
!target_node.inputs ||
target_slot >= target_node.inputs.length
) {
if (LiteGraph.debug) {
console.log("Connect: Error, slot number not found");
}
return null;
}
var changed = false;
var input = target_node.inputs[target_slot];
var link_info = null;
var output = this.outputs[slot];
if (!this.outputs[slot]){
/*console.debug("Invalid slot passed: "+slot);
console.debug(this.outputs);*/
return null;
}
// allow target node to change slot
if (target_node.onBeforeConnectInput) {
// This way node can choose another slot (or make a new one?)
target_slot = target_node.onBeforeConnectInput(target_slot); //callback
}
//check target_slot and check connection types
if (target_slot===false || target_slot===null || !LiteGraph.isValidConnection(output.type, input.type))
{
this.setDirtyCanvas(false, true);
if(changed)
this.graph.connectionChange(this, link_info);
return null;
}else{
//console.debug("valid connection",output.type, input.type);
}
//allows nodes to block connection, callback
if (target_node.onConnectInput) {
if ( target_node.onConnectInput(target_slot, output.type, output, this, slot) === false ) {
return null;
}
}
if (this.onConnectOutput) { // callback
if ( this.onConnectOutput(slot, input.type, input, target_node, target_slot) === false ) {
return null;
}
}
//if there is something already plugged there, disconnect
if (target_node.inputs[target_slot] && target_node.inputs[target_slot].link != null) {
this.graph.beforeChange();
target_node.disconnectInput(target_slot, {doProcessChange: false});
changed = true;
}
if (output.links !== null && output.links.length){
switch(output.type){
case LiteGraph.EVENT:
if (!LiteGraph.allow_multi_output_for_events){
this.graph.beforeChange();
this.disconnectOutput(slot, false, {doProcessChange: false}); // Input(target_slot, {doProcessChange: false});
changed = true;
}
break;
default:
break;
}
}
var nextId
if (LiteGraph.use_uuids)
nextId = LiteGraph.uuidv4();
else
nextId = ++this.graph.last_link_id;
//create link class
link_info = new LLink(
nextId,
input.type || output.type,
this.id,
slot,
target_node.id,
target_slot
);
//add to graph links list
this.graph.links[link_info.id] = link_info;
//connect in output
if (output.links == null) {
output.links = [];
}
output.links.push(link_info.id);
//connect in input
target_node.inputs[target_slot].link = link_info.id;
if (this.graph) {
this.graph._version++;
}
if (this.onConnectionsChange) {
this.onConnectionsChange(
LiteGraph.OUTPUT,
slot,
true,
link_info,
output
);
} //link_info has been created now, so its updated
if (target_node.onConnectionsChange) {
target_node.onConnectionsChange(
LiteGraph.INPUT,
target_slot,
true,
link_info,
input
);
}
if (this.graph && this.graph.onNodeConnectionChange) {
this.graph.onNodeConnectionChange(
LiteGraph.INPUT,
target_node,
target_slot,
this,
slot
);
this.graph.onNodeConnectionChange(
LiteGraph.OUTPUT,
this,
slot,
target_node,
target_slot
);
}
this.setDirtyCanvas(false, true);
this.graph.afterChange();
this.graph.connectionChange(this, link_info);
return link_info;
};
/**
* disconnect one output to an specific node
* @method disconnectOutput
* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
* @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected]
* @return {boolean} if it was disconnected successfully
*/
LGraphNode.prototype.disconnectOutput = function(slot, target_node) {
if (slot.constructor === String) {
slot = this.findOutputSlot(slot);
if (slot == -1) {
if (LiteGraph.debug) {
console.log("Connect: Error, no slot of name " + slot);
}
return false;
}
} else if (!this.outputs || slot >= this.outputs.length) {
if (LiteGraph.debug) {
console.log("Connect: Error, slot number not found");
}
return false;
}
//get output slot
var output = this.outputs[slot];
if (!output || !output.links || output.links.length == 0) {
return false;
}
//one of the output links in this slot
if (target_node) {
if (target_node.constructor === Number) {
target_node = this.graph.getNodeById(target_node);
}
if (!target_node) {
throw "Target Node not found";
}
for (var i = 0, l = output.links.length; i < l; i++) {
var link_id = output.links[i];
var link_info = this.graph.links[link_id];
//is the link we are searching for...
if (link_info.target_id == target_node.id) {
output.links.splice(i, 1); //remove here
var input = target_node.inputs[link_info.target_slot];
input.link = null; //remove there
delete this.graph.links[link_id]; //remove the link from the links pool
if (this.graph) {
this.graph._version++;
}
if (target_node.onConnectionsChange) {
target_node.onConnectionsChange(
LiteGraph.INPUT,
link_info.target_slot,
false,
link_info,
input
);
} //link_info hasn't been modified so its ok
if (this.onConnectionsChange) {
this.onConnectionsChange(
LiteGraph.OUTPUT,
slot,
false,
link_info,
output
);
}
if (this.graph && this.graph.onNodeConnectionChange) {
this.graph.onNodeConnectionChange(
LiteGraph.OUTPUT,
this,
slot
);
}
if (this.graph && this.graph.onNodeConnectionChange) {
this.graph.onNodeConnectionChange(
LiteGraph.OUTPUT,
this,
slot
);
this.graph.onNodeConnectionChange(
LiteGraph.INPUT,
target_node,
link_info.target_slot
);
}
break;
}
}
} //all the links in this output slot
else {
for (var i = 0, l = output.links.length; i < l; i++) {
var link_id = output.links[i];
var link_info = this.graph.links[link_id];
if (!link_info) {
//bug: it happens sometimes
continue;
}
var target_node = this.graph.getNodeById(link_info.target_id);
var input = null;
if (this.graph) {
this.graph._version++;
}
if (target_node) {
input = target_node.inputs[link_info.target_slot];
input.link = null; //remove other side link
if (target_node.onConnectionsChange) {
target_node.onConnectionsChange(
LiteGraph.INPUT,
link_info.target_slot,
false,
link_info,
input
);
} //link_info hasn't been modified so its ok
if (this.graph && this.graph.onNodeConnectionChange) {
this.graph.onNodeConnectionChange(
LiteGraph.INPUT,
target_node,
link_info.target_slot
);
}
}
delete this.graph.links[link_id]; //remove the link from the links pool
if (this.onConnectionsChange) {
this.onConnectionsChange(
LiteGraph.OUTPUT,
slot,
false,
link_info,
output
);
}
if (this.graph && this.graph.onNodeConnectionChange) {
this.graph.onNodeConnectionChange(
LiteGraph.OUTPUT,
this,
slot
);
this.graph.onNodeConnectionChange(
LiteGraph.INPUT,
target_node,
link_info.target_slot
);
}
}
output.links = null;
}
this.setDirtyCanvas(false, true);
this.graph.connectionChange(this);
return true;
};
/**
* disconnect one input
* @method disconnectInput
* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
* @return {boolean} if it was disconnected successfully
*/
LGraphNode.prototype.disconnectInput = function(slot) {
//seek for the output slot
if (slot.constructor === String) {
slot = this.findInputSlot(slot);
if (slot == -1) {
if (LiteGraph.debug) {
console.log("Connect: Error, no slot of name " + slot);
}
return false;
}
} else if (!this.inputs || slot >= this.inputs.length) {
if (LiteGraph.debug) {
console.log("Connect: Error, slot number not found");
}
return false;
}
var input = this.inputs[slot];
if (!input) {
return false;
}
var link_id = this.inputs[slot].link;
if(link_id != null)
{
this.inputs[slot].link = null;
//remove other side
var link_info = this.graph.links[link_id];
if (link_info) {
var target_node = this.graph.getNodeById(link_info.origin_id);
if (!target_node) {
return false;
}
var output = target_node.outputs[link_info.origin_slot];
if (!output || !output.links || output.links.length == 0) {
return false;
}
//search in the inputs list for this link
for (var i = 0, l = output.links.length; i < l; i++) {
if (output.links[i] == link_id) {
output.links.splice(i, 1);
break;
}
}
delete this.graph.links[link_id]; //remove from the pool
if (this.graph) {
this.graph._version++;
}
if (this.onConnectionsChange) {
this.onConnectionsChange(
LiteGraph.INPUT,
slot,
false,
link_info,
input
);
}
if (target_node.onConnectionsChange) {
target_node.onConnectionsChange(
LiteGraph.OUTPUT,
i,
false,
link_info,
output
);
}
if (this.graph && this.graph.onNodeConnectionChange) {
this.graph.onNodeConnectionChange(
LiteGraph.OUTPUT,
target_node,
i
);
this.graph.onNodeConnectionChange(LiteGraph.INPUT, this, slot);
}
}
} //link != null
this.setDirtyCanvas(false, true);
if(this.graph)
this.graph.connectionChange(this);
return true;
};
/**
* returns the center of a connection point in canvas coords
* @method getConnectionPos
* @param {boolean} is_input true if if a input slot, false if it is an output
* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
* @param {vec2} out [optional] a place to store the output, to free garbage
* @return {[x,y]} the position
**/
LGraphNode.prototype.getConnectionPos = function(
is_input,
slot_number,
out
) {
out = out || new Float32Array(2);
var num_slots = 0;
if (is_input && this.inputs) {
num_slots = this.inputs.length;
}
if (!is_input && this.outputs) {
num_slots = this.outputs.length;
}
var offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5;
if (this.flags.collapsed) {
var w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
if (this.horizontal) {
out[0] = this.pos[0] + w * 0.5;
if (is_input) {
out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT;
} else {
out[1] = this.pos[1];
}
} else {
if (is_input) {
out[0] = this.pos[0];
} else {
out[0] = this.pos[0] + w;
}
out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
}
return out;
}
//weird feature that never got finished
if (is_input && slot_number == -1) {
out[0] = this.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * 0.5;
out[1] = this.pos[1] + LiteGraph.NODE_TITLE_HEIGHT * 0.5;
return out;
}
//hard-coded pos
if (
is_input &&
num_slots > slot_number &&
this.inputs[slot_number].pos
) {
out[0] = this.pos[0] + this.inputs[slot_number].pos[0];
out[1] = this.pos[1] + this.inputs[slot_number].pos[1];
return out;
} else if (
!is_input &&
num_slots > slot_number &&
this.outputs[slot_number].pos
) {
out[0] = this.pos[0] + this.outputs[slot_number].pos[0];
out[1] = this.pos[1] + this.outputs[slot_number].pos[1];
return out;
}
//horizontal distributed slots
if (this.horizontal) {
out[0] =
this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots);
if (is_input) {
out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT;
} else {
out[1] = this.pos[1] + this.size[1];
}
return out;
}
//default vertical slots
if (is_input) {
out[0] = this.pos[0] + offset;
} else {
out[0] = this.pos[0] + this.size[0] + 1 - offset;
}
out[1] =
this.pos[1] +
(slot_number + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
(this.constructor.slot_start_y || 0);
return out;
};
/* Force align to grid */
LGraphNode.prototype.alignToGrid = function() {
this.pos[0] =
LiteGraph.CANVAS_GRID_SIZE *
Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE);
this.pos[1] =
LiteGraph.CANVAS_GRID_SIZE *
Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE);
};
/* Console output */
LGraphNode.prototype.trace = function(msg) {
if (!this.console) {
this.console = [];
}
this.console.push(msg);
if (this.console.length > LGraphNode.MAX_CONSOLE) {
this.console.shift();
}
if(this.graph.onNodeTrace)
this.graph.onNodeTrace(this, msg);
};
/* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */
LGraphNode.prototype.setDirtyCanvas = function(
dirty_foreground,
dirty_background
) {
if (!this.graph) {
return;
}
this.graph.sendActionToCanvas("setDirty", [
dirty_foreground,
dirty_background
]);
};
LGraphNode.prototype.loadImage = function(url) {
var img = new Image();
img.src = LiteGraph.node_images_path + url;
img.ready = false;
var that = this;
img.onload = function() {
this.ready = true;
that.setDirtyCanvas(true);
};
return img;
};
//safe LGraphNode action execution (not sure if safe)
/*
LGraphNode.prototype.executeAction = function(action)
{
if(action == "") return false;
if( action.indexOf(";") != -1 || action.indexOf("}") != -1)
{
this.trace("Error: Action contains unsafe characters");
return false;
}
var tokens = action.split("(");
var func_name = tokens[0];
if( typeof(this[func_name]) != "function")
{
this.trace("Error: Action not found on node: " + func_name);
return false;
}
var code = action;
try
{
var _foo = eval;
eval = null;
(new Function("with(this) { " + code + "}")).call(this);
eval = _foo;
}
catch (err)
{
this.trace("Error executing action {" + action + "} :" + err);
return false;
}
return true;
}
*/
/* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */
LGraphNode.prototype.captureInput = function(v) {
if (!this.graph || !this.graph.list_of_graphcanvas) {
return;
}
var list = this.graph.list_of_graphcanvas;
for (var i = 0; i < list.length; ++i) {
var c = list[i];
//releasing somebody elses capture?!
if (!v && c.node_capturing_input != this) {
continue;
}
//change
c.node_capturing_input = v ? this : null;
}
};
/**
* Collapse the node to make it smaller on the canvas
* @method collapse
**/
LGraphNode.prototype.collapse = function(force) {
this.graph._version++;
if (this.constructor.collapsable === false && !force) {
return;
}
if (!this.flags.collapsed) {
this.flags.collapsed = true;
} else {
this.flags.collapsed = false;
}
this.setDirtyCanvas(true, true);
};
/**
* Forces the node to do not move or realign on Z
* @method pin
**/
LGraphNode.prototype.pin = function(v) {
this.graph._version++;
if (v === undefined) {
this.flags.pinned = !this.flags.pinned;
} else {
this.flags.pinned = v;
}
};
LGraphNode.prototype.localToScreen = function(x, y, graphcanvas) {
return [
(x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0],
(y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1]
];
};
function LGraphGroup(title) {
this._ctor(title);
}
global.LGraphGroup = LiteGraph.LGraphGroup = LGraphGroup;
LGraphGroup.prototype._ctor = function(title) {
this.title = title || "Group";
this.font_size = 24;
this.color = LGraphCanvas.node_colors.pale_blue
? LGraphCanvas.node_colors.pale_blue.groupcolor
: "#AAA";
this._bounding = new Float32Array([10, 10, 140, 80]);
this._pos = this._bounding.subarray(0, 2);
this._size = this._bounding.subarray(2, 4);
this._nodes = [];
this.graph = null;
Object.defineProperty(this, "pos", {
set: function(v) {
if (!v || v.length < 2) {
return;
}
this._pos[0] = v[0];
this._pos[1] = v[1];
},
get: function() {
return this._pos;
},
enumerable: true
});
Object.defineProperty(this, "size", {
set: function(v) {
if (!v || v.length < 2) {
return;
}
this._size[0] = Math.max(140, v[0]);
this._size[1] = Math.max(80, v[1]);
},
get: function() {
return this._size;
},
enumerable: true
});
};
LGraphGroup.prototype.configure = function(o) {
this.title = o.title;
this._bounding.set(o.bounding);
this.color = o.color;
this.font_size = o.font_size;
};
LGraphGroup.prototype.serialize = function() {
var b = this._bounding;
return {
title: this.title,
bounding: [
Math.round(b[0]),
Math.round(b[1]),
Math.round(b[2]),
Math.round(b[3])
],
color: this.color,
font_size: this.font_size
};
};
LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) {
this._pos[0] += deltax;
this._pos[1] += deltay;
if (ignore_nodes) {
return;
}
for (var i = 0; i < this._nodes.length; ++i) {
var node = this._nodes[i];
node.pos[0] += deltax;
node.pos[1] += deltay;
}
};
LGraphGroup.prototype.recomputeInsideNodes = function() {
this._nodes.length = 0;
var nodes = this.graph._nodes;
var node_bounding = new Float32Array(4);
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
node.getBounding(node_bounding);
if (!overlapBounding(this._bounding, node_bounding)) {
continue;
} //out of the visible area
this._nodes.push(node);
}
};
LGraphGroup.prototype.isPointInside = LGraphNode.prototype.isPointInside;
LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas;
//****************************************
//Scale and Offset
function DragAndScale(element, skip_events) {
this.offset = new Float32Array([0, 0]);
this.scale = 1;
this.max_scale = 10;
this.min_scale = 0.1;
this.onredraw = null;
this.enabled = true;
this.last_mouse = [0, 0];
this.element = null;
this.visible_area = new Float32Array(4);
if (element) {
this.element = element;
if (!skip_events) {
this.bindEvents(element);
}
}
}
LiteGraph.DragAndScale = DragAndScale;
DragAndScale.prototype.bindEvents = function(element) {
this.last_mouse = new Float32Array(2);
this._binded_mouse_callback = this.onMouse.bind(this);
LiteGraph.pointerListenerAdd(element,"down", this._binded_mouse_callback);
LiteGraph.pointerListenerAdd(element,"move", this._binded_mouse_callback);
LiteGraph.pointerListenerAdd(element,"up", this._binded_mouse_callback);
element.addEventListener(
"mousewheel",
this._binded_mouse_callback,
false
);
element.addEventListener("wheel", this._binded_mouse_callback, false);
};
DragAndScale.prototype.computeVisibleArea = function( viewport ) {
if (!this.element) {
this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0;
return;
}
var width = this.element.width;
var height = this.element.height;
var startx = -this.offset[0];
var starty = -this.offset[1];
if( viewport )
{
startx += viewport[0] / this.scale;
starty += viewport[1] / this.scale;
width = viewport[2];
height = viewport[3];
}
var endx = startx + width / this.scale;
var endy = starty + height / this.scale;
this.visible_area[0] = startx;
this.visible_area[1] = starty;
this.visible_area[2] = endx - startx;
this.visible_area[3] = endy - starty;
};
DragAndScale.prototype.onMouse = function(e) {
if (!this.enabled) {
return;
}
var canvas = this.element;
var rect = canvas.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
e.canvasx = x;
e.canvasy = y;
e.dragging = this.dragging;
var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) );
//console.log("pointerevents: DragAndScale onMouse "+e.type+" "+is_inside);
var ignore = false;
if (this.onmouse) {
ignore = this.onmouse(e);
}
if (e.type == LiteGraph.pointerevents_method+"down" && is_inside) {
this.dragging = true;
LiteGraph.pointerListenerRemove(canvas,"move",this._binded_mouse_callback);
LiteGraph.pointerListenerAdd(document,"move",this._binded_mouse_callback);
LiteGraph.pointerListenerAdd(document,"up",this._binded_mouse_callback);
} else if (e.type == LiteGraph.pointerevents_method+"move") {
if (!ignore) {
var deltax = x - this.last_mouse[0];
var deltay = y - this.last_mouse[1];
if (this.dragging) {
this.mouseDrag(deltax, deltay);
}
}
} else if (e.type == LiteGraph.pointerevents_method+"up") {
this.dragging = false;
LiteGraph.pointerListenerRemove(document,"move",this._binded_mouse_callback);
LiteGraph.pointerListenerRemove(document,"up",this._binded_mouse_callback);
LiteGraph.pointerListenerAdd(canvas,"move",this._binded_mouse_callback);
} else if ( is_inside &&
(e.type == "mousewheel" ||
e.type == "wheel" ||
e.type == "DOMMouseScroll")
) {
e.eventType = "mousewheel";
if (e.type == "wheel") {
e.wheel = -e.deltaY;
} else {
e.wheel =
e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60;
}
//from stack overflow
e.delta = e.wheelDelta
? e.wheelDelta / 40
: e.deltaY
? -e.deltaY / 3
: 0;
this.changeDeltaScale(1.0 + e.delta * 0.05);
}
this.last_mouse[0] = x;
this.last_mouse[1] = y;
if(is_inside)
{
e.preventDefault();
e.stopPropagation();
return false;
}
};
DragAndScale.prototype.toCanvasContext = function(ctx) {
ctx.scale(this.scale, this.scale);
ctx.translate(this.offset[0], this.offset[1]);
};
DragAndScale.prototype.convertOffsetToCanvas = function(pos) {
//return [pos[0] / this.scale - this.offset[0], pos[1] / this.scale - this.offset[1]];
return [
(pos[0] + this.offset[0]) * this.scale,
(pos[1] + this.offset[1]) * this.scale
];
};
DragAndScale.prototype.convertCanvasToOffset = function(pos, out) {
out = out || [0, 0];
out[0] = pos[0] / this.scale - this.offset[0];
out[1] = pos[1] / this.scale - this.offset[1];
return out;
};
DragAndScale.prototype.mouseDrag = function(x, y) {
this.offset[0] += x / this.scale;
this.offset[1] += y / this.scale;
if (this.onredraw) {
this.onredraw(this);
}
};
DragAndScale.prototype.changeScale = function(value, zooming_center) {
if (value < this.min_scale) {
value = this.min_scale;
} else if (value > this.max_scale) {
value = this.max_scale;
}
if (value == this.scale) {
return;
}
if (!this.element) {
return;
}
var rect = this.element.getBoundingClientRect();
if (!rect) {
return;
}
zooming_center = zooming_center || [
rect.width * 0.5,
rect.height * 0.5
];
var center = this.convertCanvasToOffset(zooming_center);
this.scale = value;
if (Math.abs(this.scale - 1) < 0.01) {
this.scale = 1;
}
var new_center = this.convertCanvasToOffset(zooming_center);
var delta_offset = [
new_center[0] - center[0],
new_center[1] - center[1]
];
this.offset[0] += delta_offset[0];
this.offset[1] += delta_offset[1];
if (this.onredraw) {
this.onredraw(this);
}
};
DragAndScale.prototype.changeDeltaScale = function(value, zooming_center) {
this.changeScale(this.scale * value, zooming_center);
};
DragAndScale.prototype.reset = function() {
this.scale = 1;
this.offset[0] = 0;
this.offset[1] = 0;
};
//*********************************************************************************
// LGraphCanvas: LGraph renderer CLASS
//*********************************************************************************
/**
* This class is in charge of rendering one graph inside a canvas. And provides all the interaction required.
* Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked
*
* @class LGraphCanvas
* @constructor
* @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself)
* @param {LGraph} graph [optional]
* @param {Object} options [optional] { skip_rendering, autoresize, viewport }
*/
function LGraphCanvas(canvas, graph, options) {
this.options = options = options || {};
//if(graph === undefined)
// throw ("No graph assigned");
this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE;
if (canvas && canvas.constructor === String) {
canvas = document.querySelector(canvas);
}
this.ds = new DragAndScale();
this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much
this.title_text_font = "" + LiteGraph.NODE_TEXT_SIZE + "px Arial";
this.inner_text_font =
"normal " + LiteGraph.NODE_SUBTEXT_SIZE + "px Arial";
this.node_title_color = LiteGraph.NODE_TITLE_COLOR;
this.default_link_color = LiteGraph.LINK_COLOR;
this.default_connection_color = {
input_off: "#778",
input_on: "#7F7", //"#BBD"
output_off: "#778",
output_on: "#7F7" //"#BBD"
};
this.default_connection_color_byType = {
/*number: "#7F7",
string: "#77F",
boolean: "#F77",*/
}
this.default_connection_color_byTypeOff = {
/*number: "#474",
string: "#447",
boolean: "#744",*/
};
this.highquality_render = true;
this.use_gradients = false; //set to true to render titlebar with gradients
this.editor_alpha = 1; //used for transition
this.pause_rendering = false;
this.clear_background = true;
this.clear_background_color = "#222";
this.read_only = false; //if set to true users cannot modify the graph
this.render_only_selected = true;
this.live_mode = false;
this.show_info = true;
this.allow_dragcanvas = true;
this.allow_dragnodes = true;
this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc
this.multi_select = false; //allow selecting multi nodes without pressing extra keys
this.allow_searchbox = true;
this.allow_reconnect_links = true; //allows to change a connection with having to redo it again
this.align_to_grid = false; //snap to grid
this.drag_mode = false;
this.dragging_rectangle = null;
this.filter = null; //allows to filter to only accept some type of nodes in a graph
this.set_canvas_dirty_on_mouse_event = true; //forces to redraw the canvas if the mouse does anything
this.always_render_background = false;
this.render_shadows = true;
this.render_canvas_border = true;
this.render_connections_shadows = false; //too much cpu
this.render_connections_border = true;
this.render_curved_connections = false;
this.render_connection_arrows = false;
this.render_collapsed_slots = true;
this.render_execution_order = false;
this.render_title_colored = true;
this.render_link_tooltip = true;
this.links_render_mode = LiteGraph.SPLINE_LINK;
this.mouse = [0, 0]; //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle
this.graph_mouse = [0, 0]; //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle
this.canvas_mouse = this.graph_mouse; //LEGACY: REMOVE THIS, USE GRAPH_MOUSE INSTEAD
//to personalize the search box
this.onSearchBox = null;
this.onSearchBoxSelection = null;
//callbacks
this.onMouse = null;
this.onDrawBackground = null; //to render background objects (behind nodes and connections) in the canvas affected by transform
this.onDrawForeground = null; //to render foreground objects (above nodes and connections) in the canvas affected by transform
this.onDrawOverlay = null; //to render foreground objects not affected by transform (for GUIs)
this.onDrawLinkTooltip = null; //called when rendering a tooltip
this.onNodeMoved = null; //called after moving a node
this.onSelectionChange = null; //called if the selection changes
this.onConnectingChange = null; //called before any link changes
this.onBeforeChange = null; //called before modifying the graph
this.onAfterChange = null; //called after modifying the graph
this.connections_width = 3;
this.round_radius = 8;
this.current_node = null;
this.node_widget = null; //used for widgets
this.over_link_center = null;
this.last_mouse_position = [0, 0];
this.visible_area = this.ds.visible_area;
this.visible_links = [];
this.viewport = options.viewport || null; //to constraint render area to a portion of the canvas
//link canvas and graph
if (graph) {
graph.attachCanvas(this);
}
this.setCanvas(canvas,options.skip_events);
this.clear();
if (!options.skip_render) {
this.startRendering();
}
this.autoresize = options.autoresize;
}
global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas;
LGraphCanvas.DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=";
LGraphCanvas.link_type_colors = {
"-1": LiteGraph.EVENT_LINK_COLOR,
number: "#AAA",
node: "#DCA"
};
LGraphCanvas.gradients = {}; //cache of gradients
/**
* clears all the data inside
*
* @method clear
*/
LGraphCanvas.prototype.clear = function() {
this.frame = 0;
this.last_draw_time = 0;
this.render_time = 0;
this.fps = 0;
//this.scale = 1;
//this.offset = [0,0];
this.dragging_rectangle = null;
this.selected_nodes = {};
this.selected_group = null;
this.visible_nodes = [];
this.node_dragged = null;
this.node_over = null;
this.node_capturing_input = null;
this.connecting_node = null;
this.highlighted_links = {};
this.dragging_canvas = false;
this.dirty_canvas = true;
this.dirty_bgcanvas = true;
this.dirty_area = null;
this.node_in_panel = null;
this.node_widget = null;
this.last_mouse = [0, 0];
this.last_mouseclick = 0;
this.pointer_is_down = false;
this.pointer_is_double = false;
this.visible_area.set([0, 0, 0, 0]);
if (this.onClear) {
this.onClear();
}
};
/**
* assigns a graph, you can reassign graphs to the same canvas
*
* @method setGraph
* @param {LGraph} graph
*/
LGraphCanvas.prototype.setGraph = function(graph, skip_clear) {
if (this.graph == graph) {
return;
}
if (!skip_clear) {
this.clear();
}
if (!graph && this.graph) {
this.graph.detachCanvas(this);
return;
}
graph.attachCanvas(this);
//remove the graph stack in case a subgraph was open
if (this._graph_stack)
this._graph_stack = null;
this.setDirty(true, true);
};
/**
* returns the top level graph (in case there are subgraphs open on the canvas)
*
* @method getTopGraph
* @return {LGraph} graph
*/
LGraphCanvas.prototype.getTopGraph = function()
{
if(this._graph_stack.length)
return this._graph_stack[0];
return this.graph;
}
/**
* opens a graph contained inside a node in the current graph
*
* @method openSubgraph
* @param {LGraph} graph
*/
LGraphCanvas.prototype.openSubgraph = function(graph) {
if (!graph) {
throw "graph cannot be null";
}
if (this.graph == graph) {
throw "graph cannot be the same";
}
this.clear();
if (this.graph) {
if (!this._graph_stack) {
this._graph_stack = [];
}
this._graph_stack.push(this.graph);
}
graph.attachCanvas(this);
this.checkPanels();
this.setDirty(true, true);
};
/**
* closes a subgraph contained inside a node
*
* @method closeSubgraph
* @param {LGraph} assigns a graph
*/
LGraphCanvas.prototype.closeSubgraph = function() {
if (!this._graph_stack || this._graph_stack.length == 0) {
return;
}
var subgraph_node = this.graph._subgraph_node;
var graph = this._graph_stack.pop();
this.selected_nodes = {};
this.highlighted_links = {};
graph.attachCanvas(this);
this.setDirty(true, true);
if (subgraph_node) {
this.centerOnNode(subgraph_node);
this.selectNodes([subgraph_node]);
}
// when close sub graph back to offset [0, 0] scale 1
this.ds.offset = [0, 0]
this.ds.scale = 1
};
/**
* returns the visually active graph (in case there are more in the stack)
* @method getCurrentGraph
* @return {LGraph} the active graph
*/
LGraphCanvas.prototype.getCurrentGraph = function() {
return this.graph;
};
/**
* assigns a canvas
*
* @method setCanvas
* @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector)
*/
LGraphCanvas.prototype.setCanvas = function(canvas, skip_events) {
var that = this;
if (canvas) {
if (canvas.constructor === String) {
canvas = document.getElementById(canvas);
if (!canvas) {
throw "Error creating LiteGraph canvas: Canvas not found";
}
}
}
if (canvas === this.canvas) {
return;
}
if (!canvas && this.canvas) {
//maybe detach events from old_canvas
if (!skip_events) {
this.unbindEvents();
}
}
this.canvas = canvas;
this.ds.element = canvas;
if (!canvas) {
return;
}
//this.canvas.tabindex = "1000";
canvas.className += " lgraphcanvas";
canvas.data = this;
canvas.tabindex = "1"; //to allow key events
//bg canvas: used for non changing stuff
this.bgcanvas = null;
if (!this.bgcanvas) {
this.bgcanvas = document.createElement("canvas");
this.bgcanvas.width = this.canvas.width;
this.bgcanvas.height = this.canvas.height;
}
if (canvas.getContext == null) {
if (canvas.localName != "canvas") {
throw "Element supplied for LGraphCanvas must be a <canvas> element, you passed a " +
canvas.localName;
}
throw "This browser doesn't support Canvas";
}
var ctx = (this.ctx = canvas.getContext("2d"));
if (ctx == null) {
if (!canvas.webgl_enabled) {
console.warn(
"This canvas seems to be WebGL, enabling WebGL renderer"
);
}
this.enableWebGL();
}
//input: (move and up could be unbinded)
// why here? this._mousemove_callback = this.processMouseMove.bind(this);
// why here? this._mouseup_callback = this.processMouseUp.bind(this);
if (!skip_events) {
this.bindEvents();
}
};
//used in some events to capture them
LGraphCanvas.prototype._doNothing = function doNothing(e) {
//console.log("pointerevents: _doNothing "+e.type);
e.preventDefault();
return false;
};
LGraphCanvas.prototype._doReturnTrue = function doNothing(e) {
e.preventDefault();
return true;
};
/**
* binds mouse, keyboard, touch and drag events to the canvas
* @method bindEvents
**/
LGraphCanvas.prototype.bindEvents = function() {
if (this._events_binded) {
console.warn("LGraphCanvas: events already binded");
return;
}
//console.log("pointerevents: bindEvents");
var canvas = this.canvas;
var ref_window = this.getCanvasWindow();
var document = ref_window.document; //hack used when moving canvas between windows
this._mousedown_callback = this.processMouseDown.bind(this);
this._mousewheel_callback = this.processMouseWheel.bind(this);
// why mousemove and mouseup were not binded here?
this._mousemove_callback = this.processMouseMove.bind(this);
this._mouseup_callback = this.processMouseUp.bind(this);
//touch events -- TODO IMPLEMENT
//this._touch_callback = this.touchHandler.bind(this);
LiteGraph.pointerListenerAdd(canvas,"down", this._mousedown_callback, true); //down do not need to store the binded
canvas.addEventListener("mousewheel", this._mousewheel_callback, false);
LiteGraph.pointerListenerAdd(canvas,"up", this._mouseup_callback, true); // CHECK: ??? binded or not
LiteGraph.pointerListenerAdd(canvas,"move", this._mousemove_callback);
canvas.addEventListener("contextmenu", this._doNothing);
canvas.addEventListener(
"DOMMouseScroll",
this._mousewheel_callback,
false
);
//touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents
/*if( 'touchstart' in document.documentElement )
{
canvas.addEventListener("touchstart", this._touch_callback, true);
canvas.addEventListener("touchmove", this._touch_callback, true);
canvas.addEventListener("touchend", this._touch_callback, true);
canvas.addEventListener("touchcancel", this._touch_callback, true);
}*/
//Keyboard ******************
this._key_callback = this.processKey.bind(this);
canvas.addEventListener("keydown", this._key_callback, true);
document.addEventListener("keyup", this._key_callback, true); //in document, otherwise it doesn't fire keyup
//Dropping Stuff over nodes ************************************
this._ondrop_callback = this.processDrop.bind(this);
canvas.addEventListener("dragover", this._doNothing, false);
canvas.addEventListener("dragend", this._doNothing, false);
canvas.addEventListener("drop", this._ondrop_callback, false);
canvas.addEventListener("dragenter", this._doReturnTrue, false);
this._events_binded = true;
};
/**
* unbinds mouse events from the canvas
* @method unbindEvents
**/
LGraphCanvas.prototype.unbindEvents = function() {
if (!this._events_binded) {
console.warn("LGraphCanvas: no events binded");
return;
}
//console.log("pointerevents: unbindEvents");
var ref_window = this.getCanvasWindow();
var document = ref_window.document;
LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousedown_callback);
LiteGraph.pointerListenerRemove(this.canvas,"up", this._mousedown_callback);
LiteGraph.pointerListenerRemove(this.canvas,"down", this._mousedown_callback);
this.canvas.removeEventListener(
"mousewheel",
this._mousewheel_callback
);
this.canvas.removeEventListener(
"DOMMouseScroll",
this._mousewheel_callback
);
this.canvas.removeEventListener("keydown", this._key_callback);
document.removeEventListener("keyup", this._key_callback);
this.canvas.removeEventListener("contextmenu", this._doNothing);
this.canvas.removeEventListener("drop", this._ondrop_callback);
this.canvas.removeEventListener("dragenter", this._doReturnTrue);
//touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents
/*this.canvas.removeEventListener("touchstart", this._touch_callback );
this.canvas.removeEventListener("touchmove", this._touch_callback );
this.canvas.removeEventListener("touchend", this._touch_callback );
this.canvas.removeEventListener("touchcancel", this._touch_callback );*/
this._mousedown_callback = null;
this._mousewheel_callback = null;
this._key_callback = null;
this._ondrop_callback = null;
this._events_binded = false;
};
LGraphCanvas.getFileExtension = function(url) {
var question = url.indexOf("?");
if (question != -1) {
url = url.substr(0, question);
}
var point = url.lastIndexOf(".");
if (point == -1) {
return "";
}
return url.substr(point + 1).toLowerCase();
};
/**
* this function allows to render the canvas using WebGL instead of Canvas2D
* this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL
* @method enableWebGL
**/
LGraphCanvas.prototype.enableWebGL = function() {
if (typeof GL === "undefined") {
throw "litegl.js must be included to use a WebGL canvas";
}
if (typeof enableWebGLCanvas === "undefined") {
throw "webglCanvas.js must be included to use this feature";
}
this.gl = this.ctx = enableWebGLCanvas(this.canvas);
this.ctx.webgl = true;
this.bgcanvas = this.canvas;
this.bgctx = this.gl;
this.canvas.webgl_enabled = true;
/*
GL.create({ canvas: this.bgcanvas });
this.bgctx = enableWebGLCanvas( this.bgcanvas );
window.gl = this.gl;
*/
};
/**
* marks as dirty the canvas, this way it will be rendered again
*
* @class LGraphCanvas
* @method setDirty
* @param {bool} fgcanvas if the foreground canvas is dirty (the one containing the nodes)
* @param {bool} bgcanvas if the background canvas is dirty (the one containing the wires)
*/
LGraphCanvas.prototype.setDirty = function(fgcanvas, bgcanvas) {
if (fgcanvas) {
this.dirty_canvas = true;
}
if (bgcanvas) {
this.dirty_bgcanvas = true;
}
};
/**
* Used to attach the canvas in a popup
*
* @method getCanvasWindow
* @return {window} returns the window where the canvas is attached (the DOM root node)
*/
LGraphCanvas.prototype.getCanvasWindow = function() {
if (!this.canvas) {
return window;
}
var doc = this.canvas.ownerDocument;
return doc.defaultView || doc.parentWindow;
};
/**
* starts rendering the content of the canvas when needed
*
* @method startRendering
*/
LGraphCanvas.prototype.startRendering = function() {
if (this.is_rendering) {
return;
} //already rendering
this.is_rendering = true;
renderFrame.call(this);
function renderFrame() {
if (!this.pause_rendering) {
this.draw();
}
var window = this.getCanvasWindow();
if (this.is_rendering) {
window.requestAnimationFrame(renderFrame.bind(this));
}
}
};
/**
* stops rendering the content of the canvas (to save resources)
*
* @method stopRendering
*/
LGraphCanvas.prototype.stopRendering = function() {
this.is_rendering = false;
/*
if(this.rendering_timer_id)
{
clearInterval(this.rendering_timer_id);
this.rendering_timer_id = null;
}
*/
};
/* LiteGraphCanvas input */
//used to block future mouse events (because of im gui)
LGraphCanvas.prototype.blockClick = function()
{
this.block_click = true;
this.last_mouseclick = 0;
}
LGraphCanvas.prototype.processMouseDown = function(e) {
if( this.set_canvas_dirty_on_mouse_event )
this.dirty_canvas = true;
if (!this.graph) {
return;
}
this.adjustMouseEvent(e);
var ref_window = this.getCanvasWindow();
var document = ref_window.document;
LGraphCanvas.active_canvas = this;
var that = this;
var x = e.clientX;
var y = e.clientY;
//console.log(y,this.viewport);
//console.log("pointerevents: processMouseDown pointerId:"+e.pointerId+" which:"+e.which+" isPrimary:"+e.isPrimary+" :: x y "+x+" "+y);
this.ds.viewport = this.viewport;
var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) );
//move mouse move event to the window in case it drags outside of the canvas
if(!this.options.skip_events)
{
LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousemove_callback);
LiteGraph.pointerListenerAdd(ref_window.document,"move", this._mousemove_callback,true); //catch for the entire window
LiteGraph.pointerListenerAdd(ref_window.document,"up", this._mouseup_callback,true);
}
if(!is_inside){
return;
}
var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes, 5 );
var skip_dragging = false;
var skip_action = false;
var now = LiteGraph.getTime();
var is_primary = (e.isPrimary === undefined || !e.isPrimary);
var is_double_click = (now - this.last_mouseclick < 300) && is_primary;
this.mouse[0] = e.clientX;
this.mouse[1] = e.clientY;
this.graph_mouse[0] = e.canvasX;
this.graph_mouse[1] = e.canvasY;
this.last_click_position = [this.mouse[0],this.mouse[1]];
if (this.pointer_is_down && is_primary ){
this.pointer_is_double = true;
//console.log("pointerevents: pointer_is_double start");
}else{
this.pointer_is_double = false;
}
this.pointer_is_down = true;
this.canvas.focus();
LiteGraph.closeAllContextMenus(ref_window);
if (this.onMouse)
{
if (this.onMouse(e) == true)
return;
}
//left button mouse / single finger
if (e.which == 1 && !this.pointer_is_double)
{
if (e.ctrlKey)
{
this.dragging_rectangle = new Float32Array(4);
this.dragging_rectangle[0] = e.canvasX;
this.dragging_rectangle[1] = e.canvasY;
this.dragging_rectangle[2] = 1;
this.dragging_rectangle[3] = 1;
skip_action = true;
}
// clone node ALT dragging
if (LiteGraph.alt_drag_do_clone_nodes && e.altKey && node && this.allow_interaction && !skip_action && !this.read_only)
{
if (cloned = node.clone()){
cloned.pos[0] += 5;
cloned.pos[1] += 5;
this.graph.add(cloned,false,{doCalcSize: false});
node = cloned;
skip_action = true;
if (!block_drag_node) {
if (this.allow_dragnodes) {
this.graph.beforeChange();
this.node_dragged = node;
}
if (!this.selected_nodes[node.id]) {
this.processNodeSelected(node, e);
}
}
}
}
var clicking_canvas_bg = false;
//when clicked on top of a node
//and it is not interactive
if (node && (this.allow_interaction || node.flags.allow_interaction) && !skip_action && !this.read_only) {
if (!this.live_mode && !node.flags.pinned) {
this.bringToFront(node);
} //if it wasn't selected?
//not dragging mouse to connect two slots
if ( this.allow_interaction && !this.connecting_node && !node.flags.collapsed && !this.live_mode ) {
//Search for corner for resize
if ( !skip_action &&
node.resizable !== false &&
isInsideRectangle( e.canvasX,
e.canvasY,
node.pos[0] + node.size[0] - 5,
node.pos[1] + node.size[1] - 5,
10,
10
)
) {
this.graph.beforeChange();
this.resizing_node = node;
this.canvas.style.cursor = "se-resize";
skip_action = true;
} else {
//search for outputs
if (node.outputs) {
for ( var i = 0, l = node.outputs.length; i < l; ++i ) {
var output = node.outputs[i];
var link_pos = node.getConnectionPos(false, i);
if (
isInsideRectangle(
e.canvasX,
e.canvasY,
link_pos[0] - 15,
link_pos[1] - 10,
30,
20
)
) {
this.connecting_node = node;
this.connecting_output = output;
this.connecting_output.slot_index = i;
this.connecting_pos = node.getConnectionPos( false, i );
this.connecting_slot = i;
if (LiteGraph.shift_click_do_break_link_from){
if (e.shiftKey) {
node.disconnectOutput(i);
}
}
if (is_double_click) {
if (node.onOutputDblClick) {
node.onOutputDblClick(i, e);
}
} else {
if (node.onOutputClick) {
node.onOutputClick(i, e);
}
}
skip_action = true;
break;
}
}
}
//search for inputs
if (node.inputs) {
for ( var i = 0, l = node.inputs.length; i < l; ++i ) {
var input = node.inputs[i];
var link_pos = node.getConnectionPos(true, i);
if (
isInsideRectangle(
e.canvasX,
e.canvasY,
link_pos[0] - 15,
link_pos[1] - 10,
30,
20
)
) {
if (is_double_click) {
if (node.onInputDblClick) {
node.onInputDblClick(i, e);
}
} else {
if (node.onInputClick) {
node.onInputClick(i, e);
}
}
if (input.link !== null) {
var link_info = this.graph.links[
input.link
]; //before disconnecting
if (LiteGraph.click_do_break_link_to){
node.disconnectInput(i);
this.dirty_bgcanvas = true;
skip_action = true;
}else{
// do same action as has not node ?
}
if (
this.allow_reconnect_links ||
//this.move_destination_link_without_shift ||
e.shiftKey
) {
if (!LiteGraph.click_do_break_link_to){
node.disconnectInput(i);
}
this.connecting_node = this.graph._nodes_by_id[
link_info.origin_id
];
this.connecting_slot =
link_info.origin_slot;
this.connecting_output = this.connecting_node.outputs[
this.connecting_slot
];
this.connecting_pos = this.connecting_node.getConnectionPos( false, this.connecting_slot );
this.dirty_bgcanvas = true;
skip_action = true;
}
}else{
// has not node
}
if (!skip_action){
// connect from in to out, from to to from
this.connecting_node = node;
this.connecting_input = input;
this.connecting_input.slot_index = i;
this.connecting_pos = node.getConnectionPos( true, i );
this.connecting_slot = i;
this.dirty_bgcanvas = true;
skip_action = true;
}
}
}
}
} //not resizing
}
//it wasn't clicked on the links boxes
if (!skip_action) {
var block_drag_node = false;
var pos = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]];
//widgets
var widget = this.processNodeWidgets( node, this.graph_mouse, e );
if (widget) {
block_drag_node = true;
this.node_widget = [node, widget];
}
//double clicking
if (this.allow_interaction && is_double_click && this.selected_nodes[node.id]) {
//double click node
if (node.onDblClick) {
node.onDblClick( e, pos, this );
}
this.processNodeDblClicked(node);
block_drag_node = true;
}
//if do not capture mouse
if ( node.onMouseDown && node.onMouseDown( e, pos, this ) ) {
block_drag_node = true;
} else {
//open subgraph button
if(node.subgraph && !node.skip_subgraph_button)
{
if ( !node.flags.collapsed && pos[0] > node.size[0] - LiteGraph.NODE_TITLE_HEIGHT && pos[1] < 0 ) {
var that = this;
setTimeout(function() {
that.openSubgraph(node.subgraph);
}, 10);
}
}
if (this.live_mode) {
clicking_canvas_bg = true;
block_drag_node = true;
}
}
if (!block_drag_node) {
if (this.allow_dragnodes) {
this.graph.beforeChange();
this.node_dragged = node;
}
this.processNodeSelected(node, e);
} else { // double-click
/**
* Don't call the function if the block is already selected.
* Otherwise, it could cause the block to be unselected while its panel is open.
*/
if (!node.is_selected) this.processNodeSelected(node, e);
}
this.dirty_canvas = true;
}
} //clicked outside of nodes
else {
if (!skip_action){
//search for link connector
if(!this.read_only) {
for (var i = 0; i < this.visible_links.length; ++i) {
var link = this.visible_links[i];
var center = link._pos;
if (
!center ||
e.canvasX < center[0] - 4 ||
e.canvasX > center[0] + 4 ||
e.canvasY < center[1] - 4 ||
e.canvasY > center[1] + 4
) {
continue;
}
//link clicked
this.showLinkMenu(link, e);
this.over_link_center = null; //clear tooltip
break;
}
}
this.selected_group = this.graph.getGroupOnPos( e.canvasX, e.canvasY );
this.selected_group_resizing = false;
if (this.selected_group && !this.read_only ) {
if (e.ctrlKey) {
this.dragging_rectangle = null;
}
var dist = distance( [e.canvasX, e.canvasY], [ this.selected_group.pos[0] + this.selected_group.size[0], this.selected_group.pos[1] + this.selected_group.size[1] ] );
if (dist * this.ds.scale < 10) {
this.selected_group_resizing = true;
} else {
this.selected_group.recomputeInsideNodes();
}
}
if (is_double_click && !this.read_only && this.allow_searchbox) {
this.showSearchBox(e);
e.preventDefault();
e.stopPropagation();
}
clicking_canvas_bg = true;
}
}
if (!skip_action && clicking_canvas_bg && this.allow_dragcanvas) {
//console.log("pointerevents: dragging_canvas start");
this.dragging_canvas = true;
}
} else if (e.which == 2) {
//middle button
if (LiteGraph.middle_click_slot_add_default_node){
if (node && this.allow_interaction && !skip_action && !this.read_only){
//not dragging mouse to connect two slots
if (
!this.connecting_node &&
!node.flags.collapsed &&
!this.live_mode
) {
var mClikSlot = false;
var mClikSlot_index = false;
var mClikSlot_isOut = false;
//search for outputs
if (node.outputs) {
for ( var i = 0, l = node.outputs.length; i < l; ++i ) {
var output = node.outputs[i];
var link_pos = node.getConnectionPos(false, i);
if (isInsideRectangle(e.canvasX,e.canvasY,link_pos[0] - 15,link_pos[1] - 10,30,20)) {
mClikSlot = output;
mClikSlot_index = i;
mClikSlot_isOut = true;
break;
}
}
}
//search for inputs
if (node.inputs) {
for ( var i = 0, l = node.inputs.length; i < l; ++i ) {
var input = node.inputs[i];
var link_pos = node.getConnectionPos(true, i);
if (isInsideRectangle(e.canvasX,e.canvasY,link_pos[0] - 15,link_pos[1] - 10,30,20)) {
mClikSlot = input;
mClikSlot_index = i;
mClikSlot_isOut = false;
break;
}
}
}
//console.log("middleClickSlots? "+mClikSlot+" & "+(mClikSlot_index!==false));
if (mClikSlot && mClikSlot_index!==false){
var alphaPosY = 0.5-((mClikSlot_index+1)/((mClikSlot_isOut?node.outputs.length:node.inputs.length)));
var node_bounding = node.getBounding();
// estimate a position: this is a bad semi-bad-working mess .. REFACTOR with a correct autoplacement that knows about the others slots and nodes
var posRef = [ (!mClikSlot_isOut?node_bounding[0]:node_bounding[0]+node_bounding[2])// + node_bounding[0]/this.canvas.width*150
,e.canvasY-80// + node_bounding[0]/this.canvas.width*66 // vertical "derive"
];
var nodeCreated = this.createDefaultNodeForSlot({ nodeFrom: !mClikSlot_isOut?null:node
,slotFrom: !mClikSlot_isOut?null:mClikSlot_index
,nodeTo: !mClikSlot_isOut?node:null
,slotTo: !mClikSlot_isOut?mClikSlot_index:null
,position: posRef //,e: e
,nodeType: "AUTO" //nodeNewType
,posAdd:[!mClikSlot_isOut?-30:30, -alphaPosY*130] //-alphaPosY*30]
,posSizeFix:[!mClikSlot_isOut?-1:0, 0] //-alphaPosY*2*/
});
}
}
}
} else if (!skip_action && this.allow_dragcanvas) {
//console.log("pointerevents: dragging_canvas start from middle button");
this.dragging_canvas = true;
}
} else if (e.which == 3 || this.pointer_is_double) {
//right button
if (this.allow_interaction && !skip_action && !this.read_only){
// is it hover a node ?
if (node){
if(Object.keys(this.selected_nodes).length
&& (this.selected_nodes[node.id] || e.shiftKey || e.ctrlKey || e.metaKey)
){
// is multiselected or using shift to include the now node
if (!this.selected_nodes[node.id]) this.selectNodes([node],true); // add this if not present
}else{
// update selection
this.selectNodes([node]);
}
}
// show menu on this node
this.processContextMenu(node, e);
}
}
//TODO
//if(this.node_selected != prev_selected)
// this.onNodeSelectionChange(this.node_selected);
this.last_mouse[0] = e.clientX;
this.last_mouse[1] = e.clientY;
this.last_mouseclick = LiteGraph.getTime();
this.last_mouse_dragging = true;
/*
if( (this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null)
this.draw();
*/
this.graph.change();
//this is to ensure to defocus(blur) if a text input element is on focus
if (
!ref_window.document.activeElement ||
(ref_window.document.activeElement.nodeName.toLowerCase() !=
"input" &&
ref_window.document.activeElement.nodeName.toLowerCase() !=
"textarea")
) {
e.preventDefault();
}
e.stopPropagation();
if (this.onMouseDown) {
this.onMouseDown(e);
}
return false;
};
/**
* Called when a mouse move event has to be processed
* @method processMouseMove
**/
LGraphCanvas.prototype.processMouseMove = function(e) {
if (this.autoresize) {
this.resize();
}
if( this.set_canvas_dirty_on_mouse_event )
this.dirty_canvas = true;
if (!this.graph) {
return;
}
LGraphCanvas.active_canvas = this;
this.adjustMouseEvent(e);
var mouse = [e.clientX, e.clientY];
this.mouse[0] = mouse[0];
this.mouse[1] = mouse[1];
var delta = [
mouse[0] - this.last_mouse[0],
mouse[1] - this.last_mouse[1]
];
this.last_mouse = mouse;
this.graph_mouse[0] = e.canvasX;
this.graph_mouse[1] = e.canvasY;
//console.log("pointerevents: processMouseMove "+e.pointerId+" "+e.isPrimary);
if(this.block_click)
{
//console.log("pointerevents: processMouseMove block_click");
e.preventDefault();
return false;
}
e.dragging = this.last_mouse_dragging;
if (this.node_widget) {
this.processNodeWidgets(
this.node_widget[0],
this.graph_mouse,
e,
this.node_widget[1]
);
this.dirty_canvas = true;
}
//get node over
var node = this.graph.getNodeOnPos(e.canvasX,e.canvasY,this.visible_nodes);
if (this.dragging_rectangle)
{
this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0];
this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1];
this.dirty_canvas = true;
}
else if (this.selected_group && !this.read_only)
{
//moving/resizing a group
if (this.selected_group_resizing) {
this.selected_group.size = [
e.canvasX - this.selected_group.pos[0],
e.canvasY - this.selected_group.pos[1]
];
} else {
var deltax = delta[0] / this.ds.scale;
var deltay = delta[1] / this.ds.scale;
this.selected_group.move(deltax, deltay, e.ctrlKey);
if (this.selected_group._nodes.length) {
this.dirty_canvas = true;
}
}
this.dirty_bgcanvas = true;
} else if (this.dragging_canvas) {
////console.log("pointerevents: processMouseMove is dragging_canvas");
this.ds.offset[0] += delta[0] / this.ds.scale;
this.ds.offset[1] += delta[1] / this.ds.scale;
this.dirty_canvas = true;
this.dirty_bgcanvas = true;
} else if ((this.allow_interaction || (node && node.flags.allow_interaction)) && !this.read_only) {
if (this.connecting_node) {
this.dirty_canvas = true;
}
//remove mouseover flag
for (var i = 0, l = this.graph._nodes.length; i < l; ++i) {
if (this.graph._nodes[i].mouseOver && node != this.graph._nodes[i] ) {
//mouse leave
this.graph._nodes[i].mouseOver = false;
if (this.node_over && this.node_over.onMouseLeave) {
this.node_over.onMouseLeave(e);
}
this.node_over = null;
this.dirty_canvas = true;
}
}
//mouse over a node
if (node) {
if(node.redraw_on_mouse)
this.dirty_canvas = true;
//this.canvas.style.cursor = "move";
if (!node.mouseOver) {
//mouse enter
node.mouseOver = true;
this.node_over = node;
this.dirty_canvas = true;
if (node.onMouseEnter) {
node.onMouseEnter(e);
}
}
//in case the node wants to do something
if (node.onMouseMove) {
node.onMouseMove( e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this );
}
//if dragging a link
if (this.connecting_node) {
if (this.connecting_output){
var pos = this._highlight_input || [0, 0]; //to store the output of isOverNodeInput
//on top of input
if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) {
//mouse on top of the corner box, don't know what to do
} else {
//check if I have a slot below de mouse
var slot = this.isOverNodeInput( node, e.canvasX, e.canvasY, pos );
if (slot != -1 && node.inputs[slot]) {
var slot_type = node.inputs[slot].type;
if ( LiteGraph.isValidConnection( this.connecting_output.type, slot_type ) ) {
this._highlight_input = pos;
this._highlight_input_slot = node.inputs[slot]; // XXX CHECK THIS
}
} else {
this._highlight_input = null;
this._highlight_input_slot = null; // XXX CHECK THIS
}
}
}else if(this.connecting_input){
var pos = this._highlight_output || [0, 0]; //to store the output of isOverNodeOutput
//on top of output
if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) {
//mouse on top of the corner box, don't know what to do
} else {
//check if I have a slot below de mouse
var slot = this.isOverNodeOutput( node, e.canvasX, e.canvasY, pos );
if (slot != -1 && node.outputs[slot]) {
var slot_type = node.outputs[slot].type;
if ( LiteGraph.isValidConnection( this.connecting_input.type, slot_type ) ) {
this._highlight_output = pos;
}
} else {
this._highlight_output = null;
}
}
}
}
//Search for corner
if (this.canvas) {
if (
isInsideRectangle(
e.canvasX,
e.canvasY,
node.pos[0] + node.size[0] - 5,
node.pos[1] + node.size[1] - 5,
5,
5
)
) {
this.canvas.style.cursor = "se-resize";
} else {
this.canvas.style.cursor = "crosshair";
}
}
} else { //not over a node
//search for link connector
var over_link = null;
for (var i = 0; i < this.visible_links.length; ++i) {
var link = this.visible_links[i];
var center = link._pos;
if (
!center ||
e.canvasX < center[0] - 4 ||
e.canvasX > center[0] + 4 ||
e.canvasY < center[1] - 4 ||
e.canvasY > center[1] + 4
) {
continue;
}
over_link = link;
break;
}
if( over_link != this.over_link_center )
{
this.over_link_center = over_link;
this.dirty_canvas = true;
}
if (this.canvas) {
this.canvas.style.cursor = "";
}
} //end
//send event to node if capturing input (used with widgets that allow drag outside of the area of the node)
if ( this.node_capturing_input && this.node_capturing_input != node && this.node_capturing_input.onMouseMove ) {
this.node_capturing_input.onMouseMove(e,[e.canvasX - this.node_capturing_input.pos[0],e.canvasY - this.node_capturing_input.pos[1]], this);
}
//node being dragged
if (this.node_dragged && !this.live_mode) {
//console.log("draggin!",this.selected_nodes);
for (var i in this.selected_nodes) {
var n = this.selected_nodes[i];
n.pos[0] += delta[0] / this.ds.scale;
n.pos[1] += delta[1] / this.ds.scale;
if (!n.is_selected) this.processNodeSelected(n, e); /*
* Don't call the function if the block is already selected.
* Otherwise, it could cause the block to be unselected while dragging.
*/
}
this.dirty_canvas = true;
this.dirty_bgcanvas = true;
}
if (this.resizing_node && !this.live_mode) {
//convert mouse to node space
var desired_size = [ e.canvasX - this.resizing_node.pos[0], e.canvasY - this.resizing_node.pos[1] ];
var min_size = this.resizing_node.computeSize();
desired_size[0] = Math.max( min_size[0], desired_size[0] );
desired_size[1] = Math.max( min_size[1], desired_size[1] );
this.resizing_node.setSize( desired_size );
this.canvas.style.cursor = "se-resize";
this.dirty_canvas = true;
this.dirty_bgcanvas = true;
}
}
e.preventDefault();
return false;
};
/**
* Called when a mouse up event has to be processed
* @method processMouseUp
**/
LGraphCanvas.prototype.processMouseUp = function(e) {
var is_primary = ( e.isPrimary === undefined || e.isPrimary );
//early exit for extra pointer
if(!is_primary){
/*e.stopPropagation();
e.preventDefault();*/
//console.log("pointerevents: processMouseUp pointerN_stop "+e.pointerId+" "+e.isPrimary);
return false;
}
//console.log("pointerevents: processMouseUp "+e.pointerId+" "+e.isPrimary+" :: "+e.clientX+" "+e.clientY);
if( this.set_canvas_dirty_on_mouse_event )
this.dirty_canvas = true;
if (!this.graph)
return;
var window = this.getCanvasWindow();
var document = window.document;
LGraphCanvas.active_canvas = this;
//restore the mousemove event back to the canvas
if(!this.options.skip_events)
{
//console.log("pointerevents: processMouseUp adjustEventListener");
LiteGraph.pointerListenerRemove(document,"move", this._mousemove_callback,true);
LiteGraph.pointerListenerAdd(this.canvas,"move", this._mousemove_callback,true);
LiteGraph.pointerListenerRemove(document,"up", this._mouseup_callback,true);
}
this.adjustMouseEvent(e);
var now = LiteGraph.getTime();
e.click_time = now - this.last_mouseclick;
this.last_mouse_dragging = false;
this.last_click_position = null;
if(this.block_click)
{
//console.log("pointerevents: processMouseUp block_clicks");
this.block_click = false; //used to avoid sending twice a click in a immediate button
}
//console.log("pointerevents: processMouseUp which: "+e.which);
if (e.which == 1) {
if( this.node_widget )
{
this.processNodeWidgets( this.node_widget[0], this.graph_mouse, e );
}
//left button
this.node_widget = null;
if (this.selected_group) {
var diffx =
this.selected_group.pos[0] -
Math.round(this.selected_group.pos[0]);
var diffy =
this.selected_group.pos[1] -
Math.round(this.selected_group.pos[1]);
this.selected_group.move(diffx, diffy, e.ctrlKey);
this.selected_group.pos[0] = Math.round(
this.selected_group.pos[0]
);
this.selected_group.pos[1] = Math.round(
this.selected_group.pos[1]
);
if (this.selected_group._nodes.length) {
this.dirty_canvas = true;
}
this.selected_group = null;
}
this.selected_group_resizing = false;
var node = this.graph.getNodeOnPos(
e.canvasX,
e.canvasY,
this.visible_nodes
);
if (this.dragging_rectangle) {
if (this.graph) {
var nodes = this.graph._nodes;
var node_bounding = new Float32Array(4);
//compute bounding and flip if left to right
var w = Math.abs(this.dragging_rectangle[2]);
var h = Math.abs(this.dragging_rectangle[3]);
var startx =
this.dragging_rectangle[2] < 0
? this.dragging_rectangle[0] - w
: this.dragging_rectangle[0];
var starty =
this.dragging_rectangle[3] < 0
? this.dragging_rectangle[1] - h
: this.dragging_rectangle[1];
this.dragging_rectangle[0] = startx;
this.dragging_rectangle[1] = starty;
this.dragging_rectangle[2] = w;
this.dragging_rectangle[3] = h;
// test dragging rect size, if minimun simulate a click
if (!node || (w > 10 && h > 10 )){
//test against all nodes (not visible because the rectangle maybe start outside
var to_select = [];
for (var i = 0; i < nodes.length; ++i) {
var nodeX = nodes[i];
nodeX.getBounding(node_bounding);
if (
!overlapBounding(
this.dragging_rectangle,
node_bounding
)
) {
continue;
} //out of the visible area
to_select.push(nodeX);
}
if (to_select.length) {
this.selectNodes(to_select,e.shiftKey); // add to selection with shift
}
}else{
// will select of update selection
this.selectNodes([node],e.shiftKey||e.ctrlKey); // add to selection add to selection with ctrlKey or shiftKey
}
}
this.dragging_rectangle = null;
} else if (this.connecting_node) {
//dragging a connection
this.dirty_canvas = true;
this.dirty_bgcanvas = true;
var connInOrOut = this.connecting_output || this.connecting_input;
var connType = connInOrOut.type;
//node below mouse
if (node) {
/* no need to condition on event type.. just another type
if (
connType == LiteGraph.EVENT &&
this.isOverNodeBox(node, e.canvasX, e.canvasY)
) {
this.connecting_node.connect(
this.connecting_slot,
node,
LiteGraph.EVENT
);
} else {*/
//slot below mouse? connect
if (this.connecting_output){
var slot = this.isOverNodeInput(
node,
e.canvasX,
e.canvasY
);
if (slot != -1) {
this.connecting_node.connect(this.connecting_slot, node, slot);
} else {
//not on top of an input
// look for a good slot
this.connecting_node.connectByType(this.connecting_slot,node,connType);
}
}else if (this.connecting_input){
var slot = this.isOverNodeOutput(
node,
e.canvasX,
e.canvasY
);
if (slot != -1) {
node.connect(slot, this.connecting_node, this.connecting_slot); // this is inverted has output-input nature like
} else {
//not on top of an input
// look for a good slot
this.connecting_node.connectByTypeOutput(this.connecting_slot,node,connType);
}
}
//}
}else{
// add menu when releasing link in empty space
if (LiteGraph.release_link_on_empty_shows_menu){
if (e.shiftKey && this.allow_searchbox){
if(this.connecting_output){
this.showSearchBox(e,{node_from: this.connecting_node, slot_from: this.connecting_output, type_filter_in: this.connecting_output.type});
}else if(this.connecting_input){
this.showSearchBox(e,{node_to: this.connecting_node, slot_from: this.connecting_input, type_filter_out: this.connecting_input.type});
}
}else{
if(this.connecting_output){
this.showConnectionMenu({nodeFrom: this.connecting_node, slotFrom: this.connecting_output, e: e});
}else if(this.connecting_input){
this.showConnectionMenu({nodeTo: this.connecting_node, slotTo: this.connecting_input, e: e});
}
}
}
}
this.connecting_output = null;
this.connecting_input = null;
this.connecting_pos = null;
this.connecting_node = null;
this.connecting_slot = -1;
} //not dragging connection
else if (this.resizing_node) {
this.dirty_canvas = true;
this.dirty_bgcanvas = true;
this.graph.afterChange(this.resizing_node);
this.resizing_node = null;
} else if (this.node_dragged) {
//node being dragged?
var node = this.node_dragged;
if (
node &&
e.click_time < 300 &&
isInsideRectangle( e.canvasX, e.canvasY, node.pos[0], node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT )
) {
node.collapse();
}
this.dirty_canvas = true;
this.dirty_bgcanvas = true;
this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]);
this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]);
if (this.graph.config.align_to_grid || this.align_to_grid ) {
this.node_dragged.alignToGrid();
}
if( this.onNodeMoved )
this.onNodeMoved( this.node_dragged );
this.graph.afterChange(this.node_dragged);
this.node_dragged = null;
} //no node being dragged
else {
//get node over
var node = this.graph.getNodeOnPos(
e.canvasX,
e.canvasY,
this.visible_nodes
);
if (!node && e.click_time < 300) {
this.deselectAllNodes();
}
this.dirty_canvas = true;
this.dragging_canvas = false;
if (this.node_over && this.node_over.onMouseUp) {
this.node_over.onMouseUp( e, [ e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1] ], this );
}
if (
this.node_capturing_input &&
this.node_capturing_input.onMouseUp
) {
this.node_capturing_input.onMouseUp(e, [
e.canvasX - this.node_capturing_input.pos[0],
e.canvasY - this.node_capturing_input.pos[1]
]);
}
}
} else if (e.which == 2) {
//middle button
//trace("middle");
this.dirty_canvas = true;
this.dragging_canvas = false;
} else if (e.which == 3) {
//right button
//trace("right");
this.dirty_canvas = true;
this.dragging_canvas = false;
}
/*
if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null)
this.draw();
*/
if (is_primary)
{
this.pointer_is_down = false;
this.pointer_is_double = false;
}
this.graph.change();
//console.log("pointerevents: processMouseUp stopPropagation");
e.stopPropagation();
e.preventDefault();
return false;
};
/**
* Called when a mouse wheel event has to be processed
* @method processMouseWheel
**/
LGraphCanvas.prototype.processMouseWheel = function(e) {
if (!this.graph || !this.allow_dragcanvas) {
return;
}
var delta = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60;
this.adjustMouseEvent(e);
var x = e.clientX;
var y = e.clientY;
var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) );
if(!is_inside)
return;
var scale = this.ds.scale;
if (delta > 0) {
scale *= 1.1;
} else if (delta < 0) {
scale *= 1 / 1.1;
}
//this.setZoom( scale, [ e.clientX, e.clientY ] );
this.ds.changeScale(scale, [e.clientX, e.clientY]);
this.graph.change();
e.preventDefault();
return false; // prevent default
};
/**
* returns true if a position (in graph space) is on top of a node little corner box
* @method isOverNodeBox
**/
LGraphCanvas.prototype.isOverNodeBox = function(node, canvasx, canvasy) {
var title_height = LiteGraph.NODE_TITLE_HEIGHT;
if (
isInsideRectangle(
canvasx,
canvasy,
node.pos[0] + 2,
node.pos[1] + 2 - title_height,
title_height - 4,
title_height - 4
)
) {
return true;
}
return false;
};
/**
* returns the INDEX if a position (in graph space) is on top of a node input slot
* @method isOverNodeInput
**/
LGraphCanvas.prototype.isOverNodeInput = function(
node,
canvasx,
canvasy,
slot_pos
) {
if (node.inputs) {
for (var i = 0, l = node.inputs.length; i < l; ++i) {
var input = node.inputs[i];
var link_pos = node.getConnectionPos(true, i);
var is_inside = false;
if (node.horizontal) {
is_inside = isInsideRectangle(
canvasx,
canvasy,
link_pos[0] - 5,
link_pos[1] - 10,
10,
20
);
} else {
is_inside = isInsideRectangle(
canvasx,
canvasy,
link_pos[0] - 10,
link_pos[1] - 5,
40,
10
);
}
if (is_inside) {
if (slot_pos) {
slot_pos[0] = link_pos[0];
slot_pos[1] = link_pos[1];
}
return i;
}
}
}
return -1;
};
/**
* returns the INDEX if a position (in graph space) is on top of a node output slot
* @method isOverNodeOuput
**/
LGraphCanvas.prototype.isOverNodeOutput = function(
node,
canvasx,
canvasy,
slot_pos
) {
if (node.outputs) {
for (var i = 0, l = node.outputs.length; i < l; ++i) {
var output = node.outputs[i];
var link_pos = node.getConnectionPos(false, i);
var is_inside = false;
if (node.horizontal) {
is_inside = isInsideRectangle(
canvasx,
canvasy,
link_pos[0] - 5,
link_pos[1] - 10,
10,
20
);
} else {
is_inside = isInsideRectangle(
canvasx,
canvasy,
link_pos[0] - 10,
link_pos[1] - 5,
40,
10
);
}
if (is_inside) {
if (slot_pos) {
slot_pos[0] = link_pos[0];
slot_pos[1] = link_pos[1];
}
return i;
}
}
}
return -1;
};
/**
* process a key event
* @method processKey
**/
LGraphCanvas.prototype.processKey = function(e) {
if (!this.graph) {
return;
}
var block_default = false;
//console.log(e); //debug
if (e.target.localName == "input") {
return;
}
if (e.type == "keydown") {
if (e.keyCode == 32) {
//space
this.dragging_canvas = true;
block_default = true;
}
if (e.keyCode == 27) {
//esc
if(this.node_panel) this.node_panel.close();
if(this.options_panel) this.options_panel.close();
block_default = true;
}
//select all Control A
if (e.keyCode == 65 && e.ctrlKey) {
this.selectNodes();
block_default = true;
}
if ((e.keyCode === 67) && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
//copy
if (this.selected_nodes) {
this.copyToClipboard();
block_default = true;
}
}
if ((e.keyCode === 86) && (e.metaKey || e.ctrlKey)) {
//paste
this.pasteFromClipboard(e.shiftKey);
}
//delete or backspace
if (e.keyCode == 46 || e.keyCode == 8) {
if (
e.target.localName != "input" &&
e.target.localName != "textarea"
) {
this.deleteSelectedNodes();
block_default = true;
}
}
//collapse
//...
//TODO
if (this.selected_nodes) {
for (var i in this.selected_nodes) {
if (this.selected_nodes[i].onKeyDown) {
this.selected_nodes[i].onKeyDown(e);
}
}
}
} else if (e.type == "keyup") {
if (e.keyCode == 32) {
// space
this.dragging_canvas = false;
}
if (this.selected_nodes) {
for (var i in this.selected_nodes) {
if (this.selected_nodes[i].onKeyUp) {
this.selected_nodes[i].onKeyUp(e);
}
}
}
}
this.graph.change();
if (block_default) {
e.preventDefault();
e.stopImmediatePropagation();
return false;
}
};
LGraphCanvas.prototype.copyToClipboard = function() {
var clipboard_info = {
nodes: [],
links: []
};
var index = 0;
var selected_nodes_array = [];
for (var i in this.selected_nodes) {
var node = this.selected_nodes[i];
if (node.clonable === false)
continue;
node._relative_id = index;
selected_nodes_array.push(node);
index += 1;
}
for (var i = 0; i < selected_nodes_array.length; ++i) {
var node = selected_nodes_array[i];
var cloned = node.clone();
if(!cloned)
{
console.warn("node type not found: " + node.type );
continue;
}
clipboard_info.nodes.push(cloned.serialize());
if (node.inputs && node.inputs.length) {
for (var j = 0; j < node.inputs.length; ++j) {
var input = node.inputs[j];
if (!input || input.link == null) {
continue;
}
var link_info = this.graph.links[input.link];
if (!link_info) {
continue;
}
var target_node = this.graph.getNodeById(
link_info.origin_id
);
if (!target_node) {
continue;
}
clipboard_info.links.push([
target_node._relative_id,
link_info.origin_slot, //j,
node._relative_id,
link_info.target_slot,
target_node.id
]);
}
}
}
localStorage.setItem(
"litegrapheditor_clipboard",
JSON.stringify(clipboard_info)
);
};
LGraphCanvas.prototype.pasteFromClipboard = function(isConnectUnselected = false) {
// if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior
if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) {
return;
}
var data = localStorage.getItem("litegrapheditor_clipboard");
if (!data) {
return;
}
this.graph.beforeChange();
//create nodes
var clipboard_info = JSON.parse(data);
// calculate top-left node, could work without this processing but using diff with last node pos :: clipboard_info.nodes[clipboard_info.nodes.length-1].pos
var posMin = false;
var posMinIndexes = false;
for (var i = 0; i < clipboard_info.nodes.length; ++i) {
if (posMin){
if(posMin[0]>clipboard_info.nodes[i].pos[0]){
posMin[0] = clipboard_info.nodes[i].pos[0];
posMinIndexes[0] = i;
}
if(posMin[1]>clipboard_info.nodes[i].pos[1]){
posMin[1] = clipboard_info.nodes[i].pos[1];
posMinIndexes[1] = i;
}
}
else{
posMin = [clipboard_info.nodes[i].pos[0], clipboard_info.nodes[i].pos[1]];
posMinIndexes = [i, i];
}
}
var nodes = [];
for (var i = 0; i < clipboard_info.nodes.length; ++i) {
var node_data = clipboard_info.nodes[i];
var node = LiteGraph.createNode(node_data.type);
if (node) {
node.configure(node_data);
//paste in last known mouse position
node.pos[0] += this.graph_mouse[0] - posMin[0]; //+= 5;
node.pos[1] += this.graph_mouse[1] - posMin[1]; //+= 5;
this.graph.add(node,{doProcessChange:false});
nodes.push(node);
}
}
//create links
for (var i = 0; i < clipboard_info.links.length; ++i) {
var link_info = clipboard_info.links[i];
var origin_node;
var origin_node_relative_id = link_info[0];
if (origin_node_relative_id != null) {
origin_node = nodes[origin_node_relative_id];
} else if (LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) {
var origin_node_id = link_info[4];
if (origin_node_id) {
origin_node = this.graph.getNodeById(origin_node_id);
}
}
var target_node = nodes[link_info[2]];
if( origin_node && target_node )
origin_node.connect(link_info[1], target_node, link_info[3]);
else
console.warn("Warning, nodes missing on pasting");
}
this.selectNodes(nodes);
this.graph.afterChange();
};
/**
* process a item drop event on top the canvas
* @method processDrop
**/
LGraphCanvas.prototype.processDrop = function(e) {
e.preventDefault();
this.adjustMouseEvent(e);
var x = e.clientX;
var y = e.clientY;
var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) );
if(!is_inside){
return;
// --- BREAK ---
}
var pos = [e.canvasX, e.canvasY];
var node = this.graph ? this.graph.getNodeOnPos(pos[0], pos[1]) : null;
if (!node) {
var r = null;
if (this.onDropItem) {
r = this.onDropItem(event);
}
if (!r) {
this.checkDropItem(e);
}
return;
}
if (node.onDropFile || node.onDropData) {
var files = e.dataTransfer.files;
if (files && files.length) {
for (var i = 0; i < files.length; i++) {
var file = e.dataTransfer.files[0];
var filename = file.name;
var ext = LGraphCanvas.getFileExtension(filename);
//console.log(file);
if (node.onDropFile) {
node.onDropFile(file);
}
if (node.onDropData) {
//prepare reader
var reader = new FileReader();
reader.onload = function(event) {
//console.log(event.target);
var data = event.target.result;
node.onDropData(data, filename, file);
};
//read data
var type = file.type.split("/")[0];
if (type == "text" || type == "") {
reader.readAsText(file);
} else if (type == "image") {
reader.readAsDataURL(file);
} else {
reader.readAsArrayBuffer(file);
}
}
}
}
}
if (node.onDropItem) {
if (node.onDropItem(event)) {
return true;
}
}
if (this.onDropItem) {
return this.onDropItem(event);
}
return false;
};
//called if the graph doesn't have a default drop item behaviour
LGraphCanvas.prototype.checkDropItem = function(e) {
if (e.dataTransfer.files.length) {
var file = e.dataTransfer.files[0];
var ext = LGraphCanvas.getFileExtension(file.name).toLowerCase();
var nodetype = LiteGraph.node_types_by_file_extension[ext];
if (nodetype) {
this.graph.beforeChange();
var node = LiteGraph.createNode(nodetype.type);
node.pos = [e.canvasX, e.canvasY];
this.graph.add(node);
if (node.onDropFile) {
node.onDropFile(file);
}
this.graph.afterChange();
}
}
};
LGraphCanvas.prototype.processNodeDblClicked = function(n) {
if (this.onShowNodePanel) {
this.onShowNodePanel(n);
}
else
{
this.showShowNodePanel(n);
}
if (this.onNodeDblClicked) {
this.onNodeDblClicked(n);
}
this.setDirty(true);
};
LGraphCanvas.prototype.processNodeSelected = function(node, e) {
this.selectNode(node, e && (e.shiftKey || e.ctrlKey || this.multi_select));
if (this.onNodeSelected) {
this.onNodeSelected(node);
}
};
/**
* selects a given node (or adds it to the current selection)
* @method selectNode
**/
LGraphCanvas.prototype.selectNode = function(
node,
add_to_current_selection
) {
if (node == null) {
this.deselectAllNodes();
} else {
this.selectNodes([node], add_to_current_selection);
}
};
/**
* selects several nodes (or adds them to the current selection)
* @method selectNodes
**/
LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection )
{
if (!add_to_current_selection) {
this.deselectAllNodes();
}
nodes = nodes || this.graph._nodes;
if (typeof nodes == "string") nodes = [nodes];
for (var i in nodes) {
var node = nodes[i];
if (node.is_selected) {
this.deselectNode(node);
continue;
}
if (!node.is_selected && node.onSelected) {
node.onSelected();
}
node.is_selected = true;
this.selected_nodes[node.id] = node;
if (node.inputs) {
for (var j = 0; j < node.inputs.length; ++j) {
this.highlighted_links[node.inputs[j].link] = true;
}
}
if (node.outputs) {
for (var j = 0; j < node.outputs.length; ++j) {
var out = node.outputs[j];
if (out.links) {
for (var k = 0; k < out.links.length; ++k) {
this.highlighted_links[out.links[k]] = true;
}
}
}
}
}
if( this.onSelectionChange )
this.onSelectionChange( this.selected_nodes );
this.setDirty(true);
};
/**
* removes a node from the current selection
* @method deselectNode
**/
LGraphCanvas.prototype.deselectNode = function(node) {
if (!node.is_selected) {
return;
}
if (node.onDeselected) {
node.onDeselected();
}
node.is_selected = false;
if (this.onNodeDeselected) {
this.onNodeDeselected(node);
}
//remove highlighted
if (node.inputs) {
for (var i = 0; i < node.inputs.length; ++i) {
delete this.highlighted_links[node.inputs[i].link];
}
}
if (node.outputs) {
for (var i = 0; i < node.outputs.length; ++i) {
var out = node.outputs[i];
if (out.links) {
for (var j = 0; j < out.links.length; ++j) {
delete this.highlighted_links[out.links[j]];
}
}
}
}
};
/**
* removes all nodes from the current selection
* @method deselectAllNodes
**/
LGraphCanvas.prototype.deselectAllNodes = function() {
if (!this.graph) {
return;
}
var nodes = this.graph._nodes;
for (var i = 0, l = nodes.length; i < l; ++i) {
var node = nodes[i];
if (!node.is_selected) {
continue;
}
if (node.onDeselected) {
node.onDeselected();
}
node.is_selected = false;
if (this.onNodeDeselected) {
this.onNodeDeselected(node);
}
}
this.selected_nodes = {};
this.current_node = null;
this.highlighted_links = {};
if( this.onSelectionChange )
this.onSelectionChange( this.selected_nodes );
this.setDirty(true);
};
/**
* deletes all nodes in the current selection from the graph
* @method deleteSelectedNodes
**/
LGraphCanvas.prototype.deleteSelectedNodes = function() {
this.graph.beforeChange();
for (var i in this.selected_nodes) {
var node = this.selected_nodes[i];
if(node.block_delete)
continue;
//autoconnect when possible (very basic, only takes into account first input-output)
if(node.inputs && node.inputs.length && node.outputs && node.outputs.length && LiteGraph.isValidConnection( node.inputs[0].type, node.outputs[0].type ) && node.inputs[0].link && node.outputs[0].links && node.outputs[0].links.length )
{
var input_link = node.graph.links[ node.inputs[0].link ];
var output_link = node.graph.links[ node.outputs[0].links[0] ];
var input_node = node.getInputNode(0);
var output_node = node.getOutputNodes(0)[0];
if(input_node && output_node)
input_node.connect( input_link.origin_slot, output_node, output_link.target_slot );
}
this.graph.remove(node);
if (this.onNodeDeselected) {
this.onNodeDeselected(node);
}
}
this.selected_nodes = {};
this.current_node = null;
this.highlighted_links = {};
this.setDirty(true);
this.graph.afterChange();
};
/**
* centers the camera on a given node
* @method centerOnNode
**/
LGraphCanvas.prototype.centerOnNode = function(node) {
this.ds.offset[0] =
-node.pos[0] -
node.size[0] * 0.5 +
(this.canvas.width * 0.5) / this.ds.scale;
this.ds.offset[1] =
-node.pos[1] -
node.size[1] * 0.5 +
(this.canvas.height * 0.5) / this.ds.scale;
this.setDirty(true, true);
};
/**
* adds some useful properties to a mouse event, like the position in graph coordinates
* @method adjustMouseEvent
**/
LGraphCanvas.prototype.adjustMouseEvent = function(e) {
var clientX_rel = 0;
var clientY_rel = 0;
if (this.canvas) {
var b = this.canvas.getBoundingClientRect();
clientX_rel = e.clientX - b.left;
clientY_rel = e.clientY - b.top;
} else {
clientX_rel = e.clientX;
clientY_rel = e.clientY;
}
// e.deltaX = clientX_rel - this.last_mouse_position[0];
// e.deltaY = clientY_rel- this.last_mouse_position[1];
this.last_mouse_position[0] = clientX_rel;
this.last_mouse_position[1] = clientY_rel;
e.canvasX = clientX_rel / this.ds.scale - this.ds.offset[0];
e.canvasY = clientY_rel / this.ds.scale - this.ds.offset[1];
//console.log("pointerevents: adjustMouseEvent "+e.clientX+":"+e.clientY+" "+clientX_rel+":"+clientY_rel+" "+e.canvasX+":"+e.canvasY);
};
/**
* changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom
* @method setZoom
**/
LGraphCanvas.prototype.setZoom = function(value, zooming_center) {
this.ds.changeScale(value, zooming_center);
/*
if(!zooming_center && this.canvas)
zooming_center = [this.canvas.width * 0.5,this.canvas.height * 0.5];
var center = this.convertOffsetToCanvas( zooming_center );
this.ds.scale = value;
if(this.scale > this.max_zoom)
this.scale = this.max_zoom;
else if(this.scale < this.min_zoom)
this.scale = this.min_zoom;
var new_center = this.convertOffsetToCanvas( zooming_center );
var delta_offset = [new_center[0] - center[0], new_center[1] - center[1]];
this.offset[0] += delta_offset[0];
this.offset[1] += delta_offset[1];
*/
this.dirty_canvas = true;
this.dirty_bgcanvas = true;
};
/**
* converts a coordinate from graph coordinates to canvas2D coordinates
* @method convertOffsetToCanvas
**/
LGraphCanvas.prototype.convertOffsetToCanvas = function(pos, out) {
return this.ds.convertOffsetToCanvas(pos, out);
};
/**
* converts a coordinate from Canvas2D coordinates to graph space
* @method convertCanvasToOffset
**/
LGraphCanvas.prototype.convertCanvasToOffset = function(pos, out) {
return this.ds.convertCanvasToOffset(pos, out);
};
//converts event coordinates from canvas2D to graph coordinates
LGraphCanvas.prototype.convertEventToCanvasOffset = function(e) {
var rect = this.canvas.getBoundingClientRect();
return this.convertCanvasToOffset([
e.clientX - rect.left,
e.clientY - rect.top
]);
};
/**
* brings a node to front (above all other nodes)
* @method bringToFront
**/
LGraphCanvas.prototype.bringToFront = function(node) {
var i = this.graph._nodes.indexOf(node);
if (i == -1) {
return;
}
this.graph._nodes.splice(i, 1);
this.graph._nodes.push(node);
};
/**
* sends a node to the back (below all other nodes)
* @method sendToBack
**/
LGraphCanvas.prototype.sendToBack = function(node) {
var i = this.graph._nodes.indexOf(node);
if (i == -1) {
return;
}
this.graph._nodes.splice(i, 1);
this.graph._nodes.unshift(node);
};
/* Interaction */
/* LGraphCanvas render */
var temp = new Float32Array(4);
/**
* checks which nodes are visible (inside the camera area)
* @method computeVisibleNodes
**/
LGraphCanvas.prototype.computeVisibleNodes = function(nodes, out) {
var visible_nodes = out || [];
visible_nodes.length = 0;
nodes = nodes || this.graph._nodes;
for (var i = 0, l = nodes.length; i < l; ++i) {
var n = nodes[i];
//skip rendering nodes in live mode
if (this.live_mode && !n.onDrawBackground && !n.onDrawForeground) {
continue;
}
if (!overlapBounding(this.visible_area, n.getBounding(temp))) {
continue;
} //out of the visible area
visible_nodes.push(n);
}
return visible_nodes;
};
/**
* renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes)
* @method draw
**/
LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas) {
if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0) {
return;
}
//fps counting
var now = LiteGraph.getTime();
this.render_time = (now - this.last_draw_time) * 0.001;
this.last_draw_time = now;
if (this.graph) {
this.ds.computeVisibleArea(this.viewport);
}
if (
this.dirty_bgcanvas ||
force_bgcanvas ||
this.always_render_background ||
(this.graph &&
this.graph._last_trigger_time &&
now - this.graph._last_trigger_time < 1000)
) {
this.drawBackCanvas();
}
if (this.dirty_canvas || force_canvas) {
this.drawFrontCanvas();
}
this.fps = this.render_time ? 1.0 / this.render_time : 0;
this.frame += 1;
};
/**
* draws the front canvas (the one containing all the nodes)
* @method drawFrontCanvas
**/
LGraphCanvas.prototype.drawFrontCanvas = function() {
this.dirty_canvas = false;
if (!this.ctx) {
this.ctx = this.bgcanvas.getContext("2d");
}
var ctx = this.ctx;
if (!ctx) {
//maybe is using webgl...
return;
}
var canvas = this.canvas;
if ( ctx.start2D && !this.viewport ) {
ctx.start2D();
ctx.restore();
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
//clip dirty area if there is one, otherwise work in full canvas
var area = this.viewport || this.dirty_area;
if (area) {
ctx.save();
ctx.beginPath();
ctx.rect( area[0],area[1],area[2],area[3] );
ctx.clip();
}
//clear
//canvas.width = canvas.width;
if (this.clear_background) {
if(area)
ctx.clearRect( area[0],area[1],area[2],area[3] );
else
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
//draw bg canvas
if (this.bgcanvas == this.canvas) {
this.drawBackCanvas();
} else {
ctx.drawImage( this.bgcanvas, 0, 0 );
}
//rendering
if (this.onRender) {
this.onRender(canvas, ctx);
}
//info widget
if (this.show_info) {
this.renderInfo(ctx, area ? area[0] : 0, area ? area[1] : 0 );
}
if (this.graph) {
//apply transformations
ctx.save();
this.ds.toCanvasContext(ctx);
//draw nodes
var drawn_nodes = 0;
var visible_nodes = this.computeVisibleNodes(
null,
this.visible_nodes
);
for (var i = 0; i < visible_nodes.length; ++i) {
var node = visible_nodes[i];
//transform coords system
ctx.save();
ctx.translate(node.pos[0], node.pos[1]);
//Draw
this.drawNode(node, ctx);
drawn_nodes += 1;
//Restore
ctx.restore();
}
//on top (debug)
if (this.render_execution_order) {
this.drawExecutionOrder(ctx);
}
//connections ontop?
if (this.graph.config.links_ontop) {
if (!this.live_mode) {
this.drawConnections(ctx);
}
}
//current connection (the one being dragged by the mouse)
if (this.connecting_pos != null) {
ctx.lineWidth = this.connections_width;
var link_color = null;
var connInOrOut = this.connecting_output || this.connecting_input;
var connType = connInOrOut.type;
var connDir = connInOrOut.dir;
if(connDir == null)
{
if (this.connecting_output)
connDir = this.connecting_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT;
else
connDir = this.connecting_node.horizontal ? LiteGraph.UP : LiteGraph.LEFT;
}
var connShape = connInOrOut.shape;
switch (connType) {
case LiteGraph.EVENT:
link_color = LiteGraph.EVENT_LINK_COLOR;
break;
default:
link_color = LiteGraph.CONNECTING_LINK_COLOR;
}
//the connection being dragged by the mouse
this.renderLink(
ctx,
this.connecting_pos,
[this.graph_mouse[0], this.graph_mouse[1]],
null,
false,
null,
link_color,
connDir,
LiteGraph.CENTER
);
ctx.beginPath();
if (
connType === LiteGraph.EVENT ||
connShape === LiteGraph.BOX_SHAPE
) {
ctx.rect(
this.connecting_pos[0] - 6 + 0.5,
this.connecting_pos[1] - 5 + 0.5,
14,
10
);
ctx.fill();
ctx.beginPath();
ctx.rect(
this.graph_mouse[0] - 6 + 0.5,
this.graph_mouse[1] - 5 + 0.5,
14,
10
);
} else if (connShape === LiteGraph.ARROW_SHAPE) {
ctx.moveTo(this.connecting_pos[0] + 8, this.connecting_pos[1] + 0.5);
ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] + 6 + 0.5);
ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] - 6 + 0.5);
ctx.closePath();
}
else {
ctx.arc(
this.connecting_pos[0],
this.connecting_pos[1],
4,
0,
Math.PI * 2
);
ctx.fill();
ctx.beginPath();
ctx.arc(
this.graph_mouse[0],
this.graph_mouse[1],
4,
0,
Math.PI * 2
);
}
ctx.fill();
ctx.fillStyle = "#ffcc00";
if (this._highlight_input) {
ctx.beginPath();
var shape = this._highlight_input_slot.shape;
if (shape === LiteGraph.ARROW_SHAPE) {
ctx.moveTo(this._highlight_input[0] + 8, this._highlight_input[1] + 0.5);
ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] + 6 + 0.5);
ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] - 6 + 0.5);
ctx.closePath();
} else {
ctx.arc(
this._highlight_input[0],
this._highlight_input[1],
6,
0,
Math.PI * 2
);
}
ctx.fill();
}
if (this._highlight_output) {
ctx.beginPath();
if (shape === LiteGraph.ARROW_SHAPE) {
ctx.moveTo(this._highlight_output[0] + 8, this._highlight_output[1] + 0.5);
ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] + 6 + 0.5);
ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] - 6 + 0.5);
ctx.closePath();
} else {
ctx.arc(
this._highlight_output[0],
this._highlight_output[1],
6,
0,
Math.PI * 2
);
}
ctx.fill();
}
}
//the selection rectangle
if (this.dragging_rectangle) {
ctx.strokeStyle = "#FFF";
ctx.strokeRect(
this.dragging_rectangle[0],
this.dragging_rectangle[1],
this.dragging_rectangle[2],
this.dragging_rectangle[3]
);
}
//on top of link center
if(this.over_link_center && this.render_link_tooltip)
this.drawLinkTooltip( ctx, this.over_link_center );
else
if(this.onDrawLinkTooltip) //to remove
this.onDrawLinkTooltip(ctx,null);
//custom info
if (this.onDrawForeground) {
this.onDrawForeground(ctx, this.visible_rect);
}
ctx.restore();
}
//draws panel in the corner
if (this._graph_stack && this._graph_stack.length) {
this.drawSubgraphPanel( ctx );
}
if (this.onDrawOverlay) {
this.onDrawOverlay(ctx);
}
if (area){
ctx.restore();
}
if (ctx.finish2D) {
//this is a function I use in webgl renderer
ctx.finish2D();
}
};
/**
* draws the panel in the corner that shows subgraph properties
* @method drawSubgraphPanel
**/
LGraphCanvas.prototype.drawSubgraphPanel = function (ctx) {
var subgraph = this.graph;
var subnode = subgraph._subgraph_node;
if (!subnode) {
console.warn("subgraph without subnode");
return;
}
this.drawSubgraphPanelLeft(subgraph, subnode, ctx)
this.drawSubgraphPanelRight(subgraph, subnode, ctx)
}
LGraphCanvas.prototype.drawSubgraphPanelLeft = function (subgraph, subnode, ctx) {
var num = subnode.inputs ? subnode.inputs.length : 0;
var w = 200;
var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6);
ctx.fillStyle = "#111";
ctx.globalAlpha = 0.8;
ctx.beginPath();
ctx.roundRect(10, 10, w, (num + 1) * h + 50, [8]);
ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = "#888";
ctx.font = "14px Arial";
ctx.textAlign = "left";
ctx.fillText("Graph Inputs", 20, 34);
// var pos = this.mouse;
if (this.drawButton(w - 20, 20, 20, 20, "X", "#151515")) {
this.closeSubgraph();
return;
}
var y = 50;
ctx.font = "14px Arial";
if (subnode.inputs)
for (var i = 0; i < subnode.inputs.length; ++i) {
var input = subnode.inputs[i];
if (input.not_subgraph_input)
continue;
//input button clicked
if (this.drawButton(20, y + 2, w - 20, h - 2)) {
var type = subnode.constructor.input_node_type || "graph/input";
this.graph.beforeChange();
var newnode = LiteGraph.createNode(type);
if (newnode) {
subgraph.add(newnode);
this.block_click = false;
this.last_click_position = null;
this.selectNodes([newnode]);
this.node_dragged = newnode;
this.dragging_canvas = false;
newnode.setProperty("name", input.name);
newnode.setProperty("type", input.type);
this.node_dragged.pos[0] = this.graph_mouse[0] - 5;
this.node_dragged.pos[1] = this.graph_mouse[1] - 5;
this.graph.afterChange();
}
else
console.error("graph input node not found:", type);
}
ctx.fillStyle = "#9C9";
ctx.beginPath();
ctx.arc(w - 16, y + h * 0.5, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = "#AAA";
ctx.fillText(input.name, 30, y + h * 0.75);
// var tw = ctx.measureText(input.name);
ctx.fillStyle = "#777";
ctx.fillText(input.type, 130, y + h * 0.75);
y += h;
}
//add + button
if (this.drawButton(20, y + 2, w - 20, h - 2, "+", "#151515", "#222")) {
this.showSubgraphPropertiesDialog(subnode);
}
}
LGraphCanvas.prototype.drawSubgraphPanelRight = function (subgraph, subnode, ctx) {
var num = subnode.outputs ? subnode.outputs.length : 0;
var canvas_w = this.bgcanvas.width
var w = 200;
var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6);
ctx.fillStyle = "#111";
ctx.globalAlpha = 0.8;
ctx.beginPath();
ctx.roundRect(canvas_w - w - 10, 10, w, (num + 1) * h + 50, [8]);
ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = "#888";
ctx.font = "14px Arial";
ctx.textAlign = "left";
var title_text = "Graph Outputs"
var tw = ctx.measureText(title_text).width
ctx.fillText(title_text, (canvas_w - tw) - 20, 34);
// var pos = this.mouse;
if (this.drawButton(canvas_w - w, 20, 20, 20, "X", "#151515")) {
this.closeSubgraph();
return;
}
var y = 50;
ctx.font = "14px Arial";
if (subnode.outputs)
for (var i = 0; i < subnode.outputs.length; ++i) {
var output = subnode.outputs[i];
if (output.not_subgraph_input)
continue;
//output button clicked
if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2)) {
var type = subnode.constructor.output_node_type || "graph/output";
this.graph.beforeChange();
var newnode = LiteGraph.createNode(type);
if (newnode) {
subgraph.add(newnode);
this.block_click = false;
this.last_click_position = null;
this.selectNodes([newnode]);
this.node_dragged = newnode;
this.dragging_canvas = false;
newnode.setProperty("name", output.name);
newnode.setProperty("type", output.type);
this.node_dragged.pos[0] = this.graph_mouse[0] - 5;
this.node_dragged.pos[1] = this.graph_mouse[1] - 5;
this.graph.afterChange();
}
else
console.error("graph input node not found:", type);
}
ctx.fillStyle = "#9C9";
ctx.beginPath();
ctx.arc(canvas_w - w + 16, y + h * 0.5, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = "#AAA";
ctx.fillText(output.name, canvas_w - w + 30, y + h * 0.75);
// var tw = ctx.measureText(input.name);
ctx.fillStyle = "#777";
ctx.fillText(output.type, canvas_w - w + 130, y + h * 0.75);
y += h;
}
//add + button
if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2, "+", "#151515", "#222")) {
this.showSubgraphPropertiesDialogRight(subnode);
}
}
//Draws a button into the canvas overlay and computes if it was clicked using the immediate gui paradigm
LGraphCanvas.prototype.drawButton = function( x,y,w,h, text, bgcolor, hovercolor, textcolor )
{
var ctx = this.ctx;
bgcolor = bgcolor || LiteGraph.NODE_DEFAULT_COLOR;
hovercolor = hovercolor || "#555";
textcolor = textcolor || LiteGraph.NODE_TEXT_COLOR;
var pos = this.ds.convertOffsetToCanvas(this.graph_mouse);
var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h );
pos = this.last_click_position ? [this.last_click_position[0], this.last_click_position[1]] : null;
if(pos) {
var rect = this.canvas.getBoundingClientRect();
pos[0] -= rect.left;
pos[1] -= rect.top;
}
var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h );
ctx.fillStyle = hover ? hovercolor : bgcolor;
if(clicked)
ctx.fillStyle = "#AAA";
ctx.beginPath();
ctx.roundRect(x,y,w,h,[4] );
ctx.fill();
if(text != null)
{
if(text.constructor == String)
{
ctx.fillStyle = textcolor;
ctx.textAlign = "center";
ctx.font = ((h * 0.65)|0) + "px Arial";
ctx.fillText( text, x + w * 0.5,y + h * 0.75 );
ctx.textAlign = "left";
}
}
var was_clicked = clicked && !this.block_click;
if(clicked)
this.blockClick();
return was_clicked;
}
LGraphCanvas.prototype.isAreaClicked = function( x,y,w,h, hold_click )
{
var pos = this.mouse;
var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h );
pos = this.last_click_position;
var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h );
var was_clicked = clicked && !this.block_click;
if(clicked && hold_click)
this.blockClick();
return was_clicked;
}
/**
* draws some useful stats in the corner of the canvas
* @method renderInfo
**/
LGraphCanvas.prototype.renderInfo = function(ctx, x, y) {
x = x || 10;
y = y || this.canvas.height - 80;
ctx.save();
ctx.translate(x, y);
ctx.font = "10px Arial";
ctx.fillStyle = "#888";
ctx.textAlign = "left";
if (this.graph) {
ctx.fillText( "T: " + this.graph.globaltime.toFixed(2) + "s", 5, 13 * 1 );
ctx.fillText("I: " + this.graph.iteration, 5, 13 * 2 );
ctx.fillText("N: " + this.graph._nodes.length + " [" + this.visible_nodes.length + "]", 5, 13 * 3 );
ctx.fillText("V: " + this.graph._version, 5, 13 * 4);
ctx.fillText("FPS:" + this.fps.toFixed(2), 5, 13 * 5);
} else {
ctx.fillText("No graph selected", 5, 13 * 1);
}
ctx.restore();
};
/**
* draws the back canvas (the one containing the background and the connections)
* @method drawBackCanvas
**/
LGraphCanvas.prototype.drawBackCanvas = function() {
var canvas = this.bgcanvas;
if (
canvas.width != this.canvas.width ||
canvas.height != this.canvas.height
) {
canvas.width = this.canvas.width;
canvas.height = this.canvas.height;
}
if (!this.bgctx) {
this.bgctx = this.bgcanvas.getContext("2d");
}
var ctx = this.bgctx;
if (ctx.start) {
ctx.start();
}
var viewport = this.viewport || [0,0,ctx.canvas.width,ctx.canvas.height];
//clear
if (this.clear_background) {
ctx.clearRect( viewport[0], viewport[1], viewport[2], viewport[3] );
}
//show subgraph stack header
if (this._graph_stack && this._graph_stack.length) {
ctx.save();
var parent_graph = this._graph_stack[this._graph_stack.length - 1];
var subgraph_node = this.graph._subgraph_node;
ctx.strokeStyle = subgraph_node.bgcolor;
ctx.lineWidth = 10;
ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2);
ctx.lineWidth = 1;
ctx.font = "40px Arial";
ctx.textAlign = "center";
ctx.fillStyle = subgraph_node.bgcolor || "#AAA";
var title = "";
for (var i = 1; i < this._graph_stack.length; ++i) {
title +=
this._graph_stack[i]._subgraph_node.getTitle() + " >> ";
}
ctx.fillText(
title + subgraph_node.getTitle(),
canvas.width * 0.5,
40
);
ctx.restore();
}
var bg_already_painted = false;
if (this.onRenderBackground) {
bg_already_painted = this.onRenderBackground(canvas, ctx);
}
//reset in case of error
if ( !this.viewport )
{
ctx.restore();
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
this.visible_links.length = 0;
if (this.graph) {
//apply transformations
ctx.save();
this.ds.toCanvasContext(ctx);
//render BG
if ( this.ds.scale < 1.5 && !bg_already_painted && this.clear_background_color )
{
ctx.fillStyle = this.clear_background_color;
ctx.fillRect(
this.visible_area[0],
this.visible_area[1],
this.visible_area[2],
this.visible_area[3]
);
}
if (
this.background_image &&
this.ds.scale > 0.5 &&
!bg_already_painted
) {
if (this.zoom_modify_alpha) {
ctx.globalAlpha =
(1.0 - 0.5 / this.ds.scale) * this.editor_alpha;
} else {
ctx.globalAlpha = this.editor_alpha;
}
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = false; // ctx.mozImageSmoothingEnabled =
if (
!this._bg_img ||
this._bg_img.name != this.background_image
) {
this._bg_img = new Image();
this._bg_img.name = this.background_image;
this._bg_img.src = this.background_image;
var that = this;
this._bg_img.onload = function() {
that.draw(true, true);
};
}
var pattern = null;
if (this._pattern == null && this._bg_img.width > 0) {
pattern = ctx.createPattern(this._bg_img, "repeat");
this._pattern_img = this._bg_img;
this._pattern = pattern;
} else {
pattern = this._pattern;
}
if (pattern) {
ctx.fillStyle = pattern;
ctx.fillRect(
this.visible_area[0],
this.visible_area[1],
this.visible_area[2],
this.visible_area[3]
);
ctx.fillStyle = "transparent";
}
ctx.globalAlpha = 1.0;
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = true; //= ctx.mozImageSmoothingEnabled
}
//groups
if (this.graph._groups.length && !this.live_mode) {
this.drawGroups(canvas, ctx);
}
if (this.onDrawBackground) {
this.onDrawBackground(ctx, this.visible_area);
}
if (this.onBackgroundRender) {
//LEGACY
console.error(
"WARNING! onBackgroundRender deprecated, now is named onDrawBackground "
);
this.onBackgroundRender = null;
}
//DEBUG: show clipping area
//ctx.fillStyle = "red";
//ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20);
//bg
if (this.render_canvas_border) {
ctx.strokeStyle = "#235";
ctx.strokeRect(0, 0, canvas.width, canvas.height);
}
if (this.render_connections_shadows) {
ctx.shadowColor = "#000";
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 6;
} else {
ctx.shadowColor = "rgba(0,0,0,0)";
}
//draw connections
if (!this.live_mode) {
this.drawConnections(ctx);
}
ctx.shadowColor = "rgba(0,0,0,0)";
//restore state
ctx.restore();
}
if (ctx.finish) {
ctx.finish();
}
this.dirty_bgcanvas = false;
this.dirty_canvas = true; //to force to repaint the front canvas with the bgcanvas
};
var temp_vec2 = new Float32Array(2);
/**
* draws the given node inside the canvas
* @method drawNode
**/
LGraphCanvas.prototype.drawNode = function(node, ctx) {
var glow = false;
this.current_node = node;
var color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR;
var bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR;
//shadow and glow
if (node.mouseOver) {
glow = true;
}
var low_quality = this.ds.scale < 0.6; //zoomed out
//only render if it forces it to do it
if (this.live_mode) {
if (!node.flags.collapsed) {
ctx.shadowColor = "transparent";
if (node.onDrawForeground) {
node.onDrawForeground(ctx, this, this.canvas);
}
}
return;
}
var editor_alpha = this.editor_alpha;
ctx.globalAlpha = editor_alpha;
if (this.render_shadows && !low_quality) {
ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR;
ctx.shadowOffsetX = 2 * this.ds.scale;
ctx.shadowOffsetY = 2 * this.ds.scale;
ctx.shadowBlur = 3 * this.ds.scale;
} else {
ctx.shadowColor = "transparent";
}
//custom draw collapsed method (draw after shadows because they are affected)
if (
node.flags.collapsed &&
node.onDrawCollapsed &&
node.onDrawCollapsed(ctx, this) == true
) {
return;
}
//clip if required (mask)
var shape = node._shape || LiteGraph.BOX_SHAPE;
var size = temp_vec2;
temp_vec2.set(node.size);
var horizontal = node.horizontal; // || node.flags.horizontal;
if (node.flags.collapsed) {
ctx.font = this.inner_text_font;
var title = node.getTitle ? node.getTitle() : node.title;
if (title != null) {
node._collapsed_width = Math.min(
node.size[0],
ctx.measureText(title).width +
LiteGraph.NODE_TITLE_HEIGHT * 2
); //LiteGraph.NODE_COLLAPSED_WIDTH;
size[0] = node._collapsed_width;
size[1] = 0;
}
}
if (node.clip_area) {
//Start clipping
ctx.save();
ctx.beginPath();
if (shape == LiteGraph.BOX_SHAPE) {
ctx.rect(0, 0, size[0], size[1]);
} else if (shape == LiteGraph.ROUND_SHAPE) {
ctx.roundRect(0, 0, size[0], size[1], [10]);
} else if (shape == LiteGraph.CIRCLE_SHAPE) {
ctx.arc(
size[0] * 0.5,
size[1] * 0.5,
size[0] * 0.5,
0,
Math.PI * 2
);
}
ctx.clip();
}
//draw shape
if (node.has_errors) {
bgcolor = "red";
}
this.drawNodeShape(
node,
ctx,
size,
color,
bgcolor,
node.is_selected,
node.mouseOver
);
ctx.shadowColor = "transparent";
//draw foreground
if (node.onDrawForeground) {
node.onDrawForeground(ctx, this, this.canvas);
}
//connection slots
ctx.textAlign = horizontal ? "center" : "left";
ctx.font = this.inner_text_font;
var render_text = !low_quality;
var out_slot = this.connecting_output;
var in_slot = this.connecting_input;
ctx.lineWidth = 1;
var max_y = 0;
var slot_pos = new Float32Array(2); //to reuse
//render inputs and outputs
if (!node.flags.collapsed) {
//input connection slots
if (node.inputs) {
for (var i = 0; i < node.inputs.length; i++) {
var slot = node.inputs[i];
var slot_type = slot.type;
var slot_shape = slot.shape;
ctx.globalAlpha = editor_alpha;
//change opacity of incompatible slots when dragging a connection
if ( this.connecting_output && !LiteGraph.isValidConnection( slot.type , out_slot.type) ) {
ctx.globalAlpha = 0.4 * editor_alpha;
}
ctx.fillStyle =
slot.link != null
? slot.color_on ||
this.default_connection_color_byType[slot_type] ||
this.default_connection_color.input_on
: slot.color_off ||
this.default_connection_color_byTypeOff[slot_type] ||
this.default_connection_color_byType[slot_type] ||
this.default_connection_color.input_off;
var pos = node.getConnectionPos(true, i, slot_pos);
pos[0] -= node.pos[0];
pos[1] -= node.pos[1];
if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) {
max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5;
}
ctx.beginPath();
if (slot_type == "array"){
slot_shape = LiteGraph.GRID_SHAPE; // place in addInput? addOutput instead?
}
var doStroke = true;
if (
slot.type === LiteGraph.EVENT ||
slot.shape === LiteGraph.BOX_SHAPE
) {
if (horizontal) {
ctx.rect(
pos[0] - 5 + 0.5,
pos[1] - 8 + 0.5,
10,
14
);
} else {
ctx.rect(
pos[0] - 6 + 0.5,
pos[1] - 5 + 0.5,
14,
10
);
}
} else if (slot_shape === LiteGraph.ARROW_SHAPE) {
ctx.moveTo(pos[0] + 8, pos[1] + 0.5);
ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5);
ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5);
ctx.closePath();
} else if (slot_shape === LiteGraph.GRID_SHAPE) {
ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2);
ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2);
ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2);
ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2);
ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2);
ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2);
ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2);
ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2);
ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2);
doStroke = false;
} else {
if(low_quality)
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 ); //faster
else
ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2);
}
ctx.fill();
//render name
if (render_text) {
var text = slot.label != null ? slot.label : slot.name;
if (text) {
ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR;
if (horizontal || slot.dir == LiteGraph.UP) {
ctx.fillText(text, pos[0], pos[1] - 10);
} else {
ctx.fillText(text, pos[0] + 10, pos[1] + 5);
}
}
}
}
}
//output connection slots
ctx.textAlign = horizontal ? "center" : "right";
ctx.strokeStyle = "black";
if (node.outputs) {
for (var i = 0; i < node.outputs.length; i++) {
var slot = node.outputs[i];
var slot_type = slot.type;
var slot_shape = slot.shape;
//change opacity of incompatible slots when dragging a connection
if (this.connecting_input && !LiteGraph.isValidConnection( slot_type , in_slot.type) ) {
ctx.globalAlpha = 0.4 * editor_alpha;
}
var pos = node.getConnectionPos(false, i, slot_pos);
pos[0] -= node.pos[0];
pos[1] -= node.pos[1];
if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) {
max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5;
}
ctx.fillStyle =
slot.links && slot.links.length
? slot.color_on ||
this.default_connection_color_byType[slot_type] ||
this.default_connection_color.output_on
: slot.color_off ||
this.default_connection_color_byTypeOff[slot_type] ||
this.default_connection_color_byType[slot_type] ||
this.default_connection_color.output_off;
ctx.beginPath();
//ctx.rect( node.size[0] - 14,i*14,10,10);
if (slot_type == "array"){
slot_shape = LiteGraph.GRID_SHAPE;
}
var doStroke = true;
if (
slot_type === LiteGraph.EVENT ||
slot_shape === LiteGraph.BOX_SHAPE
) {
if (horizontal) {
ctx.rect(
pos[0] - 5 + 0.5,
pos[1] - 8 + 0.5,
10,
14
);
} else {
ctx.rect(
pos[0] - 6 + 0.5,
pos[1] - 5 + 0.5,
14,
10
);
}
} else if (slot_shape === LiteGraph.ARROW_SHAPE) {
ctx.moveTo(pos[0] + 8, pos[1] + 0.5);
ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5);
ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5);
ctx.closePath();
} else if (slot_shape === LiteGraph.GRID_SHAPE) {
ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2);
ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2);
ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2);
ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2);
ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2);
ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2);
ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2);
ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2);
ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2);
doStroke = false;
} else {
if(low_quality)
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 );
else
ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2);
}
//trigger
//if(slot.node_id != null && slot.slot == -1)
// ctx.fillStyle = "#F85";
//if(slot.links != null && slot.links.length)
ctx.fill();
if(!low_quality && doStroke)
ctx.stroke();
//render output name
if (render_text) {
var text = slot.label != null ? slot.label : slot.name;
if (text) {
ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR;
if (horizontal || slot.dir == LiteGraph.DOWN) {
ctx.fillText(text, pos[0], pos[1] - 8);
} else {
ctx.fillText(text, pos[0] - 10, pos[1] + 5);
}
}
}
}
}
ctx.textAlign = "left";
ctx.globalAlpha = 1;
if (node.widgets) {
var widgets_y = max_y;
if (horizontal || node.widgets_up) {
widgets_y = 2;
}
if( node.widgets_start_y != null )
widgets_y = node.widgets_start_y;
this.drawNodeWidgets(
node,
widgets_y,
ctx,
this.node_widget && this.node_widget[0] == node
? this.node_widget[1]
: null
);
}
} else if (this.render_collapsed_slots) {
//if collapsed
var input_slot = null;
var output_slot = null;
//get first connected slot to render
if (node.inputs) {
for (var i = 0; i < node.inputs.length; i++) {
var slot = node.inputs[i];
if (slot.link == null) {
continue;
}
input_slot = slot;
break;
}
}
if (node.outputs) {
for (var i = 0; i < node.outputs.length; i++) {
var slot = node.outputs[i];
if (!slot.links || !slot.links.length) {
continue;
}
output_slot = slot;
}
}
if (input_slot) {
var x = 0;
var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center
if (horizontal) {
x = node._collapsed_width * 0.5;
y = -LiteGraph.NODE_TITLE_HEIGHT;
}
ctx.fillStyle = "#686";
ctx.beginPath();
if (
slot.type === LiteGraph.EVENT ||
slot.shape === LiteGraph.BOX_SHAPE
) {
ctx.rect(x - 7 + 0.5, y - 4, 14, 8);
} else if (slot.shape === LiteGraph.ARROW_SHAPE) {
ctx.moveTo(x + 8, y);
ctx.lineTo(x + -4, y - 4);
ctx.lineTo(x + -4, y + 4);
ctx.closePath();
} else {
ctx.arc(x, y, 4, 0, Math.PI * 2);
}
ctx.fill();
}
if (output_slot) {
var x = node._collapsed_width;
var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center
if (horizontal) {
x = node._collapsed_width * 0.5;
y = 0;
}
ctx.fillStyle = "#686";
ctx.strokeStyle = "black";
ctx.beginPath();
if (
slot.type === LiteGraph.EVENT ||
slot.shape === LiteGraph.BOX_SHAPE
) {
ctx.rect(x - 7 + 0.5, y - 4, 14, 8);
} else if (slot.shape === LiteGraph.ARROW_SHAPE) {
ctx.moveTo(x + 6, y);
ctx.lineTo(x - 6, y - 4);
ctx.lineTo(x - 6, y + 4);
ctx.closePath();
} else {
ctx.arc(x, y, 4, 0, Math.PI * 2);
}
ctx.fill();
//ctx.stroke();
}
}
if (node.clip_area) {
ctx.restore();
}
ctx.globalAlpha = 1.0;
};
//used by this.over_link_center
LGraphCanvas.prototype.drawLinkTooltip = function( ctx, link )
{
var pos = link._pos;
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc( pos[0], pos[1], 3, 0, Math.PI * 2 );
ctx.fill();
if(link.data == null)
return;
if(this.onDrawLinkTooltip)
if( this.onDrawLinkTooltip(ctx,link,this) == true )
return;
var data = link.data;
var text = null;
if( data.constructor === Number )
text = data.toFixed(2);
else if( data.constructor === String )
text = "\"" + data + "\"";
else if( data.constructor === Boolean )
text = String(data);
else if (data.toToolTip)
text = data.toToolTip();
else
text = "[" + data.constructor.name + "]";
if(text == null)
return;
text = text.substr(0,30); //avoid weird
ctx.font = "14px Courier New";
var info = ctx.measureText(text);
var w = info.width + 20;
var h = 24;
ctx.shadowColor = "black";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 3;
ctx.fillStyle = "#454";
ctx.beginPath();
ctx.roundRect( pos[0] - w*0.5, pos[1] - 15 - h, w, h, [3]);
ctx.moveTo( pos[0] - 10, pos[1] - 15 );
ctx.lineTo( pos[0] + 10, pos[1] - 15 );
ctx.lineTo( pos[0], pos[1] - 5 );
ctx.fill();
ctx.shadowColor = "transparent";
ctx.textAlign = "center";
ctx.fillStyle = "#CEC";
ctx.fillText(text, pos[0], pos[1] - 15 - h * 0.3);
}
/**
* draws the shape of the given node in the canvas
* @method drawNodeShape
**/
var tmp_area = new Float32Array(4);
LGraphCanvas.prototype.drawNodeShape = function(
node,
ctx,
size,
fgcolor,
bgcolor,
selected,
mouse_over
) {
//bg rect
ctx.strokeStyle = fgcolor;
ctx.fillStyle = bgcolor;
var title_height = LiteGraph.NODE_TITLE_HEIGHT;
var low_quality = this.ds.scale < 0.5;
//render node area depending on shape
var shape =
node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE;
var title_mode = node.constructor.title_mode;
var render_title = true;
if (title_mode == LiteGraph.TRANSPARENT_TITLE || title_mode == LiteGraph.NO_TITLE) {
render_title = false;
} else if (title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over) {
render_title = true;
}
var area = tmp_area;
area[0] = 0; //x
area[1] = render_title ? -title_height : 0; //y
area[2] = size[0] + 1; //w
area[3] = render_title ? size[1] + title_height : size[1]; //h
var old_alpha = ctx.globalAlpha;
//full node shape
//if(node.flags.collapsed)
{
ctx.beginPath();
if (shape == LiteGraph.BOX_SHAPE || low_quality) {
ctx.fillRect(area[0], area[1], area[2], area[3]);
} else if (
shape == LiteGraph.ROUND_SHAPE ||
shape == LiteGraph.CARD_SHAPE
) {
ctx.roundRect(
area[0],
area[1],
area[2],
area[3],
shape == LiteGraph.CARD_SHAPE ? [this.round_radius,this.round_radius,0,0] : [this.round_radius]
);
} else if (shape == LiteGraph.CIRCLE_SHAPE) {
ctx.arc(
size[0] * 0.5,
size[1] * 0.5,
size[0] * 0.5,
0,
Math.PI * 2
);
}
ctx.fill();
//separator
if(!node.flags.collapsed && render_title)
{
ctx.shadowColor = "transparent";
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.fillRect(0, -1, area[2], 2);
}
}
ctx.shadowColor = "transparent";
if (node.onDrawBackground) {
node.onDrawBackground(ctx, this, this.canvas, this.graph_mouse );
}
//title bg (remember, it is rendered ABOVE the node)
if (render_title || title_mode == LiteGraph.TRANSPARENT_TITLE) {
//title bar
if (node.onDrawTitleBar) {
node.onDrawTitleBar( ctx, title_height, size, this.ds.scale, fgcolor );
} else if (
title_mode != LiteGraph.TRANSPARENT_TITLE &&
(node.constructor.title_color || this.render_title_colored)
) {
var title_color = node.constructor.title_color || fgcolor;
if (node.flags.collapsed) {
ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR;
}
//* gradient test
if (this.use_gradients) {
var grad = LGraphCanvas.gradients[title_color];
if (!grad) {
grad = LGraphCanvas.gradients[ title_color ] = ctx.createLinearGradient(0, 0, 400, 0);
grad.addColorStop(0, title_color); // TODO refactor: validate color !! prevent DOMException
grad.addColorStop(1, "#000");
}
ctx.fillStyle = grad;
} else {
ctx.fillStyle = title_color;
}
//ctx.globalAlpha = 0.5 * old_alpha;
ctx.beginPath();
if (shape == LiteGraph.BOX_SHAPE || low_quality) {
ctx.rect(0, -title_height, size[0] + 1, title_height);
} else if ( shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE ) {
ctx.roundRect(
0,
-title_height,
size[0] + 1,
title_height,
node.flags.collapsed ? [this.round_radius] : [this.round_radius,this.round_radius,0,0]
);
}
ctx.fill();
ctx.shadowColor = "transparent";
}
var colState = false;
if (LiteGraph.node_box_coloured_by_mode){
if(LiteGraph.NODE_MODES_COLORS[node.mode]){
colState = LiteGraph.NODE_MODES_COLORS[node.mode];
}
}
if (LiteGraph.node_box_coloured_when_on){
colState = node.action_triggered ? "#FFF" : (node.execute_triggered ? "#AAA" : colState);
}
//title box
var box_size = 10;
if (node.onDrawTitleBox) {
node.onDrawTitleBox(ctx, title_height, size, this.ds.scale);
} else if (
shape == LiteGraph.ROUND_SHAPE ||
shape == LiteGraph.CIRCLE_SHAPE ||
shape == LiteGraph.CARD_SHAPE
) {
if (low_quality) {
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(
title_height * 0.5,
title_height * -0.5,
box_size * 0.5 + 1,
0,
Math.PI * 2
);
ctx.fill();
}
ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR;
if(low_quality)
ctx.fillRect( title_height * 0.5 - box_size *0.5, title_height * -0.5 - box_size *0.5, box_size , box_size );
else
{
ctx.beginPath();
ctx.arc(
title_height * 0.5,
title_height * -0.5,
box_size * 0.5,
0,
Math.PI * 2
);
ctx.fill();
}
} else {
if (low_quality) {
ctx.fillStyle = "black";
ctx.fillRect(
(title_height - box_size) * 0.5 - 1,
(title_height + box_size) * -0.5 - 1,
box_size + 2,
box_size + 2
);
}
ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR;
ctx.fillRect(
(title_height - box_size) * 0.5,
(title_height + box_size) * -0.5,
box_size,
box_size
);
}
ctx.globalAlpha = old_alpha;
//title text
if (node.onDrawTitleText) {
node.onDrawTitleText(
ctx,
title_height,
size,
this.ds.scale,
this.title_text_font,
selected
);
}
if (!low_quality) {
ctx.font = this.title_text_font;
var title = String(node.getTitle());
if (title) {
if (selected) {
ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR;
} else {
ctx.fillStyle =
node.constructor.title_text_color ||
this.node_title_color;
}
if (node.flags.collapsed) {
ctx.textAlign = "left";
var measure = ctx.measureText(title);
ctx.fillText(
title.substr(0,20), //avoid urls too long
title_height,// + measure.width * 0.5,
LiteGraph.NODE_TITLE_TEXT_Y - title_height
);
ctx.textAlign = "left";
} else {
ctx.textAlign = "left";
ctx.fillText(
title,
title_height,
LiteGraph.NODE_TITLE_TEXT_Y - title_height
);
}
}
}
//subgraph box
if (!node.flags.collapsed && node.subgraph && !node.skip_subgraph_button) {
var w = LiteGraph.NODE_TITLE_HEIGHT;
var x = node.size[0] - w;
var over = LiteGraph.isInsideRectangle( this.graph_mouse[0] - node.pos[0], this.graph_mouse[1] - node.pos[1], x+2, -w+2, w-4, w-4 );
ctx.fillStyle = over ? "#888" : "#555";
if( shape == LiteGraph.BOX_SHAPE || low_quality)
ctx.fillRect(x+2, -w+2, w-4, w-4);
else
{
ctx.beginPath();
ctx.roundRect(x+2, -w+2, w-4, w-4,[4]);
ctx.fill();
}
ctx.fillStyle = "#333";
ctx.beginPath();
ctx.moveTo(x + w * 0.2, -w * 0.6);
ctx.lineTo(x + w * 0.8, -w * 0.6);
ctx.lineTo(x + w * 0.5, -w * 0.3);
ctx.fill();
}
//custom title render
if (node.onDrawTitle) {
node.onDrawTitle(ctx);
}
}
//render selection marker
if (selected) {
if (node.onBounding) {
node.onBounding(area);
}
if (title_mode == LiteGraph.TRANSPARENT_TITLE) {
area[1] -= title_height;
area[3] += title_height;
}
ctx.lineWidth = 1;
ctx.globalAlpha = 0.8;
ctx.beginPath();
if (shape == LiteGraph.BOX_SHAPE) {
ctx.rect(
-6 + area[0],
-6 + area[1],
12 + area[2],
12 + area[3]
);
} else if (
shape == LiteGraph.ROUND_SHAPE ||
(shape == LiteGraph.CARD_SHAPE && node.flags.collapsed)
) {
ctx.roundRect(
-6 + area[0],
-6 + area[1],
12 + area[2],
12 + area[3],
[this.round_radius * 2]
);
} else if (shape == LiteGraph.CARD_SHAPE) {
ctx.roundRect(
-6 + area[0],
-6 + area[1],
12 + area[2],
12 + area[3],
[this.round_radius * 2,2,this.round_radius * 2,2]
);
} else if (shape == LiteGraph.CIRCLE_SHAPE) {
ctx.arc(
size[0] * 0.5,
size[1] * 0.5,
size[0] * 0.5 + 6,
0,
Math.PI * 2
);
}
ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR;
ctx.stroke();
ctx.strokeStyle = fgcolor;
ctx.globalAlpha = 1;
}
// these counter helps in conditioning drawing based on if the node has been executed or an action occurred
if (node.execute_triggered>0) node.execute_triggered--;
if (node.action_triggered>0) node.action_triggered--;
};
var margin_area = new Float32Array(4);
var link_bounding = new Float32Array(4);
var tempA = new Float32Array(2);
var tempB = new Float32Array(2);
/**
* draws every connection visible in the canvas
* OPTIMIZE THIS: pre-catch connections position instead of recomputing them every time
* @method drawConnections
**/
LGraphCanvas.prototype.drawConnections = function(ctx) {
var now = LiteGraph.getTime();
var visible_area = this.visible_area;
margin_area[0] = visible_area[0] - 20;
margin_area[1] = visible_area[1] - 20;
margin_area[2] = visible_area[2] + 40;
margin_area[3] = visible_area[3] + 40;
//draw connections
ctx.lineWidth = this.connections_width;
ctx.fillStyle = "#AAA";
ctx.strokeStyle = "#AAA";
ctx.globalAlpha = this.editor_alpha;
//for every node
var nodes = this.graph._nodes;
for (var n = 0, l = nodes.length; n < l; ++n) {
var node = nodes[n];
//for every input (we render just inputs because it is easier as every slot can only have one input)
if (!node.inputs || !node.inputs.length) {
continue;
}
for (var i = 0; i < node.inputs.length; ++i) {
var input = node.inputs[i];
if (!input || input.link == null) {
continue;
}
var link_id = input.link;
var link = this.graph.links[link_id];
if (!link) {
continue;
}
//find link info
var start_node = this.graph.getNodeById(link.origin_id);
if (start_node == null) {
continue;
}
var start_node_slot = link.origin_slot;
var start_node_slotpos = null;
if (start_node_slot == -1) {
start_node_slotpos = [
start_node.pos[0] + 10,
start_node.pos[1] + 10
];
} else {
start_node_slotpos = start_node.getConnectionPos(
false,
start_node_slot,
tempA
);
}
var end_node_slotpos = node.getConnectionPos(true, i, tempB);
//compute link bounding
link_bounding[0] = start_node_slotpos[0];
link_bounding[1] = start_node_slotpos[1];
link_bounding[2] = end_node_slotpos[0] - start_node_slotpos[0];
link_bounding[3] = end_node_slotpos[1] - start_node_slotpos[1];
if (link_bounding[2] < 0) {
link_bounding[0] += link_bounding[2];
link_bounding[2] = Math.abs(link_bounding[2]);
}
if (link_bounding[3] < 0) {
link_bounding[1] += link_bounding[3];
link_bounding[3] = Math.abs(link_bounding[3]);
}
//skip links outside of the visible area of the canvas
if (!overlapBounding(link_bounding, margin_area)) {
continue;
}
var start_slot = start_node.outputs[start_node_slot];
var end_slot = node.inputs[i];
if (!start_slot || !end_slot) {
continue;
}
var start_dir =
start_slot.dir ||
(start_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT);
var end_dir =
end_slot.dir ||
(node.horizontal ? LiteGraph.UP : LiteGraph.LEFT);
this.renderLink(
ctx,
start_node_slotpos,
end_node_slotpos,
link,
false,
0,
null,
start_dir,
end_dir
);
//event triggered rendered on top
if (link && link._last_time && now - link._last_time < 1000) {
var f = 2.0 - (now - link._last_time) * 0.002;
var tmp = ctx.globalAlpha;
ctx.globalAlpha = tmp * f;
this.renderLink(
ctx,
start_node_slotpos,
end_node_slotpos,
link,
true,
f,
"white",
start_dir,
end_dir
);
ctx.globalAlpha = tmp;
}
}
}
ctx.globalAlpha = 1;
};
/**
* draws a link between two points
* @method renderLink
* @param {vec2} a start pos
* @param {vec2} b end pos
* @param {Object} link the link object with all the link info
* @param {boolean} skip_border ignore the shadow of the link
* @param {boolean} flow show flow animation (for events)
* @param {string} color the color for the link
* @param {number} start_dir the direction enum
* @param {number} end_dir the direction enum
* @param {number} num_sublines number of sublines (useful to represent vec3 or rgb)
**/
LGraphCanvas.prototype.renderLink = function(
ctx,
a,
b,
link,
skip_border,
flow,
color,
start_dir,
end_dir,
num_sublines
) {
if (link) {
this.visible_links.push(link);
}
//choose color
if (!color && link) {
color = link.color || LGraphCanvas.link_type_colors[link.type];
}
if (!color) {
color = this.default_link_color;
}
if (link != null && this.highlighted_links[link.id]) {
color = "#FFF";
}
start_dir = start_dir || LiteGraph.RIGHT;
end_dir = end_dir || LiteGraph.LEFT;
var dist = distance(a, b);
if (this.render_connections_border && this.ds.scale > 0.6) {
ctx.lineWidth = this.connections_width + 4;
}
ctx.lineJoin = "round";
num_sublines = num_sublines || 1;
if (num_sublines > 1) {
ctx.lineWidth = 0.5;
}
//begin line shape
ctx.beginPath();
for (var i = 0; i < num_sublines; i += 1) {
var offsety = (i - (num_sublines - 1) * 0.5) * 5;
if (this.links_render_mode == LiteGraph.SPLINE_LINK) {
ctx.moveTo(a[0], a[1] + offsety);
var start_offset_x = 0;
var start_offset_y = 0;
var end_offset_x = 0;
var end_offset_y = 0;
switch (start_dir) {
case LiteGraph.LEFT:
start_offset_x = dist * -0.25;
break;
case LiteGraph.RIGHT:
start_offset_x = dist * 0.25;
break;
case LiteGraph.UP:
start_offset_y = dist * -0.25;
break;
case LiteGraph.DOWN:
start_offset_y = dist * 0.25;
break;
}
switch (end_dir) {
case LiteGraph.LEFT:
end_offset_x = dist * -0.25;
break;
case LiteGraph.RIGHT:
end_offset_x = dist * 0.25;
break;
case LiteGraph.UP:
end_offset_y = dist * -0.25;
break;
case LiteGraph.DOWN:
end_offset_y = dist * 0.25;
break;
}
ctx.bezierCurveTo(
a[0] + start_offset_x,
a[1] + start_offset_y + offsety,
b[0] + end_offset_x,
b[1] + end_offset_y + offsety,
b[0],
b[1] + offsety
);
} else if (this.links_render_mode == LiteGraph.LINEAR_LINK) {
ctx.moveTo(a[0], a[1] + offsety);
var start_offset_x = 0;
var start_offset_y = 0;
var end_offset_x = 0;
var end_offset_y = 0;
switch (start_dir) {
case LiteGraph.LEFT:
start_offset_x = -1;
break;
case LiteGraph.RIGHT:
start_offset_x = 1;
break;
case LiteGraph.UP:
start_offset_y = -1;
break;
case LiteGraph.DOWN:
start_offset_y = 1;
break;
}
switch (end_dir) {
case LiteGraph.LEFT:
end_offset_x = -1;
break;
case LiteGraph.RIGHT:
end_offset_x = 1;
break;
case LiteGraph.UP:
end_offset_y = -1;
break;
case LiteGraph.DOWN:
end_offset_y = 1;
break;
}
var l = 15;
ctx.lineTo(
a[0] + start_offset_x * l,
a[1] + start_offset_y * l + offsety
);
ctx.lineTo(
b[0] + end_offset_x * l,
b[1] + end_offset_y * l + offsety
);
ctx.lineTo(b[0], b[1] + offsety);
} else if (this.links_render_mode == LiteGraph.STRAIGHT_LINK) {
ctx.moveTo(a[0], a[1]);
var start_x = a[0];
var start_y = a[1];
var end_x = b[0];
var end_y = b[1];
if (start_dir == LiteGraph.RIGHT) {
start_x += 10;
} else {
start_y += 10;
}
if (end_dir == LiteGraph.LEFT) {
end_x -= 10;
} else {
end_y -= 10;
}
ctx.lineTo(start_x, start_y);
ctx.lineTo((start_x + end_x) * 0.5, start_y);
ctx.lineTo((start_x + end_x) * 0.5, end_y);
ctx.lineTo(end_x, end_y);
ctx.lineTo(b[0], b[1]);
} else {
return;
} //unknown
}
//rendering the outline of the connection can be a little bit slow
if (
this.render_connections_border &&
this.ds.scale > 0.6 &&
!skip_border
) {
ctx.strokeStyle = "rgba(0,0,0,0.5)";
ctx.stroke();
}
ctx.lineWidth = this.connections_width;
ctx.fillStyle = ctx.strokeStyle = color;
ctx.stroke();
//end line shape
var pos = this.computeConnectionPoint(a, b, 0.5, start_dir, end_dir);
if (link && link._pos) {
link._pos[0] = pos[0];
link._pos[1] = pos[1];
}
//render arrow in the middle
if (
this.ds.scale >= 0.6 &&
this.highquality_render &&
end_dir != LiteGraph.CENTER
) {
//render arrow
if (this.render_connection_arrows) {
//compute two points in the connection
var posA = this.computeConnectionPoint(
a,
b,
0.25,
start_dir,
end_dir
);
var posB = this.computeConnectionPoint(
a,
b,
0.26,
start_dir,
end_dir
);
var posC = this.computeConnectionPoint(
a,
b,
0.75,
start_dir,
end_dir
);
var posD = this.computeConnectionPoint(
a,
b,
0.76,
start_dir,
end_dir
);
//compute the angle between them so the arrow points in the right direction
var angleA = 0;
var angleB = 0;
if (this.render_curved_connections) {
angleA = -Math.atan2(posB[0] - posA[0], posB[1] - posA[1]);
angleB = -Math.atan2(posD[0] - posC[0], posD[1] - posC[1]);
} else {
angleB = angleA = b[1] > a[1] ? 0 : Math.PI;
}
//render arrow
ctx.save();
ctx.translate(posA[0], posA[1]);
ctx.rotate(angleA);
ctx.beginPath();
ctx.moveTo(-5, -3);
ctx.lineTo(0, +7);
ctx.lineTo(+5, -3);
ctx.fill();
ctx.restore();
ctx.save();
ctx.translate(posC[0], posC[1]);
ctx.rotate(angleB);
ctx.beginPath();
ctx.moveTo(-5, -3);
ctx.lineTo(0, +7);
ctx.lineTo(+5, -3);
ctx.fill();
ctx.restore();
}
//circle
ctx.beginPath();
ctx.arc(pos[0], pos[1], 5, 0, Math.PI * 2);
ctx.fill();
}
//render flowing points
if (flow) {
ctx.fillStyle = color;
for (var i = 0; i < 5; ++i) {
var f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1;
var pos = this.computeConnectionPoint(
a,
b,
f,
start_dir,
end_dir
);
ctx.beginPath();
ctx.arc(pos[0], pos[1], 5, 0, 2 * Math.PI);
ctx.fill();
}
}
};
//returns the link center point based on curvature
LGraphCanvas.prototype.computeConnectionPoint = function(
a,
b,
t,
start_dir,
end_dir
) {
start_dir = start_dir || LiteGraph.RIGHT;
end_dir = end_dir || LiteGraph.LEFT;
var dist = distance(a, b);
var p0 = a;
var p1 = [a[0], a[1]];
var p2 = [b[0], b[1]];
var p3 = b;
switch (start_dir) {
case LiteGraph.LEFT:
p1[0] += dist * -0.25;
break;
case LiteGraph.RIGHT:
p1[0] += dist * 0.25;
break;
case LiteGraph.UP:
p1[1] += dist * -0.25;
break;
case LiteGraph.DOWN:
p1[1] += dist * 0.25;
break;
}
switch (end_dir) {
case LiteGraph.LEFT:
p2[0] += dist * -0.25;
break;
case LiteGraph.RIGHT:
p2[0] += dist * 0.25;
break;
case LiteGraph.UP:
p2[1] += dist * -0.25;
break;
case LiteGraph.DOWN:
p2[1] += dist * 0.25;
break;
}
var c1 = (1 - t) * (1 - t) * (1 - t);
var c2 = 3 * ((1 - t) * (1 - t)) * t;
var c3 = 3 * (1 - t) * (t * t);
var c4 = t * t * t;
var x = c1 * p0[0] + c2 * p1[0] + c3 * p2[0] + c4 * p3[0];
var y = c1 * p0[1] + c2 * p1[1] + c3 * p2[1] + c4 * p3[1];
return [x, y];
};
LGraphCanvas.prototype.drawExecutionOrder = function(ctx) {
ctx.shadowColor = "transparent";
ctx.globalAlpha = 0.25;
ctx.textAlign = "center";
ctx.strokeStyle = "white";
ctx.globalAlpha = 0.75;
var visible_nodes = this.visible_nodes;
for (var i = 0; i < visible_nodes.length; ++i) {
var node = visible_nodes[i];
ctx.fillStyle = "black";
ctx.fillRect(
node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT,
node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT,
LiteGraph.NODE_TITLE_HEIGHT,
LiteGraph.NODE_TITLE_HEIGHT
);
if (node.order == 0) {
ctx.strokeRect(
node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT + 0.5,
node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5,
LiteGraph.NODE_TITLE_HEIGHT,
LiteGraph.NODE_TITLE_HEIGHT
);
}
ctx.fillStyle = "#FFF";
ctx.fillText(
node.order,
node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5,
node.pos[1] - 6
);
}
ctx.globalAlpha = 1;
};
/**
* draws the widgets stored inside a node
* @method drawNodeWidgets
**/
LGraphCanvas.prototype.drawNodeWidgets = function(
node,
posY,
ctx,
active_widget
) {
if (!node.widgets || !node.widgets.length) {
return 0;
}
var width = node.size[0];
var widgets = node.widgets;
posY += 2;
var H = LiteGraph.NODE_WIDGET_HEIGHT;
var show_text = this.ds.scale > 0.5;
ctx.save();
ctx.globalAlpha = this.editor_alpha;
var outline_color = LiteGraph.WIDGET_OUTLINE_COLOR;
var background_color = LiteGraph.WIDGET_BGCOLOR;
var text_color = LiteGraph.WIDGET_TEXT_COLOR;
var secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
var margin = 15;
for (var i = 0; i < widgets.length; ++i) {
var w = widgets[i];
var y = posY;
if (w.y) {
y = w.y;
}
w.last_y = y;
ctx.strokeStyle = outline_color;
ctx.fillStyle = "#222";
ctx.textAlign = "left";
//ctx.lineWidth = 2;
if(w.disabled)
ctx.globalAlpha *= 0.5;
var widget_width = w.width || width;
switch (w.type) {
case "button":
if (w.clicked) {
ctx.fillStyle = "#AAA";
w.clicked = false;
this.dirty_canvas = true;
}
ctx.fillRect(margin, y, widget_width - margin * 2, H);
if(show_text && !w.disabled)
ctx.strokeRect( margin, y, widget_width - margin * 2, H );
if (show_text) {
ctx.textAlign = "center";
ctx.fillStyle = text_color;
ctx.fillText(w.label || w.name, widget_width * 0.5, y + H * 0.7);
}
break;
case "toggle":
ctx.textAlign = "left";
ctx.strokeStyle = outline_color;
ctx.fillStyle = background_color;
ctx.beginPath();
if (show_text)
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]);
else
ctx.rect(margin, y, widget_width - margin * 2, H );
ctx.fill();
if(show_text && !w.disabled)
ctx.stroke();
ctx.fillStyle = w.value ? "#89A" : "#333";
ctx.beginPath();
ctx.arc( widget_width - margin * 2, y + H * 0.5, H * 0.36, 0, Math.PI * 2 );
ctx.fill();
if (show_text) {
ctx.fillStyle = secondary_text_color;
const label = w.label || w.name;
if (label != null) {
ctx.fillText(label, margin * 2, y + H * 0.7);
}
ctx.fillStyle = w.value ? text_color : secondary_text_color;
ctx.textAlign = "right";
ctx.fillText(
w.value
? w.options.on || "true"
: w.options.off || "false",
widget_width - 40,
y + H * 0.7
);
}
break;
case "slider":
ctx.fillStyle = background_color;
ctx.fillRect(margin, y, widget_width - margin * 2, H);
var range = w.options.max - w.options.min;
var nvalue = (w.value - w.options.min) / range;
if(nvalue < 0.0) nvalue = 0.0;
if(nvalue > 1.0) nvalue = 1.0;
ctx.fillStyle = w.options.hasOwnProperty("slider_color") ? w.options.slider_color : (active_widget == w ? "#89A" : "#678");
ctx.fillRect(margin, y, nvalue * (widget_width - margin * 2), H);
if(show_text && !w.disabled)
ctx.strokeRect(margin, y, widget_width - margin * 2, H);
if (w.marker) {
var marker_nvalue = (w.marker - w.options.min) / range;
if(marker_nvalue < 0.0) marker_nvalue = 0.0;
if(marker_nvalue > 1.0) marker_nvalue = 1.0;
ctx.fillStyle = w.options.hasOwnProperty("marker_color") ? w.options.marker_color : "#AA9";
ctx.fillRect( margin + marker_nvalue * (widget_width - margin * 2), y, 2, H );
}
if (show_text) {
ctx.textAlign = "center";
ctx.fillStyle = text_color;
ctx.fillText(
w.label || w.name + " " + Number(w.value).toFixed(
w.options.precision != null
? w.options.precision
: 3
),
widget_width * 0.5,
y + H * 0.7
);
}
break;
case "number":
case "combo":
ctx.textAlign = "left";
ctx.strokeStyle = outline_color;
ctx.fillStyle = background_color;
ctx.beginPath();
if(show_text)
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5] );
else
ctx.rect(margin, y, widget_width - margin * 2, H );
ctx.fill();
if (show_text) {
if(!w.disabled)
ctx.stroke();
ctx.fillStyle = text_color;
if(!w.disabled)
{
ctx.beginPath();
ctx.moveTo(margin + 16, y + 5);
ctx.lineTo(margin + 6, y + H * 0.5);
ctx.lineTo(margin + 16, y + H - 5);
ctx.fill();
ctx.beginPath();
ctx.moveTo(widget_width - margin - 16, y + 5);
ctx.lineTo(widget_width - margin - 6, y + H * 0.5);
ctx.lineTo(widget_width - margin - 16, y + H - 5);
ctx.fill();
}
ctx.fillStyle = secondary_text_color;
ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7);
ctx.fillStyle = text_color;
ctx.textAlign = "right";
if (w.type == "number") {
ctx.fillText(
Number(w.value).toFixed(
w.options.precision !== undefined
? w.options.precision
: 3
),
widget_width - margin * 2 - 20,
y + H * 0.7
);
} else {
var v = w.value;
if( w.options.values )
{
var values = w.options.values;
if( values.constructor === Function )
values = values();
if(values && values.constructor !== Array)
v = values[ w.value ];
}
ctx.fillText(
v,
widget_width - margin * 2 - 20,
y + H * 0.7
);
}
}
break;
case "string":
case "text":
ctx.textAlign = "left";
ctx.strokeStyle = outline_color;
ctx.fillStyle = background_color;
ctx.beginPath();
if (show_text)
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]);
else
ctx.rect( margin, y, widget_width - margin * 2, H );
ctx.fill();
if (show_text) {
if(!w.disabled)
ctx.stroke();
ctx.save();
ctx.beginPath();
ctx.rect(margin, y, widget_width - margin * 2, H);
ctx.clip();
//ctx.stroke();
ctx.fillStyle = secondary_text_color;
const label = w.label || w.name;
if (label != null) {
ctx.fillText(label, margin * 2, y + H * 0.7);
}
ctx.fillStyle = text_color;
ctx.textAlign = "right";
ctx.fillText(String(w.value).substr(0,30), widget_width - margin * 2, y + H * 0.7); //30 chars max
ctx.restore();
}
break;
default:
if (w.draw) {
w.draw(ctx, node, widget_width, y, H);
}
break;
}
posY += (w.computeSize ? w.computeSize(widget_width)[1] : H) + 4;
ctx.globalAlpha = this.editor_alpha;
}
ctx.restore();
ctx.textAlign = "left";
};
/**
* process an event on widgets
* @method processNodeWidgets
**/
LGraphCanvas.prototype.processNodeWidgets = function(
node,
pos,
event,
active_widget
) {
if (!node.widgets || !node.widgets.length || (!this.allow_interaction && !node.flags.allow_interaction)) {
return null;
}
var x = pos[0] - node.pos[0];
var y = pos[1] - node.pos[1];
var width = node.size[0];
var that = this;
var ref_window = this.getCanvasWindow();
for (var i = 0; i < node.widgets.length; ++i) {
var w = node.widgets[i];
if(!w || w.disabled)
continue;
var widget_height = w.computeSize ? w.computeSize(width)[1] : LiteGraph.NODE_WIDGET_HEIGHT;
var widget_width = w.width || width;
//outside
if ( w != active_widget &&
(x < 6 || x > widget_width - 12 || y < w.last_y || y > w.last_y + widget_height || w.last_y === undefined) )
continue;
var old_value = w.value;
//if ( w == active_widget || (x > 6 && x < widget_width - 12 && y > w.last_y && y < w.last_y + widget_height) ) {
//inside widget
switch (w.type) {
case "button":
if (event.type === LiteGraph.pointerevents_method+"down") {
if (w.callback) {
setTimeout(function() {
w.callback(w, that, node, pos, event);
}, 20);
}
w.clicked = true;
this.dirty_canvas = true;
}
break;
case "slider":
var old_value = w.value;
var nvalue = clamp((x - 15) / (widget_width - 30), 0, 1);
if(w.options.read_only) break;
w.value = w.options.min + (w.options.max - w.options.min) * nvalue;
if (old_value != w.value) {
setTimeout(function() {
inner_value_change(w, w.value);
}, 20);
}
this.dirty_canvas = true;
break;
case "number":
case "combo":
var old_value = w.value;
if (event.type == LiteGraph.pointerevents_method+"move" && w.type == "number") {
if(event.deltaX)
w.value += event.deltaX * 0.1 * (w.options.step || 1);
if ( w.options.min != null && w.value < w.options.min ) {
w.value = w.options.min;
}
if ( w.options.max != null && w.value > w.options.max ) {
w.value = w.options.max;
}
} else if (event.type == LiteGraph.pointerevents_method+"down") {
var values = w.options.values;
if (values && values.constructor === Function) {
values = w.options.values(w, node);
}
var values_list = null;
if( w.type != "number")
values_list = values.constructor === Array ? values : Object.keys(values);
var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0;
if (w.type == "number") {
w.value += delta * 0.1 * (w.options.step || 1);
if ( w.options.min != null && w.value < w.options.min ) {
w.value = w.options.min;
}
if ( w.options.max != null && w.value > w.options.max ) {
w.value = w.options.max;
}
} else if (delta) { //clicked in arrow, used for combos
var index = -1;
this.last_mouseclick = 0; //avoids dobl click event
if(values.constructor === Object)
index = values_list.indexOf( String( w.value ) ) + delta;
else
index = values_list.indexOf( w.value ) + delta;
if (index >= values_list.length) {
index = values_list.length - 1;
}
if (index < 0) {
index = 0;
}
if( values.constructor === Array )
w.value = values[index];
else
w.value = index;
} else { //combo clicked
var text_values = values != values_list ? Object.values(values) : values;
var menu = new LiteGraph.ContextMenu(text_values, {
scale: Math.max(1, this.ds.scale),
event: event,
className: "dark",
callback: inner_clicked.bind(w)
},
ref_window);
function inner_clicked(v, option, event) {
if(values != values_list)
v = text_values.indexOf(v);
this.value = v;
inner_value_change(this, v);
that.dirty_canvas = true;
return false;
}
}
} //end mousedown
else if(event.type == LiteGraph.pointerevents_method+"up" && w.type == "number")
{
var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0;
if (event.click_time < 200 && delta == 0) {
this.prompt("Value",w.value,function(v) {
// check if v is a valid equation or a number
if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) {
try {//solve the equation if possible
v = eval(v);
} catch (e) { }
}
this.value = Number(v);
inner_value_change(this, this.value);
}.bind(w),
event);
}
}
if( old_value != w.value )
setTimeout(
function() {
inner_value_change(this, this.value);
}.bind(w),
20
);
this.dirty_canvas = true;
break;
case "toggle":
if (event.type == LiteGraph.pointerevents_method+"down") {
w.value = !w.value;
setTimeout(function() {
inner_value_change(w, w.value);
}, 20);
}
break;
case "string":
case "text":
if (event.type == LiteGraph.pointerevents_method+"down") {
this.prompt("Value",w.value,function(v) {
inner_value_change(this, v);
}.bind(w),
event,w.options ? w.options.multiline : false );
}
break;
default:
if (w.mouse) {
this.dirty_canvas = w.mouse(event, [x, y], node);
}
break;
} //end switch
//value changed
if( old_value != w.value )
{
if(node.onWidgetChanged)
node.onWidgetChanged( w.name,w.value,old_value,w );
node.graph._version++;
}
return w;
}//end for
function inner_value_change(widget, value) {
if(widget.type == "number"){
value = Number(value);
}
widget.value = value;
if ( widget.options && widget.options.property && node.properties[widget.options.property] !== undefined ) {
node.setProperty( widget.options.property, value );
}
if (widget.callback) {
widget.callback(widget.value, that, node, pos, event);
}
}
return null;
};
/**
* draws every group area in the background
* @method drawGroups
**/
LGraphCanvas.prototype.drawGroups = function(canvas, ctx) {
if (!this.graph) {
return;
}
var groups = this.graph._groups;
ctx.save();
ctx.globalAlpha = 0.5 * this.editor_alpha;
for (var i = 0; i < groups.length; ++i) {
var group = groups[i];
if (!overlapBounding(this.visible_area, group._bounding)) {
continue;
} //out of the visible area
ctx.fillStyle = group.color || "#335";
ctx.strokeStyle = group.color || "#335";
var pos = group._pos;
var size = group._size;
ctx.globalAlpha = 0.25 * this.editor_alpha;
ctx.beginPath();
ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], size[1]);
ctx.fill();
ctx.globalAlpha = this.editor_alpha;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(pos[0] + size[0], pos[1] + size[1]);
ctx.lineTo(pos[0] + size[0] - 10, pos[1] + size[1]);
ctx.lineTo(pos[0] + size[0], pos[1] + size[1] - 10);
ctx.fill();
var font_size =
group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE;
ctx.font = font_size + "px Arial";
ctx.textAlign = "left";
ctx.fillText(group.title, pos[0] + 4, pos[1] + font_size);
}
ctx.restore();
};
LGraphCanvas.prototype.adjustNodesSize = function() {
var nodes = this.graph._nodes;
for (var i = 0; i < nodes.length; ++i) {
nodes[i].size = nodes[i].computeSize();
}
this.setDirty(true, true);
};
/**
* resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode
* @method resize
**/
LGraphCanvas.prototype.resize = function(width, height) {
if (!width && !height) {
var parent = this.canvas.parentNode;
width = parent.offsetWidth;
height = parent.offsetHeight;
}
if (this.canvas.width == width && this.canvas.height == height) {
return;
}
this.canvas.width = width;
this.canvas.height = height;
this.bgcanvas.width = this.canvas.width;
this.bgcanvas.height = this.canvas.height;
this.setDirty(true, true);
};
/**
* switches to live mode (node shapes are not rendered, only the content)
* this feature was designed when graphs where meant to create user interfaces
* @method switchLiveMode
**/
LGraphCanvas.prototype.switchLiveMode = function(transition) {
if (!transition) {
this.live_mode = !this.live_mode;
this.dirty_canvas = true;
this.dirty_bgcanvas = true;
return;
}
var self = this;
var delta = this.live_mode ? 1.1 : 0.9;
if (this.live_mode) {
this.live_mode = false;
this.editor_alpha = 0.1;
}
var t = setInterval(function() {
self.editor_alpha *= delta;
self.dirty_canvas = true;
self.dirty_bgcanvas = true;
if (delta < 1 && self.editor_alpha < 0.01) {
clearInterval(t);
if (delta < 1) {
self.live_mode = true;
}
}
if (delta > 1 && self.editor_alpha > 0.99) {
clearInterval(t);
self.editor_alpha = 1;
}
}, 1);
};
LGraphCanvas.prototype.onNodeSelectionChange = function(node) {
return; //disabled
};
/* this is an implementation for touch not in production and not ready
*/
/*LGraphCanvas.prototype.touchHandler = function(event) {
//alert("foo");
var touches = event.changedTouches,
first = touches[0],
type = "";
switch (event.type) {
case "touchstart":
type = "mousedown";
break;
case "touchmove":
type = "mousemove";
break;
case "touchend":
type = "mouseup";
break;
default:
return;
}
//initMouseEvent(type, canBubble, cancelable, view, clickCount,
// screenX, screenY, clientX, clientY, ctrlKey,
// altKey, shiftKey, metaKey, button, relatedTarget);
// this is eventually a Dom object, get the LGraphCanvas back
if(typeof this.getCanvasWindow == "undefined"){
var window = this.lgraphcanvas.getCanvasWindow();
}else{
var window = this.getCanvasWindow();
}
var document = window.document;
var simulatedEvent = document.createEvent("MouseEvent");
simulatedEvent.initMouseEvent(
type,
true,
true,
window,
1,
first.screenX,
first.screenY,
first.clientX,
first.clientY,
false,
false,
false,
false,
0, //left
null
);
first.target.dispatchEvent(simulatedEvent);
event.preventDefault();
};*/
/* CONTEXT MENU ********************/
LGraphCanvas.onGroupAdd = function(info, entry, mouse_event) {
var canvas = LGraphCanvas.active_canvas;
var ref_window = canvas.getCanvasWindow();
var group = new LiteGraph.LGraphGroup();
group.pos = canvas.convertEventToCanvasOffset(mouse_event);
canvas.graph.add(group);
};
/**
* Determines the furthest nodes in each direction
* @param nodes {LGraphNode[]} the nodes to from which boundary nodes will be extracted
* @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}}
*/
LGraphCanvas.getBoundaryNodes = function(nodes) {
let top = null;
let right = null;
let bottom = null;
let left = null;
for (const nID in nodes) {
const node = nodes[nID];
const [x, y] = node.pos;
const [width, height] = node.size;
if (top === null || y < top.pos[1]) {
top = node;
}
if (right === null || x + width > right.pos[0] + right.size[0]) {
right = node;
}
if (bottom === null || y + height > bottom.pos[1] + bottom.size[1]) {
bottom = node;
}
if (left === null || x < left.pos[0]) {
left = node;
}
}
return {
"top": top,
"right": right,
"bottom": bottom,
"left": left
};
}
/**
* Determines the furthest nodes in each direction for the currently selected nodes
* @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}}
*/
LGraphCanvas.prototype.boundaryNodesForSelection = function() {
return LGraphCanvas.getBoundaryNodes(Object.values(this.selected_nodes));
}
/**
*
* @param {LGraphNode[]} nodes a list of nodes
* @param {"top"|"bottom"|"left"|"right"} direction Direction to align the nodes
* @param {LGraphNode?} align_to Node to align to (if null, align to the furthest node in the given direction)
*/
LGraphCanvas.alignNodes = function (nodes, direction, align_to) {
if (!nodes) {
return;
}
const canvas = LGraphCanvas.active_canvas;
let boundaryNodes = []
if (align_to === undefined) {
boundaryNodes = LGraphCanvas.getBoundaryNodes(nodes)
} else {
boundaryNodes = {
"top": align_to,
"right": align_to,
"bottom": align_to,
"left": align_to
}
}
for (const [_, node] of Object.entries(canvas.selected_nodes)) {
switch (direction) {
case "right":
node.pos[0] = boundaryNodes["right"].pos[0] + boundaryNodes["right"].size[0] - node.size[0];
break;
case "left":
node.pos[0] = boundaryNodes["left"].pos[0];
break;
case "top":
node.pos[1] = boundaryNodes["top"].pos[1];
break;
case "bottom":
node.pos[1] = boundaryNodes["bottom"].pos[1] + boundaryNodes["bottom"].size[1] - node.size[1];
break;
}
}
canvas.dirty_canvas = true;
canvas.dirty_bgcanvas = true;
};
LGraphCanvas.onNodeAlign = function(value, options, event, prev_menu, node) {
new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], {
event: event,
callback: inner_clicked,
parentMenu: prev_menu,
});
function inner_clicked(value) {
LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase(), node);
}
}
LGraphCanvas.onGroupAlign = function(value, options, event, prev_menu) {
new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], {
event: event,
callback: inner_clicked,
parentMenu: prev_menu,
});
function inner_clicked(value) {
LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase());
}
}
LGraphCanvas.onMenuAdd = function (node, options, e, prev_menu, callback) {
var canvas = LGraphCanvas.active_canvas;
var ref_window = canvas.getCanvasWindow();
var graph = canvas.graph;
if (!graph)
return;
function inner_onMenuAdded(base_category ,prev_menu){
var categories = LiteGraph.getNodeTypesCategories(canvas.filter || graph.filter).filter(function(category){return category.startsWith(base_category)});
var entries = [];
categories.map(function(category){
if (!category)
return;
var base_category_regex = new RegExp('^(' + base_category + ')');
var category_name = category.replace(base_category_regex,"").split('/')[0];
var category_path = base_category === '' ? category_name + '/' : base_category + category_name + '/';
var name = category_name;
if(name.indexOf("::") != -1) //in case it has a namespace like "shader::math/rand" it hides the namespace
name = name.split("::")[1];
var index = entries.findIndex(function(entry){return entry.value === category_path});
if (index === -1) {
entries.push({ value: category_path, content: name, has_submenu: true, callback : function(value, event, mouseEvent, contextMenu){
inner_onMenuAdded(value.value, contextMenu)
}});
}
});
var nodes = LiteGraph.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter );
nodes.map(function(node){
if (node.skip_list)
return;
var entry = { value: node.type, content: node.title, has_submenu: false , callback : function(value, event, mouseEvent, contextMenu){
var first_event = contextMenu.getFirstEvent();
canvas.graph.beforeChange();
var node = LiteGraph.createNode(value.value);
if (node) {
node.pos = canvas.convertEventToCanvasOffset(first_event);
canvas.graph.add(node);
}
if(callback)
callback(node);
canvas.graph.afterChange();
}
}
entries.push(entry);
});
new LiteGraph.ContextMenu( entries, { event: e, parentMenu: prev_menu }, ref_window );
}
inner_onMenuAdded('',prev_menu);
return false;
};
LGraphCanvas.onMenuCollapseAll = function() {};
LGraphCanvas.onMenuNodeEdit = function() {};
LGraphCanvas.showMenuNodeOptionalInputs = function(
v,
options,
e,
prev_menu,
node
) {
if (!node) {
return;
}
var that = this;
var canvas = LGraphCanvas.active_canvas;
var ref_window = canvas.getCanvasWindow();
var options = node.optional_inputs;
if (node.onGetInputs) {
options = node.onGetInputs();
}
var entries = [];
if (options) {
for (var i=0; i < options.length; i++) {
var entry = options[i];
if (!entry) {
entries.push(null);
continue;
}
var label = entry[0];
if(!entry[2])
entry[2] = {};
if (entry[2].label) {
label = entry[2].label;
}
entry[2].removable = true;
var data = { content: label, value: entry };
if (entry[1] == LiteGraph.ACTION) {
data.className = "event";
}
entries.push(data);
}
}
if (node.onMenuNodeInputs) {
var retEntries = node.onMenuNodeInputs(entries);
if(retEntries) entries = retEntries;
}
if (!entries.length) {
console.log("no input entries");
return;
}
var menu = new LiteGraph.ContextMenu(
entries,
{
event: e,
callback: inner_clicked,
parentMenu: prev_menu,
node: node
},
ref_window
);
function inner_clicked(v, e, prev) {
if (!node) {
return;
}
if (v.callback) {
v.callback.call(that, node, v, e, prev);
}
if (v.value) {
node.graph.beforeChange();
node.addInput(v.value[0], v.value[1], v.value[2]);
if (node.onNodeInputAdd) { // callback to the node when adding a slot
node.onNodeInputAdd(v.value);
}
node.setDirtyCanvas(true, true);
node.graph.afterChange();
}
}
return false;
};
LGraphCanvas.showMenuNodeOptionalOutputs = function(
v,
options,
e,
prev_menu,
node
) {
if (!node) {
return;
}
var that = this;
var canvas = LGraphCanvas.active_canvas;
var ref_window = canvas.getCanvasWindow();
var options = node.optional_outputs;
if (node.onGetOutputs) {
options = node.onGetOutputs();
}
var entries = [];
if (options) {
for (var i=0; i < options.length; i++) {
var entry = options[i];
if (!entry) {
//separator?
entries.push(null);
continue;
}
if (
node.flags &&
node.flags.skip_repeated_outputs &&
node.findOutputSlot(entry[0]) != -1
) {
continue;
} //skip the ones already on
var label = entry[0];
if(!entry[2])
entry[2] = {};
if (entry[2].label) {
label = entry[2].label;
}
entry[2].removable = true;
var data = { content: label, value: entry };
if (entry[1] == LiteGraph.EVENT) {
data.className = "event";
}
entries.push(data);
}
}
if (this.onMenuNodeOutputs) {
entries = this.onMenuNodeOutputs(entries);
}
if (LiteGraph.do_add_triggers_slots){ //canvas.allow_addOutSlot_onExecuted
if (node.findOutputSlot("onExecuted") == -1){
entries.push({content: "On Executed", value: ["onExecuted", LiteGraph.EVENT, {nameLocked: true}], className: "event"}); //, opts: {}
}
}
// add callback for modifing the menu elements onMenuNodeOutputs
if (node.onMenuNodeOutputs) {
var retEntries = node.onMenuNodeOutputs(entries);
if(retEntries) entries = retEntries;
}
if (!entries.length) {
return;
}
var menu = new LiteGraph.ContextMenu(
entries,
{
event: e,
callback: inner_clicked,
parentMenu: prev_menu,
node: node
},
ref_window
);
function inner_clicked(v, e, prev) {
if (!node) {
return;
}
if (v.callback) {
v.callback.call(that, node, v, e, prev);
}
if (!v.value) {
return;
}
var value = v.value[1];
if (
value &&
(value.constructor === Object || value.constructor === Array)
) {
//submenu why?
var entries = [];
for (var i in value) {
entries.push({ content: i, value: value[i] });
}
new LiteGraph.ContextMenu(entries, {
event: e,
callback: inner_clicked,
parentMenu: prev_menu,
node: node
});
return false;
} else {
node.graph.beforeChange();
node.addOutput(v.value[0], v.value[1], v.value[2]);
if (node.onNodeOutputAdd) { // a callback to the node when adding a slot
node.onNodeOutputAdd(v.value);
}
node.setDirtyCanvas(true, true);
node.graph.afterChange();
}
}
return false;
};
LGraphCanvas.onShowMenuNodeProperties = function(
value,
options,
e,
prev_menu,
node
) {
if (!node || !node.properties) {
return;
}
var that = this;
var canvas = LGraphCanvas.active_canvas;
var ref_window = canvas.getCanvasWindow();
var entries = [];
for (var i in node.properties) {
var value = node.properties[i] !== undefined ? node.properties[i] : " ";
if( typeof value == "object" )
value = JSON.stringify(value);
var info = node.getPropertyInfo(i);
if(info.type == "enum" || info.type == "combo")
value = LGraphCanvas.getPropertyPrintableValue( value, info.values );
//value could contain invalid html characters, clean that
value = LGraphCanvas.decodeHTML(value);
entries.push({
content:
"<span class='property_name'>" +
(info.label ? info.label : i) +
"</span>" +
"<span class='property_value'>" +
value +
"</span>",
value: i
});
}
if (!entries.length) {
return;
}
var menu = new LiteGraph.ContextMenu(
entries,
{
event: e,
callback: inner_clicked,
parentMenu: prev_menu,
allow_html: true,
node: node
},
ref_window
);
function inner_clicked(v, options, e, prev) {
if (!node) {
return;
}
var rect = this.getBoundingClientRect();
canvas.showEditPropertyValue(node, v.value, {
position: [rect.left, rect.top]
});
}
return false;
};
LGraphCanvas.decodeHTML = function(str) {
var e = document.createElement("div");
e.innerText = str;
return e.innerHTML;
};
LGraphCanvas.onMenuResizeNode = function(value, options, e, menu, node) {
if (!node) {
return;
}
var fApplyMultiNode = function(node){
node.size = node.computeSize();
if (node.onResize)
node.onResize(node.size);
}
var graphcanvas = LGraphCanvas.active_canvas;
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){
fApplyMultiNode(node);
}else{
for (var i in graphcanvas.selected_nodes) {
fApplyMultiNode(graphcanvas.selected_nodes[i]);
}
}
node.setDirtyCanvas(true, true);
};
LGraphCanvas.prototype.showLinkMenu = function(link, e) {
var that = this;
// console.log(link);
var node_left = that.graph.getNodeById( link.origin_id );
var node_right = that.graph.getNodeById( link.target_id );
var fromType = false;
if (node_left && node_left.outputs && node_left.outputs[link.origin_slot]) fromType = node_left.outputs[link.origin_slot].type;
var destType = false;
if (node_right && node_right.outputs && node_right.outputs[link.target_slot]) destType = node_right.inputs[link.target_slot].type;
var options = ["Add Node",null,"Delete",null];
var menu = new LiteGraph.ContextMenu(options, {
event: e,
title: link.data != null ? link.data.constructor.name : null,
callback: inner_clicked
});
function inner_clicked(v,options,e) {
switch (v) {
case "Add Node":
LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){
// console.debug("node autoconnect");
if(!node.inputs || !node.inputs.length || !node.outputs || !node.outputs.length){
return;
}
// leave the connection type checking inside connectByType
if (node_left.connectByType( link.origin_slot, node, fromType )){
node.connectByType( link.target_slot, node_right, destType );
node.pos[0] -= node.size[0] * 0.5;
}
});
break;
case "Delete":
that.graph.removeLink(link.id);
break;
default:
/*var nodeCreated = createDefaultNodeForSlot({ nodeFrom: node_left
,slotFrom: link.origin_slot
,nodeTo: node
,slotTo: link.target_slot
,e: e
,nodeType: "AUTO"
});
if(nodeCreated) console.log("new node in beetween "+v+" created");*/
}
}
return false;
};
LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) { // addNodeMenu for connection
var optPass = optPass || {};
var opts = Object.assign({ nodeFrom: null // input
,slotFrom: null // input
,nodeTo: null // output
,slotTo: null // output
,position: [] // pass the event coords
,nodeType: null // choose a nodetype to add, AUTO to set at first good
,posAdd:[0,0] // adjust x,y
,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h
}
,optPass
);
var that = this;
var isFrom = opts.nodeFrom && opts.slotFrom!==null;
var isTo = !isFrom && opts.nodeTo && opts.slotTo!==null;
if (!isFrom && !isTo){
console.warn("No data passed to createDefaultNodeForSlot "+opts.nodeFrom+" "+opts.slotFrom+" "+opts.nodeTo+" "+opts.slotTo);
return false;
}
if (!opts.nodeType){
console.warn("No type to createDefaultNodeForSlot");
return false;
}
var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
var slotX = isFrom ? opts.slotFrom : opts.slotTo;
var iSlotConn = false;
switch (typeof slotX){
case "string":
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false);
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
case "object":
// ok slotX
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name);
break;
case "number":
iSlotConn = slotX;
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
case "undefined":
default:
// bad ?
//iSlotConn = 0;
console.warn("Cant get slot information "+slotX);
return false;
}
if (slotX===false || iSlotConn===false){
console.warn("createDefaultNodeForSlot bad slotX "+slotX+" "+iSlotConn);
}
// check for defaults nodes for this slottype
var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type;
var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in;
if(slotTypesDefault && slotTypesDefault[fromSlotType]){
if (slotX.link !== null) {
// is connected
}else{
// is not not connected
}
nodeNewType = false;
if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){
for(var typeX in slotTypesDefault[fromSlotType]){
if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO"){
nodeNewType = slotTypesDefault[fromSlotType][typeX];
// console.log("opts.nodeType == slotTypesDefault[fromSlotType][typeX] :: "+opts.nodeType);
break; // --------
}
}
}else{
if (opts.nodeType == slotTypesDefault[fromSlotType] || opts.nodeType == "AUTO") nodeNewType = slotTypesDefault[fromSlotType];
}
if (nodeNewType) {
var nodeNewOpts = false;
if (typeof nodeNewType == "object" && nodeNewType.node){
nodeNewOpts = nodeNewType;
nodeNewType = nodeNewType.node;
}
//that.graph.beforeChange();
var newNode = LiteGraph.createNode(nodeNewType);
if(newNode){
// if is object pass options
if (nodeNewOpts){
if (nodeNewOpts.properties) {
for (var i in nodeNewOpts.properties) {
newNode.addProperty( i, nodeNewOpts.properties[i] );
}
}
if (nodeNewOpts.inputs) {
newNode.inputs = [];
for (var i in nodeNewOpts.inputs) {
newNode.addOutput(
nodeNewOpts.inputs[i][0],
nodeNewOpts.inputs[i][1]
);
}
}
if (nodeNewOpts.outputs) {
newNode.outputs = [];
for (var i in nodeNewOpts.outputs) {
newNode.addOutput(
nodeNewOpts.outputs[i][0],
nodeNewOpts.outputs[i][1]
);
}
}
if (nodeNewOpts.title) {
newNode.title = nodeNewOpts.title;
}
if (nodeNewOpts.json) {
newNode.configure(nodeNewOpts.json);
}
}
// add the node
that.graph.add(newNode);
newNode.pos = [ opts.position[0]+opts.posAdd[0]+(opts.posSizeFix[0]?opts.posSizeFix[0]*newNode.size[0]:0)
,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/
//that.graph.afterChange();
// connect the two!
if (isFrom){
opts.nodeFrom.connectByType( iSlotConn, newNode, fromSlotType );
}else{
opts.nodeTo.connectByTypeOutput( iSlotConn, newNode, fromSlotType );
}
// if connecting in between
if (isFrom && isTo){
// TODO
}
return true;
}else{
console.log("failed creating "+nodeNewType);
}
}
}
return false;
}
LGraphCanvas.prototype.showConnectionMenu = function(optPass) { // addNodeMenu for connection
var optPass = optPass || {};
var opts = Object.assign({ nodeFrom: null // input
,slotFrom: null // input
,nodeTo: null // output
,slotTo: null // output
,e: null
}
,optPass
);
var that = this;
var isFrom = opts.nodeFrom && opts.slotFrom;
var isTo = !isFrom && opts.nodeTo && opts.slotTo;
if (!isFrom && !isTo){
console.warn("No data passed to showConnectionMenu");
return false;
}
var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
var slotX = isFrom ? opts.slotFrom : opts.slotTo;
var iSlotConn = false;
switch (typeof slotX){
case "string":
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false);
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
case "object":
// ok slotX
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name);
break;
case "number":
iSlotConn = slotX;
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
default:
// bad ?
//iSlotConn = 0;
console.warn("Cant get slot information "+slotX);
return false;
}
var options = ["Add Node",null];
if (that.allow_searchbox){
options.push("Search");
options.push(null);
}
// get defaults nodes for this slottype
var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type;
var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in;
if(slotTypesDefault && slotTypesDefault[fromSlotType]){
if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){
for(var typeX in slotTypesDefault[fromSlotType]){
options.push(slotTypesDefault[fromSlotType][typeX]);
}
}else{
options.push(slotTypesDefault[fromSlotType]);
}
}
// build menu
var menu = new LiteGraph.ContextMenu(options, {
event: opts.e,
title: (slotX && slotX.name!="" ? (slotX.name + (fromSlotType?" | ":"")) : "")+(slotX && fromSlotType ? fromSlotType : ""),
callback: inner_clicked
});
// callback
function inner_clicked(v,options,e) {
//console.log("Process showConnectionMenu selection");
switch (v) {
case "Add Node":
LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){
if (isFrom){
opts.nodeFrom.connectByType( iSlotConn, node, fromSlotType );
}else{
opts.nodeTo.connectByTypeOutput( iSlotConn, node, fromSlotType );
}
});
break;
case "Search":
if(isFrom){
that.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType});
}else{
that.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType});
}
break;
default:
// check for defaults nodes for this slottype
var nodeCreated = that.createDefaultNodeForSlot(Object.assign(opts,{ position: [opts.e.canvasX, opts.e.canvasY]
,nodeType: v
}));
if (nodeCreated){
// new node created
//console.log("node "+v+" created")
}else{
// failed or v is not in defaults
}
break;
}
}
return false;
};
// TODO refactor :: this is used fot title but not for properties!
LGraphCanvas.onShowPropertyEditor = function(item, options, e, menu, node) {
var input_html = "";
var property = item.property || "title";
var value = node[property];
// TODO refactor :: use createDialog ?
var dialog = document.createElement("div");
dialog.is_modified = false;
dialog.className = "graphdialog";
dialog.innerHTML =
"<span class='name'></span><input autofocus type='text' class='value'/><button>OK</button>";
dialog.close = function() {
if (dialog.parentNode) {
dialog.parentNode.removeChild(dialog);
}
};
var title = dialog.querySelector(".name");
title.innerText = property;
var input = dialog.querySelector(".value");
if (input) {
input.value = value;
input.addEventListener("blur", function(e) {
this.focus();
});
input.addEventListener("keydown", function(e) {
dialog.is_modified = true;
if (e.keyCode == 27) {
//ESC
dialog.close();
} else if (e.keyCode == 13) {
inner(); // save
} else if (e.keyCode != 13 && e.target.localName != "textarea") {
return;
}
e.preventDefault();
e.stopPropagation();
});
}
var graphcanvas = LGraphCanvas.active_canvas;
var canvas = graphcanvas.canvas;
var rect = canvas.getBoundingClientRect();
var offsetx = -20;
var offsety = -20;
if (rect) {
offsetx -= rect.left;
offsety -= rect.top;
}
if (event) {
dialog.style.left = event.clientX + offsetx + "px";
dialog.style.top = event.clientY + offsety + "px";
} else {
dialog.style.left = canvas.width * 0.5 + offsetx + "px";
dialog.style.top = canvas.height * 0.5 + offsety + "px";
}
var button = dialog.querySelector("button");
button.addEventListener("click", inner);
canvas.parentNode.appendChild(dialog);
if(input) input.focus();
var dialogCloseTimer = null;
dialog.addEventListener("mouseleave", function(e) {
if(LiteGraph.dialog_close_on_mouse_leave)
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave)
dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close();
});
dialog.addEventListener("mouseenter", function(e) {
if(LiteGraph.dialog_close_on_mouse_leave)
if(dialogCloseTimer) clearTimeout(dialogCloseTimer);
});
function inner() {
if(input) setValue(input.value);
}
function setValue(value) {
if (item.type == "Number") {
value = Number(value);
} else if (item.type == "Boolean") {
value = Boolean(value);
}
node[property] = value;
if (dialog.parentNode) {
dialog.parentNode.removeChild(dialog);
}
node.setDirtyCanvas(true, true);
}
};
// refactor: there are different dialogs, some uses createDialog some dont
LGraphCanvas.prototype.prompt = function(title, value, callback, event, multiline) {
var that = this;
var input_html = "";
title = title || "";
var dialog = document.createElement("div");
dialog.is_modified = false;
dialog.className = "graphdialog rounded";
if(multiline)
dialog.innerHTML = "<span class='name'></span> <textarea autofocus class='value'></textarea><button class='rounded'>OK</button>";
else
dialog.innerHTML = "<span class='name'></span> <input autofocus type='text' class='value'/><button class='rounded'>OK</button>";
dialog.close = function() {
that.prompt_box = null;
if (dialog.parentNode) {
dialog.parentNode.removeChild(dialog);
}
};
var graphcanvas = LGraphCanvas.active_canvas;
var canvas = graphcanvas.canvas;
canvas.parentNode.appendChild(dialog);
if (this.ds.scale > 1) {
dialog.style.transform = "scale(" + this.ds.scale + ")";
}
var dialogCloseTimer = null;
var prevent_timeout = false;
LiteGraph.pointerListenerAdd(dialog,"leave", function(e) {
if (prevent_timeout)
return;
if(LiteGraph.dialog_close_on_mouse_leave)
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave)
dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close();
});
LiteGraph.pointerListenerAdd(dialog,"enter", function(e) {
if(LiteGraph.dialog_close_on_mouse_leave)
if(dialogCloseTimer) clearTimeout(dialogCloseTimer);
});
var selInDia = dialog.querySelectorAll("select");
if (selInDia){
// if filtering, check focus changed to comboboxes and prevent closing
selInDia.forEach(function(selIn) {
selIn.addEventListener("click", function(e) {
prevent_timeout++;
});
selIn.addEventListener("blur", function(e) {
prevent_timeout = 0;
});
selIn.addEventListener("change", function(e) {
prevent_timeout = -1;
});
});
}
if (that.prompt_box) {
that.prompt_box.close();
}
that.prompt_box = dialog;
var first = null;
var timeout = null;
var selected = null;
var name_element = dialog.querySelector(".name");
name_element.innerText = title;
var value_element = dialog.querySelector(".value");
value_element.value = value;
var input = value_element;
input.addEventListener("keydown", function(e) {
dialog.is_modified = true;
if (e.keyCode == 27) {
//ESC
dialog.close();
} else if (e.keyCode == 13 && e.target.localName != "textarea") {
if (callback) {
callback(this.value);
}
dialog.close();
} else {
return;
}
e.preventDefault();
e.stopPropagation();
});
var button = dialog.querySelector("button");
button.addEventListener("click", function(e) {
if (callback) {
callback(input.value);
}
that.setDirty(true);
dialog.close();
});
var rect = canvas.getBoundingClientRect();
var offsetx = -20;
var offsety = -20;
if (rect) {
offsetx -= rect.left;
offsety -= rect.top;
}
if (event) {
dialog.style.left = event.clientX + offsetx + "px";
dialog.style.top = event.clientY + offsety + "px";
} else {
dialog.style.left = canvas.width * 0.5 + offsetx + "px";
dialog.style.top = canvas.height * 0.5 + offsety + "px";
}
setTimeout(function() {
input.focus();
}, 10);
return dialog;
};
LGraphCanvas.search_limit = -1;
LGraphCanvas.prototype.showSearchBox = function(event, options) {
// proposed defaults
var def_options = { slot_from: null
,node_from: null
,node_to: null
,do_type_filter: LiteGraph.search_filter_enabled // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out
,type_filter_in: false // these are default: pass to set initially set values
,type_filter_out: false
,show_general_if_none_on_typefilter: true
,show_general_after_typefiltered: true
,hide_on_mouse_leave: LiteGraph.search_hide_on_mouse_leave
,show_all_if_empty: true
,show_all_on_open: LiteGraph.search_show_all_on_open
};
options = Object.assign(def_options, options || {});
//console.log(options);
var that = this;
var input_html = "";
var graphcanvas = LGraphCanvas.active_canvas;
var canvas = graphcanvas.canvas;
var root_document = canvas.ownerDocument || document;
var dialog = document.createElement("div");
dialog.className = "litegraph litesearchbox graphdialog rounded";
dialog.innerHTML = "<span class='name'>Search</span> <input autofocus type='text' class='value rounded'/>";
if (options.do_type_filter){
dialog.innerHTML += "<select class='slot_in_type_filter'><option value=''></option></select>";
dialog.innerHTML += "<select class='slot_out_type_filter'><option value=''></option></select>";
}
dialog.innerHTML += "<div class='helper'></div>";
if( root_document.fullscreenElement )
root_document.fullscreenElement.appendChild(dialog);
else
{
root_document.body.appendChild(dialog);
root_document.body.style.overflow = "hidden";
}
// dialog element has been appended
if (options.do_type_filter){
var selIn = dialog.querySelector(".slot_in_type_filter");
var selOut = dialog.querySelector(".slot_out_type_filter");
}
dialog.close = function() {
that.search_box = null;
this.blur();
canvas.focus();
root_document.body.style.overflow = "";
setTimeout(function() {
that.canvas.focus();
}, 20); //important, if canvas loses focus keys wont be captured
if (dialog.parentNode) {
dialog.parentNode.removeChild(dialog);
}
};
if (this.ds.scale > 1) {
dialog.style.transform = "scale(" + this.ds.scale + ")";
}
// hide on mouse leave
if(options.hide_on_mouse_leave){
var prevent_timeout = false;
var timeout_close = null;
LiteGraph.pointerListenerAdd(dialog,"enter", function(e) {
if (timeout_close) {
clearTimeout(timeout_close);
timeout_close = null;
}
});
LiteGraph.pointerListenerAdd(dialog,"leave", function(e) {
if (prevent_timeout){
return;
}
timeout_close = setTimeout(function() {
dialog.close();
}, 500);
});
// if filtering, check focus changed to comboboxes and prevent closing
if (options.do_type_filter){
selIn.addEventListener("click", function(e) {
prevent_timeout++;
});
selIn.addEventListener("blur", function(e) {
prevent_timeout = 0;
});
selIn.addEventListener("change", function(e) {
prevent_timeout = -1;
});
selOut.addEventListener("click", function(e) {
prevent_timeout++;
});
selOut.addEventListener("blur", function(e) {
prevent_timeout = 0;
});
selOut.addEventListener("change", function(e) {
prevent_timeout = -1;
});
}
}
if (that.search_box) {
that.search_box.close();
}
that.search_box = dialog;
var helper = dialog.querySelector(".helper");
var first = null;
var timeout = null;
var selected = null;
var input = dialog.querySelector("input");
if (input) {
input.addEventListener("blur", function(e) {
this.focus();
});
input.addEventListener("keydown", function(e) {
if (e.keyCode == 38) {
//UP
changeSelection(false);
} else if (e.keyCode == 40) {
//DOWN
changeSelection(true);
} else if (e.keyCode == 27) {
//ESC
dialog.close();
} else if (e.keyCode == 13) {
if (selected) {
select(selected.innerHTML);
} else if (first) {
select(first);
} else {
dialog.close();
}
} else {
if (timeout) {
clearInterval(timeout);
}
timeout = setTimeout(refreshHelper, 250);
return;
}
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return true;
});
}
// if should filter on type, load and fill selected and choose elements if passed
if (options.do_type_filter){
if (selIn){
var aSlots = LiteGraph.slot_types_in;
var nSlots = aSlots.length; // this for object :: Object.keys(aSlots).length;
if (options.type_filter_in == LiteGraph.EVENT || options.type_filter_in == LiteGraph.ACTION)
options.type_filter_in = "_event_";
/* this will filter on * .. but better do it manually in case
else if(options.type_filter_in === "" || options.type_filter_in === 0)
options.type_filter_in = "*";*/
for (var iK=0; iK<nSlots; iK++){
var opt = document.createElement('option');
opt.value = aSlots[iK];
opt.innerHTML = aSlots[iK];
selIn.appendChild(opt);
if(options.type_filter_in !==false && (options.type_filter_in+"").toLowerCase() == (aSlots[iK]+"").toLowerCase()){
//selIn.selectedIndex ..
opt.selected = true;
//console.log("comparing IN "+options.type_filter_in+" :: "+aSlots[iK]);
}else{
//console.log("comparing OUT "+options.type_filter_in+" :: "+aSlots[iK]);
}
}
selIn.addEventListener("change",function(){
refreshHelper();
});
}
if (selOut){
var aSlots = LiteGraph.slot_types_out;
var nSlots = aSlots.length; // this for object :: Object.keys(aSlots).length;
if (options.type_filter_out == LiteGraph.EVENT || options.type_filter_out == LiteGraph.ACTION)
options.type_filter_out = "_event_";
/* this will filter on * .. but better do it manually in case
else if(options.type_filter_out === "" || options.type_filter_out === 0)
options.type_filter_out = "*";*/
for (var iK=0; iK<nSlots; iK++){
var opt = document.createElement('option');
opt.value = aSlots[iK];
opt.innerHTML = aSlots[iK];
selOut.appendChild(opt);
if(options.type_filter_out !==false && (options.type_filter_out+"").toLowerCase() == (aSlots[iK]+"").toLowerCase()){
//selOut.selectedIndex ..
opt.selected = true;
}
}
selOut.addEventListener("change",function(){
refreshHelper();
});
}
}
//compute best position
var rect = canvas.getBoundingClientRect();
var left = ( event ? event.clientX : (rect.left + rect.width * 0.5) ) - 80;
var top = ( event ? event.clientY : (rect.top + rect.height * 0.5) ) - 20;
dialog.style.left = left + "px";
dialog.style.top = top + "px";
//To avoid out of screen problems
if(event.layerY > (rect.height - 200))
helper.style.maxHeight = (rect.height - event.layerY - 20) + "px";
/*
var offsetx = -20;
var offsety = -20;
if (rect) {
offsetx -= rect.left;
offsety -= rect.top;
}
if (event) {
dialog.style.left = event.clientX + offsetx + "px";
dialog.style.top = event.clientY + offsety + "px";
} else {
dialog.style.left = canvas.width * 0.5 + offsetx + "px";
dialog.style.top = canvas.height * 0.5 + offsety + "px";
}
canvas.parentNode.appendChild(dialog);
*/
input.focus();
if (options.show_all_on_open) refreshHelper();
function select(name) {
if (name) {
if (that.onSearchBoxSelection) {
that.onSearchBoxSelection(name, event, graphcanvas);
} else {
var extra = LiteGraph.searchbox_extras[name.toLowerCase()];
if (extra) {
name = extra.type;
}
graphcanvas.graph.beforeChange();
var node = LiteGraph.createNode(name);
if (node) {
node.pos = graphcanvas.convertEventToCanvasOffset(
event
);
graphcanvas.graph.add(node, false);
}
if (extra && extra.data) {
if (extra.data.properties) {
for (var i in extra.data.properties) {
node.addProperty( i, extra.data.properties[i] );
}
}
if (extra.data.inputs) {
node.inputs = [];
for (var i in extra.data.inputs) {
node.addOutput(
extra.data.inputs[i][0],
extra.data.inputs[i][1]
);
}
}
if (extra.data.outputs) {
node.outputs = [];
for (var i in extra.data.outputs) {
node.addOutput(
extra.data.outputs[i][0],
extra.data.outputs[i][1]
);
}
}
if (extra.data.title) {
node.title = extra.data.title;
}
if (extra.data.json) {
node.configure(extra.data.json);
}
}
// join node after inserting
if (options.node_from){
var iS = false;
switch (typeof options.slot_from){
case "string":
iS = options.node_from.findOutputSlot(options.slot_from);
break;
case "object":
if (options.slot_from.name){
iS = options.node_from.findOutputSlot(options.slot_from.name);
}else{
iS = -1;
}
if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index;
break;
case "number":
iS = options.slot_from;
break;
default:
iS = 0; // try with first if no name set
}
if (typeof options.node_from.outputs[iS] !== "undefined"){
if (iS!==false && iS>-1){
options.node_from.connectByType( iS, node, options.node_from.outputs[iS].type );
}
}else{
// console.warn("cant find slot " + options.slot_from);
}
}
if (options.node_to){
var iS = false;
switch (typeof options.slot_from){
case "string":
iS = options.node_to.findInputSlot(options.slot_from);
break;
case "object":
if (options.slot_from.name){
iS = options.node_to.findInputSlot(options.slot_from.name);
}else{
iS = -1;
}
if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index;
break;
case "number":
iS = options.slot_from;
break;
default:
iS = 0; // try with first if no name set
}
if (typeof options.node_to.inputs[iS] !== "undefined"){
if (iS!==false && iS>-1){
// try connection
options.node_to.connectByTypeOutput(iS,node,options.node_to.inputs[iS].type);
}
}else{
// console.warn("cant find slot_nodeTO " + options.slot_from);
}
}
graphcanvas.graph.afterChange();
}
}
dialog.close();
}
function changeSelection(forward) {
var prev = selected;
if (selected) {
selected.classList.remove("selected");
}
if (!selected) {
selected = forward
? helper.childNodes[0]
: helper.childNodes[helper.childNodes.length];
} else {
selected = forward
? selected.nextSibling
: selected.previousSibling;
if (!selected) {
selected = prev;
}
}
if (!selected) {
return;
}
selected.classList.add("selected");
selected.scrollIntoView({block: "end", behavior: "smooth"});
}
function refreshHelper() {
timeout = null;
var str = input.value;
first = null;
helper.innerHTML = "";
if (!str && !options.show_all_if_empty) {
return;
}
if (that.onSearchBox) {
var list = that.onSearchBox(helper, str, graphcanvas);
if (list) {
for (var i = 0; i < list.length; ++i) {
addResult(list[i]);
}
}
} else {
var c = 0;
str = str.toLowerCase();
var filter = graphcanvas.filter || graphcanvas.graph.filter;
// filter by type preprocess
if(options.do_type_filter && that.search_box){
var sIn = that.search_box.querySelector(".slot_in_type_filter");
var sOut = that.search_box.querySelector(".slot_out_type_filter");
}else{
var sIn = false;
var sOut = false;
}
//extras
for (var i in LiteGraph.searchbox_extras) {
var extra = LiteGraph.searchbox_extras[i];
if ((!options.show_all_if_empty || str) && extra.desc.toLowerCase().indexOf(str) === -1) {
continue;
}
var ctor = LiteGraph.registered_node_types[ extra.type ];
if( ctor && ctor.filter != filter )
continue;
if( ! inner_test_filter(extra.type) )
continue;
addResult( extra.desc, "searchbox_extra" );
if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) {
break;
}
}
var filtered = null;
if (Array.prototype.filter) { //filter supported
var keys = Object.keys( LiteGraph.registered_node_types ); //types
var filtered = keys.filter( inner_test_filter );
} else {
filtered = [];
for (var i in LiteGraph.registered_node_types) {
if( inner_test_filter(i) )
filtered.push(i);
}
}
for (var i = 0; i < filtered.length; i++) {
addResult(filtered[i]);
if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) {
break;
}
}
// add general type if filtering
if (options.show_general_after_typefiltered
&& (sIn.value || sOut.value)
){
filtered_extra = [];
for (var i in LiteGraph.registered_node_types) {
if( inner_test_filter(i, {inTypeOverride: sIn&&sIn.value?"*":false, outTypeOverride: sOut&&sOut.value?"*":false}) )
filtered_extra.push(i);
}
for (var i = 0; i < filtered_extra.length; i++) {
addResult(filtered_extra[i], "generic_type");
if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) {
break;
}
}
}
// check il filtering gave no results
if ((sIn.value || sOut.value) &&
( (helper.childNodes.length == 0 && options.show_general_if_none_on_typefilter) )
){
filtered_extra = [];
for (var i in LiteGraph.registered_node_types) {
if( inner_test_filter(i, {skipFilter: true}) )
filtered_extra.push(i);
}
for (var i = 0; i < filtered_extra.length; i++) {
addResult(filtered_extra[i], "not_in_filter");
if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) {
break;
}
}
}
function inner_test_filter( type, optsIn )
{
var optsIn = optsIn || {};
var optsDef = { skipFilter: false
,inTypeOverride: false
,outTypeOverride: false
};
var opts = Object.assign(optsDef,optsIn);
var ctor = LiteGraph.registered_node_types[ type ];
if(filter && ctor.filter != filter )
return false;
if ((!options.show_all_if_empty || str) && type.toLowerCase().indexOf(str) === -1)
return false;
// filter by slot IN, OUT types
if(options.do_type_filter && !opts.skipFilter){
var sType = type;
var sV = sIn.value;
if (opts.inTypeOverride!==false) sV = opts.inTypeOverride;
//if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1
if(sIn && sV){
//console.log("will check filter against "+sV);
if (LiteGraph.registered_slot_in_types[sV] && LiteGraph.registered_slot_in_types[sV].nodes){ // type is stored
//console.debug("check "+sType+" in "+LiteGraph.registered_slot_in_types[sV].nodes);
var doesInc = LiteGraph.registered_slot_in_types[sV].nodes.includes(sType);
if (doesInc!==false){
//console.log(sType+" HAS "+sV);
}else{
/*console.debug(LiteGraph.registered_slot_in_types[sV]);
console.log(+" DONT includes "+type);*/
return false;
}
}
}
var sV = sOut.value;
if (opts.outTypeOverride!==false) sV = opts.outTypeOverride;
//if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1
if(sOut && sV){
//console.log("search will check filter against "+sV);
if (LiteGraph.registered_slot_out_types[sV] && LiteGraph.registered_slot_out_types[sV].nodes){ // type is stored
//console.debug("check "+sType+" in "+LiteGraph.registered_slot_out_types[sV].nodes);
var doesInc = LiteGraph.registered_slot_out_types[sV].nodes.includes(sType);
if (doesInc!==false){
//console.log(sType+" HAS "+sV);
}else{
/*console.debug(LiteGraph.registered_slot_out_types[sV]);
console.log(+" DONT includes "+type);*/
return false;
}
}
}
}
return true;
}
}
function addResult(type, className) {
var help = document.createElement("div");
if (!first) {
first = type;
}
help.innerText = type;
help.dataset["type"] = escape(type);
help.className = "litegraph lite-search-item";
if (className) {
help.className += " " + className;
}
help.addEventListener("click", function(e) {
select(unescape(this.dataset["type"]));
});
helper.appendChild(help);
}
}
return dialog;
};
LGraphCanvas.prototype.showEditPropertyValue = function( node, property, options ) {
if (!node || node.properties[property] === undefined) {
return;
}
options = options || {};
var that = this;
var info = node.getPropertyInfo(property);
var type = info.type;
var input_html = "";
if (type == "string" || type == "number" || type == "array" || type == "object") {
input_html = "<input autofocus type='text' class='value'/>";
} else if ( (type == "enum" || type == "combo") && info.values) {
input_html = "<select autofocus type='text' class='value'>";
for (var i in info.values) {
var v = i;
if( info.values.constructor === Array )
v = info.values[i];
input_html +=
"<option value='" +
v +
"' " +
(v == node.properties[property] ? "selected" : "") +
">" +
info.values[i] +
"</option>";
}
input_html += "</select>";
} else if (type == "boolean" || type == "toggle") {
input_html =
"<input autofocus type='checkbox' class='value' " +
(node.properties[property] ? "checked" : "") +
"/>";
} else {
console.warn("unknown type: " + type);
return;
}
var dialog = this.createDialog(
"<span class='name'>" +
(info.label ? info.label : property) +
"</span>" +
input_html +
"<button>OK</button>",
options
);
var input = false;
if ((type == "enum" || type == "combo") && info.values) {
input = dialog.querySelector("select");
input.addEventListener("change", function(e) {
dialog.modified();
setValue(e.target.value);
//var index = e.target.value;
//setValue( e.options[e.selectedIndex].value );
});
} else if (type == "boolean" || type == "toggle") {
input = dialog.querySelector("input");
if (input) {
input.addEventListener("click", function(e) {
dialog.modified();
setValue(!!input.checked);
});
}
} else {
input = dialog.querySelector("input");
if (input) {
input.addEventListener("blur", function(e) {
this.focus();
});
var v = node.properties[property] !== undefined ? node.properties[property] : "";
if (type !== 'string') {
v = JSON.stringify(v);
}
input.value = v;
input.addEventListener("keydown", function(e) {
if (e.keyCode == 27) {
//ESC
dialog.close();
} else if (e.keyCode == 13) {
// ENTER
inner(); // save
} else if (e.keyCode != 13) {
dialog.modified();
return;
}
e.preventDefault();
e.stopPropagation();
});
}
}
if (input) input.focus();
var button = dialog.querySelector("button");
button.addEventListener("click", inner);
function inner() {
setValue(input.value);
}
function setValue(value) {
if(info && info.values && info.values.constructor === Object && info.values[value] != undefined )
value = info.values[value];
if (typeof node.properties[property] == "number") {
value = Number(value);
}
if (type == "array" || type == "object") {
value = JSON.parse(value);
}
node.properties[property] = value;
if (node.graph) {
node.graph._version++;
}
if (node.onPropertyChanged) {
node.onPropertyChanged(property, value);
}
if(options.onclose)
options.onclose();
dialog.close();
node.setDirtyCanvas(true, true);
}
return dialog;
};
// TODO refactor, theer are different dialog, some uses createDialog, some dont
LGraphCanvas.prototype.createDialog = function(html, options) {
var def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true };
options = Object.assign(def_options, options || {});
var dialog = document.createElement("div");
dialog.className = "graphdialog";
dialog.innerHTML = html;
dialog.is_modified = false;
var rect = this.canvas.getBoundingClientRect();
var offsetx = -20;
var offsety = -20;
if (rect) {
offsetx -= rect.left;
offsety -= rect.top;
}
if (options.position) {
offsetx += options.position[0];
offsety += options.position[1];
} else if (options.event) {
offsetx += options.event.clientX;
offsety += options.event.clientY;
} //centered
else {
offsetx += this.canvas.width * 0.5;
offsety += this.canvas.height * 0.5;
}
dialog.style.left = offsetx + "px";
dialog.style.top = offsety + "px";
this.canvas.parentNode.appendChild(dialog);
// acheck for input and use default behaviour: save on enter, close on esc
if (options.checkForInput){
var aI = [];
var focused = false;
if (aI = dialog.querySelectorAll("input")){
aI.forEach(function(iX) {
iX.addEventListener("keydown",function(e){
dialog.modified();
if (e.keyCode == 27) {
dialog.close();
} else if (e.keyCode != 13) {
return;
}
// set value ?
e.preventDefault();
e.stopPropagation();
});
if (!focused) iX.focus();
});
}
}
dialog.modified = function(){
dialog.is_modified = true;
}
dialog.close = function() {
if (dialog.parentNode) {
dialog.parentNode.removeChild(dialog);
}
};
var dialogCloseTimer = null;
var prevent_timeout = false;
dialog.addEventListener("mouseleave", function(e) {
if (prevent_timeout)
return;
if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave)
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave)
dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close();
});
dialog.addEventListener("mouseenter", function(e) {
if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave)
if(dialogCloseTimer) clearTimeout(dialogCloseTimer);
});
var selInDia = dialog.querySelectorAll("select");
if (selInDia){
// if filtering, check focus changed to comboboxes and prevent closing
selInDia.forEach(function(selIn) {
selIn.addEventListener("click", function(e) {
prevent_timeout++;
});
selIn.addEventListener("blur", function(e) {
prevent_timeout = 0;
});
selIn.addEventListener("change", function(e) {
prevent_timeout = -1;
});
});
}
return dialog;
};
LGraphCanvas.prototype.createPanel = function(title, options) {
options = options || {};
var ref_window = options.window || window;
var root = document.createElement("div");
root.className = "litegraph dialog";
root.innerHTML = "<div class='dialog-header'><span class='dialog-title'></span></div><div class='dialog-content'></div><div style='display:none;' class='dialog-alt-content'></div><div class='dialog-footer'></div>";
root.header = root.querySelector(".dialog-header");
if(options.width)
root.style.width = options.width + (options.width.constructor === Number ? "px" : "");
if(options.height)
root.style.height = options.height + (options.height.constructor === Number ? "px" : "");
if(options.closable)
{
var close = document.createElement("span");
close.innerHTML = "&#10005;";
close.classList.add("close");
close.addEventListener("click",function(){
root.close();
});
root.header.appendChild(close);
}
root.title_element = root.querySelector(".dialog-title");
root.title_element.innerText = title;
root.content = root.querySelector(".dialog-content");
root.alt_content = root.querySelector(".dialog-alt-content");
root.footer = root.querySelector(".dialog-footer");
root.close = function()
{
if (root.onClose && typeof root.onClose == "function"){
root.onClose();
}
if(root.parentNode)
root.parentNode.removeChild(root);
/* XXX CHECK THIS */
if(this.parentNode){
this.parentNode.removeChild(this);
}
/* XXX this was not working, was fixed with an IF, check this */
}
// function to swap panel content
root.toggleAltContent = function(force){
if (typeof force != "undefined"){
var vTo = force ? "block" : "none";
var vAlt = force ? "none" : "block";
}else{
var vTo = root.alt_content.style.display != "block" ? "block" : "none";
var vAlt = root.alt_content.style.display != "block" ? "none" : "block";
}
root.alt_content.style.display = vTo;
root.content.style.display = vAlt;
}
root.toggleFooterVisibility = function(force){
if (typeof force != "undefined"){
var vTo = force ? "block" : "none";
}else{
var vTo = root.footer.style.display != "block" ? "block" : "none";
}
root.footer.style.display = vTo;
}
root.clear = function()
{
this.content.innerHTML = "";
}
root.addHTML = function(code, classname, on_footer)
{
var elem = document.createElement("div");
if(classname)
elem.className = classname;
elem.innerHTML = code;
if(on_footer)
root.footer.appendChild(elem);
else
root.content.appendChild(elem);
return elem;
}
root.addButton = function( name, callback, options )
{
var elem = document.createElement("button");
elem.innerText = name;
elem.options = options;
elem.classList.add("btn");
elem.addEventListener("click",callback);
root.footer.appendChild(elem);
return elem;
}
root.addSeparator = function()
{
var elem = document.createElement("div");
elem.className = "separator";
root.content.appendChild(elem);
}
root.addWidget = function( type, name, value, options, callback )
{
options = options || {};
var str_value = String(value);
type = type.toLowerCase();
if(type == "number")
str_value = value.toFixed(3);
var elem = document.createElement("div");
elem.className = "property";
elem.innerHTML = "<span class='property_name'></span><span class='property_value'></span>";
elem.querySelector(".property_name").innerText = options.label || name;
var value_element = elem.querySelector(".property_value");
value_element.innerText = str_value;
elem.dataset["property"] = name;
elem.dataset["type"] = options.type || type;
elem.options = options;
elem.value = value;
if( type == "code" )
elem.addEventListener("click", function(e){ root.inner_showCodePad( this.dataset["property"] ); });
else if (type == "boolean")
{
elem.classList.add("boolean");
if(value)
elem.classList.add("bool-on");
elem.addEventListener("click", function(){
//var v = node.properties[this.dataset["property"]];
//node.setProperty(this.dataset["property"],!v); this.innerText = v ? "true" : "false";
var propname = this.dataset["property"];
this.value = !this.value;
this.classList.toggle("bool-on");
this.querySelector(".property_value").innerText = this.value ? "true" : "false";
innerChange(propname, this.value );
});
}
else if (type == "string" || type == "number")
{
value_element.setAttribute("contenteditable",true);
value_element.addEventListener("keydown", function(e){
if(e.code == "Enter" && (type != "string" || !e.shiftKey)) // allow for multiline
{
e.preventDefault();
this.blur();
}
});
value_element.addEventListener("blur", function(){
var v = this.innerText;
var propname = this.parentNode.dataset["property"];
var proptype = this.parentNode.dataset["type"];
if( proptype == "number")
v = Number(v);
innerChange(propname, v);
});
}
else if (type == "enum" || type == "combo") {
var str_value = LGraphCanvas.getPropertyPrintableValue( value, options.values );
value_element.innerText = str_value;
value_element.addEventListener("click", function(event){
var values = options.values || [];
var propname = this.parentNode.dataset["property"];
var elem_that = this;
var menu = new LiteGraph.ContextMenu(values,{
event: event,
className: "dark",
callback: inner_clicked
},
ref_window);
function inner_clicked(v, option, event) {
//node.setProperty(propname,v);
//graphcanvas.dirty_canvas = true;
elem_that.innerText = v;
innerChange(propname,v);
return false;
}
});
}
root.content.appendChild(elem);
function innerChange(name, value)
{
//console.log("change",name,value);
//that.dirty_canvas = true;
if(options.callback)
options.callback(name,value,options);
if(callback)
callback(name,value,options);
}
return elem;
}
if (root.onOpen && typeof root.onOpen == "function") root.onOpen();
return root;
};
LGraphCanvas.getPropertyPrintableValue = function(value, values)
{
if(!values)
return String(value);
if(values.constructor === Array)
{
return String(value);
}
if(values.constructor === Object)
{
var desc_value = "";
for(var k in values)
{
if(values[k] != value)
continue;
desc_value = k;
break;
}
return String(value) + " ("+desc_value+")";
}
}
LGraphCanvas.prototype.closePanels = function(){
var panel = document.querySelector("#node-panel");
if(panel)
panel.close();
var panel = document.querySelector("#option-panel");
if(panel)
panel.close();
}
LGraphCanvas.prototype.showShowGraphOptionsPanel = function(refOpts, obEv, refMenu, refMenu2){
if(this.constructor && this.constructor.name == "HTMLDivElement"){
// assume coming from the menu event click
if (!obEv || !obEv.event || !obEv.event.target || !obEv.event.target.lgraphcanvas){
console.warn("Canvas not found"); // need a ref to canvas obj
/*console.debug(event);
console.debug(event.target);*/
return;
}
var graphcanvas = obEv.event.target.lgraphcanvas;
}else{
// assume called internally
var graphcanvas = this;
}
graphcanvas.closePanels();
var ref_window = graphcanvas.getCanvasWindow();
panel = graphcanvas.createPanel("Options",{
closable: true
,window: ref_window
,onOpen: function(){
graphcanvas.OPTIONPANEL_IS_OPEN = true;
}
,onClose: function(){
graphcanvas.OPTIONPANEL_IS_OPEN = false;
graphcanvas.options_panel = null;
}
});
graphcanvas.options_panel = panel;
panel.id = "option-panel";
panel.classList.add("settings");
function inner_refresh(){
panel.content.innerHTML = ""; //clear
var fUpdate = function(name, value, options){
switch(name){
/*case "Render mode":
// Case ""..
if (options.values && options.key){
var kV = Object.values(options.values).indexOf(value);
if (kV>=0 && options.values[kV]){
console.debug("update graph options: "+options.key+": "+kV);
graphcanvas[options.key] = kV;
//console.debug(graphcanvas);
break;
}
}
console.warn("unexpected options");
console.debug(options);
break;*/
default:
//console.debug("want to update graph options: "+name+": "+value);
if (options && options.key){
name = options.key;
}
if (options.values){
value = Object.values(options.values).indexOf(value);
}
//console.debug("update graph option: "+name+": "+value);
graphcanvas[name] = value;
break;
}
};
// panel.addWidget( "string", "Graph name", "", {}, fUpdate); // implement
var aProps = LiteGraph.availableCanvasOptions;
aProps.sort();
for(var pI in aProps){
var pX = aProps[pI];
panel.addWidget( "boolean", pX, graphcanvas[pX], {key: pX, on: "True", off: "False"}, fUpdate);
}
var aLinks = [ graphcanvas.links_render_mode ];
panel.addWidget( "combo", "Render mode", LiteGraph.LINK_RENDER_MODES[graphcanvas.links_render_mode], {key: "links_render_mode", values: LiteGraph.LINK_RENDER_MODES}, fUpdate);
panel.addSeparator();
panel.footer.innerHTML = ""; // clear
}
inner_refresh();
graphcanvas.canvas.parentNode.appendChild( panel );
}
LGraphCanvas.prototype.showShowNodePanel = function( node )
{
this.SELECTED_NODE = node;
this.closePanels();
var ref_window = this.getCanvasWindow();
var that = this;
var graphcanvas = this;
var panel = this.createPanel(node.title || "",{
closable: true
,window: ref_window
,onOpen: function(){
graphcanvas.NODEPANEL_IS_OPEN = true;
}
,onClose: function(){
graphcanvas.NODEPANEL_IS_OPEN = false;
graphcanvas.node_panel = null;
}
});
graphcanvas.node_panel = panel;
panel.id = "node-panel";
panel.node = node;
panel.classList.add("settings");
function inner_refresh()
{
panel.content.innerHTML = ""; //clear
panel.addHTML("<span class='node_type'>"+node.type+"</span><span class='node_desc'>"+(node.constructor.desc || "")+"</span><span class='separator'></span>");
panel.addHTML("<h3>Properties</h3>");
var fUpdate = function(name,value){
graphcanvas.graph.beforeChange(node);
switch(name){
case "Title":
node.title = value;
break;
case "Mode":
var kV = Object.values(LiteGraph.NODE_MODES).indexOf(value);
if (kV>=0 && LiteGraph.NODE_MODES[kV]){
node.changeMode(kV);
}else{
console.warn("unexpected mode: "+value);
}
break;
case "Color":
if (LGraphCanvas.node_colors[value]){
node.color = LGraphCanvas.node_colors[value].color;
node.bgcolor = LGraphCanvas.node_colors[value].bgcolor;
}else{
console.warn("unexpected color: "+value);
}
break;
default:
node.setProperty(name,value);
break;
}
graphcanvas.graph.afterChange();
graphcanvas.dirty_canvas = true;
};
panel.addWidget( "string", "Title", node.title, {}, fUpdate);
panel.addWidget( "combo", "Mode", LiteGraph.NODE_MODES[node.mode], {values: LiteGraph.NODE_MODES}, fUpdate);
var nodeCol = "";
if (node.color !== undefined){
nodeCol = Object.keys(LGraphCanvas.node_colors).filter(function(nK){ return LGraphCanvas.node_colors[nK].color == node.color; });
}
panel.addWidget( "combo", "Color", nodeCol, {values: Object.keys(LGraphCanvas.node_colors)}, fUpdate);
for(var pName in node.properties)
{
var value = node.properties[pName];
var info = node.getPropertyInfo(pName);
var type = info.type || "string";
//in case the user wants control over the side panel widget
if( node.onAddPropertyToPanel && node.onAddPropertyToPanel(pName,panel) )
continue;
panel.addWidget( info.widget || info.type, pName, value, info, fUpdate);
}
panel.addSeparator();
if(node.onShowCustomPanelInfo)
node.onShowCustomPanelInfo(panel);
panel.footer.innerHTML = ""; // clear
panel.addButton("Delete",function(){
if(node.block_delete)
return;
node.graph.remove(node);
panel.close();
}).classList.add("delete");
}
panel.inner_showCodePad = function( propname )
{
panel.classList.remove("settings");
panel.classList.add("centered");
/*if(window.CodeFlask) //disabled for now
{
panel.content.innerHTML = "<div class='code'></div>";
var flask = new CodeFlask( "div.code", { language: 'js' });
flask.updateCode(node.properties[propname]);
flask.onUpdate( function(code) {
node.setProperty(propname, code);
});
}
else
{*/
panel.alt_content.innerHTML = "<textarea class='code'></textarea>";
var textarea = panel.alt_content.querySelector("textarea");
var fDoneWith = function(){
panel.toggleAltContent(false); //if(node_prop_div) node_prop_div.style.display = "block"; // panel.close();
panel.toggleFooterVisibility(true);
textarea.parentNode.removeChild(textarea);
panel.classList.add("settings");
panel.classList.remove("centered");
inner_refresh();
}
textarea.value = node.properties[propname];
textarea.addEventListener("keydown", function(e){
if(e.code == "Enter" && e.ctrlKey )
{
node.setProperty(propname, textarea.value);
fDoneWith();
}
});
panel.toggleAltContent(true);
panel.toggleFooterVisibility(false);
textarea.style.height = "calc(100% - 40px)";
/*}*/
var assign = panel.addButton( "Assign", function(){
node.setProperty(propname, textarea.value);
fDoneWith();
});
panel.alt_content.appendChild(assign); //panel.content.appendChild(assign);
var button = panel.addButton( "Close", fDoneWith);
button.style.float = "right";
panel.alt_content.appendChild(button); // panel.content.appendChild(button);
}
inner_refresh();
this.canvas.parentNode.appendChild( panel );
}
LGraphCanvas.prototype.showSubgraphPropertiesDialog = function(node)
{
console.log("showing subgraph properties dialog");
var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog");
if(old_panel)
old_panel.close();
var panel = this.createPanel("Subgraph Inputs",{closable:true, width: 500});
panel.node = node;
panel.classList.add("subgraph_dialog");
function inner_refresh()
{
panel.clear();
//show currents
if(node.inputs)
for(var i = 0; i < node.inputs.length; ++i)
{
var input = node.inputs[i];
if(input.not_subgraph_input)
continue;
var html = "<button>&#10005;</button> <span class='bullet_icon'></span><span class='name'></span><span class='type'></span>";
var elem = panel.addHTML(html,"subgraph_property");
elem.dataset["name"] = input.name;
elem.dataset["slot"] = i;
elem.querySelector(".name").innerText = input.name;
elem.querySelector(".type").innerText = input.type;
elem.querySelector("button").addEventListener("click",function(e){
node.removeInput( Number( this.parentNode.dataset["slot"] ) );
inner_refresh();
});
}
}
//add extra
var html = " + <span class='label'>Name</span><input class='name'/><span class='label'>Type</span><input class='type'></input><button>+</button>";
var elem = panel.addHTML(html,"subgraph_property extra", true);
elem.querySelector("button").addEventListener("click", function(e){
var elem = this.parentNode;
var name = elem.querySelector(".name").value;
var type = elem.querySelector(".type").value;
if(!name || node.findInputSlot(name) != -1)
return;
node.addInput(name,type);
elem.querySelector(".name").value = "";
elem.querySelector(".type").value = "";
inner_refresh();
});
inner_refresh();
this.canvas.parentNode.appendChild(panel);
return panel;
}
LGraphCanvas.prototype.showSubgraphPropertiesDialogRight = function (node) {
// console.log("showing subgraph properties dialog");
var that = this;
// old_panel if old_panel is exist close it
var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog");
if (old_panel)
old_panel.close();
// new panel
var panel = this.createPanel("Subgraph Outputs", { closable: true, width: 500 });
panel.node = node;
panel.classList.add("subgraph_dialog");
function inner_refresh() {
panel.clear();
//show currents
if (node.outputs)
for (var i = 0; i < node.outputs.length; ++i) {
var input = node.outputs[i];
if (input.not_subgraph_output)
continue;
var html = "<button>&#10005;</button> <span class='bullet_icon'></span><span class='name'></span><span class='type'></span>";
var elem = panel.addHTML(html, "subgraph_property");
elem.dataset["name"] = input.name;
elem.dataset["slot"] = i;
elem.querySelector(".name").innerText = input.name;
elem.querySelector(".type").innerText = input.type;
elem.querySelector("button").addEventListener("click", function (e) {
node.removeOutput(Number(this.parentNode.dataset["slot"]));
inner_refresh();
});
}
}
//add extra
var html = " + <span class='label'>Name</span><input class='name'/><span class='label'>Type</span><input class='type'></input><button>+</button>";
var elem = panel.addHTML(html, "subgraph_property extra", true);
elem.querySelector(".name").addEventListener("keydown", function (e) {
if (e.keyCode == 13) {
addOutput.apply(this)
}
})
elem.querySelector("button").addEventListener("click", function (e) {
addOutput.apply(this)
});
function addOutput() {
var elem = this.parentNode;
var name = elem.querySelector(".name").value;
var type = elem.querySelector(".type").value;
if (!name || node.findOutputSlot(name) != -1)
return;
node.addOutput(name, type);
elem.querySelector(".name").value = "";
elem.querySelector(".type").value = "";
inner_refresh();
}
inner_refresh();
this.canvas.parentNode.appendChild(panel);
return panel;
}
LGraphCanvas.prototype.checkPanels = function()
{
if(!this.canvas)
return;
var panels = this.canvas.parentNode.querySelectorAll(".litegraph.dialog");
for(var i = 0; i < panels.length; ++i)
{
var panel = panels[i];
if( !panel.node )
continue;
if( !panel.node.graph || panel.graph != this.graph )
panel.close();
}
}
LGraphCanvas.onMenuNodeCollapse = function(value, options, e, menu, node) {
node.graph.beforeChange(/*?*/);
var fApplyMultiNode = function(node){
node.collapse();
}
var graphcanvas = LGraphCanvas.active_canvas;
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){
fApplyMultiNode(node);
}else{
for (var i in graphcanvas.selected_nodes) {
fApplyMultiNode(graphcanvas.selected_nodes[i]);
}
}
node.graph.afterChange(/*?*/);
};
LGraphCanvas.onMenuNodePin = function(value, options, e, menu, node) {
node.pin();
};
LGraphCanvas.onMenuNodeMode = function(value, options, e, menu, node) {
new LiteGraph.ContextMenu(
LiteGraph.NODE_MODES,
{ event: e, callback: inner_clicked, parentMenu: menu, node: node }
);
function inner_clicked(v) {
if (!node) {
return;
}
var kV = Object.values(LiteGraph.NODE_MODES).indexOf(v);
var fApplyMultiNode = function(node){
if (kV>=0 && LiteGraph.NODE_MODES[kV])
node.changeMode(kV);
else{
console.warn("unexpected mode: "+v);
node.changeMode(LiteGraph.ALWAYS);
}
}
var graphcanvas = LGraphCanvas.active_canvas;
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){
fApplyMultiNode(node);
}else{
for (var i in graphcanvas.selected_nodes) {
fApplyMultiNode(graphcanvas.selected_nodes[i]);
}
}
}
return false;
};
LGraphCanvas.onMenuNodeColors = function(value, options, e, menu, node) {
if (!node) {
throw "no node for color";
}
var values = [];
values.push({
value: null,
content:
"<span style='display: block; padding-left: 4px;'>No color</span>"
});
for (var i in LGraphCanvas.node_colors) {
var color = LGraphCanvas.node_colors[i];
var value = {
value: i,
content:
"<span style='display: block; color: #999; padding-left: 4px; border-left: 8px solid " +
color.color +
"; background-color:" +
color.bgcolor +
"'>" +
i +
"</span>"
};
values.push(value);
}
new LiteGraph.ContextMenu(values, {
event: e,
callback: inner_clicked,
parentMenu: menu,
node: node
});
function inner_clicked(v) {
if (!node) {
return;
}
var color = v.value ? LGraphCanvas.node_colors[v.value] : null;
var fApplyColor = function(node){
if (color) {
if (node.constructor === LiteGraph.LGraphGroup) {
node.color = color.groupcolor;
} else {
node.color = color.color;
node.bgcolor = color.bgcolor;
}
} else {
delete node.color;
delete node.bgcolor;
}
}
var graphcanvas = LGraphCanvas.active_canvas;
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){
fApplyColor(node);
}else{
for (var i in graphcanvas.selected_nodes) {
fApplyColor(graphcanvas.selected_nodes[i]);
}
}
node.setDirtyCanvas(true, true);
}
return false;
};
LGraphCanvas.onMenuNodeShapes = function(value, options, e, menu, node) {
if (!node) {
throw "no node passed";
}
new LiteGraph.ContextMenu(LiteGraph.VALID_SHAPES, {
event: e,
callback: inner_clicked,
parentMenu: menu,
node: node
});
function inner_clicked(v) {
if (!node) {
return;
}
node.graph.beforeChange(/*?*/); //node
var fApplyMultiNode = function(node){
node.shape = v;
}
var graphcanvas = LGraphCanvas.active_canvas;
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){
fApplyMultiNode(node);
}else{
for (var i in graphcanvas.selected_nodes) {
fApplyMultiNode(graphcanvas.selected_nodes[i]);
}
}
node.graph.afterChange(/*?*/); //node
node.setDirtyCanvas(true);
}
return false;
};
LGraphCanvas.onMenuNodeRemove = function(value, options, e, menu, node) {
if (!node) {
throw "no node passed";
}
var graph = node.graph;
graph.beforeChange();
var fApplyMultiNode = function(node){
if (node.removable === false) {
return;
}
graph.remove(node);
}
var graphcanvas = LGraphCanvas.active_canvas;
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){
fApplyMultiNode(node);
}else{
for (var i in graphcanvas.selected_nodes) {
fApplyMultiNode(graphcanvas.selected_nodes[i]);
}
}
graph.afterChange();
node.setDirtyCanvas(true, true);
};
LGraphCanvas.onMenuNodeToSubgraph = function(value, options, e, menu, node) {
var graph = node.graph;
var graphcanvas = LGraphCanvas.active_canvas;
if(!graphcanvas) //??
return;
var nodes_list = Object.values( graphcanvas.selected_nodes || {} );
if( !nodes_list.length )
nodes_list = [ node ];
var subgraph_node = LiteGraph.createNode("graph/subgraph");
subgraph_node.pos = node.pos.concat();
graph.add(subgraph_node);
subgraph_node.buildFromNodes( nodes_list );
graphcanvas.deselectAllNodes();
node.setDirtyCanvas(true, true);
};
LGraphCanvas.onMenuNodeClone = function(value, options, e, menu, node) {
node.graph.beforeChange();
var newSelected = {};
var fApplyMultiNode = function(node){
if (node.clonable === false) {
return;
}
var newnode = node.clone();
if (!newnode) {
return;
}
newnode.pos = [node.pos[0] + 5, node.pos[1] + 5];
node.graph.add(newnode);
newSelected[newnode.id] = newnode;
}
var graphcanvas = LGraphCanvas.active_canvas;
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){
fApplyMultiNode(node);
}else{
for (var i in graphcanvas.selected_nodes) {
fApplyMultiNode(graphcanvas.selected_nodes[i]);
}
}
if(Object.keys(newSelected).length){
graphcanvas.selectNodes(newSelected);
}
node.graph.afterChange();
node.setDirtyCanvas(true, true);
};
LGraphCanvas.node_colors = {
red: { color: "#322", bgcolor: "#533", groupcolor: "#A88" },
brown: { color: "#332922", bgcolor: "#593930", groupcolor: "#b06634" },
green: { color: "#232", bgcolor: "#353", groupcolor: "#8A8" },
blue: { color: "#223", bgcolor: "#335", groupcolor: "#88A" },
pale_blue: {
color: "#2a363b",
bgcolor: "#3f5159",
groupcolor: "#3f789e"
},
cyan: { color: "#233", bgcolor: "#355", groupcolor: "#8AA" },
purple: { color: "#323", bgcolor: "#535", groupcolor: "#a1309b" },
yellow: { color: "#432", bgcolor: "#653", groupcolor: "#b58b2a" },
black: { color: "#222", bgcolor: "#000", groupcolor: "#444" }
};
LGraphCanvas.prototype.getCanvasMenuOptions = function() {
var options = null;
var that = this;
if (this.getMenuOptions) {
options = this.getMenuOptions();
} else {
options = [
{
content: "Add Node",
has_submenu: true,
callback: LGraphCanvas.onMenuAdd
},
{ content: "Add Group", callback: LGraphCanvas.onGroupAdd },
//{ content: "Arrange", callback: that.graph.arrange },
//{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll }
];
/*if (LiteGraph.showCanvasOptions){
options.push({ content: "Options", callback: that.showShowGraphOptionsPanel });
}*/
if (Object.keys(this.selected_nodes).length > 1) {
options.push({
content: "Align",
has_submenu: true,
callback: LGraphCanvas.onGroupAlign,
})
}
if (this._graph_stack && this._graph_stack.length > 0) {
options.push(null, {
content: "Close subgraph",
callback: this.closeSubgraph.bind(this)
});
}
}
if (this.getExtraMenuOptions) {
var extra = this.getExtraMenuOptions(this, options);
if (extra) {
options = options.concat(extra);
}
}
return options;
};
//called by processContextMenu to extract the menu list
LGraphCanvas.prototype.getNodeMenuOptions = function(node) {
var options = null;
if (node.getMenuOptions) {
options = node.getMenuOptions(this);
} else {
options = [
{
content: "Inputs",
has_submenu: true,
disabled: true,
callback: LGraphCanvas.showMenuNodeOptionalInputs
},
{
content: "Outputs",
has_submenu: true,
disabled: true,
callback: LGraphCanvas.showMenuNodeOptionalOutputs
},
null,
{
content: "Properties",
has_submenu: true,
callback: LGraphCanvas.onShowMenuNodeProperties
},
null,
{
content: "Title",
callback: LGraphCanvas.onShowPropertyEditor
},
{
content: "Mode",
has_submenu: true,
callback: LGraphCanvas.onMenuNodeMode
}];
if(node.resizable !== false){
options.push({
content: "Resize", callback: LGraphCanvas.onMenuResizeNode
});
}
options.push(
{
content: "Collapse",
callback: LGraphCanvas.onMenuNodeCollapse
},
{ content: "Pin", callback: LGraphCanvas.onMenuNodePin },
{
content: "Colors",
has_submenu: true,
callback: LGraphCanvas.onMenuNodeColors
},
{
content: "Shapes",
has_submenu: true,
callback: LGraphCanvas.onMenuNodeShapes
},
null
);
}
if (node.onGetInputs) {
var inputs = node.onGetInputs();
if (inputs && inputs.length) {
options[0].disabled = false;
}
}
if (node.onGetOutputs) {
var outputs = node.onGetOutputs();
if (outputs && outputs.length) {
options[1].disabled = false;
}
}
if (node.getExtraMenuOptions) {
var extra = node.getExtraMenuOptions(this, options);
if (extra) {
extra.push(null);
options = extra.concat(options);
}
}
if (node.clonable !== false) {
options.push({
content: "Clone",
callback: LGraphCanvas.onMenuNodeClone
});
}
if(0) //TODO
options.push({
content: "To Subgraph",
callback: LGraphCanvas.onMenuNodeToSubgraph
});
if (Object.keys(this.selected_nodes).length > 1) {
options.push({
content: "Align Selected To",
has_submenu: true,
callback: LGraphCanvas.onNodeAlign,
})
}
options.push(null, {
content: "Remove",
disabled: !(node.removable !== false && !node.block_delete ),
callback: LGraphCanvas.onMenuNodeRemove
});
if (node.graph && node.graph.onGetNodeMenuOptions) {
node.graph.onGetNodeMenuOptions(options, node);
}
return options;
};
LGraphCanvas.prototype.getGroupMenuOptions = function(node) {
var o = [
{ content: "Title", callback: LGraphCanvas.onShowPropertyEditor },
{
content: "Color",
has_submenu: true,
callback: LGraphCanvas.onMenuNodeColors
},
{
content: "Font size",
property: "font_size",
type: "Number",
callback: LGraphCanvas.onShowPropertyEditor
},
null,
{ content: "Remove", callback: LGraphCanvas.onMenuNodeRemove }
];
return o;
};
LGraphCanvas.prototype.processContextMenu = function(node, event) {
var that = this;
var canvas = LGraphCanvas.active_canvas;
var ref_window = canvas.getCanvasWindow();
var menu_info = null;
var options = {
event: event,
callback: inner_option_clicked,
extra: node
};
if(node)
options.title = node.type;
//check if mouse is in input
var slot = null;
if (node) {
slot = node.getSlotInPosition(event.canvasX, event.canvasY);
LGraphCanvas.active_node = node;
}
if (slot) {
//on slot
menu_info = [];
if (node.getSlotMenuOptions) {
menu_info = node.getSlotMenuOptions(slot);
} else {
if (
slot &&
slot.output &&
slot.output.links &&
slot.output.links.length
) {
menu_info.push({ content: "Disconnect Links", slot: slot });
}
var _slot = slot.input || slot.output;
if (_slot.removable){
menu_info.push(
_slot.locked
? "Cannot remove"
: { content: "Remove Slot", slot: slot }
);
}
if (!_slot.nameLocked){
menu_info.push({ content: "Rename Slot", slot: slot });
}
}
options.title =
(slot.input ? slot.input.type : slot.output.type) || "*";
if (slot.input && slot.input.type == LiteGraph.ACTION) {
options.title = "Action";
}
if (slot.output && slot.output.type == LiteGraph.EVENT) {
options.title = "Event";
}
} else {
if (node) {
//on node
menu_info = this.getNodeMenuOptions(node);
} else {
menu_info = this.getCanvasMenuOptions();
var group = this.graph.getGroupOnPos(
event.canvasX,
event.canvasY
);
if (group) {
//on group
menu_info.push(null, {
content: "Edit Group",
has_submenu: true,
submenu: {
title: "Group",
extra: group,
options: this.getGroupMenuOptions(group)
}
});
}
}
}
//show menu
if (!menu_info) {
return;
}
var menu = new LiteGraph.ContextMenu(menu_info, options, ref_window);
function inner_option_clicked(v, options, e) {
if (!v) {
return;
}
if (v.content == "Remove Slot") {
var info = v.slot;
node.graph.beforeChange();
if (info.input) {
node.removeInput(info.slot);
} else if (info.output) {
node.removeOutput(info.slot);
}
node.graph.afterChange();
return;
} else if (v.content == "Disconnect Links") {
var info = v.slot;
node.graph.beforeChange();
if (info.output) {
node.disconnectOutput(info.slot);
} else if (info.input) {
node.disconnectInput(info.slot);
}
node.graph.afterChange();
return;
} else if (v.content == "Rename Slot") {
var info = v.slot;
var slot_info = info.input
? node.getInputInfo(info.slot)
: node.getOutputInfo(info.slot);
var dialog = that.createDialog(
"<span class='name'>Name</span><input autofocus type='text'/><button>OK</button>",
options
);
var input = dialog.querySelector("input");
if (input && slot_info) {
input.value = slot_info.label || "";
}
var inner = function(){
node.graph.beforeChange();
if (input.value) {
if (slot_info) {
slot_info.label = input.value;
}
that.setDirty(true);
}
dialog.close();
node.graph.afterChange();
}
dialog.querySelector("button").addEventListener("click", inner);
input.addEventListener("keydown", function(e) {
dialog.is_modified = true;
if (e.keyCode == 27) {
//ESC
dialog.close();
} else if (e.keyCode == 13) {
inner(); // save
} else if (e.keyCode != 13 && e.target.localName != "textarea") {
return;
}
e.preventDefault();
e.stopPropagation();
});
input.focus();
}
//if(v.callback)
// return v.callback.call(that, node, options, e, menu, that, event );
}
};
//API *************************************************
function compareObjects(a, b) {
for (var i in a) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
LiteGraph.compareObjects = compareObjects;
function distance(a, b) {
return Math.sqrt(
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])
);
}
LiteGraph.distance = distance;
function colorToString(c) {
return (
"rgba(" +
Math.round(c[0] * 255).toFixed() +
"," +
Math.round(c[1] * 255).toFixed() +
"," +
Math.round(c[2] * 255).toFixed() +
"," +
(c.length == 4 ? c[3].toFixed(2) : "1.0") +
")"
);
}
LiteGraph.colorToString = colorToString;
function isInsideRectangle(x, y, left, top, width, height) {
if (left < x && left + width > x && top < y && top + height > y) {
return true;
}
return false;
}
LiteGraph.isInsideRectangle = isInsideRectangle;
//[minx,miny,maxx,maxy]
function growBounding(bounding, x, y) {
if (x < bounding[0]) {
bounding[0] = x;
} else if (x > bounding[2]) {
bounding[2] = x;
}
if (y < bounding[1]) {
bounding[1] = y;
} else if (y > bounding[3]) {
bounding[3] = y;
}
}
LiteGraph.growBounding = growBounding;
//point inside bounding box
function isInsideBounding(p, bb) {
if (
p[0] < bb[0][0] ||
p[1] < bb[0][1] ||
p[0] > bb[1][0] ||
p[1] > bb[1][1]
) {
return false;
}
return true;
}
LiteGraph.isInsideBounding = isInsideBounding;
//bounding overlap, format: [ startx, starty, width, height ]
function overlapBounding(a, b) {
var A_end_x = a[0] + a[2];
var A_end_y = a[1] + a[3];
var B_end_x = b[0] + b[2];
var B_end_y = b[1] + b[3];
if (
a[0] > B_end_x ||
a[1] > B_end_y ||
A_end_x < b[0] ||
A_end_y < b[1]
) {
return false;
}
return true;
}
LiteGraph.overlapBounding = overlapBounding;
//Convert a hex value to its decimal value - the inputted hex must be in the
// format of a hex triplet - the kind we use for HTML colours. The function
// will return an array with three values.
function hex2num(hex) {
if (hex.charAt(0) == "#") {
hex = hex.slice(1);
} //Remove the '#' char - if there is one.
hex = hex.toUpperCase();
var hex_alphabets = "0123456789ABCDEF";
var value = new Array(3);
var k = 0;
var int1, int2;
for (var i = 0; i < 6; i += 2) {
int1 = hex_alphabets.indexOf(hex.charAt(i));
int2 = hex_alphabets.indexOf(hex.charAt(i + 1));
value[k] = int1 * 16 + int2;
k++;
}
return value;
}
LiteGraph.hex2num = hex2num;
//Give a array with three values as the argument and the function will return
// the corresponding hex triplet.
function num2hex(triplet) {
var hex_alphabets = "0123456789ABCDEF";
var hex = "#";
var int1, int2;
for (var i = 0; i < 3; i++) {
int1 = triplet[i] / 16;
int2 = triplet[i] % 16;
hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2);
}
return hex;
}
LiteGraph.num2hex = num2hex;
/* LiteGraph GUI elements used for canvas editing *************************************/
/**
* ContextMenu from LiteGUI
*
* @class ContextMenu
* @constructor
* @param {Array} values (allows object { title: "Nice text", callback: function ... })
* @param {Object} options [optional] Some options:\
* - title: title to show on top of the menu
* - callback: function to call when an option is clicked, it receives the item information
* - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback
* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position
*/
function ContextMenu(values, options) {
options = options || {};
this.options = options;
var that = this;
//to link a menu with its parent
if (options.parentMenu) {
if (options.parentMenu.constructor !== this.constructor) {
console.error(
"parentMenu must be of class ContextMenu, ignoring it"
);
options.parentMenu = null;
} else {
this.parentMenu = options.parentMenu;
this.parentMenu.lock = true;
this.parentMenu.current_submenu = this;
}
}
var eventClass = null;
if(options.event) //use strings because comparing classes between windows doesnt work
eventClass = options.event.constructor.name;
if ( eventClass !== "MouseEvent" &&
eventClass !== "CustomEvent" &&
eventClass !== "PointerEvent"
) {
console.error(
"Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. ("+eventClass+")"
);
options.event = null;
}
var root = document.createElement("div");
root.className = "litegraph litecontextmenu litemenubar-panel";
if (options.className) {
root.className += " " + options.className;
}
root.style.minWidth = 100;
root.style.minHeight = 100;
root.style.pointerEvents = "none";
setTimeout(function() {
root.style.pointerEvents = "auto";
}, 100); //delay so the mouse up event is not caught by this element
//this prevents the default context browser menu to open in case this menu was created when pressing right button
LiteGraph.pointerListenerAdd(root,"up",
function(e) {
//console.log("pointerevents: ContextMenu up root prevent");
e.preventDefault();
return true;
},
true
);
root.addEventListener(
"contextmenu",
function(e) {
if (e.button != 2) {
//right button
return false;
}
e.preventDefault();
return false;
},
true
);
LiteGraph.pointerListenerAdd(root,"down",
function(e) {
//console.log("pointerevents: ContextMenu down");
if (e.button == 2) {
that.close();
e.preventDefault();
return true;
}
},
true
);
function on_mouse_wheel(e) {
var pos = parseInt(root.style.top);
root.style.top =
(pos + e.deltaY * options.scroll_speed).toFixed() + "px";
e.preventDefault();
return true;
}
if (!options.scroll_speed) {
options.scroll_speed = 0.1;
}
root.addEventListener("wheel", on_mouse_wheel, true);
root.addEventListener("mousewheel", on_mouse_wheel, true);
this.root = root;
//title
if (options.title) {
var element = document.createElement("div");
element.className = "litemenu-title";
element.innerHTML = options.title;
root.appendChild(element);
}
//entries
var num = 0;
for (var i=0; i < values.length; i++) {
var name = values.constructor == Array ? values[i] : i;
if (name != null && name.constructor !== String) {
name = name.content === undefined ? String(name) : name.content;
}
var value = values[i];
this.addItem(name, value, options);
num++;
}
//close on leave? touch enabled devices won't work TODO use a global device detector and condition on that
/*LiteGraph.pointerListenerAdd(root,"leave", function(e) {
console.log("pointerevents: ContextMenu leave");
if (that.lock) {
return;
}
if (root.closing_timer) {
clearTimeout(root.closing_timer);
}
root.closing_timer = setTimeout(that.close.bind(that, e), 500);
//that.close(e);
});*/
LiteGraph.pointerListenerAdd(root,"enter", function(e) {
//console.log("pointerevents: ContextMenu enter");
if (root.closing_timer) {
clearTimeout(root.closing_timer);
}
});
//insert before checking position
var root_document = document;
if (options.event) {
root_document = options.event.target.ownerDocument;
}
if (!root_document) {
root_document = document;
}
if( root_document.fullscreenElement )
root_document.fullscreenElement.appendChild(root);
else
root_document.body.appendChild(root);
//compute best position
var left = options.left || 0;
var top = options.top || 0;
if (options.event) {
left = options.event.clientX - 10;
top = options.event.clientY - 10;
if (options.title) {
top -= 20;
}
if (options.parentMenu) {
var rect = options.parentMenu.root.getBoundingClientRect();
left = rect.left + rect.width;
}
var body_rect = document.body.getBoundingClientRect();
var root_rect = root.getBoundingClientRect();
if(body_rect.height == 0)
console.error("document.body height is 0. That is dangerous, set html,body { height: 100%; }");
if (body_rect.width && left > body_rect.width - root_rect.width - 10) {
left = body_rect.width - root_rect.width - 10;
}
if (body_rect.height && top > body_rect.height - root_rect.height - 10) {
top = body_rect.height - root_rect.height - 10;
}
}
root.style.left = left + "px";
root.style.top = top + "px";
if (options.scale) {
root.style.transform = "scale(" + options.scale + ")";
}
}
ContextMenu.prototype.addItem = function(name, value, options) {
var that = this;
options = options || {};
var element = document.createElement("div");
element.className = "litemenu-entry submenu";
var disabled = false;
if (value === null) {
element.classList.add("separator");
//element.innerHTML = "<hr/>"
//continue;
} else {
element.innerHTML = value && value.title ? value.title : name;
element.value = value;
if (value) {
if (value.disabled) {
disabled = true;
element.classList.add("disabled");
}
if (value.submenu || value.has_submenu) {
element.classList.add("has_submenu");
}
}
if (typeof value == "function") {
element.dataset["value"] = name;
element.onclick_callback = value;
} else {
element.dataset["value"] = value;
}
if (value.className) {
element.className += " " + value.className;
}
}
this.root.appendChild(element);
if (!disabled) {
element.addEventListener("click", inner_onclick);
}
if (!disabled && options.autoopen) {
LiteGraph.pointerListenerAdd(element,"enter",inner_over);
}
function inner_over(e) {
var value = this.value;
if (!value || !value.has_submenu) {
return;
}
//if it is a submenu, autoopen like the item was clicked
inner_onclick.call(this, e);
}
//menu option clicked
function inner_onclick(e) {
var value = this.value;
var close_parent = true;
if (that.current_submenu) {
that.current_submenu.close(e);
}
//global callback
if (options.callback) {
var r = options.callback.call(
this,
value,
options,
e,
that,
options.node
);
if (r === true) {
close_parent = false;
}
}
//special cases
if (value) {
if (
value.callback &&
!options.ignore_item_callbacks &&
value.disabled !== true
) {
//item callback
var r = value.callback.call(
this,
value,
options,
e,
that,
options.extra
);
if (r === true) {
close_parent = false;
}
}
if (value.submenu) {
if (!value.submenu.options) {
throw "ContextMenu submenu needs options";
}
var submenu = new that.constructor(value.submenu.options, {
callback: value.submenu.callback,
event: e,
parentMenu: that,
ignore_item_callbacks:
value.submenu.ignore_item_callbacks,
title: value.submenu.title,
extra: value.submenu.extra,
autoopen: options.autoopen
});
close_parent = false;
}
}
if (close_parent && !that.lock) {
that.close();
}
}
return element;
};
ContextMenu.prototype.close = function(e, ignore_parent_menu) {
if (this.root.parentNode) {
this.root.parentNode.removeChild(this.root);
}
if (this.parentMenu && !ignore_parent_menu) {
this.parentMenu.lock = false;
this.parentMenu.current_submenu = null;
if (e === undefined) {
this.parentMenu.close();
} else if (
e &&
!ContextMenu.isCursorOverElement(e, this.parentMenu.root)
) {
ContextMenu.trigger(this.parentMenu.root, LiteGraph.pointerevents_method+"leave", e);
}
}
if (this.current_submenu) {
this.current_submenu.close(e, true);
}
if (this.root.closing_timer) {
clearTimeout(this.root.closing_timer);
}
// TODO implement : LiteGraph.contextMenuClosed(); :: keep track of opened / closed / current ContextMenu
// on key press, allow filtering/selecting the context menu elements
};
//this code is used to trigger events easily (used in the context menu mouseleave
ContextMenu.trigger = function(element, event_name, params, origin) {
var evt = document.createEvent("CustomEvent");
evt.initCustomEvent(event_name, true, true, params); //canBubble, cancelable, detail
evt.srcElement = origin;
if (element.dispatchEvent) {
element.dispatchEvent(evt);
} else if (element.__events) {
element.__events.dispatchEvent(evt);
}
//else nothing seems binded here so nothing to do
return evt;
};
//returns the top most menu
ContextMenu.prototype.getTopMenu = function() {
if (this.options.parentMenu) {
return this.options.parentMenu.getTopMenu();
}
return this;
};
ContextMenu.prototype.getFirstEvent = function() {
if (this.options.parentMenu) {
return this.options.parentMenu.getFirstEvent();
}
return this.options.event;
};
ContextMenu.isCursorOverElement = function(event, element) {
var left = event.clientX;
var top = event.clientY;
var rect = element.getBoundingClientRect();
if (!rect) {
return false;
}
if (
top > rect.top &&
top < rect.top + rect.height &&
left > rect.left &&
left < rect.left + rect.width
) {
return true;
}
return false;
};
LiteGraph.ContextMenu = ContextMenu;
LiteGraph.closeAllContextMenus = function(ref_window) {
ref_window = ref_window || window;
var elements = ref_window.document.querySelectorAll(".litecontextmenu");
if (!elements.length) {
return;
}
var result = [];
for (var i = 0; i < elements.length; i++) {
result.push(elements[i]);
}
for (var i=0; i < result.length; i++) {
if (result[i].close) {
result[i].close();
} else if (result[i].parentNode) {
result[i].parentNode.removeChild(result[i]);
}
}
};
LiteGraph.extendClass = function(target, origin) {
for (var i in origin) {
//copy class properties
if (target.hasOwnProperty(i)) {
continue;
}
target[i] = origin[i];
}
if (origin.prototype) {
//copy prototype properties
for (var i in origin.prototype) {
//only enumerable
if (!origin.prototype.hasOwnProperty(i)) {
continue;
}
if (target.prototype.hasOwnProperty(i)) {
//avoid overwriting existing ones
continue;
}
//copy getters
if (origin.prototype.__lookupGetter__(i)) {
target.prototype.__defineGetter__(
i,
origin.prototype.__lookupGetter__(i)
);
} else {
target.prototype[i] = origin.prototype[i];
}
//and setters
if (origin.prototype.__lookupSetter__(i)) {
target.prototype.__defineSetter__(
i,
origin.prototype.__lookupSetter__(i)
);
}
}
}
};
//used by some widgets to render a curve editor
function CurveEditor( points )
{
this.points = points;
this.selected = -1;
this.nearest = -1;
this.size = null; //stores last size used
this.must_update = true;
this.margin = 5;
}
CurveEditor.sampleCurve = function(f,points)
{
if(!points)
return;
for(var i = 0; i < points.length - 1; ++i)
{
var p = points[i];
var pn = points[i+1];
if(pn[0] < f)
continue;
var r = (pn[0] - p[0]);
if( Math.abs(r) < 0.00001 )
return p[1];
var local_f = (f - p[0]) / r;
return p[1] * (1.0 - local_f) + pn[1] * local_f;
}
return 0;
}
CurveEditor.prototype.draw = function( ctx, size, graphcanvas, background_color, line_color, inactive )
{
var points = this.points;
if(!points)
return;
this.size = size;
var w = size[0] - this.margin * 2;
var h = size[1] - this.margin * 2;
line_color = line_color || "#666";
ctx.save();
ctx.translate(this.margin,this.margin);
if(background_color)
{
ctx.fillStyle = "#111";
ctx.fillRect(0,0,w,h);
ctx.fillStyle = "#222";
ctx.fillRect(w*0.5,0,1,h);
ctx.strokeStyle = "#333";
ctx.strokeRect(0,0,w,h);
}
ctx.strokeStyle = line_color;
if(inactive)
ctx.globalAlpha = 0.5;
ctx.beginPath();
for(var i = 0; i < points.length; ++i)
{
var p = points[i];
ctx.lineTo( p[0] * w, (1.0 - p[1]) * h );
}
ctx.stroke();
ctx.globalAlpha = 1;
if(!inactive)
for(var i = 0; i < points.length; ++i)
{
var p = points[i];
ctx.fillStyle = this.selected == i ? "#FFF" : (this.nearest == i ? "#DDD" : "#AAA");
ctx.beginPath();
ctx.arc( p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2 );
ctx.fill();
}
ctx.restore();
}
//localpos is mouse in curve editor space
CurveEditor.prototype.onMouseDown = function( localpos, graphcanvas )
{
var points = this.points;
if(!points)
return;
if( localpos[1] < 0 )
return;
//this.captureInput(true);
var w = this.size[0] - this.margin * 2;
var h = this.size[1] - this.margin * 2;
var x = localpos[0] - this.margin;
var y = localpos[1] - this.margin;
var pos = [x,y];
var max_dist = 30 / graphcanvas.ds.scale;
//search closer one
this.selected = this.getCloserPoint(pos, max_dist);
//create one
if(this.selected == -1)
{
var point = [x / w, 1 - y / h];
points.push(point);
points.sort(function(a,b){ return a[0] - b[0]; });
this.selected = points.indexOf(point);
this.must_update = true;
}
if(this.selected != -1)
return true;
}
CurveEditor.prototype.onMouseMove = function( localpos, graphcanvas )
{
var points = this.points;
if(!points)
return;
var s = this.selected;
if(s < 0)
return;
var x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2 );
var y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2 );
var curvepos = [(localpos[0] - this.margin),(localpos[1] - this.margin)];
var max_dist = 30 / graphcanvas.ds.scale;
this._nearest = this.getCloserPoint(curvepos, max_dist);
var point = points[s];
if(point)
{
var is_edge_point = s == 0 || s == points.length - 1;
if( !is_edge_point && (localpos[0] < -10 || localpos[0] > this.size[0] + 10 || localpos[1] < -10 || localpos[1] > this.size[1] + 10) )
{
points.splice(s,1);
this.selected = -1;
return;
}
if( !is_edge_point ) //not edges
point[0] = clamp(x, 0, 1);
else
point[0] = s == 0 ? 0 : 1;
point[1] = 1.0 - clamp(y, 0, 1);
points.sort(function(a,b){ return a[0] - b[0]; });
this.selected = points.indexOf(point);
this.must_update = true;
}
}
CurveEditor.prototype.onMouseUp = function( localpos, graphcanvas )
{
this.selected = -1;
return false;
}
CurveEditor.prototype.getCloserPoint = function(pos, max_dist)
{
var points = this.points;
if(!points)
return -1;
max_dist = max_dist || 30;
var w = (this.size[0] - this.margin * 2);
var h = (this.size[1] - this.margin * 2);
var num = points.length;
var p2 = [0,0];
var min_dist = 1000000;
var closest = -1;
var last_valid = -1;
for(var i = 0; i < num; ++i)
{
var p = points[i];
p2[0] = p[0] * w;
p2[1] = (1.0 - p[1]) * h;
if(p2[0] < pos[0])
last_valid = i;
var dist = vec2.distance(pos,p2);
if(dist > min_dist || dist > max_dist)
continue;
closest = i;
min_dist = dist;
}
return closest;
}
LiteGraph.CurveEditor = CurveEditor;
//used to create nodes from wrapping functions
LiteGraph.getParameterNames = function(func) {
return (func + "")
.replace(/[/][/].*$/gm, "") // strip single-line comments
.replace(/\s+/g, "") // strip white space
.replace(/[/][*][^/*]*[*][/]/g, "") // strip multi-line comments /**/
.split("){", 1)[0]
.replace(/^[^(]*[(]/, "") // extract the parameters
.replace(/=[^,]+/g, "") // strip any ES6 defaults
.split(",")
.filter(Boolean); // split & filter [""]
};
/* helper for interaction: pointer, touch, mouse Listeners
used by LGraphCanvas DragAndScale ContextMenu*/
LiteGraph.pointerListenerAdd = function(oDOM, sEvIn, fCall, capture=false) {
if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall!=="function"){
//console.log("cant pointerListenerAdd "+oDOM+", "+sEvent+", "+fCall);
return; // -- break --
}
var sMethod = LiteGraph.pointerevents_method;
var sEvent = sEvIn;
// UNDER CONSTRUCTION
// convert pointerevents to touch event when not available
if (sMethod=="pointer" && !window.PointerEvent){
console.warn("sMethod=='pointer' && !window.PointerEvent");
console.log("Converting pointer["+sEvent+"] : down move up cancel enter TO touchstart touchmove touchend, etc ..");
switch(sEvent){
case "down":{
sMethod = "touch";
sEvent = "start";
break;
}
case "move":{
sMethod = "touch";
//sEvent = "move";
break;
}
case "up":{
sMethod = "touch";
sEvent = "end";
break;
}
case "cancel":{
sMethod = "touch";
//sEvent = "cancel";
break;
}
case "enter":{
console.log("debug: Should I send a move event?"); // ???
break;
}
// case "over": case "out": not used at now
default:{
console.warn("PointerEvent not available in this browser ? The event "+sEvent+" would not be called");
}
}
}
switch(sEvent){
//both pointer and move events
case "down": case "up": case "move": case "over": case "out": case "enter":
{
oDOM.addEventListener(sMethod+sEvent, fCall, capture);
}
// only pointerevents
case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture":
{
if (sMethod!="mouse"){
return oDOM.addEventListener(sMethod+sEvent, fCall, capture);
}
}
// not "pointer" || "mouse"
default:
return oDOM.addEventListener(sEvent, fCall, capture);
}
}
LiteGraph.pointerListenerRemove = function(oDOM, sEvent, fCall, capture=false) {
if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall!=="function"){
//console.log("cant pointerListenerRemove "+oDOM+", "+sEvent+", "+fCall);
return; // -- break --
}
switch(sEvent){
//both pointer and move events
case "down": case "up": case "move": case "over": case "out": case "enter":
{
if (LiteGraph.pointerevents_method=="pointer" || LiteGraph.pointerevents_method=="mouse"){
oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture);
}
}
// only pointerevents
case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture":
{
if (LiteGraph.pointerevents_method=="pointer"){
return oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture);
}
}
// not "pointer" || "mouse"
default:
return oDOM.removeEventListener(sEvent, fCall, capture);
}
}
function clamp(v, a, b) {
return a > v ? a : b < v ? b : v;
};
global.clamp = clamp;
if (typeof window != "undefined" && !window["requestAnimationFrame"]) {
window.requestAnimationFrame =
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
}
})(this);
if (typeof exports != "undefined") {
exports.LiteGraph = this.LiteGraph;
exports.LGraph = this.LGraph;
exports.LLink = this.LLink;
exports.LGraphNode = this.LGraphNode;
exports.LGraphGroup = this.LGraphGroup;
exports.DragAndScale = this.DragAndScale;
exports.LGraphCanvas = this.LGraphCanvas;
exports.ContextMenu = this.ContextMenu;
}
//basic nodes
(function(global) {
var LiteGraph = global.LiteGraph;
//Constant
function Time() {
this.addOutput("in ms", "number");
this.addOutput("in sec", "number");
}
Time.title = "Time";
Time.desc = "Time";
Time.prototype.onExecute = function() {
this.setOutputData(0, this.graph.globaltime * 1000);
this.setOutputData(1, this.graph.globaltime);
};
LiteGraph.registerNodeType("basic/time", Time);
//Subgraph: a node that contains a graph
function Subgraph() {
var that = this;
this.size = [140, 80];
this.properties = { enabled: true };
this.enabled = true;
//create inner graph
this.subgraph = new LiteGraph.LGraph();
this.subgraph._subgraph_node = this;
this.subgraph._is_subgraph = true;
this.subgraph.onTrigger = this.onSubgraphTrigger.bind(this);
//nodes input node added inside
this.subgraph.onInputAdded = this.onSubgraphNewInput.bind(this);
this.subgraph.onInputRenamed = this.onSubgraphRenamedInput.bind(this);
this.subgraph.onInputTypeChanged = this.onSubgraphTypeChangeInput.bind(this);
this.subgraph.onInputRemoved = this.onSubgraphRemovedInput.bind(this);
this.subgraph.onOutputAdded = this.onSubgraphNewOutput.bind(this);
this.subgraph.onOutputRenamed = this.onSubgraphRenamedOutput.bind(this);
this.subgraph.onOutputTypeChanged = this.onSubgraphTypeChangeOutput.bind(this);
this.subgraph.onOutputRemoved = this.onSubgraphRemovedOutput.bind(this);
}
Subgraph.title = "Subgraph";
Subgraph.desc = "Graph inside a node";
Subgraph.title_color = "#334";
Subgraph.prototype.onGetInputs = function() {
return [["enabled", "boolean"]];
};
/*
Subgraph.prototype.onDrawTitle = function(ctx) {
if (this.flags.collapsed) {
return;
}
ctx.fillStyle = "#555";
var w = LiteGraph.NODE_TITLE_HEIGHT;
var x = this.size[0] - w;
ctx.fillRect(x, -w, w, w);
ctx.fillStyle = "#333";
ctx.beginPath();
ctx.moveTo(x + w * 0.2, -w * 0.6);
ctx.lineTo(x + w * 0.8, -w * 0.6);
ctx.lineTo(x + w * 0.5, -w * 0.3);
ctx.fill();
};
*/
Subgraph.prototype.onDblClick = function(e, pos, graphcanvas) {
var that = this;
setTimeout(function() {
graphcanvas.openSubgraph(that.subgraph);
}, 10);
};
/*
Subgraph.prototype.onMouseDown = function(e, pos, graphcanvas) {
if (
!this.flags.collapsed &&
pos[0] > this.size[0] - LiteGraph.NODE_TITLE_HEIGHT &&
pos[1] < 0
) {
var that = this;
setTimeout(function() {
graphcanvas.openSubgraph(that.subgraph);
}, 10);
}
};
*/
Subgraph.prototype.onAction = function(action, param) {
this.subgraph.onAction(action, param);
};
Subgraph.prototype.onExecute = function() {
this.enabled = this.getInputOrProperty("enabled");
if (!this.enabled) {
return;
}
//send inputs to subgraph global inputs
if (this.inputs) {
for (var i = 0; i < this.inputs.length; i++) {
var input = this.inputs[i];
var value = this.getInputData(i);
this.subgraph.setInputData(input.name, value);
}
}
//execute
this.subgraph.runStep();
//send subgraph global outputs to outputs
if (this.outputs) {
for (var i = 0; i < this.outputs.length; i++) {
var output = this.outputs[i];
var value = this.subgraph.getOutputData(output.name);
this.setOutputData(i, value);
}
}
};
Subgraph.prototype.sendEventToAllNodes = function(eventname, param, mode) {
if (this.enabled) {
this.subgraph.sendEventToAllNodes(eventname, param, mode);
}
};
Subgraph.prototype.onDrawBackground = function (ctx, graphcanvas, canvas, pos) {
if (this.flags.collapsed)
return;
var y = this.size[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5;
// button
var over = LiteGraph.isInsideRectangle(pos[0], pos[1], this.pos[0], this.pos[1] + y, this.size[0], LiteGraph.NODE_TITLE_HEIGHT);
let overleft = LiteGraph.isInsideRectangle(pos[0], pos[1], this.pos[0], this.pos[1] + y, this.size[0] / 2, LiteGraph.NODE_TITLE_HEIGHT)
ctx.fillStyle = over ? "#555" : "#222";
ctx.beginPath();
if (this._shape == LiteGraph.BOX_SHAPE) {
if (overleft) {
ctx.rect(0, y, this.size[0] / 2 + 1, LiteGraph.NODE_TITLE_HEIGHT);
} else {
ctx.rect(this.size[0] / 2, y, this.size[0] / 2 + 1, LiteGraph.NODE_TITLE_HEIGHT);
}
}
else {
if (overleft) {
ctx.roundRect(0, y, this.size[0] / 2 + 1, LiteGraph.NODE_TITLE_HEIGHT, [0,0, 8,8]);
} else {
ctx.roundRect(this.size[0] / 2, y, this.size[0] / 2 + 1, LiteGraph.NODE_TITLE_HEIGHT, [0,0, 8,8]);
}
}
if (over) {
ctx.fill();
} else {
ctx.fillRect(0, y, this.size[0] + 1, LiteGraph.NODE_TITLE_HEIGHT);
}
// button
ctx.textAlign = "center";
ctx.font = "24px Arial";
ctx.fillStyle = over ? "#DDD" : "#999";
ctx.fillText("+", this.size[0] * 0.25, y + 24);
ctx.fillText("+", this.size[0] * 0.75, y + 24);
}
// Subgraph.prototype.onMouseDown = function(e, localpos, graphcanvas)
// {
// var y = this.size[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5;
// if(localpos[1] > y)
// {
// graphcanvas.showSubgraphPropertiesDialog(this);
// }
// }
Subgraph.prototype.onMouseDown = function (e, localpos, graphcanvas) {
var y = this.size[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5;
console.log(0)
if (localpos[1] > y) {
if (localpos[0] < this.size[0] / 2) {
console.log(1)
graphcanvas.showSubgraphPropertiesDialog(this);
} else {
console.log(2)
graphcanvas.showSubgraphPropertiesDialogRight(this);
}
}
}
Subgraph.prototype.computeSize = function()
{
var num_inputs = this.inputs ? this.inputs.length : 0;
var num_outputs = this.outputs ? this.outputs.length : 0;
return [ 200, Math.max(num_inputs,num_outputs) * LiteGraph.NODE_SLOT_HEIGHT + LiteGraph.NODE_TITLE_HEIGHT ];
}
//**** INPUTS ***********************************
Subgraph.prototype.onSubgraphTrigger = function(event, param) {
var slot = this.findOutputSlot(event);
if (slot != -1) {
this.triggerSlot(slot);
}
};
Subgraph.prototype.onSubgraphNewInput = function(name, type) {
var slot = this.findInputSlot(name);
if (slot == -1) {
//add input to the node
this.addInput(name, type);
}
};
Subgraph.prototype.onSubgraphRenamedInput = function(oldname, name) {
var slot = this.findInputSlot(oldname);
if (slot == -1) {
return;
}
var info = this.getInputInfo(slot);
info.name = name;
};
Subgraph.prototype.onSubgraphTypeChangeInput = function(name, type) {
var slot = this.findInputSlot(name);
if (slot == -1) {
return;
}
var info = this.getInputInfo(slot);
info.type = type;
};
Subgraph.prototype.onSubgraphRemovedInput = function(name) {
var slot = this.findInputSlot(name);
if (slot == -1) {
return;
}
this.removeInput(slot);
};
//**** OUTPUTS ***********************************
Subgraph.prototype.onSubgraphNewOutput = function(name, type) {
var slot = this.findOutputSlot(name);
if (slot == -1) {
this.addOutput(name, type);
}
};
Subgraph.prototype.onSubgraphRenamedOutput = function(oldname, name) {
var slot = this.findOutputSlot(oldname);
if (slot == -1) {
return;
}
var info = this.getOutputInfo(slot);
info.name = name;
};
Subgraph.prototype.onSubgraphTypeChangeOutput = function(name, type) {
var slot = this.findOutputSlot(name);
if (slot == -1) {
return;
}
var info = this.getOutputInfo(slot);
info.type = type;
};
Subgraph.prototype.onSubgraphRemovedOutput = function(name) {
var slot = this.findOutputSlot(name);
if (slot == -1) {
return;
}
this.removeOutput(slot);
};
// *****************************************************
Subgraph.prototype.getExtraMenuOptions = function(graphcanvas) {
var that = this;
return [
{
content: "Open",
callback: function() {
graphcanvas.openSubgraph(that.subgraph);
}
}
];
};
Subgraph.prototype.onResize = function(size) {
size[1] += 20;
};
Subgraph.prototype.serialize = function() {
var data = LiteGraph.LGraphNode.prototype.serialize.call(this);
data.subgraph = this.subgraph.serialize();
return data;
};
//no need to define node.configure, the default method detects node.subgraph and passes the object to node.subgraph.configure()
Subgraph.prototype.reassignSubgraphUUIDs = function(graph) {
const idMap = { nodeIDs: {}, linkIDs: {} }
for (const node of graph.nodes) {
const oldID = node.id
const newID = LiteGraph.uuidv4()
node.id = newID
if (idMap.nodeIDs[oldID] || idMap.nodeIDs[newID]) {
throw new Error(`New/old node UUID wasn't unique in changed map! ${oldID} ${newID}`)
}
idMap.nodeIDs[oldID] = newID
idMap.nodeIDs[newID] = oldID
}
for (const link of graph.links) {
const oldID = link[0]
const newID = LiteGraph.uuidv4();
link[0] = newID
if (idMap.linkIDs[oldID] || idMap.linkIDs[newID]) {
throw new Error(`New/old link UUID wasn't unique in changed map! ${oldID} ${newID}`)
}
idMap.linkIDs[oldID] = newID
idMap.linkIDs[newID] = oldID
const nodeFrom = link[1]
const nodeTo = link[3]
if (!idMap.nodeIDs[nodeFrom]) {
throw new Error(`Old node UUID not found in mapping! ${nodeFrom}`)
}
link[1] = idMap.nodeIDs[nodeFrom]
if (!idMap.nodeIDs[nodeTo]) {
throw new Error(`Old node UUID not found in mapping! ${nodeTo}`)
}
link[3] = idMap.nodeIDs[nodeTo]
}
// Reconnect links
for (const node of graph.nodes) {
if (node.inputs) {
for (const input of node.inputs) {
if (input.link) {
input.link = idMap.linkIDs[input.link]
}
}
}
if (node.outputs) {
for (const output of node.outputs) {
if (output.links) {
output.links = output.links.map(l => idMap.linkIDs[l]);
}
}
}
}
// Recurse!
for (const node of graph.nodes) {
if (node.type === "graph/subgraph") {
const merge = reassignGraphUUIDs(node.subgraph);
idMap.nodeIDs.assign(merge.nodeIDs)
idMap.linkIDs.assign(merge.linkIDs)
}
}
};
Subgraph.prototype.clone = function() {
var node = LiteGraph.createNode(this.type);
var data = this.serialize();
if (LiteGraph.use_uuids) {
// LGraph.serialize() seems to reuse objects in the original graph. But we
// need to change node IDs here, so clone it first.
const subgraph = LiteGraph.cloneObject(data.subgraph)
this.reassignSubgraphUUIDs(subgraph);
data.subgraph = subgraph;
}
delete data["id"];
delete data["inputs"];
delete data["outputs"];
node.configure(data);
return node;
};
Subgraph.prototype.buildFromNodes = function(nodes)
{
//clear all?
//TODO
//nodes that connect data between parent graph and subgraph
var subgraph_inputs = [];
var subgraph_outputs = [];
//mark inner nodes
var ids = {};
var min_x = 0;
var max_x = 0;
for(var i = 0; i < nodes.length; ++i)
{
var node = nodes[i];
ids[ node.id ] = node;
min_x = Math.min( node.pos[0], min_x );
max_x = Math.max( node.pos[0], min_x );
}
var last_input_y = 0;
var last_output_y = 0;
for(var i = 0; i < nodes.length; ++i)
{
var node = nodes[i];
//check inputs
if( node.inputs )
for(var j = 0; j < node.inputs.length; ++j)
{
var input = node.inputs[j];
if( !input || !input.link )
continue;
var link = node.graph.links[ input.link ];
if(!link)
continue;
if( ids[ link.origin_id ] )
continue;
//this.addInput(input.name,link.type);
this.subgraph.addInput(input.name,link.type);
/*
var input_node = LiteGraph.createNode("graph/input");
this.subgraph.add( input_node );
input_node.pos = [min_x - 200, last_input_y ];
last_input_y += 100;
*/
}
//check outputs
if( node.outputs )
for(var j = 0; j < node.outputs.length; ++j)
{
var output = node.outputs[j];
if( !output || !output.links || !output.links.length )
continue;
var is_external = false;
for(var k = 0; k < output.links.length; ++k)
{
var link = node.graph.links[ output.links[k] ];
if(!link)
continue;
if( ids[ link.target_id ] )
continue;
is_external = true;
break;
}
if(!is_external)
continue;
//this.addOutput(output.name,output.type);
/*
var output_node = LiteGraph.createNode("graph/output");
this.subgraph.add( output_node );
output_node.pos = [max_x + 50, last_output_y ];
last_output_y += 100;
*/
}
}
//detect inputs and outputs
//split every connection in two data_connection nodes
//keep track of internal connections
//connect external connections
//clone nodes inside subgraph and try to reconnect them
//connect edge subgraph nodes to extarnal connections nodes
}
LiteGraph.Subgraph = Subgraph;
LiteGraph.registerNodeType("graph/subgraph", Subgraph);
//Input for a subgraph
function GraphInput() {
this.addOutput("", "number");
this.name_in_graph = "";
this.properties = {
name: "",
type: "number",
value: 0
};
var that = this;
this.name_widget = this.addWidget(
"text",
"Name",
this.properties.name,
function(v) {
if (!v) {
return;
}
that.setProperty("name",v);
}
);
this.type_widget = this.addWidget(
"text",
"Type",
this.properties.type,
function(v) {
that.setProperty("type",v);
}
);
this.value_widget = this.addWidget(
"number",
"Value",
this.properties.value,
function(v) {
that.setProperty("value",v);
}
);
this.widgets_up = true;
this.size = [180, 90];
}
GraphInput.title = "Input";
GraphInput.desc = "Input of the graph";
GraphInput.prototype.onConfigure = function()
{
this.updateType();
}
//ensures the type in the node output and the type in the associated graph input are the same
GraphInput.prototype.updateType = function()
{
var type = this.properties.type;
this.type_widget.value = type;
//update output
if(this.outputs[0].type != type)
{
if (!LiteGraph.isValidConnection(this.outputs[0].type,type))
this.disconnectOutput(0);
this.outputs[0].type = type;
}
//update widget
if(type == "number")
{
this.value_widget.type = "number";
this.value_widget.value = 0;
}
else if(type == "boolean")
{
this.value_widget.type = "toggle";
this.value_widget.value = true;
}
else if(type == "string")
{
this.value_widget.type = "text";
this.value_widget.value = "";
}
else
{
this.value_widget.type = null;
this.value_widget.value = null;
}
this.properties.value = this.value_widget.value;
//update graph
if (this.graph && this.name_in_graph) {
this.graph.changeInputType(this.name_in_graph, type);
}
}
//this is executed AFTER the property has changed
GraphInput.prototype.onPropertyChanged = function(name,v)
{
if( name == "name" )
{
if (v == "" || v == this.name_in_graph || v == "enabled") {
return false;
}
if(this.graph)
{
if (this.name_in_graph) {
//already added
this.graph.renameInput( this.name_in_graph, v );
} else {
this.graph.addInput( v, this.properties.type );
}
} //what if not?!
this.name_widget.value = v;
this.name_in_graph = v;
}
else if( name == "type" )
{
this.updateType();
}
else if( name == "value" )
{
}
}
GraphInput.prototype.getTitle = function() {
if (this.flags.collapsed) {
return this.properties.name;
}
return this.title;
};
GraphInput.prototype.onAction = function(action, param) {
if (this.properties.type == LiteGraph.EVENT) {
this.triggerSlot(0, param);
}
};
GraphInput.prototype.onExecute = function() {
var name = this.properties.name;
//read from global input
var data = this.graph.inputs[name];
if (!data) {
this.setOutputData(0, this.properties.value );
return;
}
this.setOutputData(0, data.value !== undefined ? data.value : this.properties.value );
};
GraphInput.prototype.onRemoved = function() {
if (this.name_in_graph) {
this.graph.removeInput(this.name_in_graph);
}
};
LiteGraph.GraphInput = GraphInput;
LiteGraph.registerNodeType("graph/input", GraphInput);
//Output for a subgraph
function GraphOutput() {
this.addInput("", "");
this.name_in_graph = "";
this.properties = { name: "", type: "" };
var that = this;
// Object.defineProperty(this.properties, "name", {
// get: function() {
// return that.name_in_graph;
// },
// set: function(v) {
// if (v == "" || v == that.name_in_graph) {
// return;
// }
// if (that.name_in_graph) {
// //already added
// that.graph.renameOutput(that.name_in_graph, v);
// } else {
// that.graph.addOutput(v, that.properties.type);
// }
// that.name_widget.value = v;
// that.name_in_graph = v;
// },
// enumerable: true
// });
// Object.defineProperty(this.properties, "type", {
// get: function() {
// return that.inputs[0].type;
// },
// set: function(v) {
// if (v == "action" || v == "event") {
// v = LiteGraph.ACTION;
// }
// if (!LiteGraph.isValidConnection(that.inputs[0].type,v))
// that.disconnectInput(0);
// that.inputs[0].type = v;
// if (that.name_in_graph) {
// //already added
// that.graph.changeOutputType(
// that.name_in_graph,
// that.inputs[0].type
// );
// }
// that.type_widget.value = v || "";
// },
// enumerable: true
// });
this.name_widget = this.addWidget("text","Name",this.properties.name,"name");
this.type_widget = this.addWidget("text","Type",this.properties.type,"type");
this.widgets_up = true;
this.size = [180, 60];
}
GraphOutput.title = "Output";
GraphOutput.desc = "Output of the graph";
GraphOutput.prototype.onPropertyChanged = function (name, v) {
if (name == "name") {
if (v == "" || v == this.name_in_graph || v == "enabled") {
return false;
}
if (this.graph) {
if (this.name_in_graph) {
//already added
this.graph.renameOutput(this.name_in_graph, v);
} else {
this.graph.addOutput(v, this.properties.type);
}
} //what if not?!
this.name_widget.value = v;
this.name_in_graph = v;
}
else if (name == "type") {
this.updateType();
}
else if (name == "value") {
}
}
GraphOutput.prototype.updateType = function () {
var type = this.properties.type;
if (this.type_widget)
this.type_widget.value = type;
//update output
if (this.inputs[0].type != type) {
if ( type == "action" || type == "event")
type = LiteGraph.EVENT;
if (!LiteGraph.isValidConnection(this.inputs[0].type, type))
this.disconnectInput(0);
this.inputs[0].type = type;
}
//update graph
if (this.graph && this.name_in_graph) {
this.graph.changeOutputType(this.name_in_graph, type);
}
}
GraphOutput.prototype.onExecute = function() {
this._value = this.getInputData(0);
this.graph.setOutputData(this.properties.name, this._value);
};
GraphOutput.prototype.onAction = function(action, param) {
if (this.properties.type == LiteGraph.ACTION) {
this.graph.trigger( this.properties.name, param );
}
};
GraphOutput.prototype.onRemoved = function() {
if (this.name_in_graph) {
this.graph.removeOutput(this.name_in_graph);
}
};
GraphOutput.prototype.getTitle = function() {
if (this.flags.collapsed) {
return this.properties.name;
}
return this.title;
};
LiteGraph.GraphOutput = GraphOutput;
LiteGraph.registerNodeType("graph/output", GraphOutput);
//Constant
function ConstantNumber() {
this.addOutput("value", "number");
this.addProperty("value", 1.0);
this.widget = this.addWidget("number","value",1,"value");
this.widgets_up = true;
this.size = [180, 30];
}
ConstantNumber.title = "Const Number";
ConstantNumber.desc = "Constant number";
ConstantNumber.prototype.onExecute = function() {
this.setOutputData(0, parseFloat(this.properties["value"]));
};
ConstantNumber.prototype.getTitle = function() {
if (this.flags.collapsed) {
return this.properties.value;
}
return this.title;
};
ConstantNumber.prototype.setValue = function(v)
{
this.setProperty("value",v);
}
ConstantNumber.prototype.onDrawBackground = function(ctx) {
//show the current value
this.outputs[0].label = this.properties["value"].toFixed(3);
};
LiteGraph.registerNodeType("basic/const", ConstantNumber);
function ConstantBoolean() {
this.addOutput("bool", "boolean");
this.addProperty("value", true);
this.widget = this.addWidget("toggle","value",true,"value");
this.serialize_widgets = true;
this.widgets_up = true;
this.size = [140, 30];
}
ConstantBoolean.title = "Const Boolean";
ConstantBoolean.desc = "Constant boolean";
ConstantBoolean.prototype.getTitle = ConstantNumber.prototype.getTitle;
ConstantBoolean.prototype.onExecute = function() {
this.setOutputData(0, this.properties["value"]);
};
ConstantBoolean.prototype.setValue = ConstantNumber.prototype.setValue;
ConstantBoolean.prototype.onGetInputs = function() {
return [["toggle", LiteGraph.ACTION]];
};
ConstantBoolean.prototype.onAction = function(action)
{
this.setValue( !this.properties.value );
}
LiteGraph.registerNodeType("basic/boolean", ConstantBoolean);
function ConstantString() {
this.addOutput("string", "string");
this.addProperty("value", "");
this.widget = this.addWidget("text","value","","value"); //link to property value
this.widgets_up = true;
this.size = [180, 30];
}
ConstantString.title = "Const String";
ConstantString.desc = "Constant string";
ConstantString.prototype.getTitle = ConstantNumber.prototype.getTitle;
ConstantString.prototype.onExecute = function() {
this.setOutputData(0, this.properties["value"]);
};
ConstantString.prototype.setValue = ConstantNumber.prototype.setValue;
ConstantString.prototype.onDropFile = function(file)
{
var that = this;
var reader = new FileReader();
reader.onload = function(e)
{
that.setProperty("value",e.target.result);
}
reader.readAsText(file);
}
LiteGraph.registerNodeType("basic/string", ConstantString);
function ConstantObject() {
this.addOutput("obj", "object");
this.size = [120, 30];
this._object = {};
}
ConstantObject.title = "Const Object";
ConstantObject.desc = "Constant Object";
ConstantObject.prototype.onExecute = function() {
this.setOutputData(0, this._object);
};
LiteGraph.registerNodeType( "basic/object", ConstantObject );
function ConstantFile() {
this.addInput("url", "string");
this.addOutput("file", "string");
this.addProperty("url", "");
this.addProperty("type", "text");
this.widget = this.addWidget("text","url","","url");
this._data = null;
}
ConstantFile.title = "Const File";
ConstantFile.desc = "Fetches a file from an url";
ConstantFile["@type"] = { type: "enum", values: ["text","arraybuffer","blob","json"] };
ConstantFile.prototype.onPropertyChanged = function(name, value) {
if (name == "url")
{
if( value == null || value == "")
this._data = null;
else
{
this.fetchFile(value);
}
}
}
ConstantFile.prototype.onExecute = function() {
var url = this.getInputData(0) || this.properties.url;
if(url && (url != this._url || this._type != this.properties.type))
this.fetchFile(url);
this.setOutputData(0, this._data );
};
ConstantFile.prototype.setValue = ConstantNumber.prototype.setValue;
ConstantFile.prototype.fetchFile = function(url) {
var that = this;
if(!url || url.constructor !== String)
{
that._data = null;
that.boxcolor = null;
return;
}
this._url = url;
this._type = this.properties.type;
if (url.substr(0, 4) == "http" && LiteGraph.proxy) {
url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3);
}
fetch(url)
.then(function(response) {
if(!response.ok)
throw new Error("File not found");
if(that.properties.type == "arraybuffer")
return response.arrayBuffer();
else if(that.properties.type == "text")
return response.text();
else if(that.properties.type == "json")
return response.json();
else if(that.properties.type == "blob")
return response.blob();
})
.then(function(data) {
that._data = data;
that.boxcolor = "#AEA";
})
.catch(function(error) {
that._data = null;
that.boxcolor = "red";
console.error("error fetching file:",url);
});
};
ConstantFile.prototype.onDropFile = function(file)
{
var that = this;
this._url = file.name;
this._type = this.properties.type;
this.properties.url = file.name;
var reader = new FileReader();
reader.onload = function(e)
{
that.boxcolor = "#AEA";
var v = e.target.result;
if( that.properties.type == "json" )
v = JSON.parse(v);
that._data = v;
}
if(that.properties.type == "arraybuffer")
reader.readAsArrayBuffer(file);
else if(that.properties.type == "text" || that.properties.type == "json")
reader.readAsText(file);
else if(that.properties.type == "blob")
return reader.readAsBinaryString(file);
}
LiteGraph.registerNodeType("basic/file", ConstantFile);
//to store json objects
function ConstantData() {
this.addOutput("data", "object");
this.addProperty("value", "");
this.widget = this.addWidget("text","json","","value");
this.widgets_up = true;
this.size = [140, 30];
this._value = null;
}
ConstantData.title = "Const Data";
ConstantData.desc = "Constant Data";
ConstantData.prototype.onPropertyChanged = function(name, value) {
this.widget.value = value;
if (value == null || value == "") {
return;
}
try {
this._value = JSON.parse(value);
this.boxcolor = "#AEA";
} catch (err) {
this.boxcolor = "red";
}
};
ConstantData.prototype.onExecute = function() {
this.setOutputData(0, this._value);
};
ConstantData.prototype.setValue = ConstantNumber.prototype.setValue;
LiteGraph.registerNodeType("basic/data", ConstantData);
//to store json objects
function ConstantArray() {
this._value = [];
this.addInput("json", "");
this.addOutput("arrayOut", "array");
this.addOutput("length", "number");
this.addProperty("value", "[]");
this.widget = this.addWidget("text","array",this.properties.value,"value");
this.widgets_up = true;
this.size = [140, 50];
}
ConstantArray.title = "Const Array";
ConstantArray.desc = "Constant Array";
ConstantArray.prototype.onPropertyChanged = function(name, value) {
this.widget.value = value;
if (value == null || value == "") {
return;
}
try {
if(value[0] != "[")
this._value = JSON.parse("[" + value + "]");
else
this._value = JSON.parse(value);
this.boxcolor = "#AEA";
} catch (err) {
this.boxcolor = "red";
}
};
ConstantArray.prototype.onExecute = function() {
var v = this.getInputData(0);
if(v && v.length) //clone
{
if(!this._value)
this._value = new Array();
this._value.length = v.length;
for(var i = 0; i < v.length; ++i)
this._value[i] = v[i];
}
this.setOutputData(0, this._value);
this.setOutputData(1, this._value ? ( this._value.length || 0) : 0 );
};
ConstantArray.prototype.setValue = ConstantNumber.prototype.setValue;
LiteGraph.registerNodeType("basic/array", ConstantArray);
function SetArray()
{
this.addInput("arr", "array");
this.addInput("value", "");
this.addOutput("arr", "array");
this.properties = { index: 0 };
this.widget = this.addWidget("number","i",this.properties.index,"index",{precision: 0, step: 10, min: 0});
}
SetArray.title = "Set Array";
SetArray.desc = "Sets index of array";
SetArray.prototype.onExecute = function() {
var arr = this.getInputData(0);
if(!arr)
return;
var v = this.getInputData(1);
if(v === undefined )
return;
if(this.properties.index)
arr[ Math.floor(this.properties.index) ] = v;
this.setOutputData(0,arr);
};
LiteGraph.registerNodeType("basic/set_array", SetArray );
function ArrayElement() {
this.addInput("array", "array,table,string");
this.addInput("index", "number");
this.addOutput("value", "");
this.addProperty("index",0);
}
ArrayElement.title = "Array[i]";
ArrayElement.desc = "Returns an element from an array";
ArrayElement.prototype.onExecute = function() {
var array = this.getInputData(0);
var index = this.getInputData(1);
if(index == null)
index = this.properties.index;
if(array == null || index == null )
return;
this.setOutputData(0, array[Math.floor(Number(index))] );
};
LiteGraph.registerNodeType("basic/array[]", ArrayElement);
function TableElement() {
this.addInput("table", "table");
this.addInput("row", "number");
this.addInput("col", "number");
this.addOutput("value", "");
this.addProperty("row",0);
this.addProperty("column",0);
}
TableElement.title = "Table[row][col]";
TableElement.desc = "Returns an element from a table";
TableElement.prototype.onExecute = function() {
var table = this.getInputData(0);
var row = this.getInputData(1);
var col = this.getInputData(2);
if(row == null)
row = this.properties.row;
if(col == null)
col = this.properties.column;
if(table == null || row == null || col == null)
return;
var row = table[Math.floor(Number(row))];
if(row)
this.setOutputData(0, row[Math.floor(Number(col))] );
else
this.setOutputData(0, null );
};
LiteGraph.registerNodeType("basic/table[][]", TableElement);
function ObjectProperty() {
this.addInput("obj", "object");
this.addOutput("property", 0);
this.addProperty("value", 0);
this.widget = this.addWidget("text","prop.","",this.setValue.bind(this) );
this.widgets_up = true;
this.size = [140, 30];
this._value = null;
}
ObjectProperty.title = "Object property";
ObjectProperty.desc = "Outputs the property of an object";
ObjectProperty.prototype.setValue = function(v) {
this.properties.value = v;
this.widget.value = v;
};
ObjectProperty.prototype.getTitle = function() {
if (this.flags.collapsed) {
return "in." + this.properties.value;
}
return this.title;
};
ObjectProperty.prototype.onPropertyChanged = function(name, value) {
this.widget.value = value;
};
ObjectProperty.prototype.onExecute = function() {
var data = this.getInputData(0);
if (data != null) {
this.setOutputData(0, data[this.properties.value]);
}
};
LiteGraph.registerNodeType("basic/object_property", ObjectProperty);
function ObjectKeys() {
this.addInput("obj", "");
this.addOutput("keys", "array");
this.size = [140, 30];
}
ObjectKeys.title = "Object keys";
ObjectKeys.desc = "Outputs an array with the keys of an object";
ObjectKeys.prototype.onExecute = function() {
var data = this.getInputData(0);
if (data != null) {
this.setOutputData(0, Object.keys(data) );
}
};
LiteGraph.registerNodeType("basic/object_keys", ObjectKeys);
function SetObject()
{
this.addInput("obj", "");
this.addInput("value", "");
this.addOutput("obj", "");
this.properties = { property: "" };
this.name_widget = this.addWidget("text","prop.",this.properties.property,"property");
}
SetObject.title = "Set Object";
SetObject.desc = "Adds propertiesrty to object";
SetObject.prototype.onExecute = function() {
var obj = this.getInputData(0);
if(!obj)
return;
var v = this.getInputData(1);
if(v === undefined )
return;
if(this.properties.property)
obj[ this.properties.property ] = v;
this.setOutputData(0,obj);
};
LiteGraph.registerNodeType("basic/set_object", SetObject );
function MergeObjects() {
this.addInput("A", "object");
this.addInput("B", "object");
this.addOutput("out", "object");
this._result = {};
var that = this;
this.addWidget("button","clear","",function(){
that._result = {};
});
this.size = this.computeSize();
}
MergeObjects.title = "Merge Objects";
MergeObjects.desc = "Creates an object copying properties from others";
MergeObjects.prototype.onExecute = function() {
var A = this.getInputData(0);
var B = this.getInputData(1);
var C = this._result;
if(A)
for(var i in A)
C[i] = A[i];
if(B)
for(var i in B)
C[i] = B[i];
this.setOutputData(0,C);
};
LiteGraph.registerNodeType("basic/merge_objects", MergeObjects );
//Store as variable
function Variable() {
this.size = [60, 30];
this.addInput("in");
this.addOutput("out");
this.properties = { varname: "myname", container: Variable.LITEGRAPH };
this.value = null;
}
Variable.title = "Variable";
Variable.desc = "store/read variable value";
Variable.LITEGRAPH = 0; //between all graphs
Variable.GRAPH = 1; //only inside this graph
Variable.GLOBALSCOPE = 2; //attached to Window
Variable["@container"] = { type: "enum", values: {"litegraph":Variable.LITEGRAPH, "graph":Variable.GRAPH,"global": Variable.GLOBALSCOPE} };
Variable.prototype.onExecute = function() {
var container = this.getContainer();
if(this.isInputConnected(0))
{
this.value = this.getInputData(0);
container[ this.properties.varname ] = this.value;
this.setOutputData(0, this.value );
return;
}
this.setOutputData( 0, container[ this.properties.varname ] );
};
Variable.prototype.getContainer = function()
{
switch(this.properties.container)
{
case Variable.GRAPH:
if(this.graph)
return this.graph.vars;
return {};
break;
case Variable.GLOBALSCOPE:
return global;
break;
case Variable.LITEGRAPH:
default:
return LiteGraph.Globals;
break;
}
}
Variable.prototype.getTitle = function() {
return this.properties.varname;
};
LiteGraph.registerNodeType("basic/variable", Variable);
function length(v) {
if(v && v.length != null)
return Number(v.length);
return 0;
}
LiteGraph.wrapFunctionAsNode(
"basic/length",
length,
[""],
"number"
);
function length(v) {
if(v && v.length != null)
return Number(v.length);
return 0;
}
LiteGraph.wrapFunctionAsNode(
"basic/not",
function(a){ return !a; },
[""],
"boolean"
);
function DownloadData() {
this.size = [60, 30];
this.addInput("data", 0 );
this.addInput("download", LiteGraph.ACTION );
this.properties = { filename: "data.json" };
this.value = null;
var that = this;
this.addWidget("button","Download","", function(v){
if(!that.value)
return;
that.downloadAsFile();
});
}
DownloadData.title = "Download";
DownloadData.desc = "Download some data";
DownloadData.prototype.downloadAsFile = function()
{
if(this.value == null)
return;
var str = null;
if(this.value.constructor === String)
str = this.value;
else
str = JSON.stringify(this.value);
var file = new Blob([str]);
var url = URL.createObjectURL( file );
var element = document.createElement("a");
element.setAttribute('href', url);
element.setAttribute('download', this.properties.filename );
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
setTimeout( function(){ URL.revokeObjectURL( url ); }, 1000*60 ); //wait one minute to revoke url
}
DownloadData.prototype.onAction = function(action, param) {
var that = this;
setTimeout( function(){ that.downloadAsFile(); }, 100); //deferred to avoid blocking the renderer with the popup
}
DownloadData.prototype.onExecute = function() {
if (this.inputs[0]) {
this.value = this.getInputData(0);
}
};
DownloadData.prototype.getTitle = function() {
if (this.flags.collapsed) {
return this.properties.filename;
}
return this.title;
};
LiteGraph.registerNodeType("basic/download", DownloadData);
//Watch a value in the editor
function Watch() {
this.size = [60, 30];
this.addInput("value", 0, { label: "" });
this.value = 0;
}
Watch.title = "Watch";
Watch.desc = "Show value of input";
Watch.prototype.onExecute = function() {
if (this.inputs[0]) {
this.value = this.getInputData(0);
}
};
Watch.prototype.getTitle = function() {
if (this.flags.collapsed) {
return this.inputs[0].label;
}
return this.title;
};
Watch.toString = function(o) {
if (o == null) {
return "null";
} else if (o.constructor === Number) {
return o.toFixed(3);
} else if (o.constructor === Array) {
var str = "[";
for (var i = 0; i < o.length; ++i) {
str += Watch.toString(o[i]) + (i + 1 != o.length ? "," : "");
}
str += "]";
return str;
} else {
return String(o);
}
};
Watch.prototype.onDrawBackground = function(ctx) {
//show the current value
this.inputs[0].label = Watch.toString(this.value);
};
LiteGraph.registerNodeType("basic/watch", Watch);
//in case one type doesnt match other type but you want to connect them anyway
function Cast() {
this.addInput("in", 0);
this.addOutput("out", 0);
this.size = [40, 30];
}
Cast.title = "Cast";
Cast.desc = "Allows to connect different types";
Cast.prototype.onExecute = function() {
this.setOutputData(0, this.getInputData(0));
};
LiteGraph.registerNodeType("basic/cast", Cast);
//Show value inside the debug console
function Console() {
this.mode = LiteGraph.ON_EVENT;
this.size = [80, 30];
this.addProperty("msg", "");
this.addInput("log", LiteGraph.EVENT);
this.addInput("msg", 0);
}
Console.title = "Console";
Console.desc = "Show value inside the console";
Console.prototype.onAction = function(action, param) {
// param is the action
var msg = this.getInputData(1); //getInputDataByName("msg");
//if (msg == null || typeof msg == "undefined") return;
if (!msg) msg = this.properties.msg;
if (!msg) msg = "Event: "+param; // msg is undefined if the slot is lost?
if (action == "log") {
console.log(msg);
} else if (action == "warn") {
console.warn(msg);
} else if (action == "error") {
console.error(msg);
}
};
Console.prototype.onExecute = function() {
var msg = this.getInputData(1); //getInputDataByName("msg");
if (!msg) msg = this.properties.msg;
if (msg != null && typeof msg != "undefined") {
this.properties.msg = msg;
console.log(msg);
}
};
Console.prototype.onGetInputs = function() {
return [
["log", LiteGraph.ACTION],
["warn", LiteGraph.ACTION],
["error", LiteGraph.ACTION]
];
};
LiteGraph.registerNodeType("basic/console", Console);
//Show value inside the debug console
function Alert() {
this.mode = LiteGraph.ON_EVENT;
this.addProperty("msg", "");
this.addInput("", LiteGraph.EVENT);
var that = this;
this.widget = this.addWidget("text", "Text", "", "msg");
this.widgets_up = true;
this.size = [200, 30];
}
Alert.title = "Alert";
Alert.desc = "Show an alert window";
Alert.color = "#510";
Alert.prototype.onConfigure = function(o) {
this.widget.value = o.properties.msg;
};
Alert.prototype.onAction = function(action, param) {
var msg = this.properties.msg;
setTimeout(function() {
alert(msg);
}, 10);
};
LiteGraph.registerNodeType("basic/alert", Alert);
//Execites simple code
function NodeScript() {
this.size = [60, 30];
this.addProperty("onExecute", "return A;");
this.addInput("A", 0);
this.addInput("B", 0);
this.addOutput("out", 0);
this._func = null;
this.data = {};
}
NodeScript.prototype.onConfigure = function(o) {
if (o.properties.onExecute && LiteGraph.allow_scripts)
this.compileCode(o.properties.onExecute);
else
console.warn("Script not compiled, LiteGraph.allow_scripts is false");
};
NodeScript.title = "Script";
NodeScript.desc = "executes a code (max 256 characters)";
NodeScript.widgets_info = {
onExecute: { type: "code" }
};
NodeScript.prototype.onPropertyChanged = function(name, value) {
if (name == "onExecute" && LiteGraph.allow_scripts)
this.compileCode(value);
else
console.warn("Script not compiled, LiteGraph.allow_scripts is false");
};
NodeScript.prototype.compileCode = function(code) {
this._func = null;
if (code.length > 256) {
console.warn("Script too long, max 256 chars");
} else {
var code_low = code.toLowerCase();
var forbidden_words = [
"script",
"body",
"document",
"eval",
"nodescript",
"function"
]; //bad security solution
for (var i = 0; i < forbidden_words.length; ++i) {
if (code_low.indexOf(forbidden_words[i]) != -1) {
console.warn("invalid script");
return;
}
}
try {
this._func = new Function("A", "B", "C", "DATA", "node", code);
} catch (err) {
console.error("Error parsing script");
console.error(err);
}
}
};
NodeScript.prototype.onExecute = function() {
if (!this._func) {
return;
}
try {
var A = this.getInputData(0);
var B = this.getInputData(1);
var C = this.getInputData(2);
this.setOutputData(0, this._func(A, B, C, this.data, this));
} catch (err) {
console.error("Error in script");
console.error(err);
}
};
NodeScript.prototype.onGetOutputs = function() {
return [["C", ""]];
};
LiteGraph.registerNodeType("basic/script", NodeScript);
function GenericCompare() {
this.addInput("A", 0);
this.addInput("B", 0);
this.addOutput("true", "boolean");
this.addOutput("false", "boolean");
this.addProperty("A", 1);
this.addProperty("B", 1);
this.addProperty("OP", "==", "enum", { values: GenericCompare.values });
this.addWidget("combo","Op.",this.properties.OP,{ property: "OP", values: GenericCompare.values } );
this.size = [80, 60];
}
GenericCompare.values = ["==", "!="]; //[">", "<", "==", "!=", "<=", ">=", "||", "&&" ];
GenericCompare["@OP"] = {
type: "enum",
title: "operation",
values: GenericCompare.values
};
GenericCompare.title = "Compare *";
GenericCompare.desc = "evaluates condition between A and B";
GenericCompare.prototype.getTitle = function() {
return "*A " + this.properties.OP + " *B";
};
GenericCompare.prototype.onExecute = function() {
var A = this.getInputData(0);
if (A === undefined) {
A = this.properties.A;
} else {
this.properties.A = A;
}
var B = this.getInputData(1);
if (B === undefined) {
B = this.properties.B;
} else {
this.properties.B = B;
}
var result = false;
if (typeof A == typeof B){
switch (this.properties.OP) {
case "==":
case "!=":
// traverse both objects.. consider that this is not a true deep check! consider underscore or other library for thath :: _isEqual()
result = true;
switch(typeof A){
case "object":
var aProps = Object.getOwnPropertyNames(A);
var bProps = Object.getOwnPropertyNames(B);
if (aProps.length != bProps.length){
result = false;
break;
}
for (var i = 0; i < aProps.length; i++) {
var propName = aProps[i];
if (A[propName] !== B[propName]) {
result = false;
break;
}
}
break;
default:
result = A == B;
}
if (this.properties.OP == "!=") result = !result;
break;
/*case ">":
result = A > B;
break;
case "<":
result = A < B;
break;
case "<=":
result = A <= B;
break;
case ">=":
result = A >= B;
break;
case "||":
result = A || B;
break;
case "&&":
result = A && B;
break;*/
}
}
this.setOutputData(0, result);
this.setOutputData(1, !result);
};
LiteGraph.registerNodeType("basic/CompareValues", GenericCompare);
})(this);
//event related nodes
(function(global) {
var LiteGraph = global.LiteGraph;
//Show value inside the debug console
function LogEvent() {
this.size = [60, 30];
this.addInput("event", LiteGraph.ACTION);
}
LogEvent.title = "Log Event";
LogEvent.desc = "Log event in console";
LogEvent.prototype.onAction = function(action, param, options) {
console.log(action, param);
};
LiteGraph.registerNodeType("events/log", LogEvent);
//convert to Event if the value is true
function TriggerEvent() {
this.size = [60, 30];
this.addInput("if", "");
this.addOutput("true", LiteGraph.EVENT);
this.addOutput("change", LiteGraph.EVENT);
this.addOutput("false", LiteGraph.EVENT);
this.properties = { only_on_change: true };
this.prev = 0;
}
TriggerEvent.title = "TriggerEvent";
TriggerEvent.desc = "Triggers event if input evaluates to true";
TriggerEvent.prototype.onExecute = function( param, options) {
var v = this.getInputData(0);
var changed = (v != this.prev);
if(this.prev === 0)
changed = false;
var must_resend = (changed && this.properties.only_on_change) || (!changed && !this.properties.only_on_change);
if(v && must_resend )
this.triggerSlot(0, param, null, options);
if(!v && must_resend)
this.triggerSlot(2, param, null, options);
if(changed)
this.triggerSlot(1, param, null, options);
this.prev = v;
};
LiteGraph.registerNodeType("events/trigger", TriggerEvent);
//Sequence of events
function Sequence() {
var that = this;
this.addInput("", LiteGraph.ACTION);
this.addInput("", LiteGraph.ACTION);
this.addInput("", LiteGraph.ACTION);
this.addOutput("", LiteGraph.EVENT);
this.addOutput("", LiteGraph.EVENT);
this.addOutput("", LiteGraph.EVENT);
this.addWidget("button","+",null,function(){
that.addInput("", LiteGraph.ACTION);
that.addOutput("", LiteGraph.EVENT);
});
this.size = [90, 70];
this.flags = { horizontal: true, render_box: false };
}
Sequence.title = "Sequence";
Sequence.desc = "Triggers a sequence of events when an event arrives";
Sequence.prototype.getTitle = function() {
return "";
};
Sequence.prototype.onAction = function(action, param, options) {
if (this.outputs) {
options = options || {};
for (var i = 0; i < this.outputs.length; ++i) {
var output = this.outputs[i];
//needs more info about this...
if( options.action_call ) // CREATE A NEW ID FOR THE ACTION
options.action_call = options.action_call + "_seq_" + i;
else
options.action_call = this.id + "_" + (action ? action : "action")+"_seq_"+i+"_"+Math.floor(Math.random()*9999);
this.triggerSlot(i, param, null, options);
}
}
};
LiteGraph.registerNodeType("events/sequence", Sequence);
//Sequencer for events
function Stepper() {
var that = this;
this.properties = { index: 0 };
this.addInput("index", "number");
this.addInput("step", LiteGraph.ACTION);
this.addInput("reset", LiteGraph.ACTION);
this.addOutput("index", "number");
this.addOutput("", LiteGraph.EVENT);
this.addOutput("", LiteGraph.EVENT);
this.addOutput("", LiteGraph.EVENT,{removable:true});
this.addWidget("button","+",null,function(){
that.addOutput("", LiteGraph.EVENT, {removable:true});
});
this.size = [120, 120];
this.flags = { render_box: false };
}
Stepper.title = "Stepper";
Stepper.desc = "Trigger events sequentially when an tick arrives";
Stepper.prototype.onDrawBackground = function(ctx)
{
if (this.flags.collapsed) {
return;
}
var index = this.properties.index || 0;
ctx.fillStyle = "#AFB";
var w = this.size[0];
var y = (index + 1)* LiteGraph.NODE_SLOT_HEIGHT + 4;
ctx.beginPath();
ctx.moveTo(w - 30, y);
ctx.lineTo(w - 30, y + LiteGraph.NODE_SLOT_HEIGHT);
ctx.lineTo(w - 15, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
ctx.fill();
}
Stepper.prototype.onExecute = function()
{
var index = this.getInputData(0);
if(index != null)
{
index = Math.floor(index);
index = clamp( index, 0, this.outputs ? (this.outputs.length - 2) : 0 );
if( index != this.properties.index )
{
this.properties.index = index;
this.triggerSlot( index+1 );
}
}
this.setOutputData(0, this.properties.index );
}
Stepper.prototype.onAction = function(action, param) {
if(action == "reset")
this.properties.index = 0;
else if(action == "step")
{
this.triggerSlot(this.properties.index+1, param);
var n = this.outputs ? this.outputs.length - 1 : 0;
this.properties.index = (this.properties.index + 1) % n;
}
};
LiteGraph.registerNodeType("events/stepper", Stepper);
//Filter events
function FilterEvent() {
this.size = [60, 30];
this.addInput("event", LiteGraph.ACTION);
this.addOutput("event", LiteGraph.EVENT);
this.properties = {
equal_to: "",
has_property: "",
property_equal_to: ""
};
}
FilterEvent.title = "Filter Event";
FilterEvent.desc = "Blocks events that do not match the filter";
FilterEvent.prototype.onAction = function(action, param, options) {
if (param == null) {
return;
}
if (this.properties.equal_to && this.properties.equal_to != param) {
return;
}
if (this.properties.has_property) {
var prop = param[this.properties.has_property];
if (prop == null) {
return;
}
if (
this.properties.property_equal_to &&
this.properties.property_equal_to != prop
) {
return;
}
}
this.triggerSlot(0, param, null, options);
};
LiteGraph.registerNodeType("events/filter", FilterEvent);
function EventBranch() {
this.addInput("in", LiteGraph.ACTION);
this.addInput("cond", "boolean");
this.addOutput("true", LiteGraph.EVENT);
this.addOutput("false", LiteGraph.EVENT);
this.size = [120, 60];
this._value = false;
}
EventBranch.title = "Branch";
EventBranch.desc = "If condition is true, outputs triggers true, otherwise false";
EventBranch.prototype.onExecute = function() {
this._value = this.getInputData(1);
}
EventBranch.prototype.onAction = function(action, param, options) {
this._value = this.getInputData(1);
this.triggerSlot(this._value ? 0 : 1, param, null, options);
}
LiteGraph.registerNodeType("events/branch", EventBranch);
//Show value inside the debug console
function EventCounter() {
this.addInput("inc", LiteGraph.ACTION);
this.addInput("dec", LiteGraph.ACTION);
this.addInput("reset", LiteGraph.ACTION);
this.addOutput("change", LiteGraph.EVENT);
this.addOutput("num", "number");
this.addProperty("doCountExecution", false, "boolean", {name: "Count Executions"});
this.addWidget("toggle","Count Exec.",this.properties.doCountExecution,"doCountExecution");
this.num = 0;
}
EventCounter.title = "Counter";
EventCounter.desc = "Counts events";
EventCounter.prototype.getTitle = function() {
if (this.flags.collapsed) {
return String(this.num);
}
return this.title;
};
EventCounter.prototype.onAction = function(action, param, options) {
var v = this.num;
if (action == "inc") {
this.num += 1;
} else if (action == "dec") {
this.num -= 1;
} else if (action == "reset") {
this.num = 0;
}
if (this.num != v) {
this.trigger("change", this.num);
}
};
EventCounter.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed) {
return;
}
ctx.fillStyle = "#AAA";
ctx.font = "20px Arial";
ctx.textAlign = "center";
ctx.fillText(this.num, this.size[0] * 0.5, this.size[1] * 0.5);
};
EventCounter.prototype.onExecute = function() {
if(this.properties.doCountExecution){
this.num += 1;
}
this.setOutputData(1, this.num);
};
LiteGraph.registerNodeType("events/counter", EventCounter);
//Show value inside the debug console
function DelayEvent() {
this.size = [60, 30];
this.addProperty("time_in_ms", 1000);
this.addInput("event", LiteGraph.ACTION);
this.addOutput("on_time", LiteGraph.EVENT);
this._pending = [];
}
DelayEvent.title = "Delay";
DelayEvent.desc = "Delays one event";
DelayEvent.prototype.onAction = function(action, param, options) {
var time = this.properties.time_in_ms;
if (time <= 0) {
this.trigger(null, param, options);
} else {
this._pending.push([time, param]);
}
};
DelayEvent.prototype.onExecute = function(param, options) {
var dt = this.graph.elapsed_time * 1000; //in ms
if (this.isInputConnected(1)) {
this.properties.time_in_ms = this.getInputData(1);
}
for (var i = 0; i < this._pending.length; ++i) {
var actionPass = this._pending[i];
actionPass[0] -= dt;
if (actionPass[0] > 0) {
continue;
}
//remove
this._pending.splice(i, 1);
--i;
//trigger
this.trigger(null, actionPass[1], options);
}
};
DelayEvent.prototype.onGetInputs = function() {
return [["event", LiteGraph.ACTION], ["time_in_ms", "number"]];
};
LiteGraph.registerNodeType("events/delay", DelayEvent);
//Show value inside the debug console
function TimerEvent() {
this.addProperty("interval", 1000);
this.addProperty("event", "tick");
this.addOutput("on_tick", LiteGraph.EVENT);
this.time = 0;
this.last_interval = 1000;
this.triggered = false;
}
TimerEvent.title = "Timer";
TimerEvent.desc = "Sends an event every N milliseconds";
TimerEvent.prototype.onStart = function() {
this.time = 0;
};
TimerEvent.prototype.getTitle = function() {
return "Timer: " + this.last_interval.toString() + "ms";
};
TimerEvent.on_color = "#AAA";
TimerEvent.off_color = "#222";
TimerEvent.prototype.onDrawBackground = function() {
this.boxcolor = this.triggered
? TimerEvent.on_color
: TimerEvent.off_color;
this.triggered = false;
};
TimerEvent.prototype.onExecute = function() {
var dt = this.graph.elapsed_time * 1000; //in ms
var trigger = this.time == 0;
this.time += dt;
this.last_interval = Math.max(
1,
this.getInputOrProperty("interval") | 0
);
if (
!trigger &&
(this.time < this.last_interval || isNaN(this.last_interval))
) {
if (this.inputs && this.inputs.length > 1 && this.inputs[1]) {
this.setOutputData(1, false);
}
return;
}
this.triggered = true;
this.time = this.time % this.last_interval;
this.trigger("on_tick", this.properties.event);
if (this.inputs && this.inputs.length > 1 && this.inputs[1]) {
this.setOutputData(1, true);
}
};
TimerEvent.prototype.onGetInputs = function() {
return [["interval", "number"]];
};
TimerEvent.prototype.onGetOutputs = function() {
return [["tick", "boolean"]];
};
LiteGraph.registerNodeType("events/timer", TimerEvent);
function SemaphoreEvent() {
this.addInput("go", LiteGraph.ACTION );
this.addInput("green", LiteGraph.ACTION );
this.addInput("red", LiteGraph.ACTION );
this.addOutput("continue", LiteGraph.EVENT );
this.addOutput("blocked", LiteGraph.EVENT );
this.addOutput("is_green", "boolean" );
this._ready = false;
this.properties = {};
var that = this;
this.addWidget("button","reset","",function(){
that._ready = false;
});
}
SemaphoreEvent.title = "Semaphore Event";
SemaphoreEvent.desc = "Until both events are not triggered, it doesnt continue.";
SemaphoreEvent.prototype.onExecute = function()
{
this.setOutputData(1,this._ready);
this.boxcolor = this._ready ? "#9F9" : "#FA5";
}
SemaphoreEvent.prototype.onAction = function(action, param) {
if( action == "go" )
this.triggerSlot( this._ready ? 0 : 1 );
else if( action == "green" )
this._ready = true;
else if( action == "red" )
this._ready = false;
};
LiteGraph.registerNodeType("events/semaphore", SemaphoreEvent);
function OnceEvent() {
this.addInput("in", LiteGraph.ACTION );
this.addInput("reset", LiteGraph.ACTION );
this.addOutput("out", LiteGraph.EVENT );
this._once = false;
this.properties = {};
var that = this;
this.addWidget("button","reset","",function(){
that._once = false;
});
}
OnceEvent.title = "Once";
OnceEvent.desc = "Only passes an event once, then gets locked";
OnceEvent.prototype.onAction = function(action, param) {
if( action == "in" && !this._once )
{
this._once = true;
this.triggerSlot( 0, param );
}
else if( action == "reset" )
this._once = false;
};
LiteGraph.registerNodeType("events/once", OnceEvent);
function DataStore() {
this.addInput("data", 0);
this.addInput("assign", LiteGraph.ACTION);
this.addOutput("data", 0);
this._last_value = null;
this.properties = { data: null, serialize: true };
var that = this;
this.addWidget("button","store","",function(){
that.properties.data = that._last_value;
});
}
DataStore.title = "Data Store";
DataStore.desc = "Stores data and only changes when event is received";
DataStore.prototype.onExecute = function()
{
this._last_value = this.getInputData(0);
this.setOutputData(0, this.properties.data );
}
DataStore.prototype.onAction = function(action, param, options) {
this.properties.data = this._last_value;
};
DataStore.prototype.onSerialize = function(o)
{
if(o.data == null)
return;
if(this.properties.serialize == false || (o.data.constructor !== String && o.data.constructor !== Number && o.data.constructor !== Boolean && o.data.constructor !== Array && o.data.constructor !== Object ))
o.data = null;
}
LiteGraph.registerNodeType("basic/data_store", DataStore);
})(this);
//widgets
(function(global) {
var LiteGraph = global.LiteGraph;
/* Button ****************/
function WidgetButton() {
this.addOutput("", LiteGraph.EVENT);
this.addOutput("", "boolean");
this.addProperty("text", "click me");
this.addProperty("font_size", 30);
this.addProperty("message", "");
this.size = [164, 84];
this.clicked = false;
}
WidgetButton.title = "Button";
WidgetButton.desc = "Triggers an event";
WidgetButton.font = "Arial";
WidgetButton.prototype.onDrawForeground = function(ctx) {
if (this.flags.collapsed) {
return;
}
var margin = 10;
ctx.fillStyle = "black";
ctx.fillRect(
margin + 1,
margin + 1,
this.size[0] - margin * 2,
this.size[1] - margin * 2
);
ctx.fillStyle = "#AAF";
ctx.fillRect(
margin - 1,
margin - 1,
this.size[0] - margin * 2,
this.size[1] - margin * 2
);
ctx.fillStyle = this.clicked
? "white"
: this.mouseOver
? "#668"
: "#334";
ctx.fillRect(
margin,
margin,
this.size[0] - margin * 2,
this.size[1] - margin * 2
);
if (this.properties.text || this.properties.text === 0) {
var font_size = this.properties.font_size || 30;
ctx.textAlign = "center";
ctx.fillStyle = this.clicked ? "black" : "white";
ctx.font = font_size + "px " + WidgetButton.font;
ctx.fillText(
this.properties.text,
this.size[0] * 0.5,
this.size[1] * 0.5 + font_size * 0.3
);
ctx.textAlign = "left";
}
};
WidgetButton.prototype.onMouseDown = function(e, local_pos) {
if (
local_pos[0] > 1 &&
local_pos[1] > 1 &&
local_pos[0] < this.size[0] - 2 &&
local_pos[1] < this.size[1] - 2
) {
this.clicked = true;
this.setOutputData(1, this.clicked);
this.triggerSlot(0, this.properties.message);
return true;
}
};
WidgetButton.prototype.onExecute = function() {
this.setOutputData(1, this.clicked);
};
WidgetButton.prototype.onMouseUp = function(e) {
this.clicked = false;
};
LiteGraph.registerNodeType("widget/button", WidgetButton);
function WidgetToggle() {
this.addInput("", "boolean");
this.addInput("e", LiteGraph.ACTION);
this.addOutput("v", "boolean");
this.addOutput("e", LiteGraph.EVENT);
this.properties = { font: "", value: false };
this.size = [160, 44];
}
WidgetToggle.title = "Toggle";
WidgetToggle.desc = "Toggles between true or false";
WidgetToggle.prototype.onDrawForeground = function(ctx) {
if (this.flags.collapsed) {
return;
}
var size = this.size[1] * 0.5;
var margin = 0.25;
var h = this.size[1] * 0.8;
ctx.font = this.properties.font || (size * 0.8).toFixed(0) + "px Arial";
var w = ctx.measureText(this.title).width;
var x = (this.size[0] - (w + size)) * 0.5;
ctx.fillStyle = "#AAA";
ctx.fillRect(x, h - size, size, size);
ctx.fillStyle = this.properties.value ? "#AEF" : "#000";
ctx.fillRect(
x + size * margin,
h - size + size * margin,
size * (1 - margin * 2),
size * (1 - margin * 2)
);
ctx.textAlign = "left";
ctx.fillStyle = "#AAA";
ctx.fillText(this.title, size * 1.2 + x, h * 0.85);
ctx.textAlign = "left";
};
WidgetToggle.prototype.onAction = function(action) {
this.properties.value = !this.properties.value;
this.trigger("e", this.properties.value);
};
WidgetToggle.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v != null) {
this.properties.value = v;
}
this.setOutputData(0, this.properties.value);
};
WidgetToggle.prototype.onMouseDown = function(e, local_pos) {
if (
local_pos[0] > 1 &&
local_pos[1] > 1 &&
local_pos[0] < this.size[0] - 2 &&
local_pos[1] < this.size[1] - 2
) {
this.properties.value = !this.properties.value;
this.graph._version++;
this.trigger("e", this.properties.value);
return true;
}
};
LiteGraph.registerNodeType("widget/toggle", WidgetToggle);
/* Number ****************/
function WidgetNumber() {
this.addOutput("", "number");
this.size = [80, 60];
this.properties = { min: -1000, max: 1000, value: 1, step: 1 };
this.old_y = -1;
this._remainder = 0;
this._precision = 0;
this.mouse_captured = false;
}
WidgetNumber.title = "Number";
WidgetNumber.desc = "Widget to select number value";
WidgetNumber.pixels_threshold = 10;
WidgetNumber.markers_color = "#666";
WidgetNumber.prototype.onDrawForeground = function(ctx) {
var x = this.size[0] * 0.5;
var h = this.size[1];
if (h > 30) {
ctx.fillStyle = WidgetNumber.markers_color;
ctx.beginPath();
ctx.moveTo(x, h * 0.1);
ctx.lineTo(x + h * 0.1, h * 0.2);
ctx.lineTo(x + h * -0.1, h * 0.2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(x, h * 0.9);
ctx.lineTo(x + h * 0.1, h * 0.8);
ctx.lineTo(x + h * -0.1, h * 0.8);
ctx.fill();
ctx.font = (h * 0.7).toFixed(1) + "px Arial";
} else {
ctx.font = (h * 0.8).toFixed(1) + "px Arial";
}
ctx.textAlign = "center";
ctx.font = (h * 0.7).toFixed(1) + "px Arial";
ctx.fillStyle = "#EEE";
ctx.fillText(
this.properties.value.toFixed(this._precision),
x,
h * 0.75
);
};
WidgetNumber.prototype.onExecute = function() {
this.setOutputData(0, this.properties.value);
};
WidgetNumber.prototype.onPropertyChanged = function(name, value) {
var t = (this.properties.step + "").split(".");
this._precision = t.length > 1 ? t[1].length : 0;
};
WidgetNumber.prototype.onMouseDown = function(e, pos) {
if (pos[1] < 0) {
return;
}
this.old_y = e.canvasY;
this.captureInput(true);
this.mouse_captured = true;
return true;
};
WidgetNumber.prototype.onMouseMove = function(e) {
if (!this.mouse_captured) {
return;
}
var delta = this.old_y - e.canvasY;
if (e.shiftKey) {
delta *= 10;
}
if (e.metaKey || e.altKey) {
delta *= 0.1;
}
this.old_y = e.canvasY;
var steps = this._remainder + delta / WidgetNumber.pixels_threshold;
this._remainder = steps % 1;
steps = steps | 0;
var v = clamp(
this.properties.value + steps * this.properties.step,
this.properties.min,
this.properties.max
);
this.properties.value = v;
this.graph._version++;
this.setDirtyCanvas(true);
};
WidgetNumber.prototype.onMouseUp = function(e, pos) {
if (e.click_time < 200) {
var steps = pos[1] > this.size[1] * 0.5 ? -1 : 1;
this.properties.value = clamp(
this.properties.value + steps * this.properties.step,
this.properties.min,
this.properties.max
);
this.graph._version++;
this.setDirtyCanvas(true);
}
if (this.mouse_captured) {
this.mouse_captured = false;
this.captureInput(false);
}
};
LiteGraph.registerNodeType("widget/number", WidgetNumber);
/* Combo ****************/
function WidgetCombo() {
this.addOutput("", "string");
this.addOutput("change", LiteGraph.EVENT);
this.size = [80, 60];
this.properties = { value: "A", values:"A;B;C" };
this.old_y = -1;
this.mouse_captured = false;
this._values = this.properties.values.split(";");
var that = this;
this.widgets_up = true;
this.widget = this.addWidget("combo","", this.properties.value, function(v){
that.properties.value = v;
that.triggerSlot(1, v);
}, { property: "value", values: this._values } );
}
WidgetCombo.title = "Combo";
WidgetCombo.desc = "Widget to select from a list";
WidgetCombo.prototype.onExecute = function() {
this.setOutputData( 0, this.properties.value );
};
WidgetCombo.prototype.onPropertyChanged = function(name, value) {
if(name == "values")
{
this._values = value.split(";");
this.widget.options.values = this._values;
}
else if(name == "value")
{
this.widget.value = value;
}
};
LiteGraph.registerNodeType("widget/combo", WidgetCombo);
/* Knob ****************/
function WidgetKnob() {
this.addOutput("", "number");
this.size = [64, 84];
this.properties = {
min: 0,
max: 1,
value: 0.5,
color: "#7AF",
precision: 2
};
this.value = -1;
}
WidgetKnob.title = "Knob";
WidgetKnob.desc = "Circular controller";
WidgetKnob.size = [80, 100];
WidgetKnob.prototype.onDrawForeground = function(ctx) {
if (this.flags.collapsed) {
return;
}
if (this.value == -1) {
this.value =
(this.properties.value - this.properties.min) /
(this.properties.max - this.properties.min);
}
var center_x = this.size[0] * 0.5;
var center_y = this.size[1] * 0.5;
var radius = Math.min(this.size[0], this.size[1]) * 0.5 - 5;
var w = Math.floor(radius * 0.05);
ctx.globalAlpha = 1;
ctx.save();
ctx.translate(center_x, center_y);
ctx.rotate(Math.PI * 0.75);
//bg
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, radius, 0, Math.PI * 1.5);
ctx.fill();
//value
ctx.strokeStyle = "black";
ctx.fillStyle = this.properties.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(
0,
0,
radius - 4,
0,
Math.PI * 1.5 * Math.max(0.01, this.value)
);
ctx.closePath();
ctx.fill();
//ctx.stroke();
ctx.lineWidth = 1;
ctx.globalAlpha = 1;
ctx.restore();
//inner
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(center_x, center_y, radius * 0.75, 0, Math.PI * 2, true);
ctx.fill();
//miniball
ctx.fillStyle = this.mouseOver ? "white" : this.properties.color;
ctx.beginPath();
var angle = this.value * Math.PI * 1.5 + Math.PI * 0.75;
ctx.arc(
center_x + Math.cos(angle) * radius * 0.65,
center_y + Math.sin(angle) * radius * 0.65,
radius * 0.05,
0,
Math.PI * 2,
true
);
ctx.fill();
//text
ctx.fillStyle = this.mouseOver ? "white" : "#AAA";
ctx.font = Math.floor(radius * 0.5) + "px Arial";
ctx.textAlign = "center";
ctx.fillText(
this.properties.value.toFixed(this.properties.precision),
center_x,
center_y + radius * 0.15
);
};
WidgetKnob.prototype.onExecute = function() {
this.setOutputData(0, this.properties.value);
this.boxcolor = LiteGraph.colorToString([
this.value,
this.value,
this.value
]);
};
WidgetKnob.prototype.onMouseDown = function(e) {
this.center = [this.size[0] * 0.5, this.size[1] * 0.5 + 20];
this.radius = this.size[0] * 0.5;
if (
e.canvasY - this.pos[1] < 20 ||
LiteGraph.distance(
[e.canvasX, e.canvasY],
[this.pos[0] + this.center[0], this.pos[1] + this.center[1]]
) > this.radius
) {
return false;
}
this.oldmouse = [e.canvasX - this.pos[0], e.canvasY - this.pos[1]];
this.captureInput(true);
return true;
};
WidgetKnob.prototype.onMouseMove = function(e) {
if (!this.oldmouse) {
return;
}
var m = [e.canvasX - this.pos[0], e.canvasY - this.pos[1]];
var v = this.value;
v -= (m[1] - this.oldmouse[1]) * 0.01;
if (v > 1.0) {
v = 1.0;
} else if (v < 0.0) {
v = 0.0;
}
this.value = v;
this.properties.value =
this.properties.min +
(this.properties.max - this.properties.min) * this.value;
this.oldmouse = m;
this.setDirtyCanvas(true);
};
WidgetKnob.prototype.onMouseUp = function(e) {
if (this.oldmouse) {
this.oldmouse = null;
this.captureInput(false);
}
};
WidgetKnob.prototype.onPropertyChanged = function(name, value) {
if (name == "min" || name == "max" || name == "value") {
this.properties[name] = parseFloat(value);
return true; //block
}
};
LiteGraph.registerNodeType("widget/knob", WidgetKnob);
//Show value inside the debug console
function WidgetSliderGUI() {
this.addOutput("", "number");
this.properties = {
value: 0.5,
min: 0,
max: 1,
text: "V"
};
var that = this;
this.size = [140, 40];
this.slider = this.addWidget(
"slider",
"V",
this.properties.value,
function(v) {
that.properties.value = v;
},
this.properties
);
this.widgets_up = true;
}
WidgetSliderGUI.title = "Inner Slider";
WidgetSliderGUI.prototype.onPropertyChanged = function(name, value) {
if (name == "value") {
this.slider.value = value;
}
};
WidgetSliderGUI.prototype.onExecute = function() {
this.setOutputData(0, this.properties.value);
};
LiteGraph.registerNodeType("widget/internal_slider", WidgetSliderGUI);
//Widget H SLIDER
function WidgetHSlider() {
this.size = [160, 26];
this.addOutput("", "number");
this.properties = { color: "#7AF", min: 0, max: 1, value: 0.5 };
this.value = -1;
}
WidgetHSlider.title = "H.Slider";
WidgetHSlider.desc = "Linear slider controller";
WidgetHSlider.prototype.onDrawForeground = function(ctx) {
if (this.value == -1) {
this.value =
(this.properties.value - this.properties.min) /
(this.properties.max - this.properties.min);
}
//border
ctx.globalAlpha = 1;
ctx.lineWidth = 1;
ctx.fillStyle = "#000";
ctx.fillRect(2, 2, this.size[0] - 4, this.size[1] - 4);
ctx.fillStyle = this.properties.color;
ctx.beginPath();
ctx.rect(4, 4, (this.size[0] - 8) * this.value, this.size[1] - 8);
ctx.fill();
};
WidgetHSlider.prototype.onExecute = function() {
this.properties.value =
this.properties.min +
(this.properties.max - this.properties.min) * this.value;
this.setOutputData(0, this.properties.value);
this.boxcolor = LiteGraph.colorToString([
this.value,
this.value,
this.value
]);
};
WidgetHSlider.prototype.onMouseDown = function(e) {
if (e.canvasY - this.pos[1] < 0) {
return false;
}
this.oldmouse = [e.canvasX - this.pos[0], e.canvasY - this.pos[1]];
this.captureInput(true);
return true;
};
WidgetHSlider.prototype.onMouseMove = function(e) {
if (!this.oldmouse) {
return;
}
var m = [e.canvasX - this.pos[0], e.canvasY - this.pos[1]];
var v = this.value;
var delta = m[0] - this.oldmouse[0];
v += delta / this.size[0];
if (v > 1.0) {
v = 1.0;
} else if (v < 0.0) {
v = 0.0;
}
this.value = v;
this.oldmouse = m;
this.setDirtyCanvas(true);
};
WidgetHSlider.prototype.onMouseUp = function(e) {
this.oldmouse = null;
this.captureInput(false);
};
WidgetHSlider.prototype.onMouseLeave = function(e) {
//this.oldmouse = null;
};
LiteGraph.registerNodeType("widget/hslider", WidgetHSlider);
function WidgetProgress() {
this.size = [160, 26];
this.addInput("", "number");
this.properties = { min: 0, max: 1, value: 0, color: "#AAF" };
}
WidgetProgress.title = "Progress";
WidgetProgress.desc = "Shows data in linear progress";
WidgetProgress.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v != undefined) {
this.properties["value"] = v;
}
};
WidgetProgress.prototype.onDrawForeground = function(ctx) {
//border
ctx.lineWidth = 1;
ctx.fillStyle = this.properties.color;
var v =
(this.properties.value - this.properties.min) /
(this.properties.max - this.properties.min);
v = Math.min(1, v);
v = Math.max(0, v);
ctx.fillRect(2, 2, (this.size[0] - 4) * v, this.size[1] - 4);
};
LiteGraph.registerNodeType("widget/progress", WidgetProgress);
function WidgetText() {
this.addInputs("", 0);
this.properties = {
value: "...",
font: "Arial",
fontsize: 18,
color: "#AAA",
align: "left",
glowSize: 0,
decimals: 1
};
}
WidgetText.title = "Text";
WidgetText.desc = "Shows the input value";
WidgetText.widgets = [
{ name: "resize", text: "Resize box", type: "button" },
{ name: "led_text", text: "LED", type: "minibutton" },
{ name: "normal_text", text: "Normal", type: "minibutton" }
];
WidgetText.prototype.onDrawForeground = function(ctx) {
//ctx.fillStyle="#000";
//ctx.fillRect(0,0,100,60);
ctx.fillStyle = this.properties["color"];
var v = this.properties["value"];
if (this.properties["glowSize"]) {
ctx.shadowColor = this.properties.color;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = this.properties["glowSize"];
} else {
ctx.shadowColor = "transparent";
}
var fontsize = this.properties["fontsize"];
ctx.textAlign = this.properties["align"];
ctx.font = fontsize.toString() + "px " + this.properties["font"];
this.str =
typeof v == "number" ? v.toFixed(this.properties["decimals"]) : v;
if (typeof this.str == "string") {
var lines = this.str.replace(/[\r\n]/g, "\\n").split("\\n");
for (var i=0; i < lines.length; i++) {
ctx.fillText(
lines[i],
this.properties["align"] == "left" ? 15 : this.size[0] - 15,
fontsize * -0.15 + fontsize * (parseInt(i) + 1)
);
}
}
ctx.shadowColor = "transparent";
this.last_ctx = ctx;
ctx.textAlign = "left";
};
WidgetText.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v != null) {
this.properties["value"] = v;
}
//this.setDirtyCanvas(true);
};
WidgetText.prototype.resize = function() {
if (!this.last_ctx) {
return;
}
var lines = this.str.split("\\n");
this.last_ctx.font =
this.properties["fontsize"] + "px " + this.properties["font"];
var max = 0;
for (var i=0; i < lines.length; i++) {
var w = this.last_ctx.measureText(lines[i]).width;
if (max < w) {
max = w;
}
}
this.size[0] = max + 20;
this.size[1] = 4 + lines.length * this.properties["fontsize"];
this.setDirtyCanvas(true);
};
WidgetText.prototype.onPropertyChanged = function(name, value) {
this.properties[name] = value;
this.str = typeof value == "number" ? value.toFixed(3) : value;
//this.resize();
return true;
};
LiteGraph.registerNodeType("widget/text", WidgetText);
function WidgetPanel() {
this.size = [200, 100];
this.properties = {
borderColor: "#ffffff",
bgcolorTop: "#f0f0f0",
bgcolorBottom: "#e0e0e0",
shadowSize: 2,
borderRadius: 3
};
}
WidgetPanel.title = "Panel";
WidgetPanel.desc = "Non interactive panel";
WidgetPanel.widgets = [{ name: "update", text: "Update", type: "button" }];
WidgetPanel.prototype.createGradient = function(ctx) {
if (
this.properties["bgcolorTop"] == "" ||
this.properties["bgcolorBottom"] == ""
) {
this.lineargradient = 0;
return;
}
this.lineargradient = ctx.createLinearGradient(0, 0, 0, this.size[1]);
this.lineargradient.addColorStop(0, this.properties["bgcolorTop"]);
this.lineargradient.addColorStop(1, this.properties["bgcolorBottom"]);
};
WidgetPanel.prototype.onDrawForeground = function(ctx) {
if (this.flags.collapsed) {
return;
}
if (this.lineargradient == null) {
this.createGradient(ctx);
}
if (!this.lineargradient) {
return;
}
ctx.lineWidth = 1;
ctx.strokeStyle = this.properties["borderColor"];
//ctx.fillStyle = "#ebebeb";
ctx.fillStyle = this.lineargradient;
if (this.properties["shadowSize"]) {
ctx.shadowColor = "#000";
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = this.properties["shadowSize"];
} else {
ctx.shadowColor = "transparent";
}
ctx.roundRect(
0,
0,
this.size[0] - 1,
this.size[1] - 1,
this.properties["shadowSize"]
);
ctx.fill();
ctx.shadowColor = "transparent";
ctx.stroke();
};
LiteGraph.registerNodeType("widget/panel", WidgetPanel);
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
function GamepadInput() {
this.addOutput("left_x_axis", "number");
this.addOutput("left_y_axis", "number");
this.addOutput("button_pressed", LiteGraph.EVENT);
this.properties = { gamepad_index: 0, threshold: 0.1 };
this._left_axis = new Float32Array(2);
this._right_axis = new Float32Array(2);
this._triggers = new Float32Array(2);
this._previous_buttons = new Uint8Array(17);
this._current_buttons = new Uint8Array(17);
}
GamepadInput.title = "Gamepad";
GamepadInput.desc = "gets the input of the gamepad";
GamepadInput.CENTER = 0;
GamepadInput.LEFT = 1;
GamepadInput.RIGHT = 2;
GamepadInput.UP = 4;
GamepadInput.DOWN = 8;
GamepadInput.zero = new Float32Array(2);
GamepadInput.buttons = [
"a",
"b",
"x",
"y",
"lb",
"rb",
"lt",
"rt",
"back",
"start",
"ls",
"rs",
"home"
];
GamepadInput.prototype.onExecute = function() {
//get gamepad
var gamepad = this.getGamepad();
var threshold = this.properties.threshold || 0.0;
if (gamepad) {
this._left_axis[0] =
Math.abs(gamepad.xbox.axes["lx"]) > threshold
? gamepad.xbox.axes["lx"]
: 0;
this._left_axis[1] =
Math.abs(gamepad.xbox.axes["ly"]) > threshold
? gamepad.xbox.axes["ly"]
: 0;
this._right_axis[0] =
Math.abs(gamepad.xbox.axes["rx"]) > threshold
? gamepad.xbox.axes["rx"]
: 0;
this._right_axis[1] =
Math.abs(gamepad.xbox.axes["ry"]) > threshold
? gamepad.xbox.axes["ry"]
: 0;
this._triggers[0] =
Math.abs(gamepad.xbox.axes["ltrigger"]) > threshold
? gamepad.xbox.axes["ltrigger"]
: 0;
this._triggers[1] =
Math.abs(gamepad.xbox.axes["rtrigger"]) > threshold
? gamepad.xbox.axes["rtrigger"]
: 0;
}
if (this.outputs) {
for (var i = 0; i < this.outputs.length; i++) {
var output = this.outputs[i];
if (!output.links || !output.links.length) {
continue;
}
var v = null;
if (gamepad) {
switch (output.name) {
case "left_axis":
v = this._left_axis;
break;
case "right_axis":
v = this._right_axis;
break;
case "left_x_axis":
v = this._left_axis[0];
break;
case "left_y_axis":
v = this._left_axis[1];
break;
case "right_x_axis":
v = this._right_axis[0];
break;
case "right_y_axis":
v = this._right_axis[1];
break;
case "trigger_left":
v = this._triggers[0];
break;
case "trigger_right":
v = this._triggers[1];
break;
case "a_button":
v = gamepad.xbox.buttons["a"] ? 1 : 0;
break;
case "b_button":
v = gamepad.xbox.buttons["b"] ? 1 : 0;
break;
case "x_button":
v = gamepad.xbox.buttons["x"] ? 1 : 0;
break;
case "y_button":
v = gamepad.xbox.buttons["y"] ? 1 : 0;
break;
case "lb_button":
v = gamepad.xbox.buttons["lb"] ? 1 : 0;
break;
case "rb_button":
v = gamepad.xbox.buttons["rb"] ? 1 : 0;
break;
case "ls_button":
v = gamepad.xbox.buttons["ls"] ? 1 : 0;
break;
case "rs_button":
v = gamepad.xbox.buttons["rs"] ? 1 : 0;
break;
case "hat_left":
v = gamepad.xbox.hatmap & GamepadInput.LEFT;
break;
case "hat_right":
v = gamepad.xbox.hatmap & GamepadInput.RIGHT;
break;
case "hat_up":
v = gamepad.xbox.hatmap & GamepadInput.UP;
break;
case "hat_down":
v = gamepad.xbox.hatmap & GamepadInput.DOWN;
break;
case "hat":
v = gamepad.xbox.hatmap;
break;
case "start_button":
v = gamepad.xbox.buttons["start"] ? 1 : 0;
break;
case "back_button":
v = gamepad.xbox.buttons["back"] ? 1 : 0;
break;
case "button_pressed":
for (
var j = 0;
j < this._current_buttons.length;
++j
) {
if (
this._current_buttons[j] &&
!this._previous_buttons[j]
) {
this.triggerSlot(
i,
GamepadInput.buttons[j]
);
}
}
break;
default:
break;
}
} else {
//if no gamepad is connected, output 0
switch (output.name) {
case "button_pressed":
break;
case "left_axis":
case "right_axis":
v = GamepadInput.zero;
break;
default:
v = 0;
}
}
this.setOutputData(i, v);
}
}
};
GamepadInput.mapping = {a:0,b:1,x:2,y:3,lb:4,rb:5,lt:6,rt:7,back:8,start:9,ls:10,rs:11 };
GamepadInput.mapping_array = ["a","b","x","y","lb","rb","lt","rt","back","start","ls","rs"];
GamepadInput.prototype.getGamepad = function() {
var getGamepads =
navigator.getGamepads ||
navigator.webkitGetGamepads ||
navigator.mozGetGamepads;
if (!getGamepads) {
return null;
}
var gamepads = getGamepads.call(navigator);
var gamepad = null;
this._previous_buttons.set(this._current_buttons);
//pick the first connected
for (var i = this.properties.gamepad_index; i < 4; i++) {
if (!gamepads[i]) {
continue;
}
gamepad = gamepads[i];
//xbox controller mapping
var xbox = this.xbox_mapping;
if (!xbox) {
xbox = this.xbox_mapping = {
axes: [],
buttons: {},
hat: "",
hatmap: GamepadInput.CENTER
};
}
xbox.axes["lx"] = gamepad.axes[0];
xbox.axes["ly"] = gamepad.axes[1];
xbox.axes["rx"] = gamepad.axes[2];
xbox.axes["ry"] = gamepad.axes[3];
xbox.axes["ltrigger"] = gamepad.buttons[6].value;
xbox.axes["rtrigger"] = gamepad.buttons[7].value;
xbox.hat = "";
xbox.hatmap = GamepadInput.CENTER;
for (var j = 0; j < gamepad.buttons.length; j++) {
this._current_buttons[j] = gamepad.buttons[j].pressed;
if(j < 12)
{
xbox.buttons[ GamepadInput.mapping_array[j] ] = gamepad.buttons[j].pressed;
if(gamepad.buttons[j].was_pressed)
this.trigger( GamepadInput.mapping_array[j] + "_button_event" );
}
else //mapping of XBOX
switch ( j ) //I use a switch to ensure that a player with another gamepad could play
{
case 12:
if (gamepad.buttons[j].pressed) {
xbox.hat += "up";
xbox.hatmap |= GamepadInput.UP;
}
break;
case 13:
if (gamepad.buttons[j].pressed) {
xbox.hat += "down";
xbox.hatmap |= GamepadInput.DOWN;
}
break;
case 14:
if (gamepad.buttons[j].pressed) {
xbox.hat += "left";
xbox.hatmap |= GamepadInput.LEFT;
}
break;
case 15:
if (gamepad.buttons[j].pressed) {
xbox.hat += "right";
xbox.hatmap |= GamepadInput.RIGHT;
}
break;
case 16:
xbox.buttons["home"] = gamepad.buttons[j].pressed;
break;
default:
}
}
gamepad.xbox = xbox;
return gamepad;
}
};
GamepadInput.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed) {
return;
}
//render gamepad state?
var la = this._left_axis;
var ra = this._right_axis;
ctx.strokeStyle = "#88A";
ctx.strokeRect(
(la[0] + 1) * 0.5 * this.size[0] - 4,
(la[1] + 1) * 0.5 * this.size[1] - 4,
8,
8
);
ctx.strokeStyle = "#8A8";
ctx.strokeRect(
(ra[0] + 1) * 0.5 * this.size[0] - 4,
(ra[1] + 1) * 0.5 * this.size[1] - 4,
8,
8
);
var h = this.size[1] / this._current_buttons.length;
ctx.fillStyle = "#AEB";
for (var i = 0; i < this._current_buttons.length; ++i) {
if (this._current_buttons[i]) {
ctx.fillRect(0, h * i, 6, h);
}
}
};
GamepadInput.prototype.onGetOutputs = function() {
return [
["left_axis", "vec2"],
["right_axis", "vec2"],
["left_x_axis", "number"],
["left_y_axis", "number"],
["right_x_axis", "number"],
["right_y_axis", "number"],
["trigger_left", "number"],
["trigger_right", "number"],
["a_button", "number"],
["b_button", "number"],
["x_button", "number"],
["y_button", "number"],
["lb_button", "number"],
["rb_button", "number"],
["ls_button", "number"],
["rs_button", "number"],
["start_button", "number"],
["back_button", "number"],
["a_button_event", LiteGraph.EVENT ],
["b_button_event", LiteGraph.EVENT ],
["x_button_event", LiteGraph.EVENT ],
["y_button_event", LiteGraph.EVENT ],
["lb_button_event", LiteGraph.EVENT ],
["rb_button_event", LiteGraph.EVENT ],
["ls_button_event", LiteGraph.EVENT ],
["rs_button_event", LiteGraph.EVENT ],
["start_button_event", LiteGraph.EVENT ],
["back_button_event", LiteGraph.EVENT ],
["hat_left", "number"],
["hat_right", "number"],
["hat_up", "number"],
["hat_down", "number"],
["hat", "number"],
["button_pressed", LiteGraph.EVENT]
];
};
LiteGraph.registerNodeType("input/gamepad", GamepadInput);
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
//Converter
function Converter() {
this.addInput("in", 0);
this.addOutput("out", 0);
this.size = [80, 30];
}
Converter.title = "Converter";
Converter.desc = "type A to type B";
Converter.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
if (this.outputs) {
for (var i = 0; i < this.outputs.length; i++) {
var output = this.outputs[i];
if (!output.links || !output.links.length) {
continue;
}
var result = null;
switch (output.name) {
case "number":
result = v.length ? v[0] : parseFloat(v);
break;
case "vec2":
case "vec3":
case "vec4":
var result = null;
var count = 1;
switch (output.name) {
case "vec2":
count = 2;
break;
case "vec3":
count = 3;
break;
case "vec4":
count = 4;
break;
}
var result = new Float32Array(count);
if (v.length) {
for (
var j = 0;
j < v.length && j < result.length;
j++
) {
result[j] = v[j];
}
} else {
result[0] = parseFloat(v);
}
break;
}
this.setOutputData(i, result);
}
}
};
Converter.prototype.onGetOutputs = function() {
return [
["number", "number"],
["vec2", "vec2"],
["vec3", "vec3"],
["vec4", "vec4"]
];
};
LiteGraph.registerNodeType("math/converter", Converter);
//Bypass
function Bypass() {
this.addInput("in");
this.addOutput("out");
this.size = [80, 30];
}
Bypass.title = "Bypass";
Bypass.desc = "removes the type";
Bypass.prototype.onExecute = function() {
var v = this.getInputData(0);
this.setOutputData(0, v);
};
LiteGraph.registerNodeType("math/bypass", Bypass);
function ToNumber() {
this.addInput("in");
this.addOutput("out");
}
ToNumber.title = "to Number";
ToNumber.desc = "Cast to number";
ToNumber.prototype.onExecute = function() {
var v = this.getInputData(0);
this.setOutputData(0, Number(v));
};
LiteGraph.registerNodeType("math/to_number", ToNumber);
function MathRange() {
this.addInput("in", "number", { locked: true });
this.addOutput("out", "number", { locked: true });
this.addOutput("clamped", "number", { locked: true });
this.addProperty("in", 0);
this.addProperty("in_min", 0);
this.addProperty("in_max", 1);
this.addProperty("out_min", 0);
this.addProperty("out_max", 1);
this.size = [120, 50];
}
MathRange.title = "Range";
MathRange.desc = "Convert a number from one range to another";
MathRange.prototype.getTitle = function() {
if (this.flags.collapsed) {
return (this._last_v || 0).toFixed(2);
}
return this.title;
};
MathRange.prototype.onExecute = function() {
if (this.inputs) {
for (var i = 0; i < this.inputs.length; i++) {
var input = this.inputs[i];
var v = this.getInputData(i);
if (v === undefined) {
continue;
}
this.properties[input.name] = v;
}
}
var v = this.properties["in"];
if (v === undefined || v === null || v.constructor !== Number) {
v = 0;
}
var in_min = this.properties.in_min;
var in_max = this.properties.in_max;
var out_min = this.properties.out_min;
var out_max = this.properties.out_max;
/*
if( in_min > in_max )
{
in_min = in_max;
in_max = this.properties.in_min;
}
if( out_min > out_max )
{
out_min = out_max;
out_max = this.properties.out_min;
}
*/
this._last_v = ((v - in_min) / (in_max - in_min)) * (out_max - out_min) + out_min;
this.setOutputData(0, this._last_v);
this.setOutputData(1, clamp( this._last_v, out_min, out_max ));
};
MathRange.prototype.onDrawBackground = function(ctx) {
//show the current value
if (this._last_v) {
this.outputs[0].label = this._last_v.toFixed(3);
} else {
this.outputs[0].label = "?";
}
};
MathRange.prototype.onGetInputs = function() {
return [
["in_min", "number"],
["in_max", "number"],
["out_min", "number"],
["out_max", "number"]
];
};
LiteGraph.registerNodeType("math/range", MathRange);
function MathRand() {
this.addOutput("value", "number");
this.addProperty("min", 0);
this.addProperty("max", 1);
this.size = [80, 30];
}
MathRand.title = "Rand";
MathRand.desc = "Random number";
MathRand.prototype.onExecute = function() {
if (this.inputs) {
for (var i = 0; i < this.inputs.length; i++) {
var input = this.inputs[i];
var v = this.getInputData(i);
if (v === undefined) {
continue;
}
this.properties[input.name] = v;
}
}
var min = this.properties.min;
var max = this.properties.max;
this._last_v = Math.random() * (max - min) + min;
this.setOutputData(0, this._last_v);
};
MathRand.prototype.onDrawBackground = function(ctx) {
//show the current value
this.outputs[0].label = (this._last_v || 0).toFixed(3);
};
MathRand.prototype.onGetInputs = function() {
return [["min", "number"], ["max", "number"]];
};
LiteGraph.registerNodeType("math/rand", MathRand);
//basic continuous noise
function MathNoise() {
this.addInput("in", "number");
this.addOutput("out", "number");
this.addProperty("min", 0);
this.addProperty("max", 1);
this.addProperty("smooth", true);
this.addProperty("seed", 0);
this.addProperty("octaves", 1);
this.addProperty("persistence", 0.8);
this.addProperty("speed", 1);
this.size = [90, 30];
}
MathNoise.title = "Noise";
MathNoise.desc = "Random number with temporal continuity";
MathNoise.data = null;
MathNoise.getValue = function(f, smooth) {
if (!MathNoise.data) {
MathNoise.data = new Float32Array(1024);
for (var i = 0; i < MathNoise.data.length; ++i) {
MathNoise.data[i] = Math.random();
}
}
f = f % 1024;
if (f < 0) {
f += 1024;
}
var f_min = Math.floor(f);
var f = f - f_min;
var r1 = MathNoise.data[f_min];
var r2 = MathNoise.data[f_min == 1023 ? 0 : f_min + 1];
if (smooth) {
f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
}
return r1 * (1 - f) + r2 * f;
};
MathNoise.prototype.onExecute = function() {
var f = this.getInputData(0) || 0;
var iterations = this.properties.octaves || 1;
var r = 0;
var amp = 1;
var seed = this.properties.seed || 0;
f += seed;
var speed = this.properties.speed || 1;
var total_amp = 0;
for(var i = 0; i < iterations; ++i)
{
r += MathNoise.getValue(f * (1+i) * speed, this.properties.smooth) * amp;
total_amp += amp;
amp *= this.properties.persistence;
if(amp < 0.001)
break;
}
r /= total_amp;
var min = this.properties.min;
var max = this.properties.max;
this._last_v = r * (max - min) + min;
this.setOutputData(0, this._last_v);
};
MathNoise.prototype.onDrawBackground = function(ctx) {
//show the current value
this.outputs[0].label = (this._last_v || 0).toFixed(3);
};
LiteGraph.registerNodeType("math/noise", MathNoise);
//generates spikes every random time
function MathSpikes() {
this.addOutput("out", "number");
this.addProperty("min_time", 1);
this.addProperty("max_time", 2);
this.addProperty("duration", 0.2);
this.size = [90, 30];
this._remaining_time = 0;
this._blink_time = 0;
}
MathSpikes.title = "Spikes";
MathSpikes.desc = "spike every random time";
MathSpikes.prototype.onExecute = function() {
var dt = this.graph.elapsed_time; //in secs
this._remaining_time -= dt;
this._blink_time -= dt;
var v = 0;
if (this._blink_time > 0) {
var f = this._blink_time / this.properties.duration;
v = 1 / (Math.pow(f * 8 - 4, 4) + 1);
}
if (this._remaining_time < 0) {
this._remaining_time =
Math.random() *
(this.properties.max_time - this.properties.min_time) +
this.properties.min_time;
this._blink_time = this.properties.duration;
this.boxcolor = "#FFF";
} else {
this.boxcolor = "#000";
}
this.setOutputData(0, v);
};
LiteGraph.registerNodeType("math/spikes", MathSpikes);
//Math clamp
function MathClamp() {
this.addInput("in", "number");
this.addOutput("out", "number");
this.size = [80, 30];
this.addProperty("min", 0);
this.addProperty("max", 1);
}
MathClamp.title = "Clamp";
MathClamp.desc = "Clamp number between min and max";
//MathClamp.filter = "shader";
MathClamp.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
v = Math.max(this.properties.min, v);
v = Math.min(this.properties.max, v);
this.setOutputData(0, v);
};
MathClamp.prototype.getCode = function(lang) {
var code = "";
if (this.isInputConnected(0)) {
code +=
"clamp({{0}}," +
this.properties.min +
"," +
this.properties.max +
")";
}
return code;
};
LiteGraph.registerNodeType("math/clamp", MathClamp);
//Math ABS
function MathLerp() {
this.properties = { f: 0.5 };
this.addInput("A", "number");
this.addInput("B", "number");
this.addOutput("out", "number");
}
MathLerp.title = "Lerp";
MathLerp.desc = "Linear Interpolation";
MathLerp.prototype.onExecute = function() {
var v1 = this.getInputData(0);
if (v1 == null) {
v1 = 0;
}
var v2 = this.getInputData(1);
if (v2 == null) {
v2 = 0;
}
var f = this.properties.f;
var _f = this.getInputData(2);
if (_f !== undefined) {
f = _f;
}
this.setOutputData(0, v1 * (1 - f) + v2 * f);
};
MathLerp.prototype.onGetInputs = function() {
return [["f", "number"]];
};
LiteGraph.registerNodeType("math/lerp", MathLerp);
//Math ABS
function MathAbs() {
this.addInput("in", "number");
this.addOutput("out", "number");
this.size = [80, 30];
}
MathAbs.title = "Abs";
MathAbs.desc = "Absolute";
MathAbs.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
this.setOutputData(0, Math.abs(v));
};
LiteGraph.registerNodeType("math/abs", MathAbs);
//Math Floor
function MathFloor() {
this.addInput("in", "number");
this.addOutput("out", "number");
this.size = [80, 30];
}
MathFloor.title = "Floor";
MathFloor.desc = "Floor number to remove fractional part";
MathFloor.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
this.setOutputData(0, Math.floor(v));
};
LiteGraph.registerNodeType("math/floor", MathFloor);
//Math frac
function MathFrac() {
this.addInput("in", "number");
this.addOutput("out", "number");
this.size = [80, 30];
}
MathFrac.title = "Frac";
MathFrac.desc = "Returns fractional part";
MathFrac.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
this.setOutputData(0, v % 1);
};
LiteGraph.registerNodeType("math/frac", MathFrac);
//Math Floor
function MathSmoothStep() {
this.addInput("in", "number");
this.addOutput("out", "number");
this.size = [80, 30];
this.properties = { A: 0, B: 1 };
}
MathSmoothStep.title = "Smoothstep";
MathSmoothStep.desc = "Smoothstep";
MathSmoothStep.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v === undefined) {
return;
}
var edge0 = this.properties.A;
var edge1 = this.properties.B;
// Scale, bias and saturate x to 0..1 range
v = clamp((v - edge0) / (edge1 - edge0), 0.0, 1.0);
// Evaluate polynomial
v = v * v * (3 - 2 * v);
this.setOutputData(0, v);
};
LiteGraph.registerNodeType("math/smoothstep", MathSmoothStep);
//Math scale
function MathScale() {
this.addInput("in", "number", { label: "" });
this.addOutput("out", "number", { label: "" });
this.size = [80, 30];
this.addProperty("factor", 1);
}
MathScale.title = "Scale";
MathScale.desc = "v * factor";
MathScale.prototype.onExecute = function() {
var value = this.getInputData(0);
if (value != null) {
this.setOutputData(0, value * this.properties.factor);
}
};
LiteGraph.registerNodeType("math/scale", MathScale);
//Gate
function Gate() {
this.addInput("v","boolean");
this.addInput("A");
this.addInput("B");
this.addOutput("out");
}
Gate.title = "Gate";
Gate.desc = "if v is true, then outputs A, otherwise B";
Gate.prototype.onExecute = function() {
var v = this.getInputData(0);
this.setOutputData(0, this.getInputData( v ? 1 : 2 ));
};
LiteGraph.registerNodeType("math/gate", Gate);
//Math Average
function MathAverageFilter() {
this.addInput("in", "number");
this.addOutput("out", "number");
this.size = [80, 30];
this.addProperty("samples", 10);
this._values = new Float32Array(10);
this._current = 0;
}
MathAverageFilter.title = "Average";
MathAverageFilter.desc = "Average Filter";
MathAverageFilter.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
v = 0;
}
var num_samples = this._values.length;
this._values[this._current % num_samples] = v;
this._current += 1;
if (this._current > num_samples) {
this._current = 0;
}
var avr = 0;
for (var i = 0; i < num_samples; ++i) {
avr += this._values[i];
}
this.setOutputData(0, avr / num_samples);
};
MathAverageFilter.prototype.onPropertyChanged = function(name, value) {
if (value < 1) {
value = 1;
}
this.properties.samples = Math.round(value);
var old = this._values;
this._values = new Float32Array(this.properties.samples);
if (old.length <= this._values.length) {
this._values.set(old);
} else {
this._values.set(old.subarray(0, this._values.length));
}
};
LiteGraph.registerNodeType("math/average", MathAverageFilter);
//Math
function MathTendTo() {
this.addInput("in", "number");
this.addOutput("out", "number");
this.addProperty("factor", 0.1);
this.size = [80, 30];
this._value = null;
}
MathTendTo.title = "TendTo";
MathTendTo.desc = "moves the output value always closer to the input";
MathTendTo.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
v = 0;
}
var f = this.properties.factor;
if (this._value == null) {
this._value = v;
} else {
this._value = this._value * (1 - f) + v * f;
}
this.setOutputData(0, this._value);
};
LiteGraph.registerNodeType("math/tendTo", MathTendTo);
//Math operation
function MathOperation() {
this.addInput("A", "number,array,object");
this.addInput("B", "number");
this.addOutput("=", "number");
this.addProperty("A", 1);
this.addProperty("B", 1);
this.addProperty("OP", "+", "enum", { values: MathOperation.values });
this._func = MathOperation.funcs[this.properties.OP];
this._result = []; //only used for arrays
}
MathOperation.values = ["+", "-", "*", "/", "%", "^", "max", "min"];
MathOperation.funcs = {
"+": function(A,B) { return A + B; },
"-": function(A,B) { return A - B; },
"x": function(A,B) { return A * B; },
"X": function(A,B) { return A * B; },
"*": function(A,B) { return A * B; },
"/": function(A,B) { return A / B; },
"%": function(A,B) { return A % B; },
"^": function(A,B) { return Math.pow(A, B); },
"max": function(A,B) { return Math.max(A, B); },
"min": function(A,B) { return Math.min(A, B); }
};
MathOperation.title = "Operation";
MathOperation.desc = "Easy math operators";
MathOperation["@OP"] = {
type: "enum",
title: "operation",
values: MathOperation.values
};
MathOperation.size = [100, 60];
MathOperation.prototype.getTitle = function() {
if(this.properties.OP == "max" || this.properties.OP == "min")
return this.properties.OP + "(A,B)";
return "A " + this.properties.OP + " B";
};
MathOperation.prototype.setValue = function(v) {
if (typeof v == "string") {
v = parseFloat(v);
}
this.properties["value"] = v;
};
MathOperation.prototype.onPropertyChanged = function(name, value)
{
if (name != "OP")
return;
this._func = MathOperation.funcs[this.properties.OP];
if(!this._func)
{
console.warn("Unknown operation: " + this.properties.OP);
this._func = function(A) { return A; };
}
}
MathOperation.prototype.onExecute = function() {
var A = this.getInputData(0);
var B = this.getInputData(1);
if ( A != null ) {
if( A.constructor === Number )
this.properties["A"] = A;
} else {
A = this.properties["A"];
}
if (B != null) {
this.properties["B"] = B;
} else {
B = this.properties["B"];
}
var func = MathOperation.funcs[this.properties.OP];
var result;
if(A.constructor === Number)
{
result = 0;
result = func(A,B);
}
else if(A.constructor === Array)
{
result = this._result;
result.length = A.length;
for(var i = 0; i < A.length; ++i)
result[i] = func(A[i],B);
}
else
{
result = {};
for(var i in A)
result[i] = func(A[i],B);
}
this.setOutputData(0, result);
};
MathOperation.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed) {
return;
}
ctx.font = "40px Arial";
ctx.fillStyle = "#666";
ctx.textAlign = "center";
ctx.fillText(
this.properties.OP,
this.size[0] * 0.5,
(this.size[1] + LiteGraph.NODE_TITLE_HEIGHT) * 0.5
);
ctx.textAlign = "left";
};
LiteGraph.registerNodeType("math/operation", MathOperation);
LiteGraph.registerSearchboxExtra("math/operation", "MAX", {
properties: {OP:"max"},
title: "MAX()"
});
LiteGraph.registerSearchboxExtra("math/operation", "MIN", {
properties: {OP:"min"},
title: "MIN()"
});
//Math compare
function MathCompare() {
this.addInput("A", "number");
this.addInput("B", "number");
this.addOutput("A==B", "boolean");
this.addOutput("A!=B", "boolean");
this.addProperty("A", 0);
this.addProperty("B", 0);
}
MathCompare.title = "Compare";
MathCompare.desc = "compares between two values";
MathCompare.prototype.onExecute = function() {
var A = this.getInputData(0);
var B = this.getInputData(1);
if (A !== undefined) {
this.properties["A"] = A;
} else {
A = this.properties["A"];
}
if (B !== undefined) {
this.properties["B"] = B;
} else {
B = this.properties["B"];
}
for (var i = 0, l = this.outputs.length; i < l; ++i) {
var output = this.outputs[i];
if (!output.links || !output.links.length) {
continue;
}
var value;
switch (output.name) {
case "A==B":
value = A == B;
break;
case "A!=B":
value = A != B;
break;
case "A>B":
value = A > B;
break;
case "A<B":
value = A < B;
break;
case "A<=B":
value = A <= B;
break;
case "A>=B":
value = A >= B;
break;
}
this.setOutputData(i, value);
}
};
MathCompare.prototype.onGetOutputs = function() {
return [
["A==B", "boolean"],
["A!=B", "boolean"],
["A>B", "boolean"],
["A<B", "boolean"],
["A>=B", "boolean"],
["A<=B", "boolean"]
];
};
LiteGraph.registerNodeType("math/compare", MathCompare);
LiteGraph.registerSearchboxExtra("math/compare", "==", {
outputs: [["A==B", "boolean"]],
title: "A==B"
});
LiteGraph.registerSearchboxExtra("math/compare", "!=", {
outputs: [["A!=B", "boolean"]],
title: "A!=B"
});
LiteGraph.registerSearchboxExtra("math/compare", ">", {
outputs: [["A>B", "boolean"]],
title: "A>B"
});
LiteGraph.registerSearchboxExtra("math/compare", "<", {
outputs: [["A<B", "boolean"]],
title: "A<B"
});
LiteGraph.registerSearchboxExtra("math/compare", ">=", {
outputs: [["A>=B", "boolean"]],
title: "A>=B"
});
LiteGraph.registerSearchboxExtra("math/compare", "<=", {
outputs: [["A<=B", "boolean"]],
title: "A<=B"
});
function MathCondition() {
this.addInput("A", "number");
this.addInput("B", "number");
this.addOutput("true", "boolean");
this.addOutput("false", "boolean");
this.addProperty("A", 1);
this.addProperty("B", 1);
this.addProperty("OP", ">", "enum", { values: MathCondition.values });
this.addWidget("combo","Cond.",this.properties.OP,{ property: "OP", values: MathCondition.values } );
this.size = [80, 60];
}
MathCondition.values = [">", "<", "==", "!=", "<=", ">=", "||", "&&" ];
MathCondition["@OP"] = {
type: "enum",
title: "operation",
values: MathCondition.values
};
MathCondition.title = "Condition";
MathCondition.desc = "evaluates condition between A and B";
MathCondition.prototype.getTitle = function() {
return "A " + this.properties.OP + " B";
};
MathCondition.prototype.onExecute = function() {
var A = this.getInputData(0);
if (A === undefined) {
A = this.properties.A;
} else {
this.properties.A = A;
}
var B = this.getInputData(1);
if (B === undefined) {
B = this.properties.B;
} else {
this.properties.B = B;
}
var result = true;
switch (this.properties.OP) {
case ">":
result = A > B;
break;
case "<":
result = A < B;
break;
case "==":
result = A == B;
break;
case "!=":
result = A != B;
break;
case "<=":
result = A <= B;
break;
case ">=":
result = A >= B;
break;
case "||":
result = A || B;
break;
case "&&":
result = A && B;
break;
}
this.setOutputData(0, result);
this.setOutputData(1, !result);
};
LiteGraph.registerNodeType("math/condition", MathCondition);
function MathBranch() {
this.addInput("in", 0);
this.addInput("cond", "boolean");
this.addOutput("true", 0);
this.addOutput("false", 0);
this.size = [80, 60];
}
MathBranch.title = "Branch";
MathBranch.desc = "If condition is true, outputs IN in true, otherwise in false";
MathBranch.prototype.onExecute = function() {
var V = this.getInputData(0);
var cond = this.getInputData(1);
if(cond)
{
this.setOutputData(0, V);
this.setOutputData(1, null);
}
else
{
this.setOutputData(0, null);
this.setOutputData(1, V);
}
}
LiteGraph.registerNodeType("math/branch", MathBranch);
function MathAccumulate() {
this.addInput("inc", "number");
this.addOutput("total", "number");
this.addProperty("increment", 1);
this.addProperty("value", 0);
}
MathAccumulate.title = "Accumulate";
MathAccumulate.desc = "Increments a value every time";
MathAccumulate.prototype.onExecute = function() {
if (this.properties.value === null) {
this.properties.value = 0;
}
var inc = this.getInputData(0);
if (inc !== null) {
this.properties.value += inc;
} else {
this.properties.value += this.properties.increment;
}
this.setOutputData(0, this.properties.value);
};
LiteGraph.registerNodeType("math/accumulate", MathAccumulate);
//Math Trigonometry
function MathTrigonometry() {
this.addInput("v", "number");
this.addOutput("sin", "number");
this.addProperty("amplitude", 1);
this.addProperty("offset", 0);
this.bgImageUrl = "nodes/imgs/icon-sin.png";
}
MathTrigonometry.title = "Trigonometry";
MathTrigonometry.desc = "Sin Cos Tan";
//MathTrigonometry.filter = "shader";
MathTrigonometry.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
v = 0;
}
var amplitude = this.properties["amplitude"];
var slot = this.findInputSlot("amplitude");
if (slot != -1) {
amplitude = this.getInputData(slot);
}
var offset = this.properties["offset"];
slot = this.findInputSlot("offset");
if (slot != -1) {
offset = this.getInputData(slot);
}
for (var i = 0, l = this.outputs.length; i < l; ++i) {
var output = this.outputs[i];
var value;
switch (output.name) {
case "sin":
value = Math.sin(v);
break;
case "cos":
value = Math.cos(v);
break;
case "tan":
value = Math.tan(v);
break;
case "asin":
value = Math.asin(v);
break;
case "acos":
value = Math.acos(v);
break;
case "atan":
value = Math.atan(v);
break;
}
this.setOutputData(i, amplitude * value + offset);
}
};
MathTrigonometry.prototype.onGetInputs = function() {
return [["v", "number"], ["amplitude", "number"], ["offset", "number"]];
};
MathTrigonometry.prototype.onGetOutputs = function() {
return [
["sin", "number"],
["cos", "number"],
["tan", "number"],
["asin", "number"],
["acos", "number"],
["atan", "number"]
];
};
LiteGraph.registerNodeType("math/trigonometry", MathTrigonometry);
LiteGraph.registerSearchboxExtra("math/trigonometry", "SIN()", {
outputs: [["sin", "number"]],
title: "SIN()"
});
LiteGraph.registerSearchboxExtra("math/trigonometry", "COS()", {
outputs: [["cos", "number"]],
title: "COS()"
});
LiteGraph.registerSearchboxExtra("math/trigonometry", "TAN()", {
outputs: [["tan", "number"]],
title: "TAN()"
});
//math library for safe math operations without eval
function MathFormula() {
this.addInput("x", "number");
this.addInput("y", "number");
this.addOutput("", "number");
this.properties = { x: 1.0, y: 1.0, formula: "x+y" };
this.code_widget = this.addWidget(
"text",
"F(x,y)",
this.properties.formula,
function(v, canvas, node) {
node.properties.formula = v;
}
);
this.addWidget("toggle", "allow", LiteGraph.allow_scripts, function(v) {
LiteGraph.allow_scripts = v;
});
this._func = null;
}
MathFormula.title = "Formula";
MathFormula.desc = "Compute formula";
MathFormula.size = [160, 100];
MathAverageFilter.prototype.onPropertyChanged = function(name, value) {
if (name == "formula") {
this.code_widget.value = value;
}
};
MathFormula.prototype.onExecute = function() {
if (!LiteGraph.allow_scripts) {
return;
}
var x = this.getInputData(0);
var y = this.getInputData(1);
if (x != null) {
this.properties["x"] = x;
} else {
x = this.properties["x"];
}
if (y != null) {
this.properties["y"] = y;
} else {
y = this.properties["y"];
}
var f = this.properties["formula"];
var value;
try {
if (!this._func || this._func_code != this.properties.formula) {
this._func = new Function(
"x",
"y",
"TIME",
"return " + this.properties.formula
);
this._func_code = this.properties.formula;
}
value = this._func(x, y, this.graph.globaltime);
this.boxcolor = null;
} catch (err) {
this.boxcolor = "red";
}
this.setOutputData(0, value);
};
MathFormula.prototype.getTitle = function() {
return this._func_code || "Formula";
};
MathFormula.prototype.onDrawBackground = function() {
var f = this.properties["formula"];
if (this.outputs && this.outputs.length) {
this.outputs[0].label = f;
}
};
LiteGraph.registerNodeType("math/formula", MathFormula);
function Math3DVec2ToXY() {
this.addInput("vec2", "vec2");
this.addOutput("x", "number");
this.addOutput("y", "number");
}
Math3DVec2ToXY.title = "Vec2->XY";
Math3DVec2ToXY.desc = "vector 2 to components";
Math3DVec2ToXY.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
this.setOutputData(0, v[0]);
this.setOutputData(1, v[1]);
};
LiteGraph.registerNodeType("math3d/vec2-to-xy", Math3DVec2ToXY);
function Math3DXYToVec2() {
this.addInputs([["x", "number"], ["y", "number"]]);
this.addOutput("vec2", "vec2");
this.properties = { x: 0, y: 0 };
this._data = new Float32Array(2);
}
Math3DXYToVec2.title = "XY->Vec2";
Math3DXYToVec2.desc = "components to vector2";
Math3DXYToVec2.prototype.onExecute = function() {
var x = this.getInputData(0);
if (x == null) {
x = this.properties.x;
}
var y = this.getInputData(1);
if (y == null) {
y = this.properties.y;
}
var data = this._data;
data[0] = x;
data[1] = y;
this.setOutputData(0, data);
};
LiteGraph.registerNodeType("math3d/xy-to-vec2", Math3DXYToVec2);
function Math3DVec3ToXYZ() {
this.addInput("vec3", "vec3");
this.addOutput("x", "number");
this.addOutput("y", "number");
this.addOutput("z", "number");
}
Math3DVec3ToXYZ.title = "Vec3->XYZ";
Math3DVec3ToXYZ.desc = "vector 3 to components";
Math3DVec3ToXYZ.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
this.setOutputData(0, v[0]);
this.setOutputData(1, v[1]);
this.setOutputData(2, v[2]);
};
LiteGraph.registerNodeType("math3d/vec3-to-xyz", Math3DVec3ToXYZ);
function Math3DXYZToVec3() {
this.addInputs([["x", "number"], ["y", "number"], ["z", "number"]]);
this.addOutput("vec3", "vec3");
this.properties = { x: 0, y: 0, z: 0 };
this._data = new Float32Array(3);
}
Math3DXYZToVec3.title = "XYZ->Vec3";
Math3DXYZToVec3.desc = "components to vector3";
Math3DXYZToVec3.prototype.onExecute = function() {
var x = this.getInputData(0);
if (x == null) {
x = this.properties.x;
}
var y = this.getInputData(1);
if (y == null) {
y = this.properties.y;
}
var z = this.getInputData(2);
if (z == null) {
z = this.properties.z;
}
var data = this._data;
data[0] = x;
data[1] = y;
data[2] = z;
this.setOutputData(0, data);
};
LiteGraph.registerNodeType("math3d/xyz-to-vec3", Math3DXYZToVec3);
function Math3DVec4ToXYZW() {
this.addInput("vec4", "vec4");
this.addOutput("x", "number");
this.addOutput("y", "number");
this.addOutput("z", "number");
this.addOutput("w", "number");
}
Math3DVec4ToXYZW.title = "Vec4->XYZW";
Math3DVec4ToXYZW.desc = "vector 4 to components";
Math3DVec4ToXYZW.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
this.setOutputData(0, v[0]);
this.setOutputData(1, v[1]);
this.setOutputData(2, v[2]);
this.setOutputData(3, v[3]);
};
LiteGraph.registerNodeType("math3d/vec4-to-xyzw", Math3DVec4ToXYZW);
function Math3DXYZWToVec4() {
this.addInputs([
["x", "number"],
["y", "number"],
["z", "number"],
["w", "number"]
]);
this.addOutput("vec4", "vec4");
this.properties = { x: 0, y: 0, z: 0, w: 0 };
this._data = new Float32Array(4);
}
Math3DXYZWToVec4.title = "XYZW->Vec4";
Math3DXYZWToVec4.desc = "components to vector4";
Math3DXYZWToVec4.prototype.onExecute = function() {
var x = this.getInputData(0);
if (x == null) {
x = this.properties.x;
}
var y = this.getInputData(1);
if (y == null) {
y = this.properties.y;
}
var z = this.getInputData(2);
if (z == null) {
z = this.properties.z;
}
var w = this.getInputData(3);
if (w == null) {
w = this.properties.w;
}
var data = this._data;
data[0] = x;
data[1] = y;
data[2] = z;
data[3] = w;
this.setOutputData(0, data);
};
LiteGraph.registerNodeType("math3d/xyzw-to-vec4", Math3DXYZWToVec4);
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
function Math3DMat4()
{
this.addInput("T", "vec3");
this.addInput("R", "vec3");
this.addInput("S", "vec3");
this.addOutput("mat4", "mat4");
this.properties = {
"T":[0,0,0],
"R":[0,0,0],
"S":[1,1,1],
R_in_degrees: true
};
this._result = mat4.create();
this._must_update = true;
}
Math3DMat4.title = "mat4";
Math3DMat4.temp_quat = new Float32Array([0,0,0,1]);
Math3DMat4.temp_mat4 = new Float32Array(16);
Math3DMat4.temp_vec3 = new Float32Array(3);
Math3DMat4.prototype.onPropertyChanged = function(name, value)
{
this._must_update = true;
}
Math3DMat4.prototype.onExecute = function()
{
var M = this._result;
var Q = Math3DMat4.temp_quat;
var temp_mat4 = Math3DMat4.temp_mat4;
var temp_vec3 = Math3DMat4.temp_vec3;
var T = this.getInputData(0);
var R = this.getInputData(1);
var S = this.getInputData(2);
if( this._must_update || T || R || S )
{
T = T || this.properties.T;
R = R || this.properties.R;
S = S || this.properties.S;
mat4.identity( M );
mat4.translate( M, M, T );
if(this.properties.R_in_degrees)
{
temp_vec3.set( R );
vec3.scale(temp_vec3,temp_vec3,DEG2RAD);
quat.fromEuler( Q, temp_vec3 );
}
else
quat.fromEuler( Q, R );
mat4.fromQuat( temp_mat4, Q );
mat4.multiply( M, M, temp_mat4 );
mat4.scale( M, M, S );
}
this.setOutputData(0, M);
}
LiteGraph.registerNodeType("math3d/mat4", Math3DMat4);
//Math 3D operation
function Math3DOperation() {
this.addInput("A", "number,vec3");
this.addInput("B", "number,vec3");
this.addOutput("=", "number,vec3");
this.addProperty("OP", "+", "enum", { values: Math3DOperation.values });
this._result = vec3.create();
}
Math3DOperation.values = ["+", "-", "*", "/", "%", "^", "max", "min","dot","cross"];
LiteGraph.registerSearchboxExtra("math3d/operation", "CROSS()", {
properties: {"OP":"cross"},
title: "CROSS()"
});
LiteGraph.registerSearchboxExtra("math3d/operation", "DOT()", {
properties: {"OP":"dot"},
title: "DOT()"
});
Math3DOperation.title = "Operation";
Math3DOperation.desc = "Easy math 3D operators";
Math3DOperation["@OP"] = {
type: "enum",
title: "operation",
values: Math3DOperation.values
};
Math3DOperation.size = [100, 60];
Math3DOperation.prototype.getTitle = function() {
if(this.properties.OP == "max" || this.properties.OP == "min" )
return this.properties.OP + "(A,B)";
return "A " + this.properties.OP + " B";
};
Math3DOperation.prototype.onExecute = function() {
var A = this.getInputData(0);
var B = this.getInputData(1);
if(A == null || B == null)
return;
if(A.constructor === Number)
A = [A,A,A];
if(B.constructor === Number)
B = [B,B,B];
var result = this._result;
switch (this.properties.OP) {
case "+":
result = vec3.add(result,A,B);
break;
case "-":
result = vec3.sub(result,A,B);
break;
case "x":
case "X":
case "*":
result = vec3.mul(result,A,B);
break;
case "/":
result = vec3.div(result,A,B);
break;
case "%":
result[0] = A[0]%B[0];
result[1] = A[1]%B[1];
result[2] = A[2]%B[2];
break;
case "^":
result[0] = Math.pow(A[0],B[0]);
result[1] = Math.pow(A[1],B[1]);
result[2] = Math.pow(A[2],B[2]);
break;
case "max":
result[0] = Math.max(A[0],B[0]);
result[1] = Math.max(A[1],B[1]);
result[2] = Math.max(A[2],B[2]);
break;
case "min":
result[0] = Math.min(A[0],B[0]);
result[1] = Math.min(A[1],B[1]);
result[2] = Math.min(A[2],B[2]);
case "dot":
result = vec3.dot(A,B);
break;
case "cross":
vec3.cross(result,A,B);
break;
default:
console.warn("Unknown operation: " + this.properties.OP);
}
this.setOutputData(0, result);
};
Math3DOperation.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed) {
return;
}
ctx.font = "40px Arial";
ctx.fillStyle = "#666";
ctx.textAlign = "center";
ctx.fillText(
this.properties.OP,
this.size[0] * 0.5,
(this.size[1] + LiteGraph.NODE_TITLE_HEIGHT) * 0.5
);
ctx.textAlign = "left";
};
LiteGraph.registerNodeType("math3d/operation", Math3DOperation);
function Math3DVec3Scale() {
this.addInput("in", "vec3");
this.addInput("f", "number");
this.addOutput("out", "vec3");
this.properties = { f: 1 };
this._data = new Float32Array(3);
}
Math3DVec3Scale.title = "vec3_scale";
Math3DVec3Scale.desc = "scales the components of a vec3";
Math3DVec3Scale.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
var f = this.getInputData(1);
if (f == null) {
f = this.properties.f;
}
var data = this._data;
data[0] = v[0] * f;
data[1] = v[1] * f;
data[2] = v[2] * f;
this.setOutputData(0, data);
};
LiteGraph.registerNodeType("math3d/vec3-scale", Math3DVec3Scale);
function Math3DVec3Length() {
this.addInput("in", "vec3");
this.addOutput("out", "number");
}
Math3DVec3Length.title = "vec3_length";
Math3DVec3Length.desc = "returns the module of a vector";
Math3DVec3Length.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
var dist = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
this.setOutputData(0, dist);
};
LiteGraph.registerNodeType("math3d/vec3-length", Math3DVec3Length);
function Math3DVec3Normalize() {
this.addInput("in", "vec3");
this.addOutput("out", "vec3");
this._data = new Float32Array(3);
}
Math3DVec3Normalize.title = "vec3_normalize";
Math3DVec3Normalize.desc = "returns the vector normalized";
Math3DVec3Normalize.prototype.onExecute = function() {
var v = this.getInputData(0);
if (v == null) {
return;
}
var dist = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
var data = this._data;
data[0] = v[0] / dist;
data[1] = v[1] / dist;
data[2] = v[2] / dist;
this.setOutputData(0, data);
};
LiteGraph.registerNodeType("math3d/vec3-normalize", Math3DVec3Normalize);
function Math3DVec3Lerp() {
this.addInput("A", "vec3");
this.addInput("B", "vec3");
this.addInput("f", "vec3");
this.addOutput("out", "vec3");
this.properties = { f: 0.5 };
this._data = new Float32Array(3);
}
Math3DVec3Lerp.title = "vec3_lerp";
Math3DVec3Lerp.desc = "returns the interpolated vector";
Math3DVec3Lerp.prototype.onExecute = function() {
var A = this.getInputData(0);
if (A == null) {
return;
}
var B = this.getInputData(1);
if (B == null) {
return;
}
var f = this.getInputOrProperty("f");
var data = this._data;
data[0] = A[0] * (1 - f) + B[0] * f;
data[1] = A[1] * (1 - f) + B[1] * f;
data[2] = A[2] * (1 - f) + B[2] * f;
this.setOutputData(0, data);
};
LiteGraph.registerNodeType("math3d/vec3-lerp", Math3DVec3Lerp);
function Math3DVec3Dot() {
this.addInput("A", "vec3");
this.addInput("B", "vec3");
this.addOutput("out", "number");
}
Math3DVec3Dot.title = "vec3_dot";
Math3DVec3Dot.desc = "returns the dot product";
Math3DVec3Dot.prototype.onExecute = function() {
var A = this.getInputData(0);
if (A == null) {
return;
}
var B = this.getInputData(1);
if (B == null) {
return;
}
var dot = A[0] * B[0] + A[1] * B[1] + A[2] * B[2];
this.setOutputData(0, dot);
};
LiteGraph.registerNodeType("math3d/vec3-dot", Math3DVec3Dot);
//if glMatrix is installed...
if (global.glMatrix) {
function Math3DQuaternion() {
this.addOutput("quat", "quat");
this.properties = { x: 0, y: 0, z: 0, w: 1, normalize: false };
this._value = quat.create();
}
Math3DQuaternion.title = "Quaternion";
Math3DQuaternion.desc = "quaternion";
Math3DQuaternion.prototype.onExecute = function() {
this._value[0] = this.getInputOrProperty("x");
this._value[1] = this.getInputOrProperty("y");
this._value[2] = this.getInputOrProperty("z");
this._value[3] = this.getInputOrProperty("w");
if (this.properties.normalize) {
quat.normalize(this._value, this._value);
}
this.setOutputData(0, this._value);
};
Math3DQuaternion.prototype.onGetInputs = function() {
return [
["x", "number"],
["y", "number"],
["z", "number"],
["w", "number"]
];
};
LiteGraph.registerNodeType("math3d/quaternion", Math3DQuaternion);
function Math3DRotation() {
this.addInputs([["degrees", "number"], ["axis", "vec3"]]);
this.addOutput("quat", "quat");
this.properties = { angle: 90.0, axis: vec3.fromValues(0, 1, 0) };
this._value = quat.create();
}
Math3DRotation.title = "Rotation";
Math3DRotation.desc = "quaternion rotation";
Math3DRotation.prototype.onExecute = function() {
var angle = this.getInputData(0);
if (angle == null) {
angle = this.properties.angle;
}
var axis = this.getInputData(1);
if (axis == null) {
axis = this.properties.axis;
}
var R = quat.setAxisAngle(this._value, axis, angle * 0.0174532925);
this.setOutputData(0, R);
};
LiteGraph.registerNodeType("math3d/rotation", Math3DRotation);
function MathEulerToQuat() {
this.addInput("euler", "vec3");
this.addOutput("quat", "quat");
this.properties = { euler:[0,0,0], use_yaw_pitch_roll: false };
this._degs = vec3.create();
this._value = quat.create();
}
MathEulerToQuat.title = "Euler->Quat";
MathEulerToQuat.desc = "Converts euler angles (in degrees) to quaternion";
MathEulerToQuat.prototype.onExecute = function() {
var euler = this.getInputData(0);
if (euler == null) {
euler = this.properties.euler;
}
vec3.scale( this._degs, euler, DEG2RAD );
if(this.properties.use_yaw_pitch_roll)
this._degs = [this._degs[2],this._degs[0],this._degs[1]];
var R = quat.fromEuler(this._value, this._degs);
this.setOutputData(0, R);
};
LiteGraph.registerNodeType("math3d/euler_to_quat", MathEulerToQuat);
function MathQuatToEuler() {
this.addInput(["quat", "quat"]);
this.addOutput("euler", "vec3");
this._value = vec3.create();
}
MathQuatToEuler.title = "Euler->Quat";
MathQuatToEuler.desc = "Converts rotX,rotY,rotZ in degrees to quat";
MathQuatToEuler.prototype.onExecute = function() {
var q = this.getInputData(0);
if(!q)
return;
var R = quat.toEuler(this._value, q);
vec3.scale( this._value, this._value, DEG2RAD );
this.setOutputData(0, this._value);
};
LiteGraph.registerNodeType("math3d/quat_to_euler", MathQuatToEuler);
//Math3D rotate vec3
function Math3DRotateVec3() {
this.addInputs([["vec3", "vec3"], ["quat", "quat"]]);
this.addOutput("result", "vec3");
this.properties = { vec: [0, 0, 1] };
}
Math3DRotateVec3.title = "Rot. Vec3";
Math3DRotateVec3.desc = "rotate a point";
Math3DRotateVec3.prototype.onExecute = function() {
var vec = this.getInputData(0);
if (vec == null) {
vec = this.properties.vec;
}
var quat = this.getInputData(1);
if (quat == null) {
this.setOutputData(vec);
} else {
this.setOutputData(
0,
vec3.transformQuat(vec3.create(), vec, quat)
);
}
};
LiteGraph.registerNodeType("math3d/rotate_vec3", Math3DRotateVec3);
function Math3DMultQuat() {
this.addInputs([["A", "quat"], ["B", "quat"]]);
this.addOutput("A*B", "quat");
this._value = quat.create();
}
Math3DMultQuat.title = "Mult. Quat";
Math3DMultQuat.desc = "rotate quaternion";
Math3DMultQuat.prototype.onExecute = function() {
var A = this.getInputData(0);
if (A == null) {
return;
}
var B = this.getInputData(1);
if (B == null) {
return;
}
var R = quat.multiply(this._value, A, B);
this.setOutputData(0, R);
};
LiteGraph.registerNodeType("math3d/mult-quat", Math3DMultQuat);
function Math3DQuatSlerp() {
this.addInputs([
["A", "quat"],
["B", "quat"],
["factor", "number"]
]);
this.addOutput("slerp", "quat");
this.addProperty("factor", 0.5);
this._value = quat.create();
}
Math3DQuatSlerp.title = "Quat Slerp";
Math3DQuatSlerp.desc = "quaternion spherical interpolation";
Math3DQuatSlerp.prototype.onExecute = function() {
var A = this.getInputData(0);
if (A == null) {
return;
}
var B = this.getInputData(1);
if (B == null) {
return;
}
var factor = this.properties.factor;
if (this.getInputData(2) != null) {
factor = this.getInputData(2);
}
var R = quat.slerp(this._value, A, B, factor);
this.setOutputData(0, R);
};
LiteGraph.registerNodeType("math3d/quat-slerp", Math3DQuatSlerp);
//Math3D rotate vec3
function Math3DRemapRange() {
this.addInput("vec3", "vec3");
this.addOutput("remap", "vec3");
this.addOutput("clamped", "vec3");
this.properties = { clamp: true, range_min: [-1, -1, 0], range_max: [1, 1, 0], target_min: [-1,-1,0], target_max:[1,1,0] };
this._value = vec3.create();
this._clamped = vec3.create();
}
Math3DRemapRange.title = "Remap Range";
Math3DRemapRange.desc = "remap a 3D range";
Math3DRemapRange.prototype.onExecute = function() {
var vec = this.getInputData(0);
if(vec)
this._value.set(vec);
var range_min = this.properties.range_min;
var range_max = this.properties.range_max;
var target_min = this.properties.target_min;
var target_max = this.properties.target_max;
//swap to avoid errors
/*
if(range_min > range_max)
{
range_min = range_max;
range_max = this.properties.range_min;
}
if(target_min > target_max)
{
target_min = target_max;
target_max = this.properties.target_min;
}
*/
for(var i = 0; i < 3; ++i)
{
var r = range_max[i] - range_min[i];
this._clamped[i] = clamp( this._value[i], range_min[i], range_max[i] );
if(r == 0)
{
this._value[i] = (target_min[i] + target_max[i]) * 0.5;
continue;
}
var n = (this._value[i] - range_min[i]) / r;
if(this.properties.clamp)
n = clamp(n,0,1);
var t = target_max[i] - target_min[i];
this._value[i] = target_min[i] + n * t;
}
this.setOutputData(0,this._value);
this.setOutputData(1,this._clamped);
};
LiteGraph.registerNodeType("math3d/remap_range", Math3DRemapRange);
} //glMatrix
else if (LiteGraph.debug)
console.warn("No glmatrix found, some Math3D nodes may not work");
})(this);
//basic nodes
(function(global) {
var LiteGraph = global.LiteGraph;
function toString(a) {
if(a && a.constructor === Object)
{
try
{
return JSON.stringify(a);
}
catch (err)
{
return String(a);
}
}
return String(a);
}
LiteGraph.wrapFunctionAsNode("string/toString", toString, [""], "string");
function compare(a, b) {
return a == b;
}
LiteGraph.wrapFunctionAsNode(
"string/compare",
compare,
["string", "string"],
"boolean"
);
function concatenate(a, b) {
if (a === undefined) {
return b;
}
if (b === undefined) {
return a;
}
return a + b;
}
LiteGraph.wrapFunctionAsNode(
"string/concatenate",
concatenate,
["string", "string"],
"string"
);
function contains(a, b) {
if (a === undefined || b === undefined) {
return false;
}
return a.indexOf(b) != -1;
}
LiteGraph.wrapFunctionAsNode(
"string/contains",
contains,
["string", "string"],
"boolean"
);
function toUpperCase(a) {
if (a != null && a.constructor === String) {
return a.toUpperCase();
}
return a;
}
LiteGraph.wrapFunctionAsNode(
"string/toUpperCase",
toUpperCase,
["string"],
"string"
);
function split(str, separator) {
if(separator == null)
separator = this.properties.separator;
if (str == null )
return [];
if( str.constructor === String )
return str.split(separator || " ");
else if( str.constructor === Array )
{
var r = [];
for(var i = 0; i < str.length; ++i){
if (typeof str[i] == "string")
r[i] = str[i].split(separator || " ");
}
return r;
}
return null;
}
LiteGraph.wrapFunctionAsNode(
"string/split",
split,
["string,array", "string"],
"array",
{ separator: "," }
);
function toFixed(a) {
if (a != null && a.constructor === Number) {
return a.toFixed(this.properties.precision);
}
return a;
}
LiteGraph.wrapFunctionAsNode(
"string/toFixed",
toFixed,
["number"],
"string",
{ precision: 0 }
);
function StringToTable() {
this.addInput("", "string");
this.addOutput("table", "table");
this.addOutput("rows", "number");
this.addProperty("value", "");
this.addProperty("separator", ",");
this._table = null;
}
StringToTable.title = "toTable";
StringToTable.desc = "Splits a string to table";
StringToTable.prototype.onExecute = function() {
var input = this.getInputData(0);
if(!input)
return;
var separator = this.properties.separator || ",";
if(input != this._str || separator != this._last_separator )
{
this._last_separator = separator;
this._str = input;
this._table = input.split("\n").map(function(a){ return a.trim().split(separator)});
}
this.setOutputData(0, this._table );
this.setOutputData(1, this._table ? this._table.length : 0 );
};
LiteGraph.registerNodeType("string/toTable", StringToTable);
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
function Selector() {
this.addInput("sel", "number");
this.addInput("A");
this.addInput("B");
this.addInput("C");
this.addInput("D");
this.addOutput("out");
this.selected = 0;
}
Selector.title = "Selector";
Selector.desc = "selects an output";
Selector.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed) {
return;
}
ctx.fillStyle = "#AFB";
var y = (this.selected + 1) * LiteGraph.NODE_SLOT_HEIGHT + 6;
ctx.beginPath();
ctx.moveTo(50, y);
ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT);
ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
ctx.fill();
};
Selector.prototype.onExecute = function() {
var sel = this.getInputData(0);
if (sel == null || sel.constructor !== Number)
sel = 0;
this.selected = sel = Math.round(sel) % (this.inputs.length - 1);
var v = this.getInputData(sel + 1);
if (v !== undefined) {
this.setOutputData(0, v);
}
};
Selector.prototype.onGetInputs = function() {
return [["E", 0], ["F", 0], ["G", 0], ["H", 0]];
};
LiteGraph.registerNodeType("logic/selector", Selector);
function Sequence() {
this.properties = {
sequence: "A,B,C"
};
this.addInput("index", "number");
this.addInput("seq");
this.addOutput("out");
this.index = 0;
this.values = this.properties.sequence.split(",");
}
Sequence.title = "Sequence";
Sequence.desc = "select one element from a sequence from a string";
Sequence.prototype.onPropertyChanged = function(name, value) {
if (name == "sequence") {
this.values = value.split(",");
}
};
Sequence.prototype.onExecute = function() {
var seq = this.getInputData(1);
if (seq && seq != this.current_sequence) {
this.values = seq.split(",");
this.current_sequence = seq;
}
var index = this.getInputData(0);
if (index == null) {
index = 0;
}
this.index = index = Math.round(index) % this.values.length;
this.setOutputData(0, this.values[index]);
};
LiteGraph.registerNodeType("logic/sequence", Sequence);
function logicAnd(){
this.properties = { };
this.addInput("a", "boolean");
this.addInput("b", "boolean");
this.addOutput("out", "boolean");
}
logicAnd.title = "AND";
logicAnd.desc = "Return true if all inputs are true";
logicAnd.prototype.onExecute = function() {
var ret = true;
for (var inX in this.inputs){
if (!this.getInputData(inX)){
var ret = false;
break;
}
}
this.setOutputData(0, ret);
};
logicAnd.prototype.onGetInputs = function() {
return [
["and", "boolean"]
];
};
LiteGraph.registerNodeType("logic/AND", logicAnd);
function logicOr(){
this.properties = { };
this.addInput("a", "boolean");
this.addInput("b", "boolean");
this.addOutput("out", "boolean");
}
logicOr.title = "OR";
logicOr.desc = "Return true if at least one input is true";
logicOr.prototype.onExecute = function() {
var ret = false;
for (var inX in this.inputs){
if (this.getInputData(inX)){
ret = true;
break;
}
}
this.setOutputData(0, ret);
};
logicOr.prototype.onGetInputs = function() {
return [
["or", "boolean"]
];
};
LiteGraph.registerNodeType("logic/OR", logicOr);
function logicNot(){
this.properties = { };
this.addInput("in", "boolean");
this.addOutput("out", "boolean");
}
logicNot.title = "NOT";
logicNot.desc = "Return the logical negation";
logicNot.prototype.onExecute = function() {
var ret = !this.getInputData(0);
this.setOutputData(0, ret);
};
LiteGraph.registerNodeType("logic/NOT", logicNot);
function logicCompare(){
this.properties = { };
this.addInput("a", "boolean");
this.addInput("b", "boolean");
this.addOutput("out", "boolean");
}
logicCompare.title = "bool == bool";
logicCompare.desc = "Compare for logical equality";
logicCompare.prototype.onExecute = function() {
var last = null;
var ret = true;
for (var inX in this.inputs){
if (last === null) last = this.getInputData(inX);
else
if (last != this.getInputData(inX)){
ret = false;
break;
}
}
this.setOutputData(0, ret);
};
logicCompare.prototype.onGetInputs = function() {
return [
["bool", "boolean"]
];
};
LiteGraph.registerNodeType("logic/CompareBool", logicCompare);
function logicBranch(){
this.properties = { };
this.addInput("onTrigger", LiteGraph.ACTION);
this.addInput("condition", "boolean");
this.addOutput("true", LiteGraph.EVENT);
this.addOutput("false", LiteGraph.EVENT);
this.mode = LiteGraph.ON_TRIGGER;
}
logicBranch.title = "Branch";
logicBranch.desc = "Branch execution on condition";
logicBranch.prototype.onExecute = function(param, options) {
var condtition = this.getInputData(1);
if (condtition){
this.triggerSlot(0);
}else{
this.triggerSlot(1);
}
};
LiteGraph.registerNodeType("logic/IF", logicBranch);
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
function GraphicsPlot() {
this.addInput("A", "Number");
this.addInput("B", "Number");
this.addInput("C", "Number");
this.addInput("D", "Number");
this.values = [[], [], [], []];
this.properties = { scale: 2 };
}
GraphicsPlot.title = "Plot";
GraphicsPlot.desc = "Plots data over time";
GraphicsPlot.colors = ["#FFF", "#F99", "#9F9", "#99F"];
GraphicsPlot.prototype.onExecute = function(ctx) {
if (this.flags.collapsed) {
return;
}
var size = this.size;
for (var i = 0; i < 4; ++i) {
var v = this.getInputData(i);
if (v == null) {
continue;
}
var values = this.values[i];
values.push(v);
if (values.length > size[0]) {
values.shift();
}
}
};
GraphicsPlot.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed) {
return;
}
var size = this.size;
var scale = (0.5 * size[1]) / this.properties.scale;
var colors = GraphicsPlot.colors;
var offset = size[1] * 0.5;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, size[0], size[1]);
ctx.strokeStyle = "#555";
ctx.beginPath();
ctx.moveTo(0, offset);
ctx.lineTo(size[0], offset);
ctx.stroke();
if (this.inputs) {
for (var i = 0; i < 4; ++i) {
var values = this.values[i];
if (!this.inputs[i] || !this.inputs[i].link) {
continue;
}
ctx.strokeStyle = colors[i];
ctx.beginPath();
var v = values[0] * scale * -1 + offset;
ctx.moveTo(0, clamp(v, 0, size[1]));
for (var j = 1; j < values.length && j < size[0]; ++j) {
var v = values[j] * scale * -1 + offset;
ctx.lineTo(j, clamp(v, 0, size[1]));
}
ctx.stroke();
}
}
};
LiteGraph.registerNodeType("graphics/plot", GraphicsPlot);
function GraphicsImage() {
this.addOutput("frame", "image");
this.properties = { url: "" };
}
GraphicsImage.title = "Image";
GraphicsImage.desc = "Image loader";
GraphicsImage.widgets = [{ name: "load", text: "Load", type: "button" }];
GraphicsImage.supported_extensions = ["jpg", "jpeg", "png", "gif"];
GraphicsImage.prototype.onAdded = function() {
if (this.properties["url"] != "" && this.img == null) {
this.loadImage(this.properties["url"]);
}
};
GraphicsImage.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed) {
return;
}
if (this.img && this.size[0] > 5 && this.size[1] > 5 && this.img.width) {
ctx.drawImage(this.img, 0, 0, this.size[0], this.size[1]);
}
};
GraphicsImage.prototype.onExecute = function() {
if (!this.img) {
this.boxcolor = "#000";
}
if (this.img && this.img.width) {
this.setOutputData(0, this.img);
} else {
this.setOutputData(0, null);
}
if (this.img && this.img.dirty) {
this.img.dirty = false;
}
};
GraphicsImage.prototype.onPropertyChanged = function(name, value) {
this.properties[name] = value;
if (name == "url" && value != "") {
this.loadImage(value);
}
return true;
};
GraphicsImage.prototype.loadImage = function(url, callback) {
if (url == "") {
this.img = null;
return;
}
this.img = document.createElement("img");
if (url.substr(0, 4) == "http" && LiteGraph.proxy) {
url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3);
}
this.img.src = url;
this.boxcolor = "#F95";
var that = this;
this.img.onload = function() {
if (callback) {
callback(this);
}
console.log( "Image loaded, size: " + that.img.width + "x" + that.img.height );
this.dirty = true;
that.boxcolor = "#9F9";
that.setDirtyCanvas(true);
};
this.img.onerror = function() {
console.log("error loading the image:" + url);
}
};
GraphicsImage.prototype.onWidget = function(e, widget) {
if (widget.name == "load") {
this.loadImage(this.properties["url"]);
}
};
GraphicsImage.prototype.onDropFile = function(file) {
var that = this;
if (this._url) {
URL.revokeObjectURL(this._url);
}
this._url = URL.createObjectURL(file);
this.properties.url = this._url;
this.loadImage(this._url, function(img) {
that.size[1] = (img.height / img.width) * that.size[0];
});
};
LiteGraph.registerNodeType("graphics/image", GraphicsImage);
function ColorPalette() {
this.addInput("f", "number");
this.addOutput("Color", "color");
this.properties = {
colorA: "#444444",
colorB: "#44AAFF",
colorC: "#44FFAA",
colorD: "#FFFFFF"
};
}
ColorPalette.title = "Palette";
ColorPalette.desc = "Generates a color";
ColorPalette.prototype.onExecute = function() {
var c = [];
if (this.properties.colorA != null) {
c.push(hex2num(this.properties.colorA));
}
if (this.properties.colorB != null) {
c.push(hex2num(this.properties.colorB));
}
if (this.properties.colorC != null) {
c.push(hex2num(this.properties.colorC));
}
if (this.properties.colorD != null) {
c.push(hex2num(this.properties.colorD));
}
var f = this.getInputData(0);
if (f == null) {
f = 0.5;
}
if (f > 1.0) {
f = 1.0;
} else if (f < 0.0) {
f = 0.0;
}
if (c.length == 0) {
return;
}
var result = [0, 0, 0];
if (f == 0) {
result = c[0];
} else if (f == 1) {
result = c[c.length - 1];
} else {
var pos = (c.length - 1) * f;
var c1 = c[Math.floor(pos)];
var c2 = c[Math.floor(pos) + 1];
var t = pos - Math.floor(pos);
result[0] = c1[0] * (1 - t) + c2[0] * t;
result[1] = c1[1] * (1 - t) + c2[1] * t;
result[2] = c1[2] * (1 - t) + c2[2] * t;
}
/*
c[0] = 1.0 - Math.abs( Math.sin( 0.1 * reModular.getTime() * Math.PI) );
c[1] = Math.abs( Math.sin( 0.07 * reModular.getTime() * Math.PI) );
c[2] = Math.abs( Math.sin( 0.01 * reModular.getTime() * Math.PI) );
*/
for (var i=0; i < result.length; i++) {
result[i] /= 255;
}
this.boxcolor = colorToString(result);
this.setOutputData(0, result);
};
LiteGraph.registerNodeType("color/palette", ColorPalette);
function ImageFrame() {
this.addInput("", "image,canvas");
this.size = [200, 200];
}
ImageFrame.title = "Frame";
ImageFrame.desc = "Frame viewerew";
ImageFrame.widgets = [
{ name: "resize", text: "Resize box", type: "button" },
{ name: "view", text: "View Image", type: "button" }
];
ImageFrame.prototype.onDrawBackground = function(ctx) {
if (this.frame && !this.flags.collapsed) {
ctx.drawImage(this.frame, 0, 0, this.size[0], this.size[1]);
}
};
ImageFrame.prototype.onExecute = function() {
this.frame = this.getInputData(0);
this.setDirtyCanvas(true);
};
ImageFrame.prototype.onWidget = function(e, widget) {
if (widget.name == "resize" && this.frame) {
var width = this.frame.width;
var height = this.frame.height;
if (!width && this.frame.videoWidth != null) {
width = this.frame.videoWidth;
height = this.frame.videoHeight;
}
if (width && height) {
this.size = [width, height];
}
this.setDirtyCanvas(true, true);
} else if (widget.name == "view") {
this.show();
}
};
ImageFrame.prototype.show = function() {
//var str = this.canvas.toDataURL("image/png");
if (showElement && this.frame) {
showElement(this.frame);
}
};
LiteGraph.registerNodeType("graphics/frame", ImageFrame);
function ImageFade() {
this.addInputs([
["img1", "image"],
["img2", "image"],
["fade", "number"]
]);
this.addOutput("", "image");
this.properties = { fade: 0.5, width: 512, height: 512 };
}
ImageFade.title = "Image fade";
ImageFade.desc = "Fades between images";
ImageFade.widgets = [
{ name: "resizeA", text: "Resize to A", type: "button" },
{ name: "resizeB", text: "Resize to B", type: "button" }
];
ImageFade.prototype.onAdded = function() {
this.createCanvas();
var ctx = this.canvas.getContext("2d");
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, this.properties["width"], this.properties["height"]);
};
ImageFade.prototype.createCanvas = function() {
this.canvas = document.createElement("canvas");
this.canvas.width = this.properties["width"];
this.canvas.height = this.properties["height"];
};
ImageFade.prototype.onExecute = function() {
var ctx = this.canvas.getContext("2d");
this.canvas.width = this.canvas.width;
var A = this.getInputData(0);
if (A != null) {
ctx.drawImage(A, 0, 0, this.canvas.width, this.canvas.height);
}
var fade = this.getInputData(2);
if (fade == null) {
fade = this.properties["fade"];
} else {
this.properties["fade"] = fade;
}
ctx.globalAlpha = fade;
var B = this.getInputData(1);
if (B != null) {
ctx.drawImage(B, 0, 0, this.canvas.width, this.canvas.height);
}
ctx.globalAlpha = 1.0;
this.setOutputData(0, this.canvas);
this.setDirtyCanvas(true);
};
LiteGraph.registerNodeType("graphics/imagefade", ImageFade);
function ImageCrop() {
this.addInput("", "image");
this.addOutput("", "image");
this.properties = { width: 256, height: 256, x: 0, y: 0, scale: 1.0 };
this.size = [50, 20];
}
ImageCrop.title = "Crop";
ImageCrop.desc = "Crop Image";
ImageCrop.prototype.onAdded = function() {
this.createCanvas();
};
ImageCrop.prototype.createCanvas = function() {
this.canvas = document.createElement("canvas");
this.canvas.width = this.properties["width"];
this.canvas.height = this.properties["height"];
};
ImageCrop.prototype.onExecute = function() {
var input = this.getInputData(0);
if (!input) {
return;
}
if (input.width) {
var ctx = this.canvas.getContext("2d");
ctx.drawImage(
input,
-this.properties["x"],
-this.properties["y"],
input.width * this.properties["scale"],
input.height * this.properties["scale"]
);
this.setOutputData(0, this.canvas);
} else {
this.setOutputData(0, null);
}
};
ImageCrop.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed) {
return;
}
if (this.canvas) {
ctx.drawImage(
this.canvas,
0,
0,
this.canvas.width,
this.canvas.height,
0,
0,
this.size[0],
this.size[1]
);
}
};
ImageCrop.prototype.onPropertyChanged = function(name, value) {
this.properties[name] = value;
if (name == "scale") {
this.properties[name] = parseFloat(value);
if (this.properties[name] == 0) {
console.error("Error in scale");
this.properties[name] = 1.0;
}
} else {
this.properties[name] = parseInt(value);
}
this.createCanvas();
return true;
};
LiteGraph.registerNodeType("graphics/cropImage", ImageCrop);
//CANVAS stuff
function CanvasNode() {
this.addInput("clear", LiteGraph.ACTION);
this.addOutput("", "canvas");
this.properties = { width: 512, height: 512, autoclear: true };
this.canvas = document.createElement("canvas");
this.ctx = this.canvas.getContext("2d");
}
CanvasNode.title = "Canvas";
CanvasNode.desc = "Canvas to render stuff";
CanvasNode.prototype.onExecute = function() {
var canvas = this.canvas;
var w = this.properties.width | 0;
var h = this.properties.height | 0;
if (canvas.width != w) {
canvas.width = w;
}
if (canvas.height != h) {
canvas.height = h;
}
if (this.properties.autoclear) {
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
}
this.setOutputData(0, canvas);
};
CanvasNode.prototype.onAction = function(action, param) {
if (action == "clear") {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
};
LiteGraph.registerNodeType("graphics/canvas", CanvasNode);
function DrawImageNode() {
this.addInput("canvas", "canvas");
this.addInput("img", "image,canvas");
this.addInput("x", "number");
this.addInput("y", "number");
this.properties = { x: 0, y: 0, opacity: 1 };
}
DrawImageNode.title = "DrawImage";
DrawImageNode.desc = "Draws image into a canvas";
DrawImageNode.prototype.onExecute = function() {
var canvas = this.getInputData(0);
if (!canvas) {
return;
}
var img = this.getInputOrProperty("img");
if (!img) {
return;
}
var x = this.getInputOrProperty("x");
var y = this.getInputOrProperty("y");
var ctx = canvas.getContext("2d");
ctx.drawImage(img, x, y);
};
LiteGraph.registerNodeType("graphics/drawImage", DrawImageNode);
function DrawRectangleNode() {
this.addInput("canvas", "canvas");
this.addInput("x", "number");
this.addInput("y", "number");
this.addInput("w", "number");
this.addInput("h", "number");
this.properties = {
x: 0,
y: 0,
w: 10,
h: 10,
color: "white",
opacity: 1
};
}
DrawRectangleNode.title = "DrawRectangle";
DrawRectangleNode.desc = "Draws rectangle in canvas";
DrawRectangleNode.prototype.onExecute = function() {
var canvas = this.getInputData(0);
if (!canvas) {
return;
}
var x = this.getInputOrProperty("x");
var y = this.getInputOrProperty("y");
var w = this.getInputOrProperty("w");
var h = this.getInputOrProperty("h");
var ctx = canvas.getContext("2d");
ctx.fillRect(x, y, w, h);
};
LiteGraph.registerNodeType("graphics/drawRectangle", DrawRectangleNode);
function ImageVideo() {
this.addInput("t", "number");
this.addOutputs([["frame", "image"], ["t", "number"], ["d", "number"]]);
this.properties = { url: "", use_proxy: true };
}
ImageVideo.title = "Video";
ImageVideo.desc = "Video playback";
ImageVideo.widgets = [
{ name: "play", text: "PLAY", type: "minibutton" },
{ name: "stop", text: "STOP", type: "minibutton" },
{ name: "demo", text: "Demo video", type: "button" },
{ name: "mute", text: "Mute video", type: "button" }
];
ImageVideo.prototype.onExecute = function() {
if (!this.properties.url) {
return;
}
if (this.properties.url != this._video_url) {
this.loadVideo(this.properties.url);
}
if (!this._video || this._video.width == 0) {
return;
}
var t = this.getInputData(0);
if (t && t >= 0 && t <= 1.0) {
this._video.currentTime = t * this._video.duration;
this._video.pause();
}
this._video.dirty = true;
this.setOutputData(0, this._video);
this.setOutputData(1, this._video.currentTime);
this.setOutputData(2, this._video.duration);
this.setDirtyCanvas(true);
};
ImageVideo.prototype.onStart = function() {
this.play();
};
ImageVideo.prototype.onStop = function() {
this.stop();
};
ImageVideo.prototype.loadVideo = function(url) {
this._video_url = url;
var pos = url.substr(0,10).indexOf(":");
var protocol = "";
if(pos != -1)
protocol = url.substr(0,pos);
var host = "";
if(protocol)
{
host = url.substr(0,url.indexOf("/",protocol.length + 3));
host = host.substr(protocol.length+3);
}
if (
this.properties.use_proxy &&
protocol &&
LiteGraph.proxy &&
host != location.host
) {
url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3);
}
this._video = document.createElement("video");
this._video.src = url;
this._video.type = "type=video/mp4";
this._video.muted = true;
this._video.autoplay = true;
var that = this;
this._video.addEventListener("loadedmetadata", function(e) {
//onload
console.log("Duration: " + this.duration + " seconds");
console.log("Size: " + this.videoWidth + "," + this.videoHeight);
that.setDirtyCanvas(true);
this.width = this.videoWidth;
this.height = this.videoHeight;
});
this._video.addEventListener("progress", function(e) {
//onload
console.log("video loading...");
});
this._video.addEventListener("error", function(e) {
console.error("Error loading video: " + this.src);
if (this.error) {
switch (this.error.code) {
case this.error.MEDIA_ERR_ABORTED:
console.error("You stopped the video.");
break;
case this.error.MEDIA_ERR_NETWORK:
console.error("Network error - please try again later.");
break;
case this.error.MEDIA_ERR_DECODE:
console.error("Video is broken..");
break;
case this.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
console.error("Sorry, your browser can't play this video.");
break;
}
}
});
this._video.addEventListener("ended", function(e) {
console.log("Video Ended.");
this.play(); //loop
});
//document.body.appendChild(this.video);
};
ImageVideo.prototype.onPropertyChanged = function(name, value) {
this.properties[name] = value;
if (name == "url" && value != "") {
this.loadVideo(value);
}
return true;
};
ImageVideo.prototype.play = function() {
if (this._video && this._video.videoWidth ) { //is loaded
this._video.play();
}
};
ImageVideo.prototype.playPause = function() {
if (!this._video) {
return;
}
if (this._video.paused) {
this.play();
} else {
this.pause();
}
};
ImageVideo.prototype.stop = function() {
if (!this._video) {
return;
}
this._video.pause();
this._video.currentTime = 0;
};
ImageVideo.prototype.pause = function() {
if (!this._video) {
return;
}
console.log("Video paused");
this._video.pause();
};
ImageVideo.prototype.onWidget = function(e, widget) {
/*
if(widget.name == "demo")
{
this.loadVideo();
}
else if(widget.name == "play")
{
if(this._video)
this.playPause();
}
if(widget.name == "stop")
{
this.stop();
}
else if(widget.name == "mute")
{
if(this._video)
this._video.muted = !this._video.muted;
}
*/
};
LiteGraph.registerNodeType("graphics/video", ImageVideo);
// Texture Webcam *****************************************
function ImageWebcam() {
this.addOutput("Webcam", "image");
this.properties = { filterFacingMode: false, facingMode: "user" };
this.boxcolor = "black";
this.frame = 0;
}
ImageWebcam.title = "Webcam";
ImageWebcam.desc = "Webcam image";
ImageWebcam.is_webcam_open = false;
ImageWebcam.prototype.openStream = function() {
if (!navigator.mediaDevices.getUserMedia) {
console.log('getUserMedia() is not supported in your browser, use chrome and enable WebRTC from about://flags');
return;
}
this._waiting_confirmation = true;
// Not showing vendor prefixes.
var constraints = {
audio: false,
video: !this.properties.filterFacingMode ? true : { facingMode: this.properties.facingMode }
};
navigator.mediaDevices
.getUserMedia(constraints)
.then(this.streamReady.bind(this))
.catch(onFailSoHard);
var that = this;
function onFailSoHard(e) {
console.log("Webcam rejected", e);
that._webcam_stream = false;
ImageWebcam.is_webcam_open = false;
that.boxcolor = "red";
that.trigger("stream_error");
}
};
ImageWebcam.prototype.closeStream = function() {
if (this._webcam_stream) {
var tracks = this._webcam_stream.getTracks();
if (tracks.length) {
for (var i = 0; i < tracks.length; ++i) {
tracks[i].stop();
}
}
ImageWebcam.is_webcam_open = false;
this._webcam_stream = null;
this._video = null;
this.boxcolor = "black";
this.trigger("stream_closed");
}
};
ImageWebcam.prototype.onPropertyChanged = function(name, value) {
if (name == "facingMode") {
this.properties.facingMode = value;
this.closeStream();
this.openStream();
}
};
ImageWebcam.prototype.onRemoved = function() {
this.closeStream();
};
ImageWebcam.prototype.streamReady = function(localMediaStream) {
this._webcam_stream = localMediaStream;
//this._waiting_confirmation = false;
this.boxcolor = "green";
var video = this._video;
if (!video) {
video = document.createElement("video");
video.autoplay = true;
video.srcObject = localMediaStream;
this._video = video;
//document.body.appendChild( video ); //debug
//when video info is loaded (size and so)
video.onloadedmetadata = function(e) {
// Ready to go. Do some stuff.
console.log(e);
ImageWebcam.is_webcam_open = true;
};
}
this.trigger("stream_ready", video);
};
ImageWebcam.prototype.onExecute = function() {
if (this._webcam_stream == null && !this._waiting_confirmation) {
this.openStream();
}
if (!this._video || !this._video.videoWidth) {
return;
}
this._video.frame = ++this.frame;
this._video.width = this._video.videoWidth;
this._video.height = this._video.videoHeight;
this.setOutputData(0, this._video);
for (var i = 1; i < this.outputs.length; ++i) {
if (!this.outputs[i]) {
continue;
}
switch (this.outputs[i].name) {
case "width":
this.setOutputData(i, this._video.videoWidth);
break;
case "height":
this.setOutputData(i, this._video.videoHeight);
break;
}
}
};
ImageWebcam.prototype.getExtraMenuOptions = function(graphcanvas) {
var that = this;
var txt = !that.properties.show ? "Show Frame" : "Hide Frame";
return [
{
content: txt,
callback: function() {
that.properties.show = !that.properties.show;
}
}
];
};
ImageWebcam.prototype.onDrawBackground = function(ctx) {
if (
this.flags.collapsed ||
this.size[1] <= 20 ||
!this.properties.show
) {
return;
}
if (!this._video) {
return;
}
//render to graph canvas
ctx.save();
ctx.drawImage(this._video, 0, 0, this.size[0], this.size[1]);
ctx.restore();
};
ImageWebcam.prototype.onGetOutputs = function() {
return [
["width", "number"],
["height", "number"],
["stream_ready", LiteGraph.EVENT],
["stream_closed", LiteGraph.EVENT],
["stream_error", LiteGraph.EVENT]
];
};
LiteGraph.registerNodeType("graphics/webcam", ImageWebcam);
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
var LGraphCanvas = global.LGraphCanvas;
//Works with Litegl.js to create WebGL nodes
global.LGraphTexture = null;
if (typeof GL == "undefined")
return;
LGraphCanvas.link_type_colors["Texture"] = "#987";
function LGraphTexture() {
this.addOutput("tex", "Texture");
this.addOutput("name", "string");
this.properties = { name: "", filter: true };
this.size = [
LGraphTexture.image_preview_size,
LGraphTexture.image_preview_size
];
}
global.LGraphTexture = LGraphTexture;
LGraphTexture.title = "Texture";
LGraphTexture.desc = "Texture";
LGraphTexture.widgets_info = {
name: { widget: "texture" },
filter: { widget: "checkbox" }
};
//REPLACE THIS TO INTEGRATE WITH YOUR FRAMEWORK
LGraphTexture.loadTextureCallback = null; //function in charge of loading textures when not present in the container
LGraphTexture.image_preview_size = 256;
//flags to choose output texture type
LGraphTexture.UNDEFINED = 0; //not specified
LGraphTexture.PASS_THROUGH = 1; //do not apply FX (like disable but passing the in to the out)
LGraphTexture.COPY = 2; //create new texture with the same properties as the origin texture
LGraphTexture.LOW = 3; //create new texture with low precision (byte)
LGraphTexture.HIGH = 4; //create new texture with high precision (half-float)
LGraphTexture.REUSE = 5; //reuse input texture
LGraphTexture.DEFAULT = 2; //use the default
LGraphTexture.MODE_VALUES = {
"undefined": LGraphTexture.UNDEFINED,
"pass through": LGraphTexture.PASS_THROUGH,
copy: LGraphTexture.COPY,
low: LGraphTexture.LOW,
high: LGraphTexture.HIGH,
reuse: LGraphTexture.REUSE,
default: LGraphTexture.DEFAULT
};
//returns the container where all the loaded textures are stored (overwrite if you have a Resources Manager)
LGraphTexture.getTexturesContainer = function() {
return gl.textures;
};
//process the loading of a texture (overwrite it if you have a Resources Manager)
LGraphTexture.loadTexture = function(name, options) {
options = options || {};
var url = name;
if (url.substr(0, 7) == "http://") {
if (LiteGraph.proxy) {
//proxy external files
url = LiteGraph.proxy + url.substr(7);
}
}
var container = LGraphTexture.getTexturesContainer();
var tex = (container[name] = GL.Texture.fromURL(url, options));
return tex;
};
LGraphTexture.getTexture = function(name) {
var container = this.getTexturesContainer();
if (!container) {
throw "Cannot load texture, container of textures not found";
}
var tex = container[name];
if (!tex && name && name[0] != ":") {
return this.loadTexture(name);
}
return tex;
};
//used to compute the appropiate output texture
LGraphTexture.getTargetTexture = function(origin, target, mode) {
if (!origin) {
throw "LGraphTexture.getTargetTexture expects a reference texture";
}
var tex_type = null;
switch (mode) {
case LGraphTexture.LOW:
tex_type = gl.UNSIGNED_BYTE;
break;
case LGraphTexture.HIGH:
tex_type = gl.HIGH_PRECISION_FORMAT;
break;
case LGraphTexture.REUSE:
return origin;
break;
case LGraphTexture.COPY:
default:
tex_type = origin ? origin.type : gl.UNSIGNED_BYTE;
break;
}
if (
!target ||
target.width != origin.width ||
target.height != origin.height ||
target.type != tex_type ||
target.format != origin.format
) {
target = new GL.Texture(origin.width, origin.height, {
type: tex_type,
format: origin.format,
filter: gl.LINEAR
});
}
return target;
};
LGraphTexture.getTextureType = function(precision, ref_texture) {
var type = ref_texture ? ref_texture.type : gl.UNSIGNED_BYTE;
switch (precision) {
case LGraphTexture.HIGH:
type = gl.HIGH_PRECISION_FORMAT;
break;
case LGraphTexture.LOW:
type = gl.UNSIGNED_BYTE;
break;
//no default
}
return type;
};
LGraphTexture.getWhiteTexture = function() {
if (this._white_texture) {
return this._white_texture;
}
var texture = (this._white_texture = GL.Texture.fromMemory(
1,
1,
[255, 255, 255, 255],
{ format: gl.RGBA, wrap: gl.REPEAT, filter: gl.NEAREST }
));
return texture;
};
LGraphTexture.getNoiseTexture = function() {
if (this._noise_texture) {
return this._noise_texture;
}
var noise = new Uint8Array(512 * 512 * 4);
for (var i = 0; i < 512 * 512 * 4; ++i) {
noise[i] = Math.random() * 255;
}
var texture = GL.Texture.fromMemory(512, 512, noise, {
format: gl.RGBA,
wrap: gl.REPEAT,
filter: gl.NEAREST
});
this._noise_texture = texture;
return texture;
};
LGraphTexture.prototype.onDropFile = function(data, filename, file) {
if (!data) {
this._drop_texture = null;
this.properties.name = "";
} else {
var texture = null;
if (typeof data == "string") {
texture = GL.Texture.fromURL(data);
} else if (filename.toLowerCase().indexOf(".dds") != -1) {
texture = GL.Texture.fromDDSInMemory(data);
} else {
var blob = new Blob([file]);
var url = URL.createObjectURL(blob);
texture = GL.Texture.fromURL(url);
}
this._drop_texture = texture;
this.properties.name = filename;
}
};
LGraphTexture.prototype.getExtraMenuOptions = function(graphcanvas) {
var that = this;
if (!this._drop_texture) {
return;
}
return [
{
content: "Clear",
callback: function() {
that._drop_texture = null;
that.properties.name = "";
}
}
];
};
LGraphTexture.prototype.onExecute = function() {
var tex = null;
if (this.isOutputConnected(1)) {
tex = this.getInputData(0);
}
if (!tex && this._drop_texture) {
tex = this._drop_texture;
}
if (!tex && this.properties.name) {
tex = LGraphTexture.getTexture(this.properties.name);
}
if (!tex) {
this.setOutputData( 0, null );
this.setOutputData( 1, "" );
return;
}
this._last_tex = tex;
if (this.properties.filter === false) {
tex.setParameter(gl.TEXTURE_MAG_FILTER, gl.NEAREST);
} else {
tex.setParameter(gl.TEXTURE_MAG_FILTER, gl.LINEAR);
}
this.setOutputData( 0, tex );
this.setOutputData( 1, tex.fullpath || tex.filename );
for (var i = 2; i < this.outputs.length; i++) {
var output = this.outputs[i];
if (!output) {
continue;
}
var v = null;
if (output.name == "width") {
v = tex.width;
} else if (output.name == "height") {
v = tex.height;
} else if (output.name == "aspect") {
v = tex.width / tex.height;
}
this.setOutputData(i, v);
}
};
LGraphTexture.prototype.onResourceRenamed = function(
old_name,
new_name
) {
if (this.properties.name == old_name) {
this.properties.name = new_name;
}
};
LGraphTexture.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed || this.size[1] <= 20) {
return;
}
if (this._drop_texture && ctx.webgl) {
ctx.drawImage(
this._drop_texture,
0,
0,
this.size[0],
this.size[1]
);
//this._drop_texture.renderQuad(this.pos[0],this.pos[1],this.size[0],this.size[1]);
return;
}
//Different texture? then get it from the GPU
if (this._last_preview_tex != this._last_tex) {
if (ctx.webgl) {
this._canvas = this._last_tex;
} else {
var tex_canvas = LGraphTexture.generateLowResTexturePreview(
this._last_tex
);
if (!tex_canvas) {
return;
}
this._last_preview_tex = this._last_tex;
this._canvas = cloneCanvas(tex_canvas);
}
}
if (!this._canvas) {
return;
}
//render to graph canvas
ctx.save();
if (!ctx.webgl) {
//reverse image
ctx.translate(0, this.size[1]);
ctx.scale(1, -1);
}
ctx.drawImage(this._canvas, 0, 0, this.size[0], this.size[1]);
ctx.restore();
};
//very slow, used at your own risk
LGraphTexture.generateLowResTexturePreview = function(tex) {
if (!tex) {
return null;
}
var size = LGraphTexture.image_preview_size;
var temp_tex = tex;
if (tex.format == gl.DEPTH_COMPONENT) {
return null;
} //cannot generate from depth
//Generate low-level version in the GPU to speed up
if (tex.width > size || tex.height > size) {
temp_tex = this._preview_temp_tex;
if (!this._preview_temp_tex) {
temp_tex = new GL.Texture(size, size, {
minFilter: gl.NEAREST
});
this._preview_temp_tex = temp_tex;
}
//copy
tex.copyTo(temp_tex);
tex = temp_tex;
}
//create intermediate canvas with lowquality version
var tex_canvas = this._preview_canvas;
if (!tex_canvas) {
tex_canvas = createCanvas(size, size);
this._preview_canvas = tex_canvas;
}
if (temp_tex) {
temp_tex.toCanvas(tex_canvas);
}
return tex_canvas;
};
LGraphTexture.prototype.getResources = function(res) {
if(this.properties.name)
res[this.properties.name] = GL.Texture;
return res;
};
LGraphTexture.prototype.onGetInputs = function() {
return [["in", "Texture"]];
};
LGraphTexture.prototype.onGetOutputs = function() {
return [
["width", "number"],
["height", "number"],
["aspect", "number"]
];
};
//used to replace shader code
LGraphTexture.replaceCode = function( code, context )
{
return code.replace(/\{\{[a-zA-Z0-9_]*\}\}/g, function(v){
v = v.replace( /[\{\}]/g, "" );
return context[v] || "";
});
}
LiteGraph.registerNodeType("texture/texture", LGraphTexture);
//**************************
function LGraphTexturePreview() {
this.addInput("Texture", "Texture");
this.properties = { flipY: false };
this.size = [
LGraphTexture.image_preview_size,
LGraphTexture.image_preview_size
];
}
LGraphTexturePreview.title = "Preview";
LGraphTexturePreview.desc = "Show a texture in the graph canvas";
LGraphTexturePreview.allow_preview = false;
LGraphTexturePreview.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed) {
return;
}
if (!ctx.webgl && !LGraphTexturePreview.allow_preview) {
return;
} //not working well
var tex = this.getInputData(0);
if (!tex) {
return;
}
var tex_canvas = null;
if (!tex.handle && ctx.webgl) {
tex_canvas = tex;
} else {
tex_canvas = LGraphTexture.generateLowResTexturePreview(tex);
}
//render to graph canvas
ctx.save();
if (this.properties.flipY) {
ctx.translate(0, this.size[1]);
ctx.scale(1, -1);
}
ctx.drawImage(tex_canvas, 0, 0, this.size[0], this.size[1]);
ctx.restore();
};
LiteGraph.registerNodeType("texture/preview", LGraphTexturePreview);
//**************************************
function LGraphTextureSave() {
this.addInput("Texture", "Texture");
this.addOutput("tex", "Texture");
this.addOutput("name", "string");
this.properties = { name: "", generate_mipmaps: false };
}
LGraphTextureSave.title = "Save";
LGraphTextureSave.desc = "Save a texture in the repository";
LGraphTextureSave.prototype.getPreviewTexture = function()
{
return this._texture;
}
LGraphTextureSave.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (this.properties.generate_mipmaps) {
tex.bind(0);
tex.setParameter( gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR );
gl.generateMipmap(tex.texture_type);
tex.unbind(0);
}
if (this.properties.name) {
//for cases where we want to perform something when storing it
if (LGraphTexture.storeTexture) {
LGraphTexture.storeTexture(this.properties.name, tex);
} else {
var container = LGraphTexture.getTexturesContainer();
container[this.properties.name] = tex;
}
}
this._texture = tex;
this.setOutputData(0, tex);
this.setOutputData(1, this.properties.name);
};
LiteGraph.registerNodeType("texture/save", LGraphTextureSave);
//****************************************************
function LGraphTextureOperation() {
this.addInput("Texture", "Texture");
this.addInput("TextureB", "Texture");
this.addInput("value", "number");
this.addOutput("Texture", "Texture");
this.help = "<p>pixelcode must be vec3, uvcode must be vec2, is optional</p>\
<p><strong>uv:</strong> tex. coords</p><p><strong>color:</strong> texture <strong>colorB:</strong> textureB</p><p><strong>time:</strong> scene time <strong>value:</strong> input value</p><p>For multiline you must type: result = ...</p>";
this.properties = {
value: 1,
pixelcode: "color + colorB * value",
uvcode: "",
precision: LGraphTexture.DEFAULT
};
this.has_error = false;
}
LGraphTextureOperation.widgets_info = {
uvcode: { widget: "code" },
pixelcode: { widget: "code" },
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureOperation.title = "Operation";
LGraphTextureOperation.desc = "Texture shader operation";
LGraphTextureOperation.presets = {};
LGraphTextureOperation.prototype.getExtraMenuOptions = function(
graphcanvas
) {
var that = this;
var txt = !that.properties.show ? "Show Texture" : "Hide Texture";
return [
{
content: txt,
callback: function() {
that.properties.show = !that.properties.show;
}
}
];
};
LGraphTextureOperation.prototype.onPropertyChanged = function()
{
this.has_error = false;
}
LGraphTextureOperation.prototype.onDrawBackground = function(ctx) {
if (
this.flags.collapsed ||
this.size[1] <= 20 ||
!this.properties.show
) {
return;
}
if (!this._tex) {
return;
}
//only works if using a webgl renderer
if (this._tex.gl != ctx) {
return;
}
//render to graph canvas
ctx.save();
ctx.drawImage(this._tex, 0, 0, this.size[0], this.size[1]);
ctx.restore();
};
LGraphTextureOperation.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!this.isOutputConnected(0)) {
return;
} //saves work
if (this.properties.precision === LGraphTexture.PASS_THROUGH) {
this.setOutputData(0, tex);
return;
}
var texB = this.getInputData(1);
if (!this.properties.uvcode && !this.properties.pixelcode) {
return;
}
var width = 512;
var height = 512;
if (tex) {
width = tex.width;
height = tex.height;
} else if (texB) {
width = texB.width;
height = texB.height;
}
if(!texB)
texB = GL.Texture.getWhiteTexture();
var type = LGraphTexture.getTextureType( this.properties.precision, tex );
if (!tex && !this._tex) {
this._tex = new GL.Texture(width, height, { type: type, format: gl.RGBA, filter: gl.LINEAR });
} else {
this._tex = LGraphTexture.getTargetTexture( tex || this._tex, this._tex, this.properties.precision );
}
var uvcode = "";
if (this.properties.uvcode) {
uvcode = "uv = " + this.properties.uvcode;
if (this.properties.uvcode.indexOf(";") != -1) {
//there are line breaks, means multiline code
uvcode = this.properties.uvcode;
}
}
var pixelcode = "";
if (this.properties.pixelcode) {
pixelcode = "result = " + this.properties.pixelcode;
if (this.properties.pixelcode.indexOf(";") != -1) {
//there are line breaks, means multiline code
pixelcode = this.properties.pixelcode;
}
}
var shader = this._shader;
if ( !this.has_error && (!shader || this._shader_code != uvcode + "|" + pixelcode) ) {
var final_pixel_code = LGraphTexture.replaceCode( LGraphTextureOperation.pixel_shader, { UV_CODE:uvcode, PIXEL_CODE:pixelcode });
try {
shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER, final_pixel_code );
this.boxcolor = "#00FF00";
} catch (err) {
//console.log("Error compiling shader: ", err, final_pixel_code );
GL.Shader.dumpErrorToConsole(err,Shader.SCREEN_VERTEX_SHADER, final_pixel_code);
this.boxcolor = "#FF0000";
this.has_error = true;
return;
}
this._shader = shader;
this._shader_code = uvcode + "|" + pixelcode;
}
if(!this._shader)
return;
var value = this.getInputData(2);
if (value != null) {
this.properties.value = value;
} else {
value = parseFloat(this.properties.value);
}
var time = this.graph.getTime();
this._tex.drawTo(function() {
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
if (tex) {
tex.bind(0);
}
if (texB) {
texB.bind(1);
}
var mesh = Mesh.getScreenQuad();
shader
.uniforms({
u_texture: 0,
u_textureB: 1,
value: value,
texSize: [width, height,1/width,1/height],
time: time
})
.draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphTextureOperation.pixel_shader =
"precision highp float;\n\
\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_textureB;\n\
varying vec2 v_coord;\n\
uniform vec4 texSize;\n\
uniform float time;\n\
uniform float value;\n\
\n\
void main() {\n\
vec2 uv = v_coord;\n\
{{UV_CODE}};\n\
vec4 color4 = texture2D(u_texture, uv);\n\
vec3 color = color4.rgb;\n\
vec4 color4B = texture2D(u_textureB, uv);\n\
vec3 colorB = color4B.rgb;\n\
vec3 result = color;\n\
float alpha = 1.0;\n\
{{PIXEL_CODE}};\n\
gl_FragColor = vec4(result, alpha);\n\
}\n\
";
LGraphTextureOperation.registerPreset = function ( name, code )
{
LGraphTextureOperation.presets[name] = code;
}
LGraphTextureOperation.registerPreset("","");
LGraphTextureOperation.registerPreset("bypass","color");
LGraphTextureOperation.registerPreset("add","color + colorB * value");
LGraphTextureOperation.registerPreset("substract","(color - colorB) * value");
LGraphTextureOperation.registerPreset("mate","mix( color, colorB, color4B.a * value)");
LGraphTextureOperation.registerPreset("invert","vec3(1.0) - color");
LGraphTextureOperation.registerPreset("multiply","color * colorB * value");
LGraphTextureOperation.registerPreset("divide","(color / colorB) / value");
LGraphTextureOperation.registerPreset("difference","abs(color - colorB) * value");
LGraphTextureOperation.registerPreset("max","max(color, colorB) * value");
LGraphTextureOperation.registerPreset("min","min(color, colorB) * value");
LGraphTextureOperation.registerPreset("displace","texture2D(u_texture, uv + (colorB.xy - vec2(0.5)) * value).xyz");
LGraphTextureOperation.registerPreset("grayscale","vec3(color.x + color.y + color.z) * value / 3.0");
LGraphTextureOperation.registerPreset("saturation","mix( vec3(color.x + color.y + color.z) / 3.0, color, value )");
LGraphTextureOperation.registerPreset("normalmap","\n\
float z0 = texture2D(u_texture, uv + vec2(-texSize.z, -texSize.w) ).x;\n\
float z1 = texture2D(u_texture, uv + vec2(0.0, -texSize.w) ).x;\n\
float z2 = texture2D(u_texture, uv + vec2(texSize.z, -texSize.w) ).x;\n\
float z3 = texture2D(u_texture, uv + vec2(-texSize.z, 0.0) ).x;\n\
float z4 = color.x;\n\
float z5 = texture2D(u_texture, uv + vec2(texSize.z, 0.0) ).x;\n\
float z6 = texture2D(u_texture, uv + vec2(-texSize.z, texSize.w) ).x;\n\
float z7 = texture2D(u_texture, uv + vec2(0.0, texSize.w) ).x;\n\
float z8 = texture2D(u_texture, uv + vec2(texSize.z, texSize.w) ).x;\n\
vec3 normal = vec3( z2 + 2.0*z4 + z7 - z0 - 2.0*z3 - z5, z5 + 2.0*z6 + z7 -z0 - 2.0*z1 - z2, 1.0 );\n\
normal.xy *= value;\n\
result.xyz = normalize(normal) * 0.5 + vec3(0.5);\n\
");
LGraphTextureOperation.registerPreset("threshold","vec3(color.x > colorB.x * value ? 1.0 : 0.0,color.y > colorB.y * value ? 1.0 : 0.0,color.z > colorB.z * value ? 1.0 : 0.0)");
//webglstudio stuff...
LGraphTextureOperation.prototype.onInspect = function(widgets)
{
var that = this;
widgets.addCombo("Presets","",{ values: Object.keys(LGraphTextureOperation.presets), callback: function(v){
var code = LGraphTextureOperation.presets[v];
if(!code)
return;
that.setProperty("pixelcode",code);
that.title = v;
widgets.refresh();
}});
}
LiteGraph.registerNodeType("texture/operation", LGraphTextureOperation);
//****************************************************
function LGraphTextureShader() {
this.addOutput("out", "Texture");
this.properties = {
code: "",
u_value: 1,
u_color: [1,1,1,1],
width: 512,
height: 512,
precision: LGraphTexture.DEFAULT
};
this.properties.code = LGraphTextureShader.pixel_shader;
this._uniforms = { u_value: 1, u_color: vec4.create(), in_texture: 0, texSize: vec4.create(), time: 0 };
}
LGraphTextureShader.title = "Shader";
LGraphTextureShader.desc = "Texture shader";
LGraphTextureShader.widgets_info = {
code: { type: "code", lang: "glsl" },
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureShader.prototype.onPropertyChanged = function(
name,
value
) {
if (name != "code") {
return;
}
var shader = this.getShader();
if (!shader) {
return;
}
//update connections
var uniforms = shader.uniformInfo;
//remove deprecated slots
if (this.inputs) {
var already = {};
for (var i = 0; i < this.inputs.length; ++i) {
var info = this.getInputInfo(i);
if (!info) {
continue;
}
if (uniforms[info.name] && !already[info.name]) {
already[info.name] = true;
continue;
}
this.removeInput(i);
i--;
}
}
//update existing ones
for (var i in uniforms) {
var info = shader.uniformInfo[i];
if (info.loc === null) {
continue;
} //is an attribute, not a uniform
if (i == "time") {
//default one
continue;
}
var type = "number";
if (this._shader.samplers[i]) {
type = "texture";
} else {
switch (info.size) {
case 1:
type = "number";
break;
case 2:
type = "vec2";
break;
case 3:
type = "vec3";
break;
case 4:
type = "vec4";
break;
case 9:
type = "mat3";
break;
case 16:
type = "mat4";
break;
default:
continue;
}
}
var slot = this.findInputSlot(i);
if (slot == -1) {
this.addInput(i, type);
continue;
}
var input_info = this.getInputInfo(slot);
if (!input_info) {
this.addInput(i, type);
} else {
if (input_info.type == type) {
continue;
}
this.removeInput(slot, type);
this.addInput(i, type);
}
}
};
LGraphTextureShader.prototype.getShader = function() {
//replug
if (this._shader && this._shader_code == this.properties.code) {
return this._shader;
}
this._shader_code = this.properties.code;
this._shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER, this.properties.code );
if (!this._shader) {
this.boxcolor = "red";
return null;
} else {
this.boxcolor = "green";
}
return this._shader;
};
LGraphTextureShader.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var shader = this.getShader();
if (!shader) {
return;
}
var tex_slot = 0;
var in_tex = null;
//set uniforms
if(this.inputs)
for (var i = 0; i < this.inputs.length; ++i) {
var info = this.getInputInfo(i);
var data = this.getInputData(i);
if (data == null) {
continue;
}
if (data.constructor === GL.Texture) {
data.bind(tex_slot);
if (!in_tex) {
in_tex = data;
}
data = tex_slot;
tex_slot++;
}
shader.setUniform(info.name, data); //data is tex_slot
}
var uniforms = this._uniforms;
var type = LGraphTexture.getTextureType( this.properties.precision, in_tex );
//render to texture
var w = this.properties.width | 0;
var h = this.properties.height | 0;
if (w == 0) {
w = in_tex ? in_tex.width : gl.canvas.width;
}
if (h == 0) {
h = in_tex ? in_tex.height : gl.canvas.height;
}
uniforms.texSize[0] = w;
uniforms.texSize[1] = h;
uniforms.texSize[2] = 1/w;
uniforms.texSize[3] = 1/h;
uniforms.time = this.graph.getTime();
uniforms.u_value = this.properties.u_value;
uniforms.u_color.set( this.properties.u_color );
if ( !this._tex || this._tex.type != type || this._tex.width != w || this._tex.height != h ) {
this._tex = new GL.Texture(w, h, { type: type, format: gl.RGBA, filter: gl.LINEAR });
}
var tex = this._tex;
tex.drawTo(function() {
shader.uniforms(uniforms).draw(GL.Mesh.getScreenQuad());
});
this.setOutputData(0, this._tex);
};
LGraphTextureShader.pixel_shader =
"precision highp float;\n\
\n\
varying vec2 v_coord;\n\
uniform float time; //time in seconds\n\
uniform vec4 texSize; //tex resolution\n\
uniform float u_value;\n\
uniform vec4 u_color;\n\n\
void main() {\n\
vec2 uv = v_coord;\n\
vec3 color = vec3(0.0);\n\
//your code here\n\
color.xy=uv;\n\n\
gl_FragColor = vec4(color, 1.0);\n\
}\n\
";
LiteGraph.registerNodeType("texture/shader", LGraphTextureShader);
// Texture Scale Offset
function LGraphTextureScaleOffset() {
this.addInput("in", "Texture");
this.addInput("scale", "vec2");
this.addInput("offset", "vec2");
this.addOutput("out", "Texture");
this.properties = {
offset: vec2.fromValues(0, 0),
scale: vec2.fromValues(1, 1),
precision: LGraphTexture.DEFAULT
};
}
LGraphTextureScaleOffset.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureScaleOffset.title = "Scale/Offset";
LGraphTextureScaleOffset.desc = "Applies an scaling and offseting";
LGraphTextureScaleOffset.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!this.isOutputConnected(0) || !tex) {
return;
} //saves work
if (this.properties.precision === LGraphTexture.PASS_THROUGH) {
this.setOutputData(0, tex);
return;
}
var width = tex.width;
var height = tex.height;
var type = this.precision === LGraphTexture.LOW ? gl.UNSIGNED_BYTE : gl.HIGH_PRECISION_FORMAT;
if (this.precision === LGraphTexture.DEFAULT) {
type = tex.type;
}
if (
!this._tex ||
this._tex.width != width ||
this._tex.height != height ||
this._tex.type != type
) {
this._tex = new GL.Texture(width, height, {
type: type,
format: gl.RGBA,
filter: gl.LINEAR
});
}
var shader = this._shader;
if (!shader) {
shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphTextureScaleOffset.pixel_shader
);
}
var scale = this.getInputData(1);
if (scale) {
this.properties.scale[0] = scale[0];
this.properties.scale[1] = scale[1];
} else {
scale = this.properties.scale;
}
var offset = this.getInputData(2);
if (offset) {
this.properties.offset[0] = offset[0];
this.properties.offset[1] = offset[1];
} else {
offset = this.properties.offset;
}
this._tex.drawTo(function() {
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
tex.bind(0);
var mesh = Mesh.getScreenQuad();
shader
.uniforms({
u_texture: 0,
u_scale: scale,
u_offset: offset
})
.draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphTextureScaleOffset.pixel_shader =
"precision highp float;\n\
\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_textureB;\n\
varying vec2 v_coord;\n\
uniform vec2 u_scale;\n\
uniform vec2 u_offset;\n\
\n\
void main() {\n\
vec2 uv = v_coord;\n\
uv = uv / u_scale - u_offset;\n\
gl_FragColor = texture2D(u_texture, uv);\n\
}\n\
";
LiteGraph.registerNodeType(
"texture/scaleOffset",
LGraphTextureScaleOffset
);
// Warp (distort a texture) *************************
function LGraphTextureWarp() {
this.addInput("in", "Texture");
this.addInput("warp", "Texture");
this.addInput("factor", "number");
this.addOutput("out", "Texture");
this.properties = {
factor: 0.01,
scale: [1,1],
offset: [0,0],
precision: LGraphTexture.DEFAULT
};
this._uniforms = {
u_texture: 0,
u_textureB: 1,
u_factor: 1,
u_scale: vec2.create(),
u_offset: vec2.create()
};
}
LGraphTextureWarp.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureWarp.title = "Warp";
LGraphTextureWarp.desc = "Texture warp operation";
LGraphTextureWarp.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!this.isOutputConnected(0)) {
return;
} //saves work
if (this.properties.precision === LGraphTexture.PASS_THROUGH) {
this.setOutputData(0, tex);
return;
}
var texB = this.getInputData(1);
var width = 512;
var height = 512;
var type = gl.UNSIGNED_BYTE;
if (tex) {
width = tex.width;
height = tex.height;
type = tex.type;
} else if (texB) {
width = texB.width;
height = texB.height;
type = texB.type;
}
if (!tex && !this._tex) {
this._tex = new GL.Texture(width, height, {
type:
this.precision === LGraphTexture.LOW
? gl.UNSIGNED_BYTE
: gl.HIGH_PRECISION_FORMAT,
format: gl.RGBA,
filter: gl.LINEAR
});
} else {
this._tex = LGraphTexture.getTargetTexture(
tex || this._tex,
this._tex,
this.properties.precision
);
}
var shader = this._shader;
if (!shader) {
shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphTextureWarp.pixel_shader
);
}
var factor = this.getInputData(2);
if (factor != null) {
this.properties.factor = factor;
} else {
factor = parseFloat(this.properties.factor);
}
var uniforms = this._uniforms;
uniforms.u_factor = factor;
uniforms.u_scale.set( this.properties.scale );
uniforms.u_offset.set( this.properties.offset );
this._tex.drawTo(function() {
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
if (tex) {
tex.bind(0);
}
if (texB) {
texB.bind(1);
}
var mesh = Mesh.getScreenQuad();
shader
.uniforms( uniforms )
.draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphTextureWarp.pixel_shader =
"precision highp float;\n\
\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_textureB;\n\
varying vec2 v_coord;\n\
uniform float u_factor;\n\
uniform vec2 u_scale;\n\
uniform vec2 u_offset;\n\
\n\
void main() {\n\
vec2 uv = v_coord;\n\
uv += ( texture2D(u_textureB, uv).rg - vec2(0.5)) * u_factor * u_scale + u_offset;\n\
gl_FragColor = texture2D(u_texture, uv);\n\
}\n\
";
LiteGraph.registerNodeType("texture/warp", LGraphTextureWarp);
//****************************************************
// Texture to Viewport *****************************************
function LGraphTextureToViewport() {
this.addInput("Texture", "Texture");
this.properties = {
additive: false,
antialiasing: false,
filter: true,
disable_alpha: false,
gamma: 1.0,
viewport: [0,0,1,1]
};
this.size[0] = 130;
}
LGraphTextureToViewport.title = "to Viewport";
LGraphTextureToViewport.desc = "Texture to viewport";
LGraphTextureToViewport._prev_viewport = new Float32Array(4);
LGraphTextureToViewport.prototype.onDrawBackground = function( ctx )
{
if ( this.flags.collapsed || this.size[1] <= 40 )
return;
var tex = this.getInputData(0);
if (!tex) {
return;
}
ctx.drawImage( ctx == gl ? tex : gl.canvas, 10,30, this.size[0] -20, this.size[1] -40);
}
LGraphTextureToViewport.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (this.properties.disable_alpha) {
gl.disable(gl.BLEND);
} else {
gl.enable(gl.BLEND);
if (this.properties.additive) {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
} else {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
}
}
gl.disable(gl.DEPTH_TEST);
var gamma = this.properties.gamma || 1.0;
if (this.isInputConnected(1)) {
gamma = this.getInputData(1);
}
tex.setParameter(
gl.TEXTURE_MAG_FILTER,
this.properties.filter ? gl.LINEAR : gl.NEAREST
);
var old_viewport = LGraphTextureToViewport._prev_viewport;
old_viewport.set( gl.viewport_data );
var new_view = this.properties.viewport;
gl.viewport( old_viewport[0] + old_viewport[2] * new_view[0], old_viewport[1] + old_viewport[3] * new_view[1], old_viewport[2] * new_view[2], old_viewport[3] * new_view[3] );
var viewport = gl.getViewport(); //gl.getParameter(gl.VIEWPORT);
if (this.properties.antialiasing) {
if (!LGraphTextureToViewport._shader) {
LGraphTextureToViewport._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphTextureToViewport.aa_pixel_shader
);
}
var mesh = Mesh.getScreenQuad();
tex.bind(0);
LGraphTextureToViewport._shader
.uniforms({
u_texture: 0,
uViewportSize: [tex.width, tex.height],
u_igamma: 1 / gamma,
inverseVP: [1 / tex.width, 1 / tex.height]
})
.draw(mesh);
} else {
if (gamma != 1.0) {
if (!LGraphTextureToViewport._gamma_shader) {
LGraphTextureToViewport._gamma_shader = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureToViewport.gamma_pixel_shader
);
}
tex.toViewport(LGraphTextureToViewport._gamma_shader, {
u_texture: 0,
u_igamma: 1 / gamma
});
} else {
tex.toViewport();
}
}
gl.viewport( old_viewport[0], old_viewport[1], old_viewport[2], old_viewport[3] );
};
LGraphTextureToViewport.prototype.onGetInputs = function() {
return [["gamma", "number"]];
};
LGraphTextureToViewport.aa_pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 uViewportSize;\n\
uniform vec2 inverseVP;\n\
uniform float u_igamma;\n\
#define FXAA_REDUCE_MIN (1.0/ 128.0)\n\
#define FXAA_REDUCE_MUL (1.0 / 8.0)\n\
#define FXAA_SPAN_MAX 8.0\n\
\n\
/* from mitsuhiko/webgl-meincraft based on the code on geeks3d.com */\n\
vec4 applyFXAA(sampler2D tex, vec2 fragCoord)\n\
{\n\
vec4 color = vec4(0.0);\n\
/*vec2 inverseVP = vec2(1.0 / uViewportSize.x, 1.0 / uViewportSize.y);*/\n\
vec3 rgbNW = texture2D(tex, (fragCoord + vec2(-1.0, -1.0)) * inverseVP).xyz;\n\
vec3 rgbNE = texture2D(tex, (fragCoord + vec2(1.0, -1.0)) * inverseVP).xyz;\n\
vec3 rgbSW = texture2D(tex, (fragCoord + vec2(-1.0, 1.0)) * inverseVP).xyz;\n\
vec3 rgbSE = texture2D(tex, (fragCoord + vec2(1.0, 1.0)) * inverseVP).xyz;\n\
vec3 rgbM = texture2D(tex, fragCoord * inverseVP).xyz;\n\
vec3 luma = vec3(0.299, 0.587, 0.114);\n\
float lumaNW = dot(rgbNW, luma);\n\
float lumaNE = dot(rgbNE, luma);\n\
float lumaSW = dot(rgbSW, luma);\n\
float lumaSE = dot(rgbSE, luma);\n\
float lumaM = dot(rgbM, luma);\n\
float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));\n\
float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));\n\
\n\
vec2 dir;\n\
dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));\n\
dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE));\n\
\n\
float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN);\n\
\n\
float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);\n\
dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX), max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), dir * rcpDirMin)) * inverseVP;\n\
\n\
vec3 rgbA = 0.5 * (texture2D(tex, fragCoord * inverseVP + dir * (1.0 / 3.0 - 0.5)).xyz + \n\
texture2D(tex, fragCoord * inverseVP + dir * (2.0 / 3.0 - 0.5)).xyz);\n\
vec3 rgbB = rgbA * 0.5 + 0.25 * (texture2D(tex, fragCoord * inverseVP + dir * -0.5).xyz + \n\
texture2D(tex, fragCoord * inverseVP + dir * 0.5).xyz);\n\
\n\
//return vec4(rgbA,1.0);\n\
float lumaB = dot(rgbB, luma);\n\
if ((lumaB < lumaMin) || (lumaB > lumaMax))\n\
color = vec4(rgbA, 1.0);\n\
else\n\
color = vec4(rgbB, 1.0);\n\
if(u_igamma != 1.0)\n\
color.xyz = pow( color.xyz, vec3(u_igamma) );\n\
return color;\n\
}\n\
\n\
void main() {\n\
gl_FragColor = applyFXAA( u_texture, v_coord * uViewportSize) ;\n\
}\n\
";
LGraphTextureToViewport.gamma_pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform float u_igamma;\n\
void main() {\n\
vec4 color = texture2D( u_texture, v_coord);\n\
color.xyz = pow(color.xyz, vec3(u_igamma) );\n\
gl_FragColor = color;\n\
}\n\
";
LiteGraph.registerNodeType(
"texture/toviewport",
LGraphTextureToViewport
);
// Texture Copy *****************************************
function LGraphTextureCopy() {
this.addInput("Texture", "Texture");
this.addOutput("", "Texture");
this.properties = {
size: 0,
generate_mipmaps: false,
precision: LGraphTexture.DEFAULT
};
}
LGraphTextureCopy.title = "Copy";
LGraphTextureCopy.desc = "Copy Texture";
LGraphTextureCopy.widgets_info = {
size: {
widget: "combo",
values: [0, 32, 64, 128, 256, 512, 1024, 2048]
},
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureCopy.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex && !this._temp_texture) {
return;
}
if (!this.isOutputConnected(0)) {
return;
} //saves work
//copy the texture
if (tex) {
var width = tex.width;
var height = tex.height;
if (this.properties.size != 0) {
width = this.properties.size;
height = this.properties.size;
}
var temp = this._temp_texture;
var type = tex.type;
if (this.properties.precision === LGraphTexture.LOW) {
type = gl.UNSIGNED_BYTE;
} else if (this.properties.precision === LGraphTexture.HIGH) {
type = gl.HIGH_PRECISION_FORMAT;
}
if (
!temp ||
temp.width != width ||
temp.height != height ||
temp.type != type
) {
var minFilter = gl.LINEAR;
if (
this.properties.generate_mipmaps &&
isPowerOfTwo(width) &&
isPowerOfTwo(height)
) {
minFilter = gl.LINEAR_MIPMAP_LINEAR;
}
this._temp_texture = new GL.Texture(width, height, {
type: type,
format: gl.RGBA,
minFilter: minFilter,
magFilter: gl.LINEAR
});
}
tex.copyTo(this._temp_texture);
if (this.properties.generate_mipmaps) {
this._temp_texture.bind(0);
gl.generateMipmap(this._temp_texture.texture_type);
this._temp_texture.unbind(0);
}
}
this.setOutputData(0, this._temp_texture);
};
LiteGraph.registerNodeType("texture/copy", LGraphTextureCopy);
// Texture Downsample *****************************************
function LGraphTextureDownsample() {
this.addInput("Texture", "Texture");
this.addOutput("", "Texture");
this.properties = {
iterations: 1,
generate_mipmaps: false,
precision: LGraphTexture.DEFAULT
};
}
LGraphTextureDownsample.title = "Downsample";
LGraphTextureDownsample.desc = "Downsample Texture";
LGraphTextureDownsample.widgets_info = {
iterations: { type: "number", step: 1, precision: 0, min: 0 },
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureDownsample.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex && !this._temp_texture) {
return;
}
if (!this.isOutputConnected(0)) {
return;
} //saves work
//we do not allow any texture different than texture 2D
if (!tex || tex.texture_type !== GL.TEXTURE_2D) {
return;
}
if (this.properties.iterations < 1) {
this.setOutputData(0, tex);
return;
}
var shader = LGraphTextureDownsample._shader;
if (!shader) {
LGraphTextureDownsample._shader = shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphTextureDownsample.pixel_shader
);
}
var width = tex.width | 0;
var height = tex.height | 0;
var type = tex.type;
if (this.properties.precision === LGraphTexture.LOW) {
type = gl.UNSIGNED_BYTE;
} else if (this.properties.precision === LGraphTexture.HIGH) {
type = gl.HIGH_PRECISION_FORMAT;
}
var iterations = this.properties.iterations || 1;
var origin = tex;
var target = null;
var temp = [];
var options = {
type: type,
format: tex.format
};
var offset = vec2.create();
var uniforms = {
u_offset: offset
};
if (this._texture) {
GL.Texture.releaseTemporary(this._texture);
}
for (var i = 0; i < iterations; ++i) {
offset[0] = 1 / width;
offset[1] = 1 / height;
width = width >> 1 || 0;
height = height >> 1 || 0;
target = GL.Texture.getTemporary(width, height, options);
temp.push(target);
origin.setParameter(GL.TEXTURE_MAG_FILTER, GL.NEAREST);
origin.copyTo(target, shader, uniforms);
if (width == 1 && height == 1) {
break;
} //nothing else to do
origin = target;
}
//keep the last texture used
this._texture = temp.pop();
//free the rest
for (var i = 0; i < temp.length; ++i) {
GL.Texture.releaseTemporary(temp[i]);
}
if (this.properties.generate_mipmaps) {
this._texture.bind(0);
gl.generateMipmap(this._texture.texture_type);
this._texture.unbind(0);
}
this.setOutputData(0, this._texture);
};
LGraphTextureDownsample.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_offset;\n\
varying vec2 v_coord;\n\
\n\
void main() {\n\
vec4 color = texture2D(u_texture, v_coord );\n\
color += texture2D(u_texture, v_coord + vec2( u_offset.x, 0.0 ) );\n\
color += texture2D(u_texture, v_coord + vec2( 0.0, u_offset.y ) );\n\
color += texture2D(u_texture, v_coord + vec2( u_offset.x, u_offset.y ) );\n\
gl_FragColor = color * 0.25;\n\
}\n\
";
LiteGraph.registerNodeType(
"texture/downsample",
LGraphTextureDownsample
);
function LGraphTextureResize() {
this.addInput("Texture", "Texture");
this.addOutput("", "Texture");
this.properties = {
size: [512,512],
generate_mipmaps: false,
precision: LGraphTexture.DEFAULT
};
}
LGraphTextureResize.title = "Resize";
LGraphTextureResize.desc = "Resize Texture";
LGraphTextureResize.widgets_info = {
iterations: { type: "number", step: 1, precision: 0, min: 0 },
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureResize.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex && !this._temp_texture) {
return;
}
if (!this.isOutputConnected(0)) {
return;
} //saves work
//we do not allow any texture different than texture 2D
if (!tex || tex.texture_type !== GL.TEXTURE_2D) {
return;
}
var width = this.properties.size[0] | 0;
var height = this.properties.size[1] | 0;
if(width == 0)
width = tex.width;
if(height == 0)
height = tex.height;
var type = tex.type;
if (this.properties.precision === LGraphTexture.LOW) {
type = gl.UNSIGNED_BYTE;
} else if (this.properties.precision === LGraphTexture.HIGH) {
type = gl.HIGH_PRECISION_FORMAT;
}
if( !this._texture || this._texture.width != width || this._texture.height != height || this._texture.type != type )
this._texture = new GL.Texture( width, height, { type: type } );
tex.copyTo( this._texture );
if (this.properties.generate_mipmaps) {
this._texture.bind(0);
gl.generateMipmap(this._texture.texture_type);
this._texture.unbind(0);
}
this.setOutputData(0, this._texture);
};
LiteGraph.registerNodeType( "texture/resize", LGraphTextureResize );
// Texture Average *****************************************
function LGraphTextureAverage() {
this.addInput("Texture", "Texture");
this.addOutput("tex", "Texture");
this.addOutput("avg", "vec4");
this.addOutput("lum", "number");
this.properties = {
use_previous_frame: true, //to avoid stalls
high_quality: false //to use as much pixels as possible
};
this._uniforms = {
u_texture: 0,
u_mipmap_offset: 0
};
this._luminance = new Float32Array(4);
}
LGraphTextureAverage.title = "Average";
LGraphTextureAverage.desc =
"Compute a partial average (32 random samples) of a texture and stores it as a 1x1 pixel texture.\n If high_quality is true, then it generates the mipmaps first and reads from the lower one.";
LGraphTextureAverage.prototype.onExecute = function() {
if (!this.properties.use_previous_frame) {
this.updateAverage();
}
var v = this._luminance;
this.setOutputData(0, this._temp_texture);
this.setOutputData(1, v);
this.setOutputData(2, (v[0] + v[1] + v[2]) / 3);
};
//executed before rendering the frame
LGraphTextureAverage.prototype.onPreRenderExecute = function() {
this.updateAverage();
};
LGraphTextureAverage.prototype.updateAverage = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (
!this.isOutputConnected(0) &&
!this.isOutputConnected(1) &&
!this.isOutputConnected(2)
) {
return;
} //saves work
if (!LGraphTextureAverage._shader) {
LGraphTextureAverage._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphTextureAverage.pixel_shader
);
//creates 256 random numbers and stores them in two mat4
var samples = new Float32Array(16);
for (var i = 0; i < samples.length; ++i) {
samples[i] = Math.random(); //poorly distributed samples
}
//upload only once
LGraphTextureAverage._shader.uniforms({
u_samples_a: samples.subarray(0, 16),
u_samples_b: samples.subarray(16, 32)
});
}
var temp = this._temp_texture;
var type = gl.UNSIGNED_BYTE;
if (tex.type != type) {
//force floats, half floats cannot be read with gl.readPixels
type = gl.FLOAT;
}
if (!temp || temp.type != type) {
this._temp_texture = new GL.Texture(1, 1, {
type: type,
format: gl.RGBA,
filter: gl.NEAREST
});
}
this._uniforms.u_mipmap_offset = 0;
if(this.properties.high_quality)
{
if( !this._temp_pot2_texture || this._temp_pot2_texture.type != type )
this._temp_pot2_texture = new GL.Texture(512, 512, {
type: type,
format: gl.RGBA,
minFilter: gl.LINEAR_MIPMAP_LINEAR,
magFilter: gl.LINEAR
});
tex.copyTo( this._temp_pot2_texture );
tex = this._temp_pot2_texture;
tex.bind(0);
gl.generateMipmap(GL.TEXTURE_2D);
this._uniforms.u_mipmap_offset = 9;
}
var shader = LGraphTextureAverage._shader;
var uniforms = this._uniforms;
uniforms.u_mipmap_offset = this.properties.mipmap_offset;
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
this._temp_texture.drawTo(function() {
tex.toViewport(shader, uniforms);
});
if (this.isOutputConnected(1) || this.isOutputConnected(2)) {
var pixel = this._temp_texture.getPixels();
if (pixel) {
var v = this._luminance;
var type = this._temp_texture.type;
v.set(pixel);
if (type == gl.UNSIGNED_BYTE) {
vec4.scale(v, v, 1 / 255);
} else if (
type == GL.HALF_FLOAT ||
type == GL.HALF_FLOAT_OES
) {
//no half floats possible, hard to read back unless copyed to a FLOAT texture, so temp_texture is always forced to FLOAT
}
}
}
};
LGraphTextureAverage.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
uniform mat4 u_samples_a;\n\
uniform mat4 u_samples_b;\n\
uniform sampler2D u_texture;\n\
uniform float u_mipmap_offset;\n\
varying vec2 v_coord;\n\
\n\
void main() {\n\
vec4 color = vec4(0.0);\n\
//random average\n\
for(int i = 0; i < 4; ++i)\n\
for(int j = 0; j < 4; ++j)\n\
{\n\
color += texture2D(u_texture, vec2( u_samples_a[i][j], u_samples_b[i][j] ), u_mipmap_offset );\n\
color += texture2D(u_texture, vec2( 1.0 - u_samples_a[i][j], 1.0 - u_samples_b[i][j] ), u_mipmap_offset );\n\
}\n\
gl_FragColor = color * 0.03125;\n\
}\n\
";
LiteGraph.registerNodeType("texture/average", LGraphTextureAverage);
// Computes operation between pixels (max, min) *****************************************
function LGraphTextureMinMax() {
this.addInput("Texture", "Texture");
this.addOutput("min_t", "Texture");
this.addOutput("max_t", "Texture");
this.addOutput("min", "vec4");
this.addOutput("max", "vec4");
this.properties = {
mode: "max",
use_previous_frame: true //to avoid stalls
};
this._uniforms = {
u_texture: 0
};
this._max = new Float32Array(4);
this._min = new Float32Array(4);
this._textures_chain = [];
}
LGraphTextureMinMax.widgets_info = {
mode: { widget: "combo", values: ["min","max","avg"] }
};
LGraphTextureMinMax.title = "MinMax";
LGraphTextureMinMax.desc = "Compute the scene min max";
LGraphTextureMinMax.prototype.onExecute = function() {
if (!this.properties.use_previous_frame) {
this.update();
}
this.setOutputData(0, this._temp_texture);
this.setOutputData(1, this._luminance);
};
//executed before rendering the frame
LGraphTextureMinMax.prototype.onPreRenderExecute = function() {
this.update();
};
LGraphTextureMinMax.prototype.update = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if ( !this.isOutputConnected(0) && !this.isOutputConnected(1) ) {
return;
} //saves work
if (!LGraphTextureMinMax._shader) {
LGraphTextureMinMax._shader = new GL.Shader( GL.Shader.SCREEN_VERTEX_SHADER, LGraphTextureMinMax.pixel_shader );
}
var temp = this._temp_texture;
var type = gl.UNSIGNED_BYTE;
if (tex.type != type) {
//force floats, half floats cannot be read with gl.readPixels
type = gl.FLOAT;
}
var size = 512;
if( !this._textures_chain.length || this._textures_chain[0].type != type )
{
var index = 0;
while(i)
{
this._textures_chain[i] = new GL.Texture( size, size, {
type: type,
format: gl.RGBA,
filter: gl.NEAREST
});
size = size >> 2;
i++;
if(size == 1)
break;
}
}
tex.copyTo( this._textures_chain[0] );
var prev = this._textures_chain[0];
for(var i = 1; i <= this._textures_chain.length; ++i)
{
var tex = this._textures_chain[i];
prev = tex;
}
var shader = LGraphTextureMinMax._shader;
var uniforms = this._uniforms;
uniforms.u_mipmap_offset = this.properties.mipmap_offset;
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
this._temp_texture.drawTo(function() {
tex.toViewport(shader, uniforms);
});
};
LGraphTextureMinMax.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
uniform mat4 u_samples_a;\n\
uniform mat4 u_samples_b;\n\
uniform sampler2D u_texture;\n\
uniform float u_mipmap_offset;\n\
varying vec2 v_coord;\n\
\n\
void main() {\n\
vec4 color = vec4(0.0);\n\
//random average\n\
for(int i = 0; i < 4; ++i)\n\
for(int j = 0; j < 4; ++j)\n\
{\n\
color += texture2D(u_texture, vec2( u_samples_a[i][j], u_samples_b[i][j] ), u_mipmap_offset );\n\
color += texture2D(u_texture, vec2( 1.0 - u_samples_a[i][j], 1.0 - u_samples_b[i][j] ), u_mipmap_offset );\n\
}\n\
gl_FragColor = color * 0.03125;\n\
}\n\
";
//LiteGraph.registerNodeType("texture/clustered_operation", LGraphTextureClusteredOperation);
function LGraphTextureTemporalSmooth() {
this.addInput("in", "Texture");
this.addInput("factor", "Number");
this.addOutput("out", "Texture");
this.properties = { factor: 0.5 };
this._uniforms = {
u_texture: 0,
u_textureB: 1,
u_factor: this.properties.factor
};
}
LGraphTextureTemporalSmooth.title = "Smooth";
LGraphTextureTemporalSmooth.desc = "Smooth texture over time";
LGraphTextureTemporalSmooth.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex || !this.isOutputConnected(0)) {
return;
}
if (!LGraphTextureTemporalSmooth._shader) {
LGraphTextureTemporalSmooth._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphTextureTemporalSmooth.pixel_shader
);
}
var temp = this._temp_texture;
if (
!temp ||
temp.type != tex.type ||
temp.width != tex.width ||
temp.height != tex.height
) {
var options = {
type: tex.type,
format: gl.RGBA,
filter: gl.NEAREST
};
this._temp_texture = new GL.Texture(tex.width, tex.height, options );
this._temp_texture2 = new GL.Texture(tex.width, tex.height, options );
tex.copyTo(this._temp_texture2);
}
var tempA = this._temp_texture;
var tempB = this._temp_texture2;
var shader = LGraphTextureTemporalSmooth._shader;
var uniforms = this._uniforms;
uniforms.u_factor = 1.0 - this.getInputOrProperty("factor");
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
tempA.drawTo(function() {
tempB.bind(1);
tex.toViewport(shader, uniforms);
});
this.setOutputData(0, tempA);
//swap
this._temp_texture = tempB;
this._temp_texture2 = tempA;
};
LGraphTextureTemporalSmooth.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_textureB;\n\
uniform float u_factor;\n\
varying vec2 v_coord;\n\
\n\
void main() {\n\
gl_FragColor = mix( texture2D( u_texture, v_coord ), texture2D( u_textureB, v_coord ), u_factor );\n\
}\n\
";
LiteGraph.registerNodeType( "texture/temporal_smooth", LGraphTextureTemporalSmooth );
function LGraphTextureLinearAvgSmooth() {
this.addInput("in", "Texture");
this.addOutput("avg", "Texture");
this.addOutput("array", "Texture");
this.properties = { samples: 64, frames_interval: 1 };
this._uniforms = {
u_texture: 0,
u_textureB: 1,
u_samples: this.properties.samples,
u_isamples: 1/this.properties.samples
};
this.frame = 0;
}
LGraphTextureLinearAvgSmooth.title = "Lineal Avg Smooth";
LGraphTextureLinearAvgSmooth.desc = "Smooth texture linearly over time";
LGraphTextureLinearAvgSmooth["@samples"] = { type: "number", min: 1, max: 64, step: 1, precision: 1 };
LGraphTextureLinearAvgSmooth.prototype.getPreviewTexture = function()
{
return this._temp_texture2;
}
LGraphTextureLinearAvgSmooth.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex || !this.isOutputConnected(0)) {
return;
}
if (!LGraphTextureLinearAvgSmooth._shader) {
LGraphTextureLinearAvgSmooth._shader_copy = new GL.Shader( GL.Shader.SCREEN_VERTEX_SHADER, LGraphTextureLinearAvgSmooth.pixel_shader_copy );
LGraphTextureLinearAvgSmooth._shader_avg = new GL.Shader( GL.Shader.SCREEN_VERTEX_SHADER, LGraphTextureLinearAvgSmooth.pixel_shader_avg );
}
var samples = clamp(this.properties.samples,0,64);
var frame = this.frame;
var interval = this.properties.frames_interval;
if( interval == 0 || frame % interval == 0 )
{
var temp = this._temp_texture;
if ( !temp || temp.type != tex.type || temp.width != samples ) {
var options = {
type: tex.type,
format: gl.RGBA,
filter: gl.NEAREST
};
this._temp_texture = new GL.Texture( samples, 1, options );
this._temp_texture2 = new GL.Texture( samples, 1, options );
this._temp_texture_out = new GL.Texture( 1, 1, options );
}
var tempA = this._temp_texture;
var tempB = this._temp_texture2;
var shader_copy = LGraphTextureLinearAvgSmooth._shader_copy;
var shader_avg = LGraphTextureLinearAvgSmooth._shader_avg;
var uniforms = this._uniforms;
uniforms.u_samples = samples;
uniforms.u_isamples = 1.0 / samples;
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
tempA.drawTo(function() {
tempB.bind(1);
tex.toViewport( shader_copy, uniforms );
});
this._temp_texture_out.drawTo(function() {
tempA.toViewport( shader_avg, uniforms );
});
this.setOutputData( 0, this._temp_texture_out );
//swap
this._temp_texture = tempB;
this._temp_texture2 = tempA;
}
else
this.setOutputData(0, this._temp_texture_out);
this.setOutputData(1, this._temp_texture2);
this.frame++;
};
LGraphTextureLinearAvgSmooth.pixel_shader_copy =
"precision highp float;\n\
precision highp float;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_textureB;\n\
uniform float u_isamples;\n\
varying vec2 v_coord;\n\
\n\
void main() {\n\
if( v_coord.x <= u_isamples )\n\
gl_FragColor = texture2D( u_texture, vec2(0.5) );\n\
else\n\
gl_FragColor = texture2D( u_textureB, v_coord - vec2(u_isamples,0.0) );\n\
}\n\
";
LGraphTextureLinearAvgSmooth.pixel_shader_avg =
"precision highp float;\n\
precision highp float;\n\
uniform sampler2D u_texture;\n\
uniform int u_samples;\n\
uniform float u_isamples;\n\
varying vec2 v_coord;\n\
\n\
void main() {\n\
vec4 color = vec4(0.0);\n\
for(int i = 0; i < 64; ++i)\n\
{\n\
color += texture2D( u_texture, vec2( float(i)*u_isamples,0.0) );\n\
if(i == (u_samples - 1))\n\
break;\n\
}\n\
gl_FragColor = color * u_isamples;\n\
}\n\
";
LiteGraph.registerNodeType( "texture/linear_avg_smooth", LGraphTextureLinearAvgSmooth );
// Image To Texture *****************************************
function LGraphImageToTexture() {
this.addInput("Image", "image");
this.addOutput("", "Texture");
this.properties = {};
}
LGraphImageToTexture.title = "Image to Texture";
LGraphImageToTexture.desc = "Uploads an image to the GPU";
//LGraphImageToTexture.widgets_info = { size: { widget:"combo", values:[0,32,64,128,256,512,1024,2048]} };
LGraphImageToTexture.prototype.onExecute = function() {
var img = this.getInputData(0);
if (!img) {
return;
}
var width = img.videoWidth || img.width;
var height = img.videoHeight || img.height;
//this is in case we are using a webgl canvas already, no need to reupload it
if (img.gltexture) {
this.setOutputData(0, img.gltexture);
return;
}
var temp = this._temp_texture;
if (!temp || temp.width != width || temp.height != height) {
this._temp_texture = new GL.Texture(width, height, {
format: gl.RGBA,
filter: gl.LINEAR
});
}
try {
this._temp_texture.uploadImage(img);
} catch (err) {
console.error(
"image comes from an unsafe location, cannot be uploaded to webgl: " +
err
);
return;
}
this.setOutputData(0, this._temp_texture);
};
LiteGraph.registerNodeType(
"texture/imageToTexture",
LGraphImageToTexture
);
// Texture LUT *****************************************
function LGraphTextureLUT() {
this.addInput("Texture", "Texture");
this.addInput("LUT", "Texture");
this.addInput("Intensity", "number");
this.addOutput("", "Texture");
this.properties = { enabled: true, intensity: 1, precision: LGraphTexture.DEFAULT, texture: null };
if (!LGraphTextureLUT._shader) {
LGraphTextureLUT._shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER, LGraphTextureLUT.pixel_shader );
}
}
LGraphTextureLUT.widgets_info = {
texture: { widget: "texture" },
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureLUT.title = "LUT";
LGraphTextureLUT.desc = "Apply LUT to Texture";
LGraphTextureLUT.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var tex = this.getInputData(0);
if (this.properties.precision === LGraphTexture.PASS_THROUGH || this.properties.enabled === false) {
this.setOutputData(0, tex);
return;
}
if (!tex) {
return;
}
var lut_tex = this.getInputData(1);
if (!lut_tex) {
lut_tex = LGraphTexture.getTexture(this.properties.texture);
}
if (!lut_tex) {
this.setOutputData(0, tex);
return;
}
lut_tex.bind(0);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_S,
gl.CLAMP_TO_EDGE
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_T,
gl.CLAMP_TO_EDGE
);
gl.bindTexture(gl.TEXTURE_2D, null);
var intensity = this.properties.intensity;
if (this.isInputConnected(2)) {
this.properties.intensity = intensity = this.getInputData(2);
}
this._tex = LGraphTexture.getTargetTexture(
tex,
this._tex,
this.properties.precision
);
//var mesh = Mesh.getScreenQuad();
this._tex.drawTo(function() {
lut_tex.bind(1);
tex.toViewport(LGraphTextureLUT._shader, {
u_texture: 0,
u_textureB: 1,
u_amount: intensity
});
});
this.setOutputData(0, this._tex);
};
LGraphTextureLUT.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_textureB;\n\
uniform float u_amount;\n\
\n\
void main() {\n\
lowp vec4 textureColor = clamp( texture2D(u_texture, v_coord), vec4(0.0), vec4(1.0) );\n\
mediump float blueColor = textureColor.b * 63.0;\n\
mediump vec2 quad1;\n\
quad1.y = floor(floor(blueColor) / 8.0);\n\
quad1.x = floor(blueColor) - (quad1.y * 8.0);\n\
mediump vec2 quad2;\n\
quad2.y = floor(ceil(blueColor) / 8.0);\n\
quad2.x = ceil(blueColor) - (quad2.y * 8.0);\n\
highp vec2 texPos1;\n\
texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);\n\
texPos1.y = 1.0 - ((quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g));\n\
highp vec2 texPos2;\n\
texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);\n\
texPos2.y = 1.0 - ((quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g));\n\
lowp vec4 newColor1 = texture2D(u_textureB, texPos1);\n\
lowp vec4 newColor2 = texture2D(u_textureB, texPos2);\n\
lowp vec4 newColor = mix(newColor1, newColor2, fract(blueColor));\n\
gl_FragColor = vec4( mix( textureColor.rgb, newColor.rgb, u_amount), textureColor.w);\n\
}\n\
";
LiteGraph.registerNodeType("texture/LUT", LGraphTextureLUT);
// Texture LUT *****************************************
function LGraphTextureEncode() {
this.addInput("Texture", "Texture");
this.addInput("Atlas", "Texture");
this.addOutput("", "Texture");
this.properties = { enabled: true, num_row_symbols: 4, symbol_size: 16, brightness: 1, colorize: false, filter: false, invert: false, precision: LGraphTexture.DEFAULT, generate_mipmaps: false, texture: null };
if (!LGraphTextureEncode._shader) {
LGraphTextureEncode._shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER, LGraphTextureEncode.pixel_shader );
}
this._uniforms = {
u_texture: 0,
u_textureB: 1,
u_row_simbols: 4,
u_simbol_size: 16,
u_res: vec2.create()
};
}
LGraphTextureEncode.widgets_info = {
texture: { widget: "texture" },
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureEncode.title = "Encode";
LGraphTextureEncode.desc = "Apply a texture atlas to encode a texture";
LGraphTextureEncode.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var tex = this.getInputData(0);
if (this.properties.precision === LGraphTexture.PASS_THROUGH || this.properties.enabled === false) {
this.setOutputData(0, tex);
return;
}
if (!tex) {
return;
}
var symbols_tex = this.getInputData(1);
if (!symbols_tex) {
symbols_tex = LGraphTexture.getTexture(this.properties.texture);
}
if (!symbols_tex) {
this.setOutputData(0, tex);
return;
}
symbols_tex.bind(0);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.properties.filter ? gl.LINEAR : gl.NEAREST );
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.properties.filter ? gl.LINEAR : gl.NEAREST );
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE );
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE );
gl.bindTexture(gl.TEXTURE_2D, null);
var uniforms = this._uniforms;
uniforms.u_row_simbols = Math.floor(this.properties.num_row_symbols);
uniforms.u_symbol_size = this.properties.symbol_size;
uniforms.u_brightness = this.properties.brightness;
uniforms.u_invert = this.properties.invert ? 1 : 0;
uniforms.u_colorize = this.properties.colorize ? 1 : 0;
this._tex = LGraphTexture.getTargetTexture( tex, this._tex, this.properties.precision );
uniforms.u_res[0] = this._tex.width;
uniforms.u_res[1] = this._tex.height;
this._tex.bind(0);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST );
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST );
this._tex.drawTo(function() {
symbols_tex.bind(1);
tex.toViewport(LGraphTextureEncode._shader, uniforms);
});
if (this.properties.generate_mipmaps) {
this._tex.bind(0);
gl.generateMipmap(this._tex.texture_type);
this._tex.unbind(0);
}
this.setOutputData(0, this._tex);
};
LGraphTextureEncode.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_textureB;\n\
uniform float u_row_simbols;\n\
uniform float u_symbol_size;\n\
uniform float u_brightness;\n\
uniform float u_invert;\n\
uniform float u_colorize;\n\
uniform vec2 u_res;\n\
\n\
void main() {\n\
vec2 total_symbols = u_res / u_symbol_size;\n\
vec2 uv = floor(v_coord * total_symbols) / total_symbols; //pixelate \n\
vec2 local_uv = mod(v_coord * u_res, u_symbol_size) / u_symbol_size;\n\
lowp vec4 textureColor = texture2D(u_texture, uv );\n\
float lum = clamp(u_brightness * (textureColor.x + textureColor.y + textureColor.z)/3.0,0.0,1.0);\n\
if( u_invert == 1.0 ) lum = 1.0 - lum;\n\
float index = floor( lum * (u_row_simbols * u_row_simbols - 1.0));\n\
float col = mod( index, u_row_simbols );\n\
float row = u_row_simbols - floor( index / u_row_simbols ) - 1.0;\n\
vec2 simbol_uv = ( vec2( col, row ) + local_uv ) / u_row_simbols;\n\
vec4 color = texture2D( u_textureB, simbol_uv );\n\
if(u_colorize == 1.0)\n\
color *= textureColor;\n\
gl_FragColor = color;\n\
}\n\
";
LiteGraph.registerNodeType("texture/encode", LGraphTextureEncode);
// Texture Channels *****************************************
function LGraphTextureChannels() {
this.addInput("Texture", "Texture");
this.addOutput("R", "Texture");
this.addOutput("G", "Texture");
this.addOutput("B", "Texture");
this.addOutput("A", "Texture");
//this.properties = { use_single_channel: true };
if (!LGraphTextureChannels._shader) {
LGraphTextureChannels._shader = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureChannels.pixel_shader
);
}
}
LGraphTextureChannels.title = "Texture to Channels";
LGraphTextureChannels.desc = "Split texture channels";
LGraphTextureChannels.prototype.onExecute = function() {
var texA = this.getInputData(0);
if (!texA) {
return;
}
if (!this._channels) {
this._channels = Array(4);
}
//var format = this.properties.use_single_channel ? gl.LUMINANCE : gl.RGBA; //not supported by WebGL1
var format = gl.RGB;
var connections = 0;
for (var i = 0; i < 4; i++) {
if (this.isOutputConnected(i)) {
if (
!this._channels[i] ||
this._channels[i].width != texA.width ||
this._channels[i].height != texA.height ||
this._channels[i].type != texA.type ||
this._channels[i].format != format
) {
this._channels[i] = new GL.Texture(
texA.width,
texA.height,
{
type: texA.type,
format: format,
filter: gl.LINEAR
}
);
}
connections++;
} else {
this._channels[i] = null;
}
}
if (!connections) {
return;
}
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = Mesh.getScreenQuad();
var shader = LGraphTextureChannels._shader;
var masks = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
for (var i = 0; i < 4; i++) {
if (!this._channels[i]) {
continue;
}
this._channels[i].drawTo(function() {
texA.bind(0);
shader
.uniforms({ u_texture: 0, u_mask: masks[i] })
.draw(mesh);
});
this.setOutputData(i, this._channels[i]);
}
};
LGraphTextureChannels.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec4 u_mask;\n\
\n\
void main() {\n\
gl_FragColor = vec4( vec3( length( texture2D(u_texture, v_coord) * u_mask )), 1.0 );\n\
}\n\
";
LiteGraph.registerNodeType(
"texture/textureChannels",
LGraphTextureChannels
);
// Texture Channels to Texture *****************************************
function LGraphChannelsTexture() {
this.addInput("R", "Texture");
this.addInput("G", "Texture");
this.addInput("B", "Texture");
this.addInput("A", "Texture");
this.addOutput("Texture", "Texture");
this.properties = {
precision: LGraphTexture.DEFAULT,
R: 1,
G: 1,
B: 1,
A: 1
};
this._color = vec4.create();
this._uniforms = {
u_textureR: 0,
u_textureG: 1,
u_textureB: 2,
u_textureA: 3,
u_color: this._color
};
}
LGraphChannelsTexture.title = "Channels to Texture";
LGraphChannelsTexture.desc = "Split texture channels";
LGraphChannelsTexture.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphChannelsTexture.prototype.onExecute = function() {
var white = LGraphTexture.getWhiteTexture();
var texR = this.getInputData(0) || white;
var texG = this.getInputData(1) || white;
var texB = this.getInputData(2) || white;
var texA = this.getInputData(3) || white;
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = Mesh.getScreenQuad();
if (!LGraphChannelsTexture._shader) {
LGraphChannelsTexture._shader = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphChannelsTexture.pixel_shader
);
}
var shader = LGraphChannelsTexture._shader;
var w = Math.max(texR.width, texG.width, texB.width, texA.width);
var h = Math.max(
texR.height,
texG.height,
texB.height,
texA.height
);
var type =
this.properties.precision == LGraphTexture.HIGH
? LGraphTexture.HIGH_PRECISION_FORMAT
: gl.UNSIGNED_BYTE;
if (
!this._texture ||
this._texture.width != w ||
this._texture.height != h ||
this._texture.type != type
) {
this._texture = new GL.Texture(w, h, {
type: type,
format: gl.RGBA,
filter: gl.LINEAR
});
}
var color = this._color;
color[0] = this.properties.R;
color[1] = this.properties.G;
color[2] = this.properties.B;
color[3] = this.properties.A;
var uniforms = this._uniforms;
this._texture.drawTo(function() {
texR.bind(0);
texG.bind(1);
texB.bind(2);
texA.bind(3);
shader.uniforms(uniforms).draw(mesh);
});
this.setOutputData(0, this._texture);
};
LGraphChannelsTexture.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_textureR;\n\
uniform sampler2D u_textureG;\n\
uniform sampler2D u_textureB;\n\
uniform sampler2D u_textureA;\n\
uniform vec4 u_color;\n\
\n\
void main() {\n\
gl_FragColor = u_color * vec4( \
texture2D(u_textureR, v_coord).r,\
texture2D(u_textureG, v_coord).r,\
texture2D(u_textureB, v_coord).r,\
texture2D(u_textureA, v_coord).r);\n\
}\n\
";
LiteGraph.registerNodeType(
"texture/channelsTexture",
LGraphChannelsTexture
);
// Texture Color *****************************************
function LGraphTextureColor() {
this.addOutput("Texture", "Texture");
this._tex_color = vec4.create();
this.properties = {
color: vec4.create(),
precision: LGraphTexture.DEFAULT
};
}
LGraphTextureColor.title = "Color";
LGraphTextureColor.desc =
"Generates a 1x1 texture with a constant color";
LGraphTextureColor.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureColor.prototype.onDrawBackground = function(ctx) {
var c = this.properties.color;
ctx.fillStyle =
"rgb(" +
Math.floor(clamp(c[0], 0, 1) * 255) +
"," +
Math.floor(clamp(c[1], 0, 1) * 255) +
"," +
Math.floor(clamp(c[2], 0, 1) * 255) +
")";
if (this.flags.collapsed) {
this.boxcolor = ctx.fillStyle;
} else {
ctx.fillRect(0, 0, this.size[0], this.size[1]);
}
};
LGraphTextureColor.prototype.onExecute = function() {
var type =
this.properties.precision == LGraphTexture.HIGH
? LGraphTexture.HIGH_PRECISION_FORMAT
: gl.UNSIGNED_BYTE;
if (!this._tex || this._tex.type != type) {
this._tex = new GL.Texture(1, 1, {
format: gl.RGBA,
type: type,
minFilter: gl.NEAREST
});
}
var color = this.properties.color;
if (this.inputs) {
for (var i = 0; i < this.inputs.length; i++) {
var input = this.inputs[i];
var v = this.getInputData(i);
if (v === undefined) {
continue;
}
switch (input.name) {
case "RGB":
case "RGBA":
color.set(v);
break;
case "R":
color[0] = v;
break;
case "G":
color[1] = v;
break;
case "B":
color[2] = v;
break;
case "A":
color[3] = v;
break;
}
}
}
if (vec4.sqrDist(this._tex_color, color) > 0.001) {
this._tex_color.set(color);
this._tex.fill(color);
}
this.setOutputData(0, this._tex);
};
LGraphTextureColor.prototype.onGetInputs = function() {
return [
["RGB", "vec3"],
["RGBA", "vec4"],
["R", "number"],
["G", "number"],
["B", "number"],
["A", "number"]
];
};
LiteGraph.registerNodeType("texture/color", LGraphTextureColor);
// Texture Channels to Texture *****************************************
function LGraphTextureGradient() {
this.addInput("A", "color");
this.addInput("B", "color");
this.addOutput("Texture", "Texture");
this.properties = {
angle: 0,
scale: 1,
A: [0, 0, 0],
B: [1, 1, 1],
texture_size: 32
};
if (!LGraphTextureGradient._shader) {
LGraphTextureGradient._shader = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureGradient.pixel_shader
);
}
this._uniforms = {
u_angle: 0,
u_colorA: vec3.create(),
u_colorB: vec3.create()
};
}
LGraphTextureGradient.title = "Gradient";
LGraphTextureGradient.desc = "Generates a gradient";
LGraphTextureGradient["@A"] = { type: "color" };
LGraphTextureGradient["@B"] = { type: "color" };
LGraphTextureGradient["@texture_size"] = {
type: "enum",
values: [32, 64, 128, 256, 512]
};
LGraphTextureGradient.prototype.onExecute = function() {
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = GL.Mesh.getScreenQuad();
var shader = LGraphTextureGradient._shader;
var A = this.getInputData(0);
if (!A) {
A = this.properties.A;
}
var B = this.getInputData(1);
if (!B) {
B = this.properties.B;
}
//angle and scale
for (var i = 2; i < this.inputs.length; i++) {
var input = this.inputs[i];
var v = this.getInputData(i);
if (v === undefined) {
continue;
}
this.properties[input.name] = v;
}
var uniforms = this._uniforms;
this._uniforms.u_angle = this.properties.angle * DEG2RAD;
this._uniforms.u_scale = this.properties.scale;
vec3.copy(uniforms.u_colorA, A);
vec3.copy(uniforms.u_colorB, B);
var size = parseInt(this.properties.texture_size);
if (!this._tex || this._tex.width != size) {
this._tex = new GL.Texture(size, size, {
format: gl.RGB,
filter: gl.LINEAR
});
}
this._tex.drawTo(function() {
shader.uniforms(uniforms).draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphTextureGradient.prototype.onGetInputs = function() {
return [["angle", "number"], ["scale", "number"]];
};
LGraphTextureGradient.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform float u_angle;\n\
uniform float u_scale;\n\
uniform vec3 u_colorA;\n\
uniform vec3 u_colorB;\n\
\n\
vec2 rotate(vec2 v, float angle)\n\
{\n\
vec2 result;\n\
float _cos = cos(angle);\n\
float _sin = sin(angle);\n\
result.x = v.x * _cos - v.y * _sin;\n\
result.y = v.x * _sin + v.y * _cos;\n\
return result;\n\
}\n\
void main() {\n\
float f = (rotate(u_scale * (v_coord - vec2(0.5)), u_angle) + vec2(0.5)).x;\n\
vec3 color = mix(u_colorA,u_colorB,clamp(f,0.0,1.0));\n\
gl_FragColor = vec4(color,1.0);\n\
}\n\
";
LiteGraph.registerNodeType("texture/gradient", LGraphTextureGradient);
// Texture Mix *****************************************
function LGraphTextureMix() {
this.addInput("A", "Texture");
this.addInput("B", "Texture");
this.addInput("Mixer", "Texture");
this.addOutput("Texture", "Texture");
this.properties = { factor: 0.5, size_from_biggest: true, invert: false, precision: LGraphTexture.DEFAULT };
this._uniforms = {
u_textureA: 0,
u_textureB: 1,
u_textureMix: 2,
u_mix: vec4.create()
};
}
LGraphTextureMix.title = "Mix";
LGraphTextureMix.desc = "Generates a texture mixing two textures";
LGraphTextureMix.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureMix.prototype.onExecute = function() {
var texA = this.getInputData(0);
if (!this.isOutputConnected(0)) {
return;
} //saves work
if (this.properties.precision === LGraphTexture.PASS_THROUGH) {
this.setOutputData(0, texA);
return;
}
var texB = this.getInputData(1);
if (!texA || !texB) {
return;
}
var texMix = this.getInputData(2);
var factor = this.getInputData(3);
this._tex = LGraphTexture.getTargetTexture(
this.properties.size_from_biggest && texB.width > texA.width ? texB : texA,
this._tex,
this.properties.precision
);
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = Mesh.getScreenQuad();
var shader = null;
var uniforms = this._uniforms;
if (texMix) {
shader = LGraphTextureMix._shader_tex;
if (!shader) {
shader = LGraphTextureMix._shader_tex = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureMix.pixel_shader,
{ MIX_TEX: "" }
);
}
} else {
shader = LGraphTextureMix._shader_factor;
if (!shader) {
shader = LGraphTextureMix._shader_factor = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureMix.pixel_shader
);
}
var f = factor == null ? this.properties.factor : factor;
uniforms.u_mix.set([f, f, f, f]);
}
var invert = this.properties.invert;
this._tex.drawTo(function() {
texA.bind( invert ? 1 : 0 );
texB.bind( invert ? 0 : 1 );
if (texMix) {
texMix.bind(2);
}
shader.uniforms(uniforms).draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphTextureMix.prototype.onGetInputs = function() {
return [["factor", "number"]];
};
LGraphTextureMix.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_textureA;\n\
uniform sampler2D u_textureB;\n\
#ifdef MIX_TEX\n\
uniform sampler2D u_textureMix;\n\
#else\n\
uniform vec4 u_mix;\n\
#endif\n\
\n\
void main() {\n\
#ifdef MIX_TEX\n\
vec4 f = texture2D(u_textureMix, v_coord);\n\
#else\n\
vec4 f = u_mix;\n\
#endif\n\
gl_FragColor = mix( texture2D(u_textureA, v_coord), texture2D(u_textureB, v_coord), f );\n\
}\n\
";
LiteGraph.registerNodeType("texture/mix", LGraphTextureMix);
// Texture Edges detection *****************************************
function LGraphTextureEdges() {
this.addInput("Tex.", "Texture");
this.addOutput("Edges", "Texture");
this.properties = {
invert: true,
threshold: false,
factor: 1,
precision: LGraphTexture.DEFAULT
};
if (!LGraphTextureEdges._shader) {
LGraphTextureEdges._shader = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureEdges.pixel_shader
);
}
}
LGraphTextureEdges.title = "Edges";
LGraphTextureEdges.desc = "Detects edges";
LGraphTextureEdges.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureEdges.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var tex = this.getInputData(0);
if (this.properties.precision === LGraphTexture.PASS_THROUGH) {
this.setOutputData(0, tex);
return;
}
if (!tex) {
return;
}
this._tex = LGraphTexture.getTargetTexture(
tex,
this._tex,
this.properties.precision
);
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = Mesh.getScreenQuad();
var shader = LGraphTextureEdges._shader;
var invert = this.properties.invert;
var factor = this.properties.factor;
var threshold = this.properties.threshold ? 1 : 0;
this._tex.drawTo(function() {
tex.bind(0);
shader
.uniforms({
u_texture: 0,
u_isize: [1 / tex.width, 1 / tex.height],
u_factor: factor,
u_threshold: threshold,
u_invert: invert ? 1 : 0
})
.draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphTextureEdges.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_isize;\n\
uniform int u_invert;\n\
uniform float u_factor;\n\
uniform float u_threshold;\n\
\n\
void main() {\n\
vec4 center = texture2D(u_texture, v_coord);\n\
vec4 up = texture2D(u_texture, v_coord + u_isize * vec2(0.0,1.0) );\n\
vec4 down = texture2D(u_texture, v_coord + u_isize * vec2(0.0,-1.0) );\n\
vec4 left = texture2D(u_texture, v_coord + u_isize * vec2(1.0,0.0) );\n\
vec4 right = texture2D(u_texture, v_coord + u_isize * vec2(-1.0,0.0) );\n\
vec4 diff = abs(center - up) + abs(center - down) + abs(center - left) + abs(center - right);\n\
diff *= u_factor;\n\
if(u_invert == 1)\n\
diff.xyz = vec3(1.0) - diff.xyz;\n\
if( u_threshold == 0.0 )\n\
gl_FragColor = vec4( diff.xyz, center.a );\n\
else\n\
gl_FragColor = vec4( diff.x > 0.5 ? 1.0 : 0.0, diff.y > 0.5 ? 1.0 : 0.0, diff.z > 0.5 ? 1.0 : 0.0, center.a );\n\
}\n\
";
LiteGraph.registerNodeType("texture/edges", LGraphTextureEdges);
// Texture Depth *****************************************
function LGraphTextureDepthRange() {
this.addInput("Texture", "Texture");
this.addInput("Distance", "number");
this.addInput("Range", "number");
this.addOutput("Texture", "Texture");
this.properties = {
distance: 100,
range: 50,
only_depth: false,
high_precision: false
};
this._uniforms = {
u_texture: 0,
u_distance: 100,
u_range: 50,
u_camera_planes: null
};
}
LGraphTextureDepthRange.title = "Depth Range";
LGraphTextureDepthRange.desc = "Generates a texture with a depth range";
LGraphTextureDepthRange.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var tex = this.getInputData(0);
if (!tex) {
return;
}
var precision = gl.UNSIGNED_BYTE;
if (this.properties.high_precision) {
precision = gl.half_float_ext ? gl.HALF_FLOAT_OES : gl.FLOAT;
}
if (
!this._temp_texture ||
this._temp_texture.type != precision ||
this._temp_texture.width != tex.width ||
this._temp_texture.height != tex.height
) {
this._temp_texture = new GL.Texture(tex.width, tex.height, {
type: precision,
format: gl.RGBA,
filter: gl.LINEAR
});
}
var uniforms = this._uniforms;
//iterations
var distance = this.properties.distance;
if (this.isInputConnected(1)) {
distance = this.getInputData(1);
this.properties.distance = distance;
}
var range = this.properties.range;
if (this.isInputConnected(2)) {
range = this.getInputData(2);
this.properties.range = range;
}
uniforms.u_distance = distance;
uniforms.u_range = range;
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = Mesh.getScreenQuad();
if (!LGraphTextureDepthRange._shader) {
LGraphTextureDepthRange._shader = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureDepthRange.pixel_shader
);
LGraphTextureDepthRange._shader_onlydepth = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureDepthRange.pixel_shader,
{ ONLY_DEPTH: "" }
);
}
var shader = this.properties.only_depth
? LGraphTextureDepthRange._shader_onlydepth
: LGraphTextureDepthRange._shader;
//NEAR AND FAR PLANES
var planes = null;
if (tex.near_far_planes) {
planes = tex.near_far_planes;
} else if (window.LS && LS.Renderer._main_camera) {
planes = LS.Renderer._main_camera._uniforms.u_camera_planes;
} else {
planes = [0.1, 1000];
} //hardcoded
uniforms.u_camera_planes = planes;
this._temp_texture.drawTo(function() {
tex.bind(0);
shader.uniforms(uniforms).draw(mesh);
});
this._temp_texture.near_far_planes = planes;
this.setOutputData(0, this._temp_texture);
};
LGraphTextureDepthRange.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_camera_planes;\n\
uniform float u_distance;\n\
uniform float u_range;\n\
\n\
float LinearDepth()\n\
{\n\
float zNear = u_camera_planes.x;\n\
float zFar = u_camera_planes.y;\n\
float depth = texture2D(u_texture, v_coord).x;\n\
depth = depth * 2.0 - 1.0;\n\
return zNear * (depth + 1.0) / (zFar + zNear - depth * (zFar - zNear));\n\
}\n\
\n\
void main() {\n\
float depth = LinearDepth();\n\
#ifdef ONLY_DEPTH\n\
gl_FragColor = vec4(depth);\n\
#else\n\
float diff = abs(depth * u_camera_planes.y - u_distance);\n\
float dof = 1.0;\n\
if(diff <= u_range)\n\
dof = diff / u_range;\n\
gl_FragColor = vec4(dof);\n\
#endif\n\
}\n\
";
LiteGraph.registerNodeType( "texture/depth_range", LGraphTextureDepthRange );
// Texture Depth *****************************************
function LGraphTextureLinearDepth() {
this.addInput("Texture", "Texture");
this.addOutput("Texture", "Texture");
this.properties = {
precision: LGraphTexture.DEFAULT,
invert: false
};
this._uniforms = {
u_texture: 0,
u_camera_planes: null, //filled later
u_ires: vec2.create()
};
}
LGraphTextureLinearDepth.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureLinearDepth.title = "Linear Depth";
LGraphTextureLinearDepth.desc = "Creates a color texture with linear depth";
LGraphTextureLinearDepth.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var tex = this.getInputData(0);
if (!tex || (tex.format != gl.DEPTH_COMPONENT && tex.format != gl.DEPTH_STENCIL) ) {
return;
}
var precision = this.properties.precision == LGraphTexture.HIGH ? gl.HIGH_PRECISION_FORMAT : gl.UNSIGNED_BYTE;
if ( !this._temp_texture || this._temp_texture.type != precision || this._temp_texture.width != tex.width || this._temp_texture.height != tex.height ) {
this._temp_texture = new GL.Texture(tex.width, tex.height, {
type: precision,
format: gl.RGB,
filter: gl.LINEAR
});
}
var uniforms = this._uniforms;
uniforms.u_invert = this.properties.invert ? 1 : 0;
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = Mesh.getScreenQuad();
if(!LGraphTextureLinearDepth._shader)
LGraphTextureLinearDepth._shader = new GL.Shader( GL.Shader.SCREEN_VERTEX_SHADER, LGraphTextureLinearDepth.pixel_shader);
var shader = LGraphTextureLinearDepth._shader;
//NEAR AND FAR PLANES
var planes = null;
if (tex.near_far_planes) {
planes = tex.near_far_planes;
} else if (window.LS && LS.Renderer._main_camera) {
planes = LS.Renderer._main_camera._uniforms.u_camera_planes;
} else {
planes = [0.1, 1000];
} //hardcoded
uniforms.u_camera_planes = planes;
//uniforms.u_ires.set([1/tex.width, 1/tex.height]);
uniforms.u_ires.set([0,0]);
this._temp_texture.drawTo(function() {
tex.bind(0);
shader.uniforms(uniforms).draw(mesh);
});
this._temp_texture.near_far_planes = planes;
this.setOutputData(0, this._temp_texture);
};
LGraphTextureLinearDepth.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_camera_planes;\n\
uniform int u_invert;\n\
uniform vec2 u_ires;\n\
\n\
void main() {\n\
float zNear = u_camera_planes.x;\n\
float zFar = u_camera_planes.y;\n\
float depth = texture2D(u_texture, v_coord + u_ires*0.5).x * 2.0 - 1.0;\n\
float f = zNear * (depth + 1.0) / (zFar + zNear - depth * (zFar - zNear));\n\
if( u_invert == 1 )\n\
f = 1.0 - f;\n\
gl_FragColor = vec4(vec3(f),1.0);\n\
}\n\
";
LiteGraph.registerNodeType( "texture/linear_depth", LGraphTextureLinearDepth );
// Texture Blur *****************************************
function LGraphTextureBlur() {
this.addInput("Texture", "Texture");
this.addInput("Iterations", "number");
this.addInput("Intensity", "number");
this.addOutput("Blurred", "Texture");
this.properties = {
intensity: 1,
iterations: 1,
preserve_aspect: false,
scale: [1, 1],
precision: LGraphTexture.DEFAULT
};
}
LGraphTextureBlur.title = "Blur";
LGraphTextureBlur.desc = "Blur a texture";
LGraphTextureBlur.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureBlur.max_iterations = 20;
LGraphTextureBlur.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (!this.isOutputConnected(0)) {
return;
} //saves work
var temp = this._final_texture;
if (
!temp ||
temp.width != tex.width ||
temp.height != tex.height ||
temp.type != tex.type
) {
//we need two textures to do the blurring
//this._temp_texture = new GL.Texture( tex.width, tex.height, { type: tex.type, format: gl.RGBA, filter: gl.LINEAR });
temp = this._final_texture = new GL.Texture(
tex.width,
tex.height,
{ type: tex.type, format: gl.RGBA, filter: gl.LINEAR }
);
}
//iterations
var iterations = this.properties.iterations;
if (this.isInputConnected(1)) {
iterations = this.getInputData(1);
this.properties.iterations = iterations;
}
iterations = Math.min(
Math.floor(iterations),
LGraphTextureBlur.max_iterations
);
if (iterations == 0) {
//skip blurring
this.setOutputData(0, tex);
return;
}
var intensity = this.properties.intensity;
if (this.isInputConnected(2)) {
intensity = this.getInputData(2);
this.properties.intensity = intensity;
}
//blur sometimes needs an aspect correction
var aspect = LiteGraph.camera_aspect;
if (!aspect && window.gl !== undefined) {
aspect = gl.canvas.height / gl.canvas.width;
}
if (!aspect) {
aspect = 1;
}
aspect = this.properties.preserve_aspect ? aspect : 1;
var scale = this.properties.scale || [1, 1];
tex.applyBlur(aspect * scale[0], scale[1], intensity, temp);
for (var i = 1; i < iterations; ++i) {
temp.applyBlur(
aspect * scale[0] * (i + 1),
scale[1] * (i + 1),
intensity
);
}
this.setOutputData(0, temp);
};
/*
LGraphTextureBlur.pixel_shader = "precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_offset;\n\
uniform float u_intensity;\n\
void main() {\n\
vec4 sum = vec4(0.0);\n\
vec4 center = texture2D(u_texture, v_coord);\n\
sum += texture2D(u_texture, v_coord + u_offset * -4.0) * 0.05/0.98;\n\
sum += texture2D(u_texture, v_coord + u_offset * -3.0) * 0.09/0.98;\n\
sum += texture2D(u_texture, v_coord + u_offset * -2.0) * 0.12/0.98;\n\
sum += texture2D(u_texture, v_coord + u_offset * -1.0) * 0.15/0.98;\n\
sum += center * 0.16/0.98;\n\
sum += texture2D(u_texture, v_coord + u_offset * 4.0) * 0.05/0.98;\n\
sum += texture2D(u_texture, v_coord + u_offset * 3.0) * 0.09/0.98;\n\
sum += texture2D(u_texture, v_coord + u_offset * 2.0) * 0.12/0.98;\n\
sum += texture2D(u_texture, v_coord + u_offset * 1.0) * 0.15/0.98;\n\
gl_FragColor = u_intensity * sum;\n\
}\n\
";
*/
LiteGraph.registerNodeType("texture/blur", LGraphTextureBlur);
//Independent glow FX
//based on https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/
function FXGlow()
{
this.intensity = 0.5;
this.persistence = 0.6;
this.iterations = 8;
this.threshold = 0.8;
this.scale = 1;
this.dirt_texture = null;
this.dirt_factor = 0.5;
this._textures = [];
this._uniforms = {
u_intensity: 1,
u_texture: 0,
u_glow_texture: 1,
u_threshold: 0,
u_texel_size: vec2.create()
};
}
FXGlow.prototype.applyFX = function( tex, output_texture, glow_texture, average_texture ) {
var width = tex.width;
var height = tex.height;
var texture_info = {
format: tex.format,
type: tex.type,
minFilter: GL.LINEAR,
magFilter: GL.LINEAR,
wrap: gl.CLAMP_TO_EDGE
};
var uniforms = this._uniforms;
var textures = this._textures;
//cut
var shader = FXGlow._cut_shader;
if (!shader) {
shader = FXGlow._cut_shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
FXGlow.cut_pixel_shader
);
}
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
uniforms.u_threshold = this.threshold;
var currentDestination = (textures[0] = GL.Texture.getTemporary(
width,
height,
texture_info
));
tex.blit( currentDestination, shader.uniforms(uniforms) );
var currentSource = currentDestination;
var iterations = this.iterations;
iterations = clamp(iterations, 1, 16) | 0;
var texel_size = uniforms.u_texel_size;
var intensity = this.intensity;
uniforms.u_intensity = 1;
uniforms.u_delta = this.scale; //1
//downscale/upscale shader
var shader = FXGlow._shader;
if (!shader) {
shader = FXGlow._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
FXGlow.scale_pixel_shader
);
}
var i = 1;
//downscale
for (; i < iterations; i++) {
width = width >> 1;
if ((height | 0) > 1) {
height = height >> 1;
}
if (width < 2) {
break;
}
currentDestination = textures[i] = GL.Texture.getTemporary(
width,
height,
texture_info
);
texel_size[0] = 1 / currentSource.width;
texel_size[1] = 1 / currentSource.height;
currentSource.blit(
currentDestination,
shader.uniforms(uniforms)
);
currentSource = currentDestination;
}
//average
if (average_texture) {
texel_size[0] = 1 / currentSource.width;
texel_size[1] = 1 / currentSource.height;
uniforms.u_intensity = intensity;
uniforms.u_delta = 1;
currentSource.blit(average_texture, shader.uniforms(uniforms));
}
//upscale and blend
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
uniforms.u_intensity = this.persistence;
uniforms.u_delta = 0.5;
// i-=2 => -1 to point to last element in array, -1 to go to texture above
for ( i -= 2; i >= 0; i-- )
{
currentDestination = textures[i];
textures[i] = null;
texel_size[0] = 1 / currentSource.width;
texel_size[1] = 1 / currentSource.height;
currentSource.blit(
currentDestination,
shader.uniforms(uniforms)
);
GL.Texture.releaseTemporary(currentSource);
currentSource = currentDestination;
}
gl.disable(gl.BLEND);
//glow
if (glow_texture) {
currentSource.blit(glow_texture);
}
//final composition
if ( output_texture ) {
var final_texture = output_texture;
var dirt_texture = this.dirt_texture;
var dirt_factor = this.dirt_factor;
uniforms.u_intensity = intensity;
shader = dirt_texture
? FXGlow._dirt_final_shader
: FXGlow._final_shader;
if (!shader) {
if (dirt_texture) {
shader = FXGlow._dirt_final_shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
FXGlow.final_pixel_shader,
{ USE_DIRT: "" }
);
} else {
shader = FXGlow._final_shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
FXGlow.final_pixel_shader
);
}
}
final_texture.drawTo(function() {
tex.bind(0);
currentSource.bind(1);
if (dirt_texture) {
shader.setUniform("u_dirt_factor", dirt_factor);
shader.setUniform(
"u_dirt_texture",
dirt_texture.bind(2)
);
}
shader.toViewport(uniforms);
});
}
GL.Texture.releaseTemporary(currentSource);
};
FXGlow.cut_pixel_shader =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform float u_threshold;\n\
void main() {\n\
gl_FragColor = max( texture2D( u_texture, v_coord ) - vec4( u_threshold ), vec4(0.0) );\n\
}";
FXGlow.scale_pixel_shader =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_texel_size;\n\
uniform float u_delta;\n\
uniform float u_intensity;\n\
\n\
vec4 sampleBox(vec2 uv) {\n\
vec4 o = u_texel_size.xyxy * vec2(-u_delta, u_delta).xxyy;\n\
vec4 s = texture2D( u_texture, uv + o.xy ) + texture2D( u_texture, uv + o.zy) + texture2D( u_texture, uv + o.xw) + texture2D( u_texture, uv + o.zw);\n\
return s * 0.25;\n\
}\n\
void main() {\n\
gl_FragColor = u_intensity * sampleBox( v_coord );\n\
}";
FXGlow.final_pixel_shader =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_glow_texture;\n\
#ifdef USE_DIRT\n\
uniform sampler2D u_dirt_texture;\n\
#endif\n\
uniform vec2 u_texel_size;\n\
uniform float u_delta;\n\
uniform float u_intensity;\n\
uniform float u_dirt_factor;\n\
\n\
vec4 sampleBox(vec2 uv) {\n\
vec4 o = u_texel_size.xyxy * vec2(-u_delta, u_delta).xxyy;\n\
vec4 s = texture2D( u_glow_texture, uv + o.xy ) + texture2D( u_glow_texture, uv + o.zy) + texture2D( u_glow_texture, uv + o.xw) + texture2D( u_glow_texture, uv + o.zw);\n\
return s * 0.25;\n\
}\n\
void main() {\n\
vec4 glow = sampleBox( v_coord );\n\
#ifdef USE_DIRT\n\
glow = mix( glow, glow * texture2D( u_dirt_texture, v_coord ), u_dirt_factor );\n\
#endif\n\
gl_FragColor = texture2D( u_texture, v_coord ) + u_intensity * glow;\n\
}";
// Texture Glow *****************************************
function LGraphTextureGlow() {
this.addInput("in", "Texture");
this.addInput("dirt", "Texture");
this.addOutput("out", "Texture");
this.addOutput("glow", "Texture");
this.properties = {
enabled: true,
intensity: 1,
persistence: 0.99,
iterations: 16,
threshold: 0,
scale: 1,
dirt_factor: 0.5,
precision: LGraphTexture.DEFAULT
};
this.fx = new FXGlow();
}
LGraphTextureGlow.title = "Glow";
LGraphTextureGlow.desc = "Filters a texture giving it a glow effect";
LGraphTextureGlow.widgets_info = {
iterations: {
type: "number",
min: 0,
max: 16,
step: 1,
precision: 0
},
threshold: {
type: "number",
min: 0,
max: 10,
step: 0.01,
precision: 2
},
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureGlow.prototype.onGetInputs = function() {
return [
["enabled", "boolean"],
["threshold", "number"],
["intensity", "number"],
["persistence", "number"],
["iterations", "number"],
["dirt_factor", "number"]
];
};
LGraphTextureGlow.prototype.onGetOutputs = function() {
return [["average", "Texture"]];
};
LGraphTextureGlow.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (!this.isAnyOutputConnected()) {
return;
} //saves work
if (
this.properties.precision === LGraphTexture.PASS_THROUGH ||
this.getInputOrProperty("enabled") === false
) {
this.setOutputData(0, tex);
return;
}
var width = tex.width;
var height = tex.height;
var fx = this.fx;
fx.threshold = this.getInputOrProperty("threshold");
fx.iterations = this.getInputOrProperty("iterations");
fx.intensity = this.getInputOrProperty("intensity");
fx.persistence = this.getInputOrProperty("persistence");
fx.dirt_texture = this.getInputData(1);
fx.dirt_factor = this.getInputOrProperty("dirt_factor");
fx.scale = this.properties.scale;
var type = LGraphTexture.getTextureType( this.properties.precision, tex );
var average_texture = null;
if (this.isOutputConnected(2)) {
average_texture = this._average_texture;
if (
!average_texture ||
average_texture.type != tex.type ||
average_texture.format != tex.format
) {
average_texture = this._average_texture = new GL.Texture(
1,
1,
{
type: tex.type,
format: tex.format,
filter: gl.LINEAR
}
);
}
}
var glow_texture = null;
if (this.isOutputConnected(1)) {
glow_texture = this._glow_texture;
if (
!glow_texture ||
glow_texture.width != tex.width ||
glow_texture.height != tex.height ||
glow_texture.type != type ||
glow_texture.format != tex.format
) {
glow_texture = this._glow_texture = new GL.Texture(
tex.width,
tex.height,
{ type: type, format: tex.format, filter: gl.LINEAR }
);
}
}
var final_texture = null;
if (this.isOutputConnected(0)) {
final_texture = this._final_texture;
if (
!final_texture ||
final_texture.width != tex.width ||
final_texture.height != tex.height ||
final_texture.type != type ||
final_texture.format != tex.format
) {
final_texture = this._final_texture = new GL.Texture(
tex.width,
tex.height,
{ type: type, format: tex.format, filter: gl.LINEAR }
);
}
}
//apply FX
fx.applyFX(tex, final_texture, glow_texture, average_texture );
if (this.isOutputConnected(0))
this.setOutputData(0, final_texture);
if (this.isOutputConnected(1))
this.setOutputData(1, average_texture);
if (this.isOutputConnected(2))
this.setOutputData(2, glow_texture);
};
LiteGraph.registerNodeType("texture/glow", LGraphTextureGlow);
// Texture Filter *****************************************
function LGraphTextureKuwaharaFilter() {
this.addInput("Texture", "Texture");
this.addOutput("Filtered", "Texture");
this.properties = { intensity: 1, radius: 5 };
}
LGraphTextureKuwaharaFilter.title = "Kuwahara Filter";
LGraphTextureKuwaharaFilter.desc =
"Filters a texture giving an artistic oil canvas painting";
LGraphTextureKuwaharaFilter.max_radius = 10;
LGraphTextureKuwaharaFilter._shaders = [];
LGraphTextureKuwaharaFilter.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (!this.isOutputConnected(0)) {
return;
} //saves work
var temp = this._temp_texture;
if (
!temp ||
temp.width != tex.width ||
temp.height != tex.height ||
temp.type != tex.type
) {
this._temp_texture = new GL.Texture(tex.width, tex.height, {
type: tex.type,
format: gl.RGBA,
filter: gl.LINEAR
});
}
//iterations
var radius = this.properties.radius;
radius = Math.min(
Math.floor(radius),
LGraphTextureKuwaharaFilter.max_radius
);
if (radius == 0) {
//skip blurring
this.setOutputData(0, tex);
return;
}
var intensity = this.properties.intensity;
//blur sometimes needs an aspect correction
var aspect = LiteGraph.camera_aspect;
if (!aspect && window.gl !== undefined) {
aspect = gl.canvas.height / gl.canvas.width;
}
if (!aspect) {
aspect = 1;
}
aspect = this.properties.preserve_aspect ? aspect : 1;
if (!LGraphTextureKuwaharaFilter._shaders[radius]) {
LGraphTextureKuwaharaFilter._shaders[radius] = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureKuwaharaFilter.pixel_shader,
{ RADIUS: radius.toFixed(0) }
);
}
var shader = LGraphTextureKuwaharaFilter._shaders[radius];
var mesh = GL.Mesh.getScreenQuad();
tex.bind(0);
this._temp_texture.drawTo(function() {
shader
.uniforms({
u_texture: 0,
u_intensity: intensity,
u_resolution: [tex.width, tex.height],
u_iResolution: [1 / tex.width, 1 / tex.height]
})
.draw(mesh);
});
this.setOutputData(0, this._temp_texture);
};
//from https://www.shadertoy.com/view/MsXSz4
LGraphTextureKuwaharaFilter.pixel_shader =
"\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform float u_intensity;\n\
uniform vec2 u_resolution;\n\
uniform vec2 u_iResolution;\n\
#ifndef RADIUS\n\
#define RADIUS 7\n\
#endif\n\
void main() {\n\
\n\
const int radius = RADIUS;\n\
vec2 fragCoord = v_coord;\n\
vec2 src_size = u_iResolution;\n\
vec2 uv = v_coord;\n\
float n = float((radius + 1) * (radius + 1));\n\
int i;\n\
int j;\n\
vec3 m0 = vec3(0.0); vec3 m1 = vec3(0.0); vec3 m2 = vec3(0.0); vec3 m3 = vec3(0.0);\n\
vec3 s0 = vec3(0.0); vec3 s1 = vec3(0.0); vec3 s2 = vec3(0.0); vec3 s3 = vec3(0.0);\n\
vec3 c;\n\
\n\
for (int j = -radius; j <= 0; ++j) {\n\
for (int i = -radius; i <= 0; ++i) {\n\
c = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\
m0 += c;\n\
s0 += c * c;\n\
}\n\
}\n\
\n\
for (int j = -radius; j <= 0; ++j) {\n\
for (int i = 0; i <= radius; ++i) {\n\
c = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\
m1 += c;\n\
s1 += c * c;\n\
}\n\
}\n\
\n\
for (int j = 0; j <= radius; ++j) {\n\
for (int i = 0; i <= radius; ++i) {\n\
c = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\
m2 += c;\n\
s2 += c * c;\n\
}\n\
}\n\
\n\
for (int j = 0; j <= radius; ++j) {\n\
for (int i = -radius; i <= 0; ++i) {\n\
c = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\
m3 += c;\n\
s3 += c * c;\n\
}\n\
}\n\
\n\
float min_sigma2 = 1e+2;\n\
m0 /= n;\n\
s0 = abs(s0 / n - m0 * m0);\n\
\n\
float sigma2 = s0.r + s0.g + s0.b;\n\
if (sigma2 < min_sigma2) {\n\
min_sigma2 = sigma2;\n\
gl_FragColor = vec4(m0, 1.0);\n\
}\n\
\n\
m1 /= n;\n\
s1 = abs(s1 / n - m1 * m1);\n\
\n\
sigma2 = s1.r + s1.g + s1.b;\n\
if (sigma2 < min_sigma2) {\n\
min_sigma2 = sigma2;\n\
gl_FragColor = vec4(m1, 1.0);\n\
}\n\
\n\
m2 /= n;\n\
s2 = abs(s2 / n - m2 * m2);\n\
\n\
sigma2 = s2.r + s2.g + s2.b;\n\
if (sigma2 < min_sigma2) {\n\
min_sigma2 = sigma2;\n\
gl_FragColor = vec4(m2, 1.0);\n\
}\n\
\n\
m3 /= n;\n\
s3 = abs(s3 / n - m3 * m3);\n\
\n\
sigma2 = s3.r + s3.g + s3.b;\n\
if (sigma2 < min_sigma2) {\n\
min_sigma2 = sigma2;\n\
gl_FragColor = vec4(m3, 1.0);\n\
}\n\
}\n\
";
LiteGraph.registerNodeType(
"texture/kuwahara",
LGraphTextureKuwaharaFilter
);
// Texture *****************************************
function LGraphTextureXDoGFilter() {
this.addInput("Texture", "Texture");
this.addOutput("Filtered", "Texture");
this.properties = {
sigma: 1.4,
k: 1.6,
p: 21.7,
epsilon: 79,
phi: 0.017
};
}
LGraphTextureXDoGFilter.title = "XDoG Filter";
LGraphTextureXDoGFilter.desc =
"Filters a texture giving an artistic ink style";
LGraphTextureXDoGFilter.max_radius = 10;
LGraphTextureXDoGFilter._shaders = [];
LGraphTextureXDoGFilter.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (!this.isOutputConnected(0)) {
return;
} //saves work
var temp = this._temp_texture;
if (
!temp ||
temp.width != tex.width ||
temp.height != tex.height ||
temp.type != tex.type
) {
this._temp_texture = new GL.Texture(tex.width, tex.height, {
type: tex.type,
format: gl.RGBA,
filter: gl.LINEAR
});
}
if (!LGraphTextureXDoGFilter._xdog_shader) {
LGraphTextureXDoGFilter._xdog_shader = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphTextureXDoGFilter.xdog_pixel_shader
);
}
var shader = LGraphTextureXDoGFilter._xdog_shader;
var mesh = GL.Mesh.getScreenQuad();
var sigma = this.properties.sigma;
var k = this.properties.k;
var p = this.properties.p;
var epsilon = this.properties.epsilon;
var phi = this.properties.phi;
tex.bind(0);
this._temp_texture.drawTo(function() {
shader
.uniforms({
src: 0,
sigma: sigma,
k: k,
p: p,
epsilon: epsilon,
phi: phi,
cvsWidth: tex.width,
cvsHeight: tex.height
})
.draw(mesh);
});
this.setOutputData(0, this._temp_texture);
};
//from https://github.com/RaymondMcGuire/GPU-Based-Image-Processing-Tools/blob/master/lib_webgl/scripts/main.js
LGraphTextureXDoGFilter.xdog_pixel_shader =
"\n\
precision highp float;\n\
uniform sampler2D src;\n\n\
uniform float cvsHeight;\n\
uniform float cvsWidth;\n\n\
uniform float sigma;\n\
uniform float k;\n\
uniform float p;\n\
uniform float epsilon;\n\
uniform float phi;\n\
varying vec2 v_coord;\n\n\
float cosh(float val)\n\
{\n\
float tmp = exp(val);\n\
float cosH = (tmp + 1.0 / tmp) / 2.0;\n\
return cosH;\n\
}\n\n\
float tanh(float val)\n\
{\n\
float tmp = exp(val);\n\
float tanH = (tmp - 1.0 / tmp) / (tmp + 1.0 / tmp);\n\
return tanH;\n\
}\n\n\
float sinh(float val)\n\
{\n\
float tmp = exp(val);\n\
float sinH = (tmp - 1.0 / tmp) / 2.0;\n\
return sinH;\n\
}\n\n\
void main(void){\n\
vec3 destColor = vec3(0.0);\n\
float tFrag = 1.0 / cvsHeight;\n\
float sFrag = 1.0 / cvsWidth;\n\
vec2 Frag = vec2(sFrag,tFrag);\n\
vec2 uv = gl_FragCoord.st;\n\
float twoSigmaESquared = 2.0 * sigma * sigma;\n\
float twoSigmaRSquared = twoSigmaESquared * k * k;\n\
int halfWidth = int(ceil( 1.0 * sigma * k ));\n\n\
const int MAX_NUM_ITERATION = 99999;\n\
vec2 sum = vec2(0.0);\n\
vec2 norm = vec2(0.0);\n\n\
for(int cnt=0;cnt<MAX_NUM_ITERATION;cnt++){\n\
if(cnt > (2*halfWidth+1)*(2*halfWidth+1)){break;}\n\
int i = int(cnt / (2*halfWidth+1)) - halfWidth;\n\
int j = cnt - halfWidth - int(cnt / (2*halfWidth+1)) * (2*halfWidth+1);\n\n\
float d = length(vec2(i,j));\n\
vec2 kernel = vec2( exp( -d * d / twoSigmaESquared ), \n\
exp( -d * d / twoSigmaRSquared ));\n\n\
vec2 L = texture2D(src, (uv + vec2(i,j)) * Frag).xx;\n\n\
norm += kernel;\n\
sum += kernel * L;\n\
}\n\n\
sum /= norm;\n\n\
float H = 100.0 * ((1.0 + p) * sum.x - p * sum.y);\n\
float edge = ( H > epsilon )? 1.0 : 1.0 + tanh( phi * (H - epsilon));\n\
destColor = vec3(edge);\n\
gl_FragColor = vec4(destColor, 1.0);\n\
}";
LiteGraph.registerNodeType("texture/xDoG", LGraphTextureXDoGFilter);
// Texture Webcam *****************************************
function LGraphTextureWebcam() {
this.addOutput("Webcam", "Texture");
this.properties = { texture_name: "", facingMode: "user" };
this.boxcolor = "black";
this.version = 0;
}
LGraphTextureWebcam.title = "Webcam";
LGraphTextureWebcam.desc = "Webcam texture";
LGraphTextureWebcam.is_webcam_open = false;
LGraphTextureWebcam.prototype.openStream = function() {
if (!navigator.getUserMedia) {
//console.log('getUserMedia() is not supported in your browser, use chrome and enable WebRTC from about://flags');
return;
}
this._waiting_confirmation = true;
// Not showing vendor prefixes.
var constraints = {
audio: false,
video: { facingMode: this.properties.facingMode }
};
navigator.mediaDevices
.getUserMedia(constraints)
.then(this.streamReady.bind(this))
.catch(onFailSoHard);
var that = this;
function onFailSoHard(e) {
LGraphTextureWebcam.is_webcam_open = false;
console.log("Webcam rejected", e);
that._webcam_stream = false;
that.boxcolor = "red";
that.trigger("stream_error");
}
};
LGraphTextureWebcam.prototype.closeStream = function() {
if (this._webcam_stream) {
var tracks = this._webcam_stream.getTracks();
if (tracks.length) {
for (var i = 0; i < tracks.length; ++i) {
tracks[i].stop();
}
}
LGraphTextureWebcam.is_webcam_open = false;
this._webcam_stream = null;
this._video = null;
this.boxcolor = "black";
this.trigger("stream_closed");
}
};
LGraphTextureWebcam.prototype.streamReady = function(localMediaStream) {
this._webcam_stream = localMediaStream;
//this._waiting_confirmation = false;
this.boxcolor = "green";
var video = this._video;
if (!video) {
video = document.createElement("video");
video.autoplay = true;
video.srcObject = localMediaStream;
this._video = video;
//document.body.appendChild( video ); //debug
//when video info is loaded (size and so)
video.onloadedmetadata = function(e) {
// Ready to go. Do some stuff.
LGraphTextureWebcam.is_webcam_open = true;
console.log(e);
};
}
this.trigger("stream_ready", video);
};
LGraphTextureWebcam.prototype.onPropertyChanged = function(
name,
value
) {
if (name == "facingMode") {
this.properties.facingMode = value;
this.closeStream();
this.openStream();
}
};
LGraphTextureWebcam.prototype.onRemoved = function() {
if (!this._webcam_stream) {
return;
}
var tracks = this._webcam_stream.getTracks();
if (tracks.length) {
for (var i = 0; i < tracks.length; ++i) {
tracks[i].stop();
}
}
this._webcam_stream = null;
this._video = null;
};
LGraphTextureWebcam.prototype.onDrawBackground = function(ctx) {
if (this.flags.collapsed || this.size[1] <= 20) {
return;
}
if (!this._video) {
return;
}
//render to graph canvas
ctx.save();
if (!ctx.webgl) {
//reverse image
ctx.drawImage(this._video, 0, 0, this.size[0], this.size[1]);
} else {
if (this._video_texture) {
ctx.drawImage(
this._video_texture,
0,
0,
this.size[0],
this.size[1]
);
}
}
ctx.restore();
};
LGraphTextureWebcam.prototype.onExecute = function() {
if (this._webcam_stream == null && !this._waiting_confirmation) {
this.openStream();
}
if (!this._video || !this._video.videoWidth) {
return;
}
var width = this._video.videoWidth;
var height = this._video.videoHeight;
var temp = this._video_texture;
if (!temp || temp.width != width || temp.height != height) {
this._video_texture = new GL.Texture(width, height, {
format: gl.RGB,
filter: gl.LINEAR
});
}
this._video_texture.uploadImage(this._video);
this._video_texture.version = ++this.version;
if (this.properties.texture_name) {
var container = LGraphTexture.getTexturesContainer();
container[this.properties.texture_name] = this._video_texture;
}
this.setOutputData(0, this._video_texture);
for (var i = 1; i < this.outputs.length; ++i) {
if (!this.outputs[i]) {
continue;
}
switch (this.outputs[i].name) {
case "width":
this.setOutputData(i, this._video.videoWidth);
break;
case "height":
this.setOutputData(i, this._video.videoHeight);
break;
}
}
};
LGraphTextureWebcam.prototype.onGetOutputs = function() {
return [
["width", "number"],
["height", "number"],
["stream_ready", LiteGraph.EVENT],
["stream_closed", LiteGraph.EVENT],
["stream_error", LiteGraph.EVENT]
];
};
LiteGraph.registerNodeType("texture/webcam", LGraphTextureWebcam);
//from https://github.com/spite/Wagner
function LGraphLensFX() {
this.addInput("in", "Texture");
this.addInput("f", "number");
this.addOutput("out", "Texture");
this.properties = {
enabled: true,
factor: 1,
precision: LGraphTexture.LOW
};
this._uniforms = { u_texture: 0, u_factor: 1 };
}
LGraphLensFX.title = "Lens FX";
LGraphLensFX.desc = "distortion and chromatic aberration";
LGraphLensFX.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphLensFX.prototype.onGetInputs = function() {
return [["enabled", "boolean"]];
};
LGraphLensFX.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (!this.isOutputConnected(0)) {
return;
} //saves work
if (
this.properties.precision === LGraphTexture.PASS_THROUGH ||
this.getInputOrProperty("enabled") === false
) {
this.setOutputData(0, tex);
return;
}
var temp = this._temp_texture;
if (
!temp ||
temp.width != tex.width ||
temp.height != tex.height ||
temp.type != tex.type
) {
temp = this._temp_texture = new GL.Texture(
tex.width,
tex.height,
{ type: tex.type, format: gl.RGBA, filter: gl.LINEAR }
);
}
var shader = LGraphLensFX._shader;
if (!shader) {
shader = LGraphLensFX._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphLensFX.pixel_shader
);
}
var factor = this.getInputData(1);
if (factor == null) {
factor = this.properties.factor;
}
var uniforms = this._uniforms;
uniforms.u_factor = factor;
//apply shader
gl.disable(gl.DEPTH_TEST);
temp.drawTo(function() {
tex.bind(0);
shader.uniforms(uniforms).draw(GL.Mesh.getScreenQuad());
});
this.setOutputData(0, temp);
};
LGraphLensFX.pixel_shader =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform float u_factor;\n\
vec2 barrelDistortion(vec2 coord, float amt) {\n\
vec2 cc = coord - 0.5;\n\
float dist = dot(cc, cc);\n\
return coord + cc * dist * amt;\n\
}\n\
\n\
float sat( float t )\n\
{\n\
return clamp( t, 0.0, 1.0 );\n\
}\n\
\n\
float linterp( float t ) {\n\
return sat( 1.0 - abs( 2.0*t - 1.0 ) );\n\
}\n\
\n\
float remap( float t, float a, float b ) {\n\
return sat( (t - a) / (b - a) );\n\
}\n\
\n\
vec4 spectrum_offset( float t ) {\n\
vec4 ret;\n\
float lo = step(t,0.5);\n\
float hi = 1.0-lo;\n\
float w = linterp( remap( t, 1.0/6.0, 5.0/6.0 ) );\n\
ret = vec4(lo,1.0,hi, 1.) * vec4(1.0-w, w, 1.0-w, 1.);\n\
\n\
return pow( ret, vec4(1.0/2.2) );\n\
}\n\
\n\
const float max_distort = 2.2;\n\
const int num_iter = 12;\n\
const float reci_num_iter_f = 1.0 / float(num_iter);\n\
\n\
void main()\n\
{ \n\
vec2 uv=v_coord;\n\
vec4 sumcol = vec4(0.0);\n\
vec4 sumw = vec4(0.0); \n\
for ( int i=0; i<num_iter;++i )\n\
{\n\
float t = float(i) * reci_num_iter_f;\n\
vec4 w = spectrum_offset( t );\n\
sumw += w;\n\
sumcol += w * texture2D( u_texture, barrelDistortion(uv, .6 * max_distort*t * u_factor ) );\n\
}\n\
gl_FragColor = sumcol / sumw;\n\
}";
LiteGraph.registerNodeType("texture/lensfx", LGraphLensFX);
function LGraphTextureFromData() {
this.addInput("in", "");
this.properties = { precision: LGraphTexture.LOW, width: 0, height: 0, channels: 1 };
this.addOutput("out", "Texture");
}
LGraphTextureFromData.title = "Data->Tex";
LGraphTextureFromData.desc = "Generates or applies a curve to a texture";
LGraphTextureFromData.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureFromData.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var data = this.getInputData(0);
if(!data)
return;
var channels = this.properties.channels;
var w = this.properties.width;
var h = this.properties.height;
if(!w || !h)
{
w = Math.floor(data.length / channels);
h = 1;
}
var format = gl.RGBA;
if( channels == 3 )
format = gl.RGB;
else if( channels == 1 )
format = gl.LUMINANCE;
var temp = this._temp_texture;
var type = LGraphTexture.getTextureType( this.properties.precision );
if ( !temp || temp.width != w || temp.height != h || temp.type != type ) {
temp = this._temp_texture = new GL.Texture( w, h, { type: type, format: format, filter: gl.LINEAR } );
}
temp.uploadData( data );
this.setOutputData(0, temp);
}
LiteGraph.registerNodeType("texture/fromdata", LGraphTextureFromData);
//applies a curve (or generates one)
function LGraphTextureCurve() {
this.addInput("in", "Texture");
this.addOutput("out", "Texture");
this.properties = { precision: LGraphTexture.LOW, split_channels: false };
this._values = new Uint8Array(256*4);
this._values.fill(255);
this._curve_texture = null;
this._uniforms = { u_texture: 0, u_curve: 1, u_range: 1.0 };
this._must_update = true;
this._points = {
RGB: [[0,0],[1,1]],
R: [[0,0],[1,1]],
G: [[0,0],[1,1]],
B: [[0,0],[1,1]]
};
this.curve_editor = null;
this.addWidget("toggle","Split Channels",false,"split_channels");
this.addWidget("combo","Channel","RGB",{ values:["RGB","R","G","B"]});
this.curve_offset = 68;
this.size = [ 240, 160 ];
}
LGraphTextureCurve.title = "Curve";
LGraphTextureCurve.desc = "Generates or applies a curve to a texture";
LGraphTextureCurve.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureCurve.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var tex = this.getInputData(0);
var temp = this._temp_texture;
if(!tex) //generate one texture, nothing else
{
if(this._must_update || !this._curve_texture )
this.updateCurve();
this.setOutputData(0, this._curve_texture);
return;
}
var type = LGraphTexture.getTextureType( this.properties.precision, tex );
//apply curve to input texture
if ( !temp || temp.type != type || temp.width != tex.width || temp.height != tex.height || temp.format != tex.format)
temp = this._temp_texture = new GL.Texture( tex.width, tex.height, { type: type, format: tex.format, filter: gl.LINEAR } );
var shader = LGraphTextureCurve._shader;
if (!shader) {
shader = LGraphTextureCurve._shader = new GL.Shader( GL.Shader.SCREEN_VERTEX_SHADER, LGraphTextureCurve.pixel_shader );
}
if(this._must_update || !this._curve_texture )
this.updateCurve();
var uniforms = this._uniforms;
var curve_texture = this._curve_texture;
//apply shader
temp.drawTo(function() {
gl.disable(gl.DEPTH_TEST);
tex.bind(0);
curve_texture.bind(1);
shader.uniforms(uniforms).draw(GL.Mesh.getScreenQuad());
});
this.setOutputData(0, temp);
}
LGraphTextureCurve.prototype.sampleCurve = function(f,points)
{
var points = points || this._points.RGB;
if(!points)
return;
for(var i = 0; i < points.length - 1; ++i)
{
var p = points[i];
var pn = points[i+1];
if(pn[0] < f)
continue;
var r = (pn[0] - p[0]);
if( Math.abs(r) < 0.00001 )
return p[1];
var local_f = (f - p[0]) / r;
return p[1] * (1.0 - local_f) + pn[1] * local_f;
}
return 0;
}
LGraphTextureCurve.prototype.updateCurve = function()
{
var values = this._values;
var num = values.length / 4;
var split = this.properties.split_channels;
for(var i = 0; i < num; ++i)
{
if(split)
{
values[i*4] = clamp( this.sampleCurve(i/num,this._points.R)*255,0,255);
values[i*4+1] = clamp( this.sampleCurve(i/num,this._points.G)*255,0,255);
values[i*4+2] = clamp( this.sampleCurve(i/num,this._points.B)*255,0,255);
}
else
{
var v = this.sampleCurve(i/num);//sample curve
values[i*4] = values[i*4+1] = values[i*4+2] = clamp(v*255,0,255);
}
values[i*4+3] = 255; //alpha fixed
}
if(!this._curve_texture)
this._curve_texture = new GL.Texture(256,1,{ format: gl.RGBA, magFilter: gl.LINEAR, wrap: gl.CLAMP_TO_EDGE });
this._curve_texture.uploadData(values,null,true);
}
LGraphTextureCurve.prototype.onSerialize = function(o)
{
var curves = {};
for(var i in this._points)
curves[i] = this._points[i].concat();
o.curves = curves;
}
LGraphTextureCurve.prototype.onConfigure = function(o)
{
this._points = o.curves;
if(this.curve_editor)
curve_editor.points = this._points;
this._must_update = true;
}
LGraphTextureCurve.prototype.onMouseDown = function(e, localpos, graphcanvas)
{
if(this.curve_editor)
{
var r = this.curve_editor.onMouseDown([localpos[0],localpos[1]-this.curve_offset], graphcanvas);
if(r)
this.captureInput(true);
return r;
}
}
LGraphTextureCurve.prototype.onMouseMove = function(e, localpos, graphcanvas)
{
if(this.curve_editor)
return this.curve_editor.onMouseMove([localpos[0],localpos[1]-this.curve_offset], graphcanvas);
}
LGraphTextureCurve.prototype.onMouseUp = function(e, localpos, graphcanvas)
{
if(this.curve_editor)
return this.curve_editor.onMouseUp([localpos[0],localpos[1]-this.curve_offset], graphcanvas);
this.captureInput(false);
}
LGraphTextureCurve.channel_line_colors = { "RGB":"#666","R":"#F33","G":"#3F3","B":"#33F" };
LGraphTextureCurve.prototype.onDrawBackground = function(ctx, graphcanvas)
{
if(this.flags.collapsed)
return;
if(!this.curve_editor)
this.curve_editor = new LiteGraph.CurveEditor(this._points.R);
ctx.save();
ctx.translate(0,this.curve_offset);
var channel = this.widgets[1].value;
if(this.properties.split_channels)
{
if(channel == "RGB")
{
this.widgets[1].value = channel = "R";
this.widgets[1].disabled = false;
}
this.curve_editor.points = this._points.R;
this.curve_editor.draw( ctx, [this.size[0],this.size[1] - this.curve_offset], graphcanvas, "#111", LGraphTextureCurve.channel_line_colors.R, true );
ctx.globalCompositeOperation = "lighten";
this.curve_editor.points = this._points.G;
this.curve_editor.draw( ctx, [this.size[0],this.size[1] - this.curve_offset], graphcanvas, null, LGraphTextureCurve.channel_line_colors.G, true );
this.curve_editor.points = this._points.B;
this.curve_editor.draw( ctx, [this.size[0],this.size[1] - this.curve_offset], graphcanvas, null, LGraphTextureCurve.channel_line_colors.B, true );
ctx.globalCompositeOperation = "source-over";
}
else
{
this.widgets[1].value = channel = "RGB";
this.widgets[1].disabled = true;
}
this.curve_editor.points = this._points[channel];
this.curve_editor.draw( ctx, [this.size[0],this.size[1] - this.curve_offset], graphcanvas, this.properties.split_channels ? null : "#111", LGraphTextureCurve.channel_line_colors[channel] );
ctx.restore();
}
LGraphTextureCurve.pixel_shader =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_curve;\n\
uniform float u_range;\n\
\n\
void main() {\n\
vec4 color = texture2D( u_texture, v_coord ) * u_range;\n\
color.x = texture2D( u_curve, vec2( color.x, 0.5 ) ).x;\n\
color.y = texture2D( u_curve, vec2( color.y, 0.5 ) ).y;\n\
color.z = texture2D( u_curve, vec2( color.z, 0.5 ) ).z;\n\
//color.w = texture2D( u_curve, vec2( color.w, 0.5 ) ).w;\n\
gl_FragColor = color;\n\
}";
LiteGraph.registerNodeType("texture/curve", LGraphTextureCurve);
//simple exposition, but plan to expand it to support different gamma curves
function LGraphExposition() {
this.addInput("in", "Texture");
this.addInput("exp", "number");
this.addOutput("out", "Texture");
this.properties = { exposition: 1, precision: LGraphTexture.LOW };
this._uniforms = { u_texture: 0, u_exposition: 1 };
}
LGraphExposition.title = "Exposition";
LGraphExposition.desc = "Controls texture exposition";
LGraphExposition.widgets_info = {
exposition: { widget: "slider", min: 0, max: 3 },
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphExposition.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (!this.isOutputConnected(0)) {
return;
} //saves work
var temp = this._temp_texture;
if (
!temp ||
temp.width != tex.width ||
temp.height != tex.height ||
temp.type != tex.type
) {
temp = this._temp_texture = new GL.Texture(
tex.width,
tex.height,
{ type: tex.type, format: gl.RGBA, filter: gl.LINEAR }
);
}
var shader = LGraphExposition._shader;
if (!shader) {
shader = LGraphExposition._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphExposition.pixel_shader
);
}
var exp = this.properties.exposition;
var exp_input = this.getInputData(1);
if (exp_input != null) {
exp = this.properties.exposition = exp_input;
}
var uniforms = this._uniforms;
//apply shader
temp.drawTo(function() {
gl.disable(gl.DEPTH_TEST);
tex.bind(0);
shader.uniforms(uniforms).draw(GL.Mesh.getScreenQuad());
});
this.setOutputData(0, temp);
};
LGraphExposition.pixel_shader =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform float u_exposition;\n\
\n\
void main() {\n\
vec4 color = texture2D( u_texture, v_coord );\n\
gl_FragColor = vec4( color.xyz * u_exposition, color.a );\n\
}";
LiteGraph.registerNodeType("texture/exposition", LGraphExposition);
function LGraphToneMapping() {
this.addInput("in", "Texture");
this.addInput("avg", "number,Texture");
this.addOutput("out", "Texture");
this.properties = {
enabled: true,
scale: 1,
gamma: 1,
average_lum: 1,
lum_white: 1,
precision: LGraphTexture.LOW
};
this._uniforms = {
u_texture: 0,
u_lumwhite2: 1,
u_igamma: 1,
u_scale: 1,
u_average_lum: 1
};
}
LGraphToneMapping.title = "Tone Mapping";
LGraphToneMapping.desc =
"Applies Tone Mapping to convert from high to low";
LGraphToneMapping.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphToneMapping.prototype.onGetInputs = function() {
return [["enabled", "boolean"]];
};
LGraphToneMapping.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (!tex) {
return;
}
if (!this.isOutputConnected(0)) {
return;
} //saves work
if (
this.properties.precision === LGraphTexture.PASS_THROUGH ||
this.getInputOrProperty("enabled") === false
) {
this.setOutputData(0, tex);
return;
}
var temp = this._temp_texture;
if (
!temp ||
temp.width != tex.width ||
temp.height != tex.height ||
temp.type != tex.type
) {
temp = this._temp_texture = new GL.Texture(
tex.width,
tex.height,
{ type: tex.type, format: gl.RGBA, filter: gl.LINEAR }
);
}
var avg = this.getInputData(1);
if (avg == null) {
avg = this.properties.average_lum;
}
var uniforms = this._uniforms;
var shader = null;
if (avg.constructor === Number) {
this.properties.average_lum = avg;
uniforms.u_average_lum = this.properties.average_lum;
shader = LGraphToneMapping._shader;
if (!shader) {
shader = LGraphToneMapping._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphToneMapping.pixel_shader
);
}
} else if (avg.constructor === GL.Texture) {
uniforms.u_average_texture = avg.bind(1);
shader = LGraphToneMapping._shader_texture;
if (!shader) {
shader = LGraphToneMapping._shader_texture = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphToneMapping.pixel_shader,
{ AVG_TEXTURE: "" }
);
}
}
uniforms.u_lumwhite2 =
this.properties.lum_white * this.properties.lum_white;
uniforms.u_scale = this.properties.scale;
uniforms.u_igamma = 1 / this.properties.gamma;
//apply shader
gl.disable(gl.DEPTH_TEST);
temp.drawTo(function() {
tex.bind(0);
shader.uniforms(uniforms).draw(GL.Mesh.getScreenQuad());
});
this.setOutputData(0, this._temp_texture);
};
LGraphToneMapping.pixel_shader =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform float u_scale;\n\
#ifdef AVG_TEXTURE\n\
uniform sampler2D u_average_texture;\n\
#else\n\
uniform float u_average_lum;\n\
#endif\n\
uniform float u_lumwhite2;\n\
uniform float u_igamma;\n\
vec3 RGB2xyY (vec3 rgb)\n\
{\n\
const mat3 RGB2XYZ = mat3(0.4124, 0.3576, 0.1805,\n\
0.2126, 0.7152, 0.0722,\n\
0.0193, 0.1192, 0.9505);\n\
vec3 XYZ = RGB2XYZ * rgb;\n\
\n\
float f = (XYZ.x + XYZ.y + XYZ.z);\n\
return vec3(XYZ.x / f,\n\
XYZ.y / f,\n\
XYZ.y);\n\
}\n\
\n\
void main() {\n\
vec4 color = texture2D( u_texture, v_coord );\n\
vec3 rgb = color.xyz;\n\
float average_lum = 0.0;\n\
#ifdef AVG_TEXTURE\n\
vec3 pixel = texture2D(u_average_texture,vec2(0.5)).xyz;\n\
average_lum = (pixel.x + pixel.y + pixel.z) / 3.0;\n\
#else\n\
average_lum = u_average_lum;\n\
#endif\n\
//Ld - this part of the code is the same for both versions\n\
float lum = dot(rgb, vec3(0.2126, 0.7152, 0.0722));\n\
float L = (u_scale / average_lum) * lum;\n\
float Ld = (L * (1.0 + L / u_lumwhite2)) / (1.0 + L);\n\
//first\n\
//vec3 xyY = RGB2xyY(rgb);\n\
//xyY.z *= Ld;\n\
//rgb = xyYtoRGB(xyY);\n\
//second\n\
rgb = (rgb / lum) * Ld;\n\
rgb = max(rgb,vec3(0.001));\n\
rgb = pow( rgb, vec3( u_igamma ) );\n\
gl_FragColor = vec4( rgb, color.a );\n\
}";
LiteGraph.registerNodeType("texture/tonemapping", LGraphToneMapping);
function LGraphTexturePerlin() {
this.addOutput("out", "Texture");
this.properties = {
width: 512,
height: 512,
seed: 0,
persistence: 0.1,
octaves: 8,
scale: 1,
offset: [0, 0],
amplitude: 1,
precision: LGraphTexture.DEFAULT
};
this._key = 0;
this._texture = null;
this._uniforms = {
u_persistence: 0.1,
u_seed: 0,
u_offset: vec2.create(),
u_scale: 1,
u_viewport: vec2.create()
};
}
LGraphTexturePerlin.title = "Perlin";
LGraphTexturePerlin.desc = "Generates a perlin noise texture";
LGraphTexturePerlin.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES },
width: { type: "number", precision: 0, step: 1 },
height: { type: "number", precision: 0, step: 1 },
octaves: { type: "number", precision: 0, step: 1, min: 1, max: 50 }
};
LGraphTexturePerlin.prototype.onGetInputs = function() {
return [
["seed", "number"],
["persistence", "number"],
["octaves", "number"],
["scale", "number"],
["amplitude", "number"],
["offset", "vec2"]
];
};
LGraphTexturePerlin.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var w = this.properties.width | 0;
var h = this.properties.height | 0;
if (w == 0) {
w = gl.viewport_data[2];
} //0 means default
if (h == 0) {
h = gl.viewport_data[3];
} //0 means default
var type = LGraphTexture.getTextureType(this.properties.precision);
var temp = this._texture;
if (
!temp ||
temp.width != w ||
temp.height != h ||
temp.type != type
) {
temp = this._texture = new GL.Texture(w, h, {
type: type,
format: gl.RGB,
filter: gl.LINEAR
});
}
var persistence = this.getInputOrProperty("persistence");
var octaves = this.getInputOrProperty("octaves");
var offset = this.getInputOrProperty("offset");
var scale = this.getInputOrProperty("scale");
var amplitude = this.getInputOrProperty("amplitude");
var seed = this.getInputOrProperty("seed");
//reusing old texture
var key =
"" +
w +
h +
type +
persistence +
octaves +
scale +
seed +
offset[0] +
offset[1] +
amplitude;
if (key == this._key) {
this.setOutputData(0, temp);
return;
}
this._key = key;
//gather uniforms
var uniforms = this._uniforms;
uniforms.u_persistence = persistence;
uniforms.u_octaves = octaves;
uniforms.u_offset.set(offset);
uniforms.u_scale = scale;
uniforms.u_amplitude = amplitude;
uniforms.u_seed = seed * 128;
uniforms.u_viewport[0] = w;
uniforms.u_viewport[1] = h;
//render
var shader = LGraphTexturePerlin._shader;
if (!shader) {
shader = LGraphTexturePerlin._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphTexturePerlin.pixel_shader
);
}
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
temp.drawTo(function() {
shader.uniforms(uniforms).draw(GL.Mesh.getScreenQuad());
});
this.setOutputData(0, temp);
};
LGraphTexturePerlin.pixel_shader =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform vec2 u_offset;\n\
uniform float u_scale;\n\
uniform float u_persistence;\n\
uniform int u_octaves;\n\
uniform float u_amplitude;\n\
uniform vec2 u_viewport;\n\
uniform float u_seed;\n\
#define M_PI 3.14159265358979323846\n\
\n\
float rand(vec2 c){ return fract(sin(dot(c.xy ,vec2( 12.9898 + u_seed,78.233 + u_seed))) * 43758.5453); }\n\
\n\
float noise(vec2 p, float freq ){\n\
float unit = u_viewport.x/freq;\n\
vec2 ij = floor(p/unit);\n\
vec2 xy = mod(p,unit)/unit;\n\
//xy = 3.*xy*xy-2.*xy*xy*xy;\n\
xy = .5*(1.-cos(M_PI*xy));\n\
float a = rand((ij+vec2(0.,0.)));\n\
float b = rand((ij+vec2(1.,0.)));\n\
float c = rand((ij+vec2(0.,1.)));\n\
float d = rand((ij+vec2(1.,1.)));\n\
float x1 = mix(a, b, xy.x);\n\
float x2 = mix(c, d, xy.x);\n\
return mix(x1, x2, xy.y);\n\
}\n\
\n\
float pNoise(vec2 p, int res){\n\
float persistance = u_persistence;\n\
float n = 0.;\n\
float normK = 0.;\n\
float f = 4.;\n\
float amp = 1.0;\n\
int iCount = 0;\n\
for (int i = 0; i<50; i++){\n\
n+=amp*noise(p, f);\n\
f*=2.;\n\
normK+=amp;\n\
amp*=persistance;\n\
if (iCount >= res)\n\
break;\n\
iCount++;\n\
}\n\
float nf = n/normK;\n\
return nf*nf*nf*nf;\n\
}\n\
void main() {\n\
vec2 uv = v_coord * u_scale * u_viewport + u_offset * u_scale;\n\
vec4 color = vec4( pNoise( uv, u_octaves ) * u_amplitude );\n\
gl_FragColor = color;\n\
}";
LiteGraph.registerNodeType("texture/perlin", LGraphTexturePerlin);
function LGraphTextureCanvas2D() {
this.addInput("v");
this.addOutput("out", "Texture");
this.properties = {
code: LGraphTextureCanvas2D.default_code,
width: 512,
height: 512,
clear: true,
precision: LGraphTexture.DEFAULT,
use_html_canvas: false
};
this._func = null;
this._temp_texture = null;
this.compileCode();
}
LGraphTextureCanvas2D.title = "Canvas2D";
LGraphTextureCanvas2D.desc = "Executes Canvas2D code inside a texture or the viewport.";
LGraphTextureCanvas2D.help = "Set width and height to 0 to match viewport size.";
LGraphTextureCanvas2D.default_code = "//vars: canvas,ctx,time\nctx.fillStyle='red';\nctx.fillRect(0,0,50,50);\n";
LGraphTextureCanvas2D.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES },
code: { type: "code" },
width: { type: "number", precision: 0, step: 1 },
height: { type: "number", precision: 0, step: 1 }
};
LGraphTextureCanvas2D.prototype.onPropertyChanged = function( name, value ) {
if (name == "code" )
this.compileCode( value );
}
LGraphTextureCanvas2D.prototype.compileCode = function( code ) {
this._func = null;
if( !LiteGraph.allow_scripts )
return;
try {
this._func = new Function( "canvas", "ctx", "time", "script","v", code );
this.boxcolor = "#00FF00";
} catch (err) {
this.boxcolor = "#FF0000";
console.error("Error parsing script");
console.error(err);
}
};
LGraphTextureCanvas2D.prototype.onExecute = function() {
var func = this._func;
if (!func || !this.isOutputConnected(0)) {
return;
}
this.executeDraw( func );
}
LGraphTextureCanvas2D.prototype.executeDraw = function( func_context ) {
var width = this.properties.width || gl.canvas.width;
var height = this.properties.height || gl.canvas.height;
var temp = this._temp_texture;
var type = LGraphTexture.getTextureType( this.properties.precision );
if (!temp || temp.width != width || temp.height != height || temp.type != type ) {
temp = this._temp_texture = new GL.Texture(width, height, {
format: gl.RGBA,
filter: gl.LINEAR,
type: type
});
}
var v = this.getInputData(0);
var properties = this.properties;
var that = this;
var time = this.graph.getTime();
var ctx = gl;
var canvas = gl.canvas;
if( this.properties.use_html_canvas || !global.enableWebGLCanvas )
{
if(!this._canvas)
{
canvas = this._canvas = createCanvas(width.height);
ctx = this._ctx = canvas.getContext("2d");
}
else
{
canvas = this._canvas;
ctx = this._ctx;
}
canvas.width = width;
canvas.height = height;
}
if(ctx == gl) //using Canvas2DtoWebGL
temp.drawTo(function() {
gl.start2D();
if(properties.clear)
{
gl.clearColor(0,0,0,0);
gl.clear( gl.COLOR_BUFFER_BIT );
}
try {
if (func_context.draw) {
func_context.draw.call(that, canvas, ctx, time, func_context, v);
} else {
func_context.call(that, canvas, ctx, time, func_context,v);
}
that.boxcolor = "#00FF00";
} catch (err) {
that.boxcolor = "#FF0000";
console.error("Error executing script");
console.error(err);
}
gl.finish2D();
});
else //rendering to offscreen canvas and uploading to texture
{
if(properties.clear)
ctx.clearRect(0,0,canvas.width,canvas.height);
try {
if (func_context.draw) {
func_context.draw.call(this, canvas, ctx, time, func_context, v);
} else {
func_context.call(this, canvas, ctx, time, func_context,v);
}
this.boxcolor = "#00FF00";
} catch (err) {
this.boxcolor = "#FF0000";
console.error("Error executing script");
console.error(err);
}
temp.uploadImage( canvas );
}
this.setOutputData(0, temp);
};
LiteGraph.registerNodeType("texture/canvas2D", LGraphTextureCanvas2D);
// To do chroma keying *****************
function LGraphTextureMatte() {
this.addInput("in", "Texture");
this.addOutput("out", "Texture");
this.properties = {
key_color: vec3.fromValues(0, 1, 0),
threshold: 0.8,
slope: 0.2,
precision: LGraphTexture.DEFAULT
};
}
LGraphTextureMatte.title = "Matte";
LGraphTextureMatte.desc = "Extracts background";
LGraphTextureMatte.widgets_info = {
key_color: { widget: "color" },
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphTextureMatte.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var tex = this.getInputData(0);
if (this.properties.precision === LGraphTexture.PASS_THROUGH) {
this.setOutputData(0, tex);
return;
}
if (!tex) {
return;
}
this._tex = LGraphTexture.getTargetTexture(
tex,
this._tex,
this.properties.precision
);
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
if (!this._uniforms) {
this._uniforms = {
u_texture: 0,
u_key_color: this.properties.key_color,
u_threshold: 1,
u_slope: 1
};
}
var uniforms = this._uniforms;
var mesh = Mesh.getScreenQuad();
var shader = LGraphTextureMatte._shader;
if (!shader) {
shader = LGraphTextureMatte._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphTextureMatte.pixel_shader
);
}
uniforms.u_key_color = this.properties.key_color;
uniforms.u_threshold = this.properties.threshold;
uniforms.u_slope = this.properties.slope;
this._tex.drawTo(function() {
tex.bind(0);
shader.uniforms(uniforms).draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphTextureMatte.pixel_shader =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec3 u_key_color;\n\
uniform float u_threshold;\n\
uniform float u_slope;\n\
\n\
void main() {\n\
vec3 color = texture2D( u_texture, v_coord ).xyz;\n\
float diff = length( normalize(color) - normalize(u_key_color) );\n\
float edge = u_threshold * (1.0 - u_slope);\n\
float alpha = smoothstep( edge, u_threshold, diff);\n\
gl_FragColor = vec4( color, alpha );\n\
}";
LiteGraph.registerNodeType("texture/matte", LGraphTextureMatte);
//***********************************
function LGraphCubemapToTexture2D() {
this.addInput("in", "texture");
this.addInput("yaw", "number");
this.addOutput("out", "texture");
this.properties = { yaw: 0 };
}
LGraphCubemapToTexture2D.title = "CubemapToTexture2D";
LGraphCubemapToTexture2D.desc = "Transforms a CUBEMAP texture into a TEXTURE2D in Polar Representation";
LGraphCubemapToTexture2D.prototype.onExecute = function() {
if (!this.isOutputConnected(0))
return;
var tex = this.getInputData(0);
if ( !tex || tex.texture_type != GL.TEXTURE_CUBE_MAP )
return;
if( this._last_tex && ( this._last_tex.height != tex.height || this._last_tex.type != tex.type ))
this._last_tex = null;
var yaw = this.getInputOrProperty("yaw");
this._last_tex = GL.Texture.cubemapToTexture2D( tex, tex.height, this._last_tex, true, yaw );
this.setOutputData( 0, this._last_tex );
};
LiteGraph.registerNodeType( "texture/cubemapToTexture2D", LGraphCubemapToTexture2D );
})(this);
(function(global) {
if (typeof GL == "undefined")
return;
var LiteGraph = global.LiteGraph;
var LGraphCanvas = global.LGraphCanvas;
var SHADERNODES_COLOR = "#345";
var LGShaders = LiteGraph.Shaders = {};
var GLSL_types = LGShaders.GLSL_types = ["float","vec2","vec3","vec4","mat3","mat4","sampler2D","samplerCube"];
var GLSL_types_const = LGShaders.GLSL_types_const = ["float","vec2","vec3","vec4"];
var GLSL_functions_desc = {
"radians": "T radians(T degrees)",
"degrees": "T degrees(T radians)",
"sin": "T sin(T angle)",
"cos": "T cos(T angle)",
"tan": "T tan(T angle)",
"asin": "T asin(T x)",
"acos": "T acos(T x)",
"atan": "T atan(T x)",
"atan2": "T atan(T x,T y)",
"pow": "T pow(T x,T y)",
"exp": "T exp(T x)",
"log": "T log(T x)",
"exp2": "T exp2(T x)",
"log2": "T log2(T x)",
"sqrt": "T sqrt(T x)",
"inversesqrt": "T inversesqrt(T x)",
"abs": "T abs(T x)",
"sign": "T sign(T x)",
"floor": "T floor(T x)",
"round": "T round(T x)",
"ceil": "T ceil(T x)",
"fract": "T fract(T x)",
"mod": "T mod(T x,T y)", //"T mod(T x,float y)"
"min": "T min(T x,T y)",
"max": "T max(T x,T y)",
"clamp": "T clamp(T x,T minVal = 0.0,T maxVal = 1.0)",
"mix": "T mix(T x,T y,T a)", //"T mix(T x,T y,float a)"
"step": "T step(T edge, T edge2, T x)", //"T step(float edge, T x)"
"smoothstep": "T smoothstep(T edge, T edge2, T x)", //"T smoothstep(float edge, T x)"
"length":"float length(T x)",
"distance":"float distance(T p0, T p1)",
"normalize":"T normalize(T x)",
"dot": "float dot(T x,T y)",
"cross": "vec3 cross(vec3 x,vec3 y)",
"reflect": "vec3 reflect(vec3 V,vec3 N)",
"refract": "vec3 refract(vec3 V,vec3 N, float IOR)"
};
//parse them
var GLSL_functions = {};
var GLSL_functions_name = [];
parseGLSLDescriptions();
LGShaders.ALL_TYPES = "float,vec2,vec3,vec4";
function parseGLSLDescriptions()
{
GLSL_functions_name.length = 0;
for(var i in GLSL_functions_desc)
{
var op = GLSL_functions_desc[i];
var index = op.indexOf(" ");
var return_type = op.substr(0,index);
var index2 = op.indexOf("(",index);
var func_name = op.substr(index,index2-index).trim();
var params = op.substr(index2 + 1, op.length - index2 - 2).split(",");
for(var j in params)
{
var p = params[j].split(" ").filter(function(a){ return a; });
params[j] = { type: p[0].trim(), name: p[1].trim() };
if(p[2] == "=")
params[j].value = p[3].trim();
}
GLSL_functions[i] = { return_type: return_type, func: func_name, params: params };
GLSL_functions_name.push( func_name );
//console.log( GLSL_functions[i] );
}
}
//common actions to all shader node classes
function registerShaderNode( type, node_ctor )
{
//static attributes
node_ctor.color = SHADERNODES_COLOR;
node_ctor.filter = "shader";
//common methods
node_ctor.prototype.clearDestination = function(){ this.shader_destination = {}; }
node_ctor.prototype.propagateDestination = function propagateDestination( dest_name )
{
this.shader_destination[ dest_name ] = true;
if(this.inputs)
for(var i = 0; i < this.inputs.length; ++i)
{
var origin_node = this.getInputNode(i);
if(origin_node)
origin_node.propagateDestination( dest_name );
}
}
if(!node_ctor.prototype.onPropertyChanged)
node_ctor.prototype.onPropertyChanged = function()
{
if(this.graph)
this.graph._version++;
}
/*
if(!node_ctor.prototype.onGetCode)
node_ctor.prototype.onGetCode = function()
{
//check destination to avoid lonely nodes
if(!this.shader_destination)
return;
//grab inputs with types
var inputs = [];
if(this.inputs)
for(var i = 0; i < this.inputs.length; ++i)
inputs.push({ type: this.getInputData(i), name: getInputLinkID(this,i) });
var outputs = [];
if(this.outputs)
for(var i = 0; i < this.outputs.length; ++i)
outputs.push({ name: getOutputLinkID(this,i) });
//pass to code func
var results = this.extractCode(inputs);
//grab output, pass to next
if(results)
for(var i = 0; i < results.length; ++i)
{
var r = results[i];
if(!r)
continue;
this.setOutputData(i,r.value);
}
}
*/
LiteGraph.registerNodeType( "shader::" + type, node_ctor );
}
function getShaderNodeVarName( node, name )
{
return "VAR_" + (name || "TEMP") + "_" + node.id;
}
function getInputLinkID( node, slot )
{
if(!node.inputs)
return null;
var link = node.getInputLink( slot );
if( !link )
return null;
var origin_node = node.graph.getNodeById( link.origin_id );
if( !origin_node )
return null;
if(origin_node.getOutputVarName)
return origin_node.getOutputVarName(link.origin_slot);
//generate
return "link_" + origin_node.id + "_" + link.origin_slot;
}
function getOutputLinkID( node, slot )
{
if (!node.isOutputConnected(slot))
return null;
return "link_" + node.id + "_" + slot;
}
LGShaders.registerShaderNode = registerShaderNode;
LGShaders.getInputLinkID = getInputLinkID;
LGShaders.getOutputLinkID = getOutputLinkID;
LGShaders.getShaderNodeVarName = getShaderNodeVarName;
LGShaders.parseGLSLDescriptions = parseGLSLDescriptions;
//given a const number, it transform it to a string that matches a type
var valueToGLSL = LiteGraph.valueToGLSL = function valueToGLSL( v, type, precision )
{
var n = 5; //num decimals
if(precision != null)
n = precision;
if(!type)
{
if(v.constructor === Number)
type = "float";
else if(v.length)
{
switch(v.length)
{
case 2: type = "vec2"; break;
case 3: type = "vec3"; break;
case 4: type = "vec4"; break;
case 9: type = "mat3"; break;
case 16: type = "mat4"; break;
default:
throw("unknown type for glsl value size");
}
}
else
throw("unknown type for glsl value: " + v.constructor);
}
switch(type)
{
case 'float': return v.toFixed(n); break;
case 'vec2': return "vec2(" + v[0].toFixed(n) + "," + v[1].toFixed(n) + ")"; break;
case 'color3':
case 'vec3': return "vec3(" + v[0].toFixed(n) + "," + v[1].toFixed(n) + "," + v[2].toFixed(n) + ")"; break;
case 'color4':
case 'vec4': return "vec4(" + v[0].toFixed(n) + "," + v[1].toFixed(n) + "," + v[2].toFixed(n) + "," + v[3].toFixed(n) + ")"; break;
case 'mat3': return "mat3(1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0)"; break; //not fully supported yet
case 'mat4': return "mat4(1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0)"; break;//not fully supported yet
default:
throw("unknown glsl type in valueToGLSL:", type);
}
return "";
}
//makes sure that a var is of a type, and if not, it converts it
var varToTypeGLSL = LiteGraph.varToTypeGLSL = function varToTypeGLSL( v, input_type, output_type )
{
if(input_type == output_type)
return v;
if(v == null)
switch(output_type)
{
case "float": return "0.0";
case "vec2": return "vec2(0.0)";
case "vec3": return "vec3(0.0)";
case "vec4": return "vec4(0.0,0.0,0.0,1.0)";
default: //null
return null;
}
if(!output_type)
throw("error: no output type specified");
if(output_type == "float")
{
switch(input_type)
{
//case "float":
case "vec2":
case "vec3":
case "vec4":
return v + ".x";
break;
default: //null
return "0.0";
break;
}
}
else if(output_type == "vec2")
{
switch(input_type)
{
case "float":
return "vec2("+v+")";
//case "vec2":
case "vec3":
case "vec4":
return v + ".xy";
default: //null
return "vec2(0.0)";
}
}
else if(output_type == "vec3")
{
switch(input_type)
{
case "float":
return "vec3("+v+")";
case "vec2":
return "vec3(" + v + ",0.0)";
//case "vec3":
case "vec4":
return v + ".xyz";
default: //null
return "vec3(0.0)";
}
}
else if(output_type == "vec4")
{
switch(input_type)
{
case "float":
return "vec4("+v+")";
case "vec2":
return "vec4(" + v + ",0.0,1.0)";
case "vec3":
return "vec4(" + v + ",1.0)";
default: //null
return "vec4(0.0,0.0,0.0,1.0)";
}
}
throw("type cannot be converted");
}
//used to plug incompatible stuff
var convertVarToGLSLType = LiteGraph.convertVarToGLSLType = function convertVarToGLSLType( varname, type, target_type )
{
if(type == target_type)
return varname;
if(type == "float")
return target_type + "(" + varname + ")";
if(target_type == "vec2") //works for vec2,vec3 and vec4
return "vec2(" + varname + ".xy)";
if(target_type == "vec3") //works for vec2,vec3 and vec4
{
if(type == "vec2")
return "vec3(" + varname + ",0.0)";
if(type == "vec4")
return "vec4(" + varname + ".xyz)";
}
if(target_type == "vec4")
{
if(type == "vec2")
return "vec4(" + varname + ",0.0,0.0)";
if(target_type == "vec3")
return "vec4(" + varname + ",1.0)";
}
return null;
}
//used to host a shader body **************************************
function LGShaderContext()
{
//to store the code template
this.vs_template = "";
this.fs_template = "";
//required so nodes now where to fetch the input data
this.buffer_names = {
uvs: "v_coord"
};
this.extra = {}; //to store custom info from the nodes (like if this shader supports a feature, etc)
this._functions = {};
this._uniforms = {};
this._codeparts = {};
this._uniform_value = null;
}
LGShaderContext.prototype.clear = function()
{
this._uniforms = {};
this._functions = {};
this._codeparts = {};
this._uniform_value = null;
this.extra = {};
}
LGShaderContext.prototype.addUniform = function( name, type, value )
{
this._uniforms[ name ] = type;
if(value != null)
{
if(!this._uniform_value)
this._uniform_value = {};
this._uniform_value[name] = value;
}
}
LGShaderContext.prototype.addFunction = function( name, code )
{
this._functions[name] = code;
}
LGShaderContext.prototype.addCode = function( hook, code, destinations )
{
destinations = destinations || {"":""};
for(var i in destinations)
{
var h = i ? i + "_" + hook : hook;
if(!this._codeparts[ h ])
this._codeparts[ h ] = code + "\n";
else
this._codeparts[ h ] += code + "\n";
}
}
//the system works by grabbing code fragments from every node and concatenating them in blocks depending on where must they be attached
LGShaderContext.prototype.computeCodeBlocks = function( graph, extra_uniforms )
{
//prepare context
this.clear();
//grab output nodes
var vertexout = graph.findNodesByType("shader::output/vertex");
vertexout = vertexout && vertexout.length ? vertexout[0] : null;
var fragmentout = graph.findNodesByType("shader::output/fragcolor");
fragmentout = fragmentout && fragmentout.length ? fragmentout[0] : null;
if(!fragmentout) //??
return null;
//propagate back destinations
graph.sendEventToAllNodes( "clearDestination" );
if(vertexout)
vertexout.propagateDestination("vs");
if(fragmentout)
fragmentout.propagateDestination("fs");
//gets code from graph
graph.sendEventToAllNodes("onGetCode", this );
var uniforms = "";
for(var i in this._uniforms)
uniforms += "uniform " + this._uniforms[i] + " " + i + ";\n";
if(extra_uniforms)
for(var i in extra_uniforms)
uniforms += "uniform " + extra_uniforms[i] + " " + i + ";\n";
var functions = "";
for(var i in this._functions)
functions += "//" + i + "\n" + this._functions[i] + "\n";
var blocks = this._codeparts;
blocks.uniforms = uniforms;
blocks.functions = functions;
return blocks;
}
//replaces blocks using the vs and fs template and returns the final codes
LGShaderContext.prototype.computeShaderCode = function( graph )
{
var blocks = this.computeCodeBlocks( graph );
var vs_code = GL.Shader.replaceCodeUsingContext( this.vs_template, blocks );
var fs_code = GL.Shader.replaceCodeUsingContext( this.fs_template, blocks );
return {
vs_code: vs_code,
fs_code: fs_code
};
}
//generates the shader code from the template and the
LGShaderContext.prototype.computeShader = function( graph, shader )
{
var finalcode = this.computeShaderCode( graph );
console.log( finalcode.vs_code, finalcode.fs_code );
if(!LiteGraph.catch_exceptions)
{
this._shader_error = true;
if(shader)
shader.updateShader( finalcode.vs_code, finalcode.fs_code );
else
shader = new GL.Shader( finalcode.vs_code, finalcode.fs_code );
this._shader_error = false;
return shader;
}
try
{
if(shader)
shader.updateShader( finalcode.vs_code, finalcode.fs_code );
else
shader = new GL.Shader( finalcode.vs_code, finalcode.fs_code );
this._shader_error = false;
return shader;
}
catch (err)
{
if(!this._shader_error)
{
console.error(err);
if(err.indexOf("Fragment shader") != -1)
console.log( finalcode.fs_code.split("\n").map(function(v,i){ return i + ".- " + v; }).join("\n") );
else
console.log( finalcode.vs_code );
}
this._shader_error = true;
return null;
}
return null;//never here
}
LGShaderContext.prototype.getShader = function( graph )
{
//if graph not changed?
if(this._shader && this._shader._version == graph._version)
return this._shader;
//compile shader
var shader = this.computeShader( graph, this._shader );
if(!shader)
return null;
this._shader = shader;
shader._version = graph._version;
return shader;
}
//some shader nodes could require to fill the box with some uniforms
LGShaderContext.prototype.fillUniforms = function( uniforms, param )
{
if(!this._uniform_value)
return;
for(var i in this._uniform_value)
{
var v = this._uniform_value[i];
if(v == null)
continue;
if(v.constructor === Function)
uniforms[i] = v.call( this, param );
else if(v.constructor === GL.Texture)
{
//todo...
}
else
uniforms[i] = v;
}
}
LiteGraph.ShaderContext = LiteGraph.Shaders.Context = LGShaderContext;
// LGraphShaderGraph *****************************
// applies a shader graph to texture, it can be uses as an example
function LGraphShaderGraph() {
//before inputs
this.subgraph = new LiteGraph.LGraph();
this.subgraph._subgraph_node = this;
this.subgraph._is_subgraph = true;
this.subgraph.filter = "shader";
this.addInput("in", "texture");
this.addOutput("out", "texture");
this.properties = { width: 0, height: 0, alpha: false, precision: typeof(LGraphTexture) != "undefined" ? LGraphTexture.DEFAULT : 2 };
var inputNode = this.subgraph.findNodesByType("shader::input/uniform")[0];
inputNode.pos = [200,300];
var sampler = LiteGraph.createNode("shader::texture/sampler2D");
sampler.pos = [400,300];
this.subgraph.add( sampler );
var outnode = LiteGraph.createNode("shader::output/fragcolor");
outnode.pos = [600,300];
this.subgraph.add( outnode );
inputNode.connect( 0, sampler );
sampler.connect( 0, outnode );
this.size = [180,60];
this.redraw_on_mouse = true; //force redraw
this._uniforms = {};
this._shader = null;
this._context = new LGShaderContext();
this._context.vs_template = "#define VERTEX\n" + GL.Shader.SCREEN_VERTEX_SHADER;
this._context.fs_template = LGraphShaderGraph.template;
}
LGraphShaderGraph.template = "\n\
#define FRAGMENT\n\
precision highp float;\n\
varying vec2 v_coord;\n\
{{varying}}\n\
{{uniforms}}\n\
{{functions}}\n\
{{fs_functions}}\n\
void main() {\n\n\
vec2 uv = v_coord;\n\
vec4 fragcolor = vec4(0.0);\n\
vec4 fragcolor1 = vec4(0.0);\n\
{{fs_code}}\n\
gl_FragColor = fragcolor;\n\
}\n\
";
LGraphShaderGraph.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphShaderGraph.title = "ShaderGraph";
LGraphShaderGraph.desc = "Builds a shader using a graph";
LGraphShaderGraph.input_node_type = "input/uniform";
LGraphShaderGraph.output_node_type = "output/fragcolor";
LGraphShaderGraph.title_color = SHADERNODES_COLOR;
LGraphShaderGraph.prototype.onSerialize = function(o)
{
o.subgraph = this.subgraph.serialize();
}
LGraphShaderGraph.prototype.onConfigure = function(o)
{
this.subgraph.configure(o.subgraph);
}
LGraphShaderGraph.prototype.onExecute = function() {
if (!this.isOutputConnected(0))
return;
//read input texture
var intex = this.getInputData(0);
if(intex && intex.constructor != GL.Texture)
intex = null;
var w = this.properties.width | 0;
var h = this.properties.height | 0;
if (w == 0) {
w = intex ? intex.width : gl.viewport_data[2];
} //0 means default
if (h == 0) {
h = intex ? intex.height : gl.viewport_data[3];
} //0 means default
var type = LGraphTexture.getTextureType( this.properties.precision, intex );
var texture = this._texture;
if ( !texture || texture.width != w || texture.height != h || texture.type != type ) {
texture = this._texture = new GL.Texture(w, h, {
type: type,
format: this.alpha ? gl.RGBA : gl.RGB,
filter: gl.LINEAR
});
}
var shader = this.getShader( this.subgraph );
if(!shader)
return;
var uniforms = this._uniforms;
this._context.fillUniforms( uniforms );
var tex_slot = 0;
if(this.inputs)
for(var i = 0; i < this.inputs.length; ++i)
{
var input = this.inputs[i];
var data = this.getInputData(i);
if(input.type == "texture")
{
if(!data)
data = GL.Texture.getWhiteTexture();
data = data.bind(tex_slot++);
}
if(data != null)
uniforms[ "u_" + input.name ] = data;
}
var mesh = GL.Mesh.getScreenQuad();
gl.disable( gl.DEPTH_TEST );
gl.disable( gl.BLEND );
texture.drawTo(function(){
shader.uniforms( uniforms );
shader.draw( mesh );
});
//use subgraph output
this.setOutputData(0, texture );
};
//add input node inside subgraph
LGraphShaderGraph.prototype.onInputAdded = function( slot_info )
{
var subnode = LiteGraph.createNode("shader::input/uniform");
subnode.setProperty("name",slot_info.name);
subnode.setProperty("type",slot_info.type);
this.subgraph.add( subnode );
}
//remove all
LGraphShaderGraph.prototype.onInputRemoved = function( slot, slot_info )
{
var nodes = this.subgraph.findNodesByType("shader::input/uniform");
for(var i = 0; i < nodes.length; ++i)
{
var node = nodes[i];
if(node.properties.name == slot_info.name )
this.subgraph.remove( node );
}
}
LGraphShaderGraph.prototype.computeSize = function()
{
var num_inputs = this.inputs ? this.inputs.length : 0;
var num_outputs = this.outputs ? this.outputs.length : 0;
return [ 200, Math.max(num_inputs,num_outputs) * LiteGraph.NODE_SLOT_HEIGHT + LiteGraph.NODE_TITLE_HEIGHT + 10];
}
LGraphShaderGraph.prototype.getShader = function()
{
var shader = this._context.getShader( this.subgraph );
if(!shader)
this.boxcolor = "red";
else
this.boxcolor = null;
return shader;
}
LGraphShaderGraph.prototype.onDrawBackground = function(ctx, graphcanvas, canvas, pos)
{
if(this.flags.collapsed)
return;
//allows to preview the node if the canvas is a webgl canvas
var tex = this.getOutputData(0);
var inputs_y = this.inputs ? this.inputs.length * LiteGraph.NODE_SLOT_HEIGHT : 0;
if (tex && ctx == tex.gl && this.size[1] > inputs_y + LiteGraph.NODE_TITLE_HEIGHT ) {
ctx.drawImage( tex, 10,y, this.size[0] - 20, this.size[1] - inputs_y - LiteGraph.NODE_TITLE_HEIGHT );
}
var y = this.size[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5;
//button
var over = LiteGraph.isInsideRectangle(pos[0],pos[1],this.pos[0],this.pos[1] + y,this.size[0],LiteGraph.NODE_TITLE_HEIGHT);
ctx.fillStyle = over ? "#555" : "#222";
ctx.beginPath();
if (this._shape == LiteGraph.BOX_SHAPE)
ctx.rect(0, y, this.size[0]+1, LiteGraph.NODE_TITLE_HEIGHT);
else
ctx.roundRect( 0, y, this.size[0]+1, LiteGraph.NODE_TITLE_HEIGHT, 0, 8);
ctx.fill();
//button
ctx.textAlign = "center";
ctx.font = "24px Arial";
ctx.fillStyle = over ? "#DDD" : "#999";
ctx.fillText( "+", this.size[0] * 0.5, y + 24 );
}
LGraphShaderGraph.prototype.onMouseDown = function(e, localpos, graphcanvas)
{
var y = this.size[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5;
if(localpos[1] > y)
{
graphcanvas.showSubgraphPropertiesDialog(this);
}
}
LGraphShaderGraph.prototype.onDrawSubgraphBackground = function(graphcanvas)
{
//TODO
}
LGraphShaderGraph.prototype.getExtraMenuOptions = function(graphcanvas)
{
var that = this;
var options = [{ content: "Print Code", callback: function(){
var code = that._context.computeShaderCode();
console.log( code.vs_code, code.fs_code );
}}];
return options;
}
LiteGraph.registerNodeType( "texture/shaderGraph", LGraphShaderGraph );
function shaderNodeFromFunction( classname, params, return_type, code )
{
//TODO
}
//Shader Nodes ***********************************************************
//applies a shader graph to a code
function LGraphShaderUniform() {
this.addOutput("out", "");
this.properties = { name: "", type: "" };
}
LGraphShaderUniform.title = "Uniform";
LGraphShaderUniform.desc = "Input data for the shader";
LGraphShaderUniform.prototype.getTitle = function()
{
if( this.properties.name && this.flags.collapsed)
return this.properties.type + " " + this.properties.name;
return "Uniform";
}
LGraphShaderUniform.prototype.onPropertyChanged = function(name,value)
{
this.outputs[0].name = this.properties.type + " " + this.properties.name;
}
LGraphShaderUniform.prototype.onGetCode = function( context )
{
if(!this.shader_destination)
return;
var type = this.properties.type;
if( !type )
{
if( !context.onGetPropertyInfo )
return;
var info = context.onGetPropertyInfo( this.property.name );
if(!info)
return;
type = info.type;
}
if(type == "number")
type = "float";
else if(type == "texture")
type = "sampler2D";
if ( LGShaders.GLSL_types.indexOf(type) == -1 )
return;
context.addUniform( "u_" + this.properties.name, type );
this.setOutputData( 0, type );
}
LGraphShaderUniform.prototype.getOutputVarName = function(slot)
{
return "u_" + this.properties.name;
}
registerShaderNode( "input/uniform", LGraphShaderUniform );
function LGraphShaderAttribute() {
this.addOutput("out", "vec2");
this.properties = { name: "coord", type: "vec2" };
}
LGraphShaderAttribute.title = "Attribute";
LGraphShaderAttribute.desc = "Input data from mesh attribute";
LGraphShaderAttribute.prototype.getTitle = function()
{
return "att. " + this.properties.name;
}
LGraphShaderAttribute.prototype.onGetCode = function( context )
{
if(!this.shader_destination)
return;
var type = this.properties.type;
if( !type || LGShaders.GLSL_types.indexOf(type) == -1 )
return;
if(type == "number")
type = "float";
if( this.properties.name != "coord")
{
context.addCode( "varying", " varying " + type +" v_" + this.properties.name + ";" );
//if( !context.varyings[ this.properties.name ] )
//context.addCode( "vs_code", "v_" + this.properties.name + " = " + input_name + ";" );
}
this.setOutputData( 0, type );
}
LGraphShaderAttribute.prototype.getOutputVarName = function(slot)
{
return "v_" + this.properties.name;
}
registerShaderNode( "input/attribute", LGraphShaderAttribute );
function LGraphShaderSampler2D() {
this.addInput("tex", "sampler2D");
this.addInput("uv", "vec2");
this.addOutput("rgba", "vec4");
this.addOutput("rgb", "vec3");
}
LGraphShaderSampler2D.title = "Sampler2D";
LGraphShaderSampler2D.desc = "Reads a pixel from a texture";
LGraphShaderSampler2D.prototype.onGetCode = function( context )
{
if(!this.shader_destination)
return;
var texname = getInputLinkID( this, 0 );
var varname = getShaderNodeVarName(this);
var code = "vec4 " + varname + " = vec4(0.0);\n";
if(texname)
{
var uvname = getInputLinkID( this, 1 ) || context.buffer_names.uvs;
code += varname + " = texture2D("+texname+","+uvname+");\n";
}
var link0 = getOutputLinkID( this, 0 );
if(link0)
code += "vec4 " + getOutputLinkID( this, 0 ) + " = "+varname+";\n";
var link1 = getOutputLinkID( this, 1 );
if(link1)
code += "vec3 " + getOutputLinkID( this, 1 ) + " = "+varname+".xyz;\n";
context.addCode( "code", code, this.shader_destination );
this.setOutputData( 0, "vec4" );
this.setOutputData( 1, "vec3" );
}
registerShaderNode( "texture/sampler2D", LGraphShaderSampler2D );
//*********************************
function LGraphShaderConstant()
{
this.addOutput("","float");
this.properties = {
type: "float",
value: 0
};
this.addWidget("combo","type","float",null, { values: GLSL_types_const, property: "type" } );
this.updateWidgets();
}
LGraphShaderConstant.title = "const";
LGraphShaderConstant.prototype.getTitle = function()
{
if(this.flags.collapsed)
return valueToGLSL( this.properties.value, this.properties.type, 2 );
return "Const";
}
LGraphShaderConstant.prototype.onPropertyChanged = function(name,value)
{
var that = this;
if(name == "type")
{
if(this.outputs[0].type != value)
{
this.disconnectOutput(0);
this.outputs[0].type = value;
}
this.widgets.length = 1; //remove extra widgets
this.updateWidgets();
}
if(name == "value")
{
if(!value.length)
this.widgets[1].value = value;
else
{
this.widgets[1].value = value[1];
if(value.length > 2)
this.widgets[2].value = value[2];
if(value.length > 3)
this.widgets[3].value = value[3];
}
}
}
LGraphShaderConstant.prototype.updateWidgets = function( old_value )
{
var that = this;
var old_value = this.properties.value;
var options = { step: 0.01 };
switch(this.properties.type)
{
case 'float':
this.properties.value = 0;
this.addWidget("number","v",0,{ step:0.01, property: "value" });
break;
case 'vec2':
this.properties.value = old_value && old_value.length == 2 ? [old_value[0],old_value[1]] : [0,0,0];
this.addWidget("number","x",this.properties.value[0], function(v){ that.properties.value[0] = v; },options);
this.addWidget("number","y",this.properties.value[1], function(v){ that.properties.value[1] = v; },options);
break;
case 'vec3':
this.properties.value = old_value && old_value.length == 3 ? [old_value[0],old_value[1],old_value[2]] : [0,0,0];
this.addWidget("number","x",this.properties.value[0], function(v){ that.properties.value[0] = v; },options);
this.addWidget("number","y",this.properties.value[1], function(v){ that.properties.value[1] = v; },options);
this.addWidget("number","z",this.properties.value[2], function(v){ that.properties.value[2] = v; },options);
break;
case 'vec4':
this.properties.value = old_value && old_value.length == 4 ? [old_value[0],old_value[1],old_value[2],old_value[3]] : [0,0,0,0];
this.addWidget("number","x",this.properties.value[0], function(v){ that.properties.value[0] = v; },options);
this.addWidget("number","y",this.properties.value[1], function(v){ that.properties.value[1] = v; },options);
this.addWidget("number","z",this.properties.value[2], function(v){ that.properties.value[2] = v; },options);
this.addWidget("number","w",this.properties.value[3], function(v){ that.properties.value[3] = v; },options);
break;
default:
console.error("unknown type for constant");
}
}
LGraphShaderConstant.prototype.onGetCode = function( context )
{
if(!this.shader_destination)
return;
var value = valueToGLSL( this.properties.value, this.properties.type );
var link_name = getOutputLinkID(this,0);
if(!link_name) //not connected
return;
var code = " " + this.properties.type + " " + link_name + " = " + value + ";";
context.addCode( "code", code, this.shader_destination );
this.setOutputData( 0, this.properties.type );
}
registerShaderNode( "const/const", LGraphShaderConstant );
function LGraphShaderVec2()
{
this.addInput("xy","vec2");
this.addInput("x","float");
this.addInput("y","float");
this.addOutput("xy","vec2");
this.addOutput("x","float");
this.addOutput("y","float");
this.properties = { x: 0, y: 0 };
}
LGraphShaderVec2.title = "vec2";
LGraphShaderVec2.varmodes = ["xy","x","y"];
LGraphShaderVec2.prototype.onPropertyChanged = function()
{
if(this.graph)
this.graph._version++;
}
LGraphShaderVec2.prototype.onGetCode = function( context )
{
if(!this.shader_destination)
return;
var props = this.properties;
var varname = getShaderNodeVarName(this);
var code = " vec2 " + varname + " = " + valueToGLSL([props.x,props.y]) + ";\n";
for(var i = 0;i < LGraphShaderVec2.varmodes.length; ++i)
{
var varmode = LGraphShaderVec2.varmodes[i];
var inlink = getInputLinkID(this,i);
if(!inlink)
continue;
code += " " + varname + "."+varmode+" = " + inlink + ";\n";
}
for(var i = 0;i < LGraphShaderVec2.varmodes.length; ++i)
{
var varmode = LGraphShaderVec2.varmodes[i];
var outlink = getOutputLinkID(this,i);
if(!outlink)
continue;
var type = GLSL_types_const[varmode.length - 1];
code += " "+type+" " + outlink + " = " + varname + "." + varmode + ";\n";
this.setOutputData( i, type );
}
context.addCode( "code", code, this.shader_destination );
}
registerShaderNode( "const/vec2", LGraphShaderVec2 );
function LGraphShaderVec3()
{
this.addInput("xyz","vec3");
this.addInput("x","float");
this.addInput("y","float");
this.addInput("z","float");
this.addInput("xy","vec2");
this.addInput("xz","vec2");
this.addInput("yz","vec2");
this.addOutput("xyz","vec3");
this.addOutput("x","float");
this.addOutput("y","float");
this.addOutput("z","float");
this.addOutput("xy","vec2");
this.addOutput("xz","vec2");
this.addOutput("yz","vec2");
this.properties = { x:0, y: 0, z: 0 };
}
LGraphShaderVec3.title = "vec3";
LGraphShaderVec3.varmodes = ["xyz","x","y","z","xy","xz","yz"];
LGraphShaderVec3.prototype.onPropertyChanged = function()
{
if(this.graph)
this.graph._version++;
}
LGraphShaderVec3.prototype.onGetCode = function( context )
{
if(!this.shader_destination)
return;
var props = this.properties;
var varname = getShaderNodeVarName(this);
var code = "vec3 " + varname + " = " + valueToGLSL([props.x,props.y,props.z]) + ";\n";
for(var i = 0;i < LGraphShaderVec3.varmodes.length; ++i)
{
var varmode = LGraphShaderVec3.varmodes[i];
var inlink = getInputLinkID(this,i);
if(!inlink)
continue;
code += " " + varname + "."+varmode+" = " + inlink + ";\n";
}
for(var i = 0; i < LGraphShaderVec3.varmodes.length; ++i)
{
var varmode = LGraphShaderVec3.varmodes[i];
var outlink = getOutputLinkID(this,i);
if(!outlink)
continue;
var type = GLSL_types_const[varmode.length - 1];
code += " "+type+" " + outlink + " = " + varname + "." + varmode + ";\n";
this.setOutputData( i, type );
}
context.addCode( "code", code, this.shader_destination );
}
registerShaderNode( "const/vec3", LGraphShaderVec3 );
function LGraphShaderVec4()
{
this.addInput("xyzw","vec4");
this.addInput("xyz","vec3");
this.addInput("x","float");
this.addInput("y","float");
this.addInput("z","float");
this.addInput("w","float");
this.addInput("xy","vec2");
this.addInput("yz","vec2");
this.addInput("zw","vec2");
this.addOutput("xyzw","vec4");
this.addOutput("xyz","vec3");
this.addOutput("x","float");
this.addOutput("y","float");
this.addOutput("z","float");
this.addOutput("xy","vec2");
this.addOutput("yz","vec2");
this.addOutput("zw","vec2");
this.properties = { x:0, y: 0, z: 0, w: 0 };
}
LGraphShaderVec4.title = "vec4";
LGraphShaderVec4.varmodes = ["xyzw","xyz","x","y","z","w","xy","yz","zw"];
LGraphShaderVec4.prototype.onPropertyChanged = function()
{
if(this.graph)
this.graph._version++;
}
LGraphShaderVec4.prototype.onGetCode = function( context )
{
if(!this.shader_destination)
return;
var props = this.properties;
var varname = getShaderNodeVarName(this);
var code = "vec4 " + varname + " = " + valueToGLSL([props.x,props.y,props.z,props.w]) + ";\n";
for(var i = 0;i < LGraphShaderVec4.varmodes.length; ++i)
{
var varmode = LGraphShaderVec4.varmodes[i];
var inlink = getInputLinkID(this,i);
if(!inlink)
continue;
code += " " + varname + "."+varmode+" = " + inlink + ";\n";
}
for(var i = 0;i < LGraphShaderVec4.varmodes.length; ++i)
{
var varmode = LGraphShaderVec4.varmodes[i];
var outlink = getOutputLinkID(this,i);
if(!outlink)
continue;
var type = GLSL_types_const[varmode.length - 1];
code += " "+type+" " + outlink + " = " + varname + "." + varmode + ";\n";
this.setOutputData( i, type );
}
context.addCode( "code", code, this.shader_destination );
}
registerShaderNode( "const/vec4", LGraphShaderVec4 );
//*********************************
function LGraphShaderFragColor() {
this.addInput("color", LGShaders.ALL_TYPES );
this.block_delete = true;
}
LGraphShaderFragColor.title = "FragColor";
LGraphShaderFragColor.desc = "Pixel final color";
LGraphShaderFragColor.prototype.onGetCode = function( context )
{
var link_name = getInputLinkID( this, 0 );
if(!link_name)
return;
var type = this.getInputData(0);
var code = varToTypeGLSL( link_name, type, "vec4" );
context.addCode("fs_code", "fragcolor = " + code + ";");
}
registerShaderNode( "output/fragcolor", LGraphShaderFragColor );
/*
function LGraphShaderDiscard()
{
this.addInput("v","T");
this.addInput("min","T");
this.properties = { min_value: 0.0 };
this.addWidget("number","min",0,{ step: 0.01, property: "min_value" });
}
LGraphShaderDiscard.title = "Discard";
LGraphShaderDiscard.prototype.onGetCode = function( context )
{
if(!this.isOutputConnected(0))
return;
var inlink = getInputLinkID(this,0);
var inlink1 = getInputLinkID(this,1);
if(!inlink && !inlink1) //not connected
return;
context.addCode("code", return_type + " " + outlink + " = ( (" + inlink + " - "+minv+") / ("+ maxv+" - "+minv+") ) * ("+ maxv2+" - "+minv2+") + " + minv2 + ";", this.shader_destination );
this.setOutputData( 0, return_type );
}
registerShaderNode( "output/discard", LGraphShaderDiscard );
*/
// *************************************************
function LGraphShaderOperation()
{
this.addInput("A", LGShaders.ALL_TYPES );
this.addInput("B", LGShaders.ALL_TYPES );
this.addOutput("out","");
this.properties = {
operation: "*"
};
this.addWidget("combo","op.",this.properties.operation,{ property: "operation", values: LGraphShaderOperation.operations });
}
LGraphShaderOperation.title = "Operation";
LGraphShaderOperation.operations = ["+","-","*","/"];
LGraphShaderOperation.prototype.getTitle = function()
{
if(this.flags.collapsed)
return "A" + this.properties.operation + "B";
else
return "Operation";
}
LGraphShaderOperation.prototype.onGetCode = function( context )
{
if(!this.shader_destination)
return;
if(!this.isOutputConnected(0))
return;
var inlinks = [];
for(var i = 0; i < 3; ++i)
inlinks.push( { name: getInputLinkID(this,i), type: this.getInputData(i) || "float" } );
var outlink = getOutputLinkID(this,0);
if(!outlink) //not connected
return;
//func_desc
var base_type = inlinks[0].type;
var return_type = base_type;
var op = this.properties.operation;
var params = [];
for(var i = 0; i < 2; ++i)
{
var param_code = inlinks[i].name;
if(param_code == null) //not plugged
{
param_code = p.value != null ? p.value : "(1.0)";
inlinks[i].type = "float";
}
//convert
if( inlinks[i].type != base_type )
{
if( inlinks[i].type == "float" && (op == "*" || op == "/") )
{
//I find hard to create the opposite condition now, so I prefeer an else
}
else
param_code = convertVarToGLSLType( param_code, inlinks[i].type, base_type );
}
params.push( param_code );
}
context.addCode("code", return_type + " " + outlink + " = "+ params[0] + op + params[1] + ";", this.shader_destination );
this.setOutputData( 0, return_type );
}
registerShaderNode( "math/operation", LGraphShaderOperation );
function LGraphShaderFunc()
{
this.addInput("A", LGShaders.ALL_TYPES );
this.addInput("B", LGShaders.ALL_TYPES );
this.addOutput("out","");
this.properties = {
func: "floor"
};
this._current = "floor";
this.addWidget("combo","func",this.properties.func,{ property: "func", values: GLSL_functions_name });
}
LGraphShaderFunc.title = "Func";
LGraphShaderFunc.prototype.onPropertyChanged = function(name,value)
{
if(this.graph)
this.graph._version++;
if(name == "func")
{
var func_desc = GLSL_functions[ value ];
if(!func_desc)
return;
//remove extra inputs
for(var i = func_desc.params.length; i < this.inputs.length; ++i)
this.removeInput(i);
//add and update inputs
for(var i = 0; i < func_desc.params.length; ++i)
{
var p = func_desc.params[i];
if( this.inputs[i] )
this.inputs[i].name = p.name + (p.value ? " (" + p.value + ")" : "");
else
this.addInput( p.name, LGShaders.ALL_TYPES );
}
}
}
LGraphShaderFunc.prototype.getTitle = function()
{
if(this.flags.collapsed)
return this.properties.func;
else
return "Func";
}
LGraphShaderFunc.prototype.onGetCode = function( context )
{
if(!this.shader_destination)
return;
if(!this.isOutputConnected(0))
return;
var inlinks = [];
for(var i = 0; i < 3; ++i)
inlinks.push( { name: getInputLinkID(this,i), type: this.getInputData(i) || "float" } );
var outlink = getOutputLinkID(this,0);
if(!outlink) //not connected
return;
var func_desc = GLSL_functions[ this.properties.func ];
if(!func_desc)
return;
//func_desc
var base_type = inlinks[0].type;
var return_type = func_desc.return_type;
if( return_type == "T" )
return_type = base_type;
var params = [];
for(var i = 0; i < func_desc.params.length; ++i)
{
var p = func_desc.params[i];
var param_code = inlinks[i].name;
if(param_code == null) //not plugged
{
param_code = p.value != null ? p.value : "(1.0)";
inlinks[i].type = "float";
}
if( (p.type == "T" && inlinks[i].type != base_type) ||
(p.type != "T" && inlinks[i].type != base_type) )
param_code = convertVarToGLSLType( param_code, inlinks[i].type, base_type );
params.push( param_code );
}
context.addFunction("round","float round(float v){ return floor(v+0.5); }\nvec2 round(vec2 v){ return floor(v+vec2(0.5));}\nvec3 round(vec3 v){ return floor(v+vec3(0.5));}\nvec4 round(vec4 v){ return floor(v+vec4(0.5)); }\n");
context.addCode("code", return_type + " " + outlink + " = "+func_desc.func+"("+params.join(",")+");", this.shader_destination );
this.setOutputData( 0, return_type );
}
registerShaderNode( "math/func", LGraphShaderFunc );
function LGraphShaderSnippet()
{
this.addInput("A", LGShaders.ALL_TYPES );
this.addInput("B", LGShaders.ALL_TYPES );
this.addOutput("C","vec4");
this.properties = {
code:"C = A+B",
type: "vec4"
}
this.addWidget("text","code",this.properties.code,{ property: "code" });
this.addWidget("combo","type",this.properties.type,{ values:["float","vec2","vec3","vec4"], property: "type" });
}
LGraphShaderSnippet.title = "Snippet";
LGraphShaderSnippet.prototype.onPropertyChanged = function(name,value)
{
if(this.graph)
this.graph._version++;
if(name == "type"&& this.outputs[0].type != value)
{
this.disconnectOutput(0);
this.outputs[0].type = value;
}
}
LGraphShaderSnippet.prototype.getTitle = function()
{
if(this.flags.collapsed)
return this.properties.code;
else
return "Snippet";
}
LGraphShaderSnippet.prototype.onGetCode = function( context )
{
if(!this.shader_destination || !this.isOutputConnected(0))
return;
var inlinkA = getInputLinkID(this,0);
if(!inlinkA)
inlinkA = "1.0";
var inlinkB = getInputLinkID(this,1);
if(!inlinkB)
inlinkB = "1.0";
var outlink = getOutputLinkID(this,0);
if(!outlink) //not connected
return;
var inA_type = this.getInputData(0) || "float";
var inB_type = this.getInputData(1) || "float";
var return_type = this.properties.type;
//cannot resolve input
if(inA_type == "T" || inB_type == "T")
{
return null;
}
var funcname = "funcSnippet" + this.id;
var func_code = "\n" + return_type + " " + funcname + "( " + inA_type + " A, " + inB_type + " B) {\n";
func_code += " " + return_type + " C = " + return_type + "(0.0);\n";
func_code += " " + this.properties.code + ";\n";
func_code += " return C;\n}\n";
context.addCode("functions", func_code, this.shader_destination );
context.addCode("code", return_type + " " + outlink + " = "+funcname+"("+inlinkA+","+inlinkB+");", this.shader_destination );
this.setOutputData( 0, return_type );
}
registerShaderNode( "utils/snippet", LGraphShaderSnippet );
//************************************
function LGraphShaderRand()
{
this.addOutput("out","float");
}
LGraphShaderRand.title = "Rand";
LGraphShaderRand.prototype.onGetCode = function( context )
{
if(!this.shader_destination || !this.isOutputConnected(0))
return;
var outlink = getOutputLinkID(this,0);
context.addUniform( "u_rand" + this.id, "float", function(){ return Math.random(); });
context.addCode("code", "float " + outlink + " = u_rand" + this.id +";", this.shader_destination );
this.setOutputData( 0, "float" );
}
registerShaderNode( "input/rand", LGraphShaderRand );
//noise
//https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
function LGraphShaderNoise()
{
this.addInput("out", LGShaders.ALL_TYPES );
this.addInput("scale", "float" );
this.addOutput("out","float");
this.properties = {
type: "noise",
scale: 1
};
this.addWidget("combo","type", this.properties.type, { property: "type", values: LGraphShaderNoise.NOISE_TYPES });
this.addWidget("number","scale", this.properties.scale, { property: "scale" });
}
LGraphShaderNoise.NOISE_TYPES = ["noise","rand"];
LGraphShaderNoise.title = "noise";
LGraphShaderNoise.prototype.onGetCode = function( context )
{
if(!this.shader_destination || !this.isOutputConnected(0))
return;
var inlink = getInputLinkID(this,0);
var outlink = getOutputLinkID(this,0);
var intype = this.getInputData(0);
if(!inlink)
{
intype = "vec2";
inlink = context.buffer_names.uvs;
}
context.addFunction("noise",LGraphShaderNoise.shader_functions);
context.addUniform( "u_noise_scale" + this.id, "float", this.properties.scale );
if( intype == "float" )
context.addCode("code", "float " + outlink + " = snoise( vec2(" + inlink +") * u_noise_scale" + this.id +");", this.shader_destination );
else if( intype == "vec2" || intype == "vec3" )
context.addCode("code", "float " + outlink + " = snoise(" + inlink +" * u_noise_scale" + this.id +");", this.shader_destination );
else if( intype == "vec4" )
context.addCode("code", "float " + outlink + " = snoise(" + inlink +".xyz * u_noise_scale" + this.id +");", this.shader_destination );
this.setOutputData( 0, "float" );
}
registerShaderNode( "math/noise", LGraphShaderNoise );
LGraphShaderNoise.shader_functions = "\n\
vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }\n\
\n\
float snoise(vec2 v){\n\
const vec4 C = vec4(0.211324865405187, 0.366025403784439,-0.577350269189626, 0.024390243902439);\n\
vec2 i = floor(v + dot(v, C.yy) );\n\
vec2 x0 = v - i + dot(i, C.xx);\n\
vec2 i1;\n\
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n\
vec4 x12 = x0.xyxy + C.xxzz;\n\
x12.xy -= i1;\n\
i = mod(i, 289.0);\n\
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))\n\
+ i.x + vec3(0.0, i1.x, 1.0 ));\n\
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),dot(x12.zw,x12.zw)), 0.0);\n\
m = m*m ;\n\
m = m*m ;\n\
vec3 x = 2.0 * fract(p * C.www) - 1.0;\n\
vec3 h = abs(x) - 0.5;\n\
vec3 ox = floor(x + 0.5);\n\
vec3 a0 = x - ox;\n\
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );\n\
vec3 g;\n\
g.x = a0.x * x0.x + h.x * x0.y;\n\
g.yz = a0.yz * x12.xz + h.yz * x12.yw;\n\
return 130.0 * dot(m, g);\n\
}\n\
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}\n\
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}\n\
\n\
float snoise(vec3 v){ \n\
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;\n\
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n\
\n\
// First corner\n\
vec3 i = floor(v + dot(v, C.yyy) );\n\
vec3 x0 = v - i + dot(i, C.xxx) ;\n\
\n\
// Other corners\n\
vec3 g = step(x0.yzx, x0.xyz);\n\
vec3 l = 1.0 - g;\n\
vec3 i1 = min( g.xyz, l.zxy );\n\
vec3 i2 = max( g.xyz, l.zxy );\n\
\n\
// x0 = x0 - 0. + 0.0 * C \n\
vec3 x1 = x0 - i1 + 1.0 * C.xxx;\n\
vec3 x2 = x0 - i2 + 2.0 * C.xxx;\n\
vec3 x3 = x0 - 1. + 3.0 * C.xxx;\n\
\n\
// Permutations\n\
i = mod(i, 289.0 ); \n\
vec4 p = permute( permute( permute( \n\
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))\n\
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 )) \n\
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));\n\
\n\
// Gradients\n\
// ( N*N points uniformly over a square, mapped onto an octahedron.)\n\
float n_ = 1.0/7.0; // N=7\n\
vec3 ns = n_ * D.wyz - D.xzx;\n\
\n\
vec4 j = p - 49.0 * floor(p * ns.z *ns.z); // mod(p,N*N)\n\
\n\
vec4 x_ = floor(j * ns.z);\n\
vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)\n\
\n\
vec4 x = x_ *ns.x + ns.yyyy;\n\
vec4 y = y_ *ns.x + ns.yyyy;\n\
vec4 h = 1.0 - abs(x) - abs(y);\n\
\n\
vec4 b0 = vec4( x.xy, y.xy );\n\
vec4 b1 = vec4( x.zw, y.zw );\n\
\n\
vec4 s0 = floor(b0)*2.0 + 1.0;\n\
vec4 s1 = floor(b1)*2.0 + 1.0;\n\
vec4 sh = -step(h, vec4(0.0));\n\
\n\
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;\n\
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;\n\
\n\
vec3 p0 = vec3(a0.xy,h.x);\n\
vec3 p1 = vec3(a0.zw,h.y);\n\
vec3 p2 = vec3(a1.xy,h.z);\n\
vec3 p3 = vec3(a1.zw,h.w);\n\
\n\
//Normalise gradients\n\
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));\n\
p0 *= norm.x;\n\
p1 *= norm.y;\n\
p2 *= norm.z;\n\
p3 *= norm.w;\n\
\n\
// Mix final noise value\n\
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n\
m = m * m;\n\
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),dot(p2,x2), dot(p3,x3) ) );\n\
}\n\
\n\
vec3 hash3( vec2 p ){\n\
vec3 q = vec3( dot(p,vec2(127.1,311.7)), \n\
dot(p,vec2(269.5,183.3)), \n\
dot(p,vec2(419.2,371.9)) );\n\
return fract(sin(q)*43758.5453);\n\
}\n\
vec4 hash4( vec3 p ){\n\
vec4 q = vec4( dot(p,vec3(127.1,311.7,257.3)), \n\
dot(p,vec3(269.5,183.3,335.1)), \n\
dot(p,vec3(314.5,235.1,467.3)), \n\
dot(p,vec3(419.2,371.9,114.9)) );\n\
return fract(sin(q)*43758.5453);\n\
}\n\
\n\
float iqnoise( in vec2 x, float u, float v ){\n\
vec2 p = floor(x);\n\
vec2 f = fract(x);\n\
\n\
float k = 1.0+63.0*pow(1.0-v,4.0);\n\
\n\
float va = 0.0;\n\
float wt = 0.0;\n\
for( int j=-2; j<=2; j++ )\n\
for( int i=-2; i<=2; i++ )\n\
{\n\
vec2 g = vec2( float(i),float(j) );\n\
vec3 o = hash3( p + g )*vec3(u,u,1.0);\n\
vec2 r = g - f + o.xy;\n\
float d = dot(r,r);\n\
float ww = pow( 1.0-smoothstep(0.0,1.414,sqrt(d)), k );\n\
va += o.z*ww;\n\
wt += ww;\n\
}\n\
\n\
return va/wt;\n\
}\n\
"
function LGraphShaderTime()
{
this.addOutput("out","float");
}
LGraphShaderTime.title = "Time";
LGraphShaderTime.prototype.onGetCode = function( context )
{
if(!this.shader_destination || !this.isOutputConnected(0))
return;
var outlink = getOutputLinkID(this,0);
context.addUniform( "u_time" + this.id, "float", function(){ return getTime() * 0.001; });
context.addCode("code", "float " + outlink + " = u_time" + this.id +";", this.shader_destination );
this.setOutputData( 0, "float" );
}
registerShaderNode( "input/time", LGraphShaderTime );
function LGraphShaderDither()
{
this.addInput("in","T");
this.addOutput("out","float");
}
LGraphShaderDither.title = "Dither";
LGraphShaderDither.prototype.onGetCode = function( context )
{
if(!this.shader_destination || !this.isOutputConnected(0))
return;
var inlink = getInputLinkID(this,0);
var return_type = "float";
var outlink = getOutputLinkID(this,0);
var intype = this.getInputData(0);
inlink = varToTypeGLSL( inlink, intype, "float" );
context.addFunction("dither8x8", LGraphShaderDither.dither_func);
context.addCode("code", return_type + " " + outlink + " = dither8x8("+ inlink +");", this.shader_destination );
this.setOutputData( 0, return_type );
}
LGraphShaderDither.dither_values = [0.515625,0.140625,0.640625,0.046875,0.546875,0.171875,0.671875,0.765625,0.265625,0.890625,0.390625,0.796875,0.296875,0.921875,0.421875,0.203125,0.703125,0.078125,0.578125,0.234375,0.734375,0.109375,0.609375,0.953125,0.453125,0.828125,0.328125,0.984375,0.484375,0.859375,0.359375,0.0625,0.5625,0.1875,0.6875,0.03125,0.53125,0.15625,0.65625,0.8125,0.3125,0.9375,0.4375,0.78125,0.28125,0.90625,0.40625,0.25,0.75,0.125,0.625,0.21875,0.71875,0.09375,0.59375,1.0001,0.5,0.875,0.375,0.96875,0.46875,0.84375,0.34375];
LGraphShaderDither.dither_func = "\n\
float dither8x8(float brightness) {\n\
vec2 position = vec2(0.0);\n\
#ifdef FRAGMENT\n\
position = gl_FragCoord.xy;\n\
#endif\n\
int x = int(mod(position.x, 8.0));\n\
int y = int(mod(position.y, 8.0));\n\
int index = x + y * 8;\n\
float limit = 0.0;\n\
if (x < 8) {\n\
if(index==0) limit = 0.015625;\n\
"+(LGraphShaderDither.dither_values.map( function(v,i){ return "else if(index== "+(i+1)+") limit = " + v + ";"}).join("\n"))+"\n\
}\n\
return brightness < limit ? 0.0 : 1.0;\n\
}\n",
registerShaderNode( "math/dither", LGraphShaderDither );
function LGraphShaderRemap()
{
this.addInput("", LGShaders.ALL_TYPES );
this.addOutput("","");
this.properties = {
min_value: 0,
max_value: 1,
min_value2: 0,
max_value2: 1
};
this.addWidget("number","min",0,{ step: 0.1, property: "min_value" });
this.addWidget("number","max",1,{ step: 0.1, property: "max_value" });
this.addWidget("number","min2",0,{ step: 0.1, property: "min_value2"});
this.addWidget("number","max2",1,{ step: 0.1, property: "max_value2"});
}
LGraphShaderRemap.title = "Remap";
LGraphShaderRemap.prototype.onPropertyChanged = function()
{
if(this.graph)
this.graph._version++;
}
LGraphShaderRemap.prototype.onConnectionsChange = function()
{
var return_type = this.getInputDataType(0);
this.outputs[0].type = return_type || "T";
}
LGraphShaderRemap.prototype.onGetCode = function( context )
{
if(!this.shader_destination || !this.isOutputConnected(0))
return;
var inlink = getInputLinkID(this,0);
var outlink = getOutputLinkID(this,0);
if(!inlink && !outlink) //not connected
return;
var return_type = this.getInputDataType(0);
this.outputs[0].type = return_type;
if(return_type == "T")
{
console.warn("node type is T and cannot be resolved");
return;
}
if(!inlink)
{
context.addCode("code"," " + return_type + " " + outlink + " = " + return_type + "(0.0);\n");
return;
}
var minv = valueToGLSL( this.properties.min_value );
var maxv = valueToGLSL( this.properties.max_value );
var minv2 = valueToGLSL( this.properties.min_value2 );
var maxv2 = valueToGLSL( this.properties.max_value2 );
context.addCode("code", return_type + " " + outlink + " = ( (" + inlink + " - "+minv+") / ("+ maxv+" - "+minv+") ) * ("+ maxv2+" - "+minv2+") + " + minv2 + ";", this.shader_destination );
this.setOutputData( 0, return_type );
}
registerShaderNode( "math/remap", LGraphShaderRemap );
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
var view_matrix = new Float32Array(16);
var projection_matrix = new Float32Array(16);
var viewprojection_matrix = new Float32Array(16);
var model_matrix = new Float32Array(16);
var global_uniforms = {
u_view: view_matrix,
u_projection: projection_matrix,
u_viewprojection: viewprojection_matrix,
u_model: model_matrix
};
LiteGraph.LGraphRender = {
onRequestCameraMatrices: null //overwrite with your 3D engine specifics, it will receive (view_matrix, projection_matrix,viewprojection_matrix) and must be filled
};
function generateGeometryId() {
return (Math.random() * 100000)|0;
}
function LGraphPoints3D() {
this.addInput("obj", "");
this.addInput("radius", "number");
this.addOutput("out", "geometry");
this.addOutput("points", "[vec3]");
this.properties = {
radius: 1,
num_points: 4096,
generate_normals: true,
regular: false,
mode: LGraphPoints3D.SPHERE,
force_update: false
};
this.points = new Float32Array( this.properties.num_points * 3 );
this.normals = new Float32Array( this.properties.num_points * 3 );
this.must_update = true;
this.version = 0;
var that = this;
this.addWidget("button","update",null, function(){ that.must_update = true; });
this.geometry = {
vertices: null,
_id: generateGeometryId()
}
this._old_obj = null;
this._last_radius = null;
}
global.LGraphPoints3D = LGraphPoints3D;
LGraphPoints3D.RECTANGLE = 1;
LGraphPoints3D.CIRCLE = 2;
LGraphPoints3D.CUBE = 10;
LGraphPoints3D.SPHERE = 11;
LGraphPoints3D.HEMISPHERE = 12;
LGraphPoints3D.INSIDE_SPHERE = 13;
LGraphPoints3D.OBJECT = 20;
LGraphPoints3D.OBJECT_UNIFORMLY = 21;
LGraphPoints3D.OBJECT_INSIDE = 22;
LGraphPoints3D.MODE_VALUES = { "rectangle":LGraphPoints3D.RECTANGLE, "circle":LGraphPoints3D.CIRCLE, "cube":LGraphPoints3D.CUBE, "sphere":LGraphPoints3D.SPHERE, "hemisphere":LGraphPoints3D.HEMISPHERE, "inside_sphere":LGraphPoints3D.INSIDE_SPHERE, "object":LGraphPoints3D.OBJECT, "object_uniformly":LGraphPoints3D.OBJECT_UNIFORMLY, "object_inside":LGraphPoints3D.OBJECT_INSIDE };
LGraphPoints3D.widgets_info = {
mode: { widget: "combo", values: LGraphPoints3D.MODE_VALUES }
};
LGraphPoints3D.title = "list of points";
LGraphPoints3D.desc = "returns an array of points";
LGraphPoints3D.prototype.onPropertyChanged = function(name,value)
{
this.must_update = true;
}
LGraphPoints3D.prototype.onExecute = function() {
var obj = this.getInputData(0);
if( obj != this._old_obj || (obj && obj._version != this._old_obj_version) )
{
this._old_obj = obj;
this.must_update = true;
}
var radius = this.getInputData(1);
if(radius == null)
radius = this.properties.radius;
if( this._last_radius != radius )
{
this._last_radius = radius;
this.must_update = true;
}
if(this.must_update || this.properties.force_update )
{
this.must_update = false;
this.updatePoints();
}
this.geometry.vertices = this.points;
this.geometry.normals = this.normals;
this.geometry._version = this.version;
this.setOutputData( 0, this.geometry );
}
LGraphPoints3D.prototype.updatePoints = function() {
var num_points = this.properties.num_points|0;
if(num_points < 1)
num_points = 1;
if(!this.points || this.points.length != num_points * 3)
this.points = new Float32Array( num_points * 3 );
if(this.properties.generate_normals)
{
if (!this.normals || this.normals.length != this.points.length)
this.normals = new Float32Array( this.points.length );
}
else
this.normals = null;
var radius = this._last_radius || this.properties.radius;
var mode = this.properties.mode;
var obj = this.getInputData(0);
this._old_obj_version = obj ? obj._version : null;
this.points = LGraphPoints3D.generatePoints( radius, num_points, mode, this.points, this.normals, this.properties.regular, obj );
this.version++;
}
//global
LGraphPoints3D.generatePoints = function( radius, num_points, mode, points, normals, regular, obj )
{
var size = num_points * 3;
if(!points || points.length != size)
points = new Float32Array( size );
var temp = new Float32Array(3);
var UP = new Float32Array([0,1,0]);
if(regular)
{
if( mode == LGraphPoints3D.RECTANGLE)
{
var side = Math.floor(Math.sqrt(num_points));
for(var i = 0; i < side; ++i)
for(var j = 0; j < side; ++j)
{
var pos = i*3 + j*3*side;
points[pos] = ((i/side) - 0.5) * radius * 2;
points[pos+1] = 0;
points[pos+2] = ((j/side) - 0.5) * radius * 2;
}
points = new Float32Array( points.subarray(0,side*side*3) );
if(normals)
{
for(var i = 0; i < normals.length; i+=3)
normals.set(UP, i);
}
}
else if( mode == LGraphPoints3D.SPHERE)
{
var side = Math.floor(Math.sqrt(num_points));
for(var i = 0; i < side; ++i)
for(var j = 0; j < side; ++j)
{
var pos = i*3 + j*3*side;
polarToCartesian( temp, (i/side) * 2 * Math.PI, ((j/side) - 0.5) * 2 * Math.PI, radius );
points[pos] = temp[0];
points[pos+1] = temp[1];
points[pos+2] = temp[2];
}
points = new Float32Array( points.subarray(0,side*side*3) );
if(normals)
LGraphPoints3D.generateSphericalNormals( points, normals );
}
else if( mode == LGraphPoints3D.CIRCLE)
{
for(var i = 0; i < size; i+=3)
{
var angle = 2 * Math.PI * (i/size);
points[i] = Math.cos( angle ) * radius;
points[i+1] = 0;
points[i+2] = Math.sin( angle ) * radius;
}
if(normals)
{
for(var i = 0; i < normals.length; i+=3)
normals.set(UP, i);
}
}
}
else //non regular
{
if( mode == LGraphPoints3D.RECTANGLE)
{
for(var i = 0; i < size; i+=3)
{
points[i] = (Math.random() - 0.5) * radius * 2;
points[i+1] = 0;
points[i+2] = (Math.random() - 0.5) * radius * 2;
}
if(normals)
{
for(var i = 0; i < normals.length; i+=3)
normals.set(UP, i);
}
}
else if( mode == LGraphPoints3D.CUBE)
{
for(var i = 0; i < size; i+=3)
{
points[i] = (Math.random() - 0.5) * radius * 2;
points[i+1] = (Math.random() - 0.5) * radius * 2;
points[i+2] = (Math.random() - 0.5) * radius * 2;
}
if(normals)
{
for(var i = 0; i < normals.length; i+=3)
normals.set(UP, i);
}
}
else if( mode == LGraphPoints3D.SPHERE)
{
LGraphPoints3D.generateSphere( points, size, radius );
if(normals)
LGraphPoints3D.generateSphericalNormals( points, normals );
}
else if( mode == LGraphPoints3D.HEMISPHERE)
{
LGraphPoints3D.generateHemisphere( points, size, radius );
if(normals)
LGraphPoints3D.generateSphericalNormals( points, normals );
}
else if( mode == LGraphPoints3D.CIRCLE)
{
LGraphPoints3D.generateInsideCircle( points, size, radius );
if(normals)
LGraphPoints3D.generateSphericalNormals( points, normals );
}
else if( mode == LGraphPoints3D.INSIDE_SPHERE)
{
LGraphPoints3D.generateInsideSphere( points, size, radius );
if(normals)
LGraphPoints3D.generateSphericalNormals( points, normals );
}
else if( mode == LGraphPoints3D.OBJECT)
{
LGraphPoints3D.generateFromObject( points, normals, size, obj, false );
}
else if( mode == LGraphPoints3D.OBJECT_UNIFORMLY)
{
LGraphPoints3D.generateFromObject( points, normals, size, obj, true );
}
else if( mode == LGraphPoints3D.OBJECT_INSIDE)
{
LGraphPoints3D.generateFromInsideObject( points, size, obj );
//if(normals)
// LGraphPoints3D.generateSphericalNormals( points, normals );
}
else
console.warn("wrong mode in LGraphPoints3D");
}
return points;
}
LGraphPoints3D.generateSphericalNormals = function(points, normals)
{
var temp = new Float32Array(3);
for(var i = 0; i < normals.length; i+=3)
{
temp[0] = points[i];
temp[1] = points[i+1];
temp[2] = points[i+2];
vec3.normalize(temp,temp);
normals.set(temp,i);
}
}
LGraphPoints3D.generateSphere = function (points, size, radius)
{
for(var i = 0; i < size; i+=3)
{
var r1 = Math.random();
var r2 = Math.random();
var x = 2 * Math.cos( 2 * Math.PI * r1 ) * Math.sqrt( r2 * (1-r2) );
var y = 1 - 2 * r2;
var z = 2 * Math.sin( 2 * Math.PI * r1 ) * Math.sqrt( r2 * (1-r2) );
points[i] = x * radius;
points[i+1] = y * radius;
points[i+2] = z * radius;
}
}
LGraphPoints3D.generateHemisphere = function (points, size, radius)
{
for(var i = 0; i < size; i+=3)
{
var r1 = Math.random();
var r2 = Math.random();
var x = Math.cos( 2 * Math.PI * r1 ) * Math.sqrt(1 - r2*r2 );
var y = r2;
var z = Math.sin( 2 * Math.PI * r1 ) * Math.sqrt(1 - r2*r2 );
points[i] = x * radius;
points[i+1] = y * radius;
points[i+2] = z * radius;
}
}
LGraphPoints3D.generateInsideCircle = function (points, size, radius)
{
for(var i = 0; i < size; i+=3)
{
var r1 = Math.random();
var r2 = Math.random();
var x = Math.cos( 2 * Math.PI * r1 ) * Math.sqrt(1 - r2*r2 );
var y = r2;
var z = Math.sin( 2 * Math.PI * r1 ) * Math.sqrt(1 - r2*r2 );
points[i] = x * radius;
points[i+1] = 0;
points[i+2] = z * radius;
}
}
LGraphPoints3D.generateInsideSphere = function (points, size, radius)
{
for(var i = 0; i < size; i+=3)
{
var u = Math.random();
var v = Math.random();
var theta = u * 2.0 * Math.PI;
var phi = Math.acos(2.0 * v - 1.0);
var r = Math.cbrt(Math.random()) * radius;
var sinTheta = Math.sin(theta);
var cosTheta = Math.cos(theta);
var sinPhi = Math.sin(phi);
var cosPhi = Math.cos(phi);
points[i] = r * sinPhi * cosTheta;
points[i+1] = r * sinPhi * sinTheta;
points[i+2] = r * cosPhi;
}
}
function findRandomTriangle( areas, f )
{
var l = areas.length;
var imin = 0;
var imid = 0;
var imax = l;
if(l == 0)
return -1;
if(l == 1)
return 0;
//dichotomic search
while (imax >= imin)
{
imid = ((imax + imin)*0.5)|0;
var t = areas[ imid ];
if( t == f )
return imid;
if( imin == (imax - 1) )
return imin;
if (t < f)
imin = imid;
else
imax = imid;
}
return imid;
}
LGraphPoints3D.generateFromObject = function( points, normals, size, obj, evenly )
{
if(!obj)
return;
var vertices = null;
var mesh_normals = null;
var indices = null;
var areas = null;
if( obj.constructor === GL.Mesh )
{
vertices = obj.vertexBuffers.vertices.data;
mesh_normals = obj.vertexBuffers.normals ? obj.vertexBuffers.normals.data : null;
indices = obj.indexBuffers.indices ? obj.indexBuffers.indices.data : null;
if(!indices)
indices = obj.indexBuffers.triangles ? obj.indexBuffers.triangles.data : null;
}
if(!vertices)
return null;
var num_triangles = indices ? indices.length / 3 : vertices.length / (3*3);
var total_area = 0; //sum of areas of all triangles
if(evenly)
{
areas = new Float32Array(num_triangles); //accum
for(var i = 0; i < num_triangles; ++i)
{
if(indices)
{
a = indices[i*3]*3;
b = indices[i*3+1]*3;
c = indices[i*3+2]*3;
}
else
{
a = i*9;
b = i*9+3;
c = i*9+6;
}
var P1 = vertices.subarray(a,a+3);
var P2 = vertices.subarray(b,b+3);
var P3 = vertices.subarray(c,c+3);
var aL = vec3.distance( P1, P2 );
var bL = vec3.distance( P2, P3 );
var cL = vec3.distance( P3, P1 );
var s = (aL + bL+ cL) / 2;
total_area += Math.sqrt(s * (s - aL) * (s - bL) * (s - cL));
areas[i] = total_area;
}
for(var i = 0; i < num_triangles; ++i) //normalize
areas[i] /= total_area;
}
for(var i = 0; i < size; i+=3)
{
var r = Math.random();
var index = evenly ? findRandomTriangle( areas, r ) : Math.floor(r * num_triangles );
//get random triangle
var a = 0;
var b = 0;
var c = 0;
if(indices)
{
a = indices[index*3]*3;
b = indices[index*3+1]*3;
c = indices[index*3+2]*3;
}
else
{
a = index*9;
b = index*9+3;
c = index*9+6;
}
var s = Math.random();
var t = Math.random();
var sqrt_s = Math.sqrt(s);
var af = 1 - sqrt_s;
var bf = sqrt_s * ( 1 - t);
var cf = t * sqrt_s;
points[i] = af * vertices[a] + bf*vertices[b] + cf*vertices[c];
points[i+1] = af * vertices[a+1] + bf*vertices[b+1] + cf*vertices[c+1];
points[i+2] = af * vertices[a+2] + bf*vertices[b+2] + cf*vertices[c+2];
if(normals && mesh_normals)
{
normals[i] = af * mesh_normals[a] + bf*mesh_normals[b] + cf*mesh_normals[c];
normals[i+1] = af * mesh_normals[a+1] + bf*mesh_normals[b+1] + cf*mesh_normals[c+1];
normals[i+2] = af * mesh_normals[a+2] + bf*mesh_normals[b+2] + cf*mesh_normals[c+2];
var N = normals.subarray(i,i+3);
vec3.normalize(N,N);
}
}
}
LGraphPoints3D.generateFromInsideObject = function( points, size, mesh )
{
if(!mesh || mesh.constructor !== GL.Mesh)
return;
var aabb = mesh.getBoundingBox();
if(!mesh.octree)
mesh.octree = new GL.Octree( mesh );
var octree = mesh.octree;
var origin = vec3.create();
var direction = vec3.fromValues(1,0,0);
var temp = vec3.create();
var i = 0;
var tries = 0;
while(i < size && tries < points.length * 10) //limit to avoid problems
{
tries += 1
var r = vec3.random(temp); //random point inside the aabb
r[0] = (r[0] * 2 - 1) * aabb[3] + aabb[0];
r[1] = (r[1] * 2 - 1) * aabb[4] + aabb[1];
r[2] = (r[2] * 2 - 1) * aabb[5] + aabb[2];
origin.set(r);
var hit = octree.testRay( origin, direction, 0, 10000, true, GL.Octree.ALL );
if(!hit || hit.length % 2 == 0) //not inside
continue;
points.set( r, i );
i+=3;
}
}
LiteGraph.registerNodeType( "geometry/points3D", LGraphPoints3D );
function LGraphPointsToInstances() {
this.addInput("points", "geometry");
this.addOutput("instances", "[mat4]");
this.properties = {
mode: 1,
autoupdate: true
};
this.must_update = true;
this.matrices = [];
this.first_time = true;
}
LGraphPointsToInstances.NORMAL = 0;
LGraphPointsToInstances.VERTICAL = 1;
LGraphPointsToInstances.SPHERICAL = 2;
LGraphPointsToInstances.RANDOM = 3;
LGraphPointsToInstances.RANDOM_VERTICAL = 4;
LGraphPointsToInstances.modes = {"normal":0,"vertical":1,"spherical":2,"random":3,"random_vertical":4};
LGraphPointsToInstances.widgets_info = {
mode: { widget: "combo", values: LGraphPointsToInstances.modes }
};
LGraphPointsToInstances.title = "points to inst";
LGraphPointsToInstances.prototype.onExecute = function()
{
var geo = this.getInputData(0);
if( !geo )
{
this.setOutputData(0,null);
return;
}
if( !this.isOutputConnected(0) )
return;
var has_changed = (geo._version != this._version || geo._id != this._geometry_id);
if( has_changed && this.properties.autoupdate || this.first_time )
{
this.first_time = false;
this.updateInstances( geo );
}
this.setOutputData( 0, this.matrices );
}
LGraphPointsToInstances.prototype.updateInstances = function( geometry )
{
var vertices = geometry.vertices;
if(!vertices)
return null;
var normals = geometry.normals;
var matrices = this.matrices;
var num_points = vertices.length / 3;
if( matrices.length != num_points)
matrices.length = num_points;
var identity = mat4.create();
var temp = vec3.create();
var zero = vec3.create();
var UP = vec3.fromValues(0,1,0);
var FRONT = vec3.fromValues(0,0,-1);
var RIGHT = vec3.fromValues(1,0,0);
var R = quat.create();
var front = vec3.create();
var right = vec3.create();
var top = vec3.create();
for(var i = 0; i < vertices.length; i += 3)
{
var index = i/3;
var m = matrices[index];
if(!m)
m = matrices[index] = mat4.create();
m.set( identity );
var point = vertices.subarray(i,i+3);
switch(this.properties.mode)
{
case LGraphPointsToInstances.NORMAL:
mat4.setTranslation( m, point );
if(normals)
{
var normal = normals.subarray(i,i+3);
top.set( normal );
vec3.normalize( top, top );
vec3.cross( right, FRONT, top );
vec3.normalize( right, right );
vec3.cross( front, right, top );
vec3.normalize( front, front );
m.set(right,0);
m.set(top,4);
m.set(front,8);
mat4.setTranslation( m, point );
}
break;
case LGraphPointsToInstances.VERTICAL:
mat4.setTranslation( m, point );
break;
case LGraphPointsToInstances.SPHERICAL:
front.set( point );
vec3.normalize( front, front );
vec3.cross( right, UP, front );
vec3.normalize( right, right );
vec3.cross( top, front, right );
vec3.normalize( top, top );
m.set(right,0);
m.set(top,4);
m.set(front,8);
mat4.setTranslation( m, point );
break;
case LGraphPointsToInstances.RANDOM:
temp[0] = Math.random()*2 - 1;
temp[1] = Math.random()*2 - 1;
temp[2] = Math.random()*2 - 1;
vec3.normalize( temp, temp );
quat.setAxisAngle( R, temp, Math.random() * 2 * Math.PI );
mat4.fromQuat(m, R);
mat4.setTranslation( m, point );
break;
case LGraphPointsToInstances.RANDOM_VERTICAL:
quat.setAxisAngle( R, UP, Math.random() * 2 * Math.PI );
mat4.fromQuat(m, R);
mat4.setTranslation( m, point );
break;
}
}
this._version = geometry._version;
this._geometry_id = geometry._id;
}
LiteGraph.registerNodeType( "geometry/points_to_instances", LGraphPointsToInstances );
function LGraphGeometryTransform() {
this.addInput("in", "geometry,[mat4]");
this.addInput("mat4", "mat4");
this.addOutput("out", "geometry");
this.properties = {};
this.geometry = {
type: "triangles",
vertices: null,
_id: generateGeometryId(),
_version: 0
};
this._last_geometry_id = -1;
this._last_version = -1;
this._last_key = "";
this.must_update = true;
}
LGraphGeometryTransform.title = "Transform";
LGraphGeometryTransform.prototype.onExecute = function() {
var input = this.getInputData(0);
var model = this.getInputData(1);
if(!input)
return;
//array of matrices
if(input.constructor === Array)
{
if(input.length == 0)
return;
this.outputs[0].type = "[mat4]";
if( !this.isOutputConnected(0) )
return;
if(!model)
{
this.setOutputData(0,input);
return;
}
if(!this._output)
this._output = new Array();
if(this._output.length != input.length)
this._output.length = input.length;
for(var i = 0; i < input.length; ++i)
{
var m = this._output[i];
if(!m)
m = this._output[i] = mat4.create();
mat4.multiply(m,input[i],model);
}
this.setOutputData(0,this._output);
return;
}
//geometry
if(!input.vertices || !input.vertices.length)
return;
var geo = input;
this.outputs[0].type = "geometry";
if( !this.isOutputConnected(0) )
return;
if(!model)
{
this.setOutputData(0,geo);
return;
}
var key = typedArrayToArray(model).join(",");
if( this.must_update || geo._id != this._last_geometry_id || geo._version != this._last_version || key != this._last_key )
{
this.updateGeometry(geo, model);
this._last_key = key;
this._last_version = geo._version;
this._last_geometry_id = geo._id;
this.must_update = false;
}
this.setOutputData(0,this.geometry);
}
LGraphGeometryTransform.prototype.updateGeometry = function(geometry, model) {
var old_vertices = geometry.vertices;
var vertices = this.geometry.vertices;
if( !vertices || vertices.length != old_vertices.length )
vertices = this.geometry.vertices = new Float32Array( old_vertices.length );
var temp = vec3.create();
for(var i = 0, l = vertices.length; i < l; i+=3)
{
temp[0] = old_vertices[i]; temp[1] = old_vertices[i+1]; temp[2] = old_vertices[i+2];
mat4.multiplyVec3( temp, model, temp );
vertices[i] = temp[0]; vertices[i+1] = temp[1]; vertices[i+2] = temp[2];
}
if(geometry.normals)
{
if( !this.geometry.normals || this.geometry.normals.length != geometry.normals.length )
this.geometry.normals = new Float32Array( geometry.normals.length );
var normals = this.geometry.normals;
var normal_model = mat4.invert(mat4.create(), model);
if(normal_model)
mat4.transpose(normal_model, normal_model);
var old_normals = geometry.normals;
for(var i = 0, l = normals.length; i < l; i+=3)
{
temp[0] = old_normals[i]; temp[1] = old_normals[i+1]; temp[2] = old_normals[i+2];
mat4.multiplyVec3( temp, normal_model, temp );
normals[i] = temp[0]; normals[i+1] = temp[1]; normals[i+2] = temp[2];
}
}
this.geometry.type = geometry.type;
this.geometry._version++;
}
LiteGraph.registerNodeType( "geometry/transform", LGraphGeometryTransform );
function LGraphGeometryPolygon() {
this.addInput("sides", "number");
this.addInput("radius", "number");
this.addOutput("out", "geometry");
this.properties = { sides: 6, radius: 1, uvs: false }
this.geometry = {
type: "line_loop",
vertices: null,
_id: generateGeometryId()
};
this.geometry_id = -1;
this.version = -1;
this.must_update = true;
this.last_info = { sides: -1, radius: -1 };
}
LGraphGeometryPolygon.title = "Polygon";
LGraphGeometryPolygon.prototype.onExecute = function() {
if( !this.isOutputConnected(0) )
return;
var sides = this.getInputOrProperty("sides");
var radius = this.getInputOrProperty("radius");
sides = Math.max(3,sides)|0;
//update
if( this.last_info.sides != sides || this.last_info.radius != radius )
this.updateGeometry(sides, radius);
this.setOutputData(0,this.geometry);
}
LGraphGeometryPolygon.prototype.updateGeometry = function(sides, radius) {
var num = 3*sides;
var vertices = this.geometry.vertices;
if( !vertices || vertices.length != num )
vertices = this.geometry.vertices = new Float32Array( 3*sides );
var delta = (Math.PI * 2) / sides;
var gen_uvs = this.properties.uvs;
if(gen_uvs)
{
uvs = this.geometry.coords = new Float32Array( 3*sides );
}
for(var i = 0; i < sides; ++i)
{
var angle = delta * -i;
var x = Math.cos( angle ) * radius;
var y = 0;
var z = Math.sin( angle ) * radius;
vertices[i*3] = x;
vertices[i*3+1] = y;
vertices[i*3+2] = z;
if(gen_uvs)
{
}
}
this.geometry._id = ++this.geometry_id;
this.geometry._version = ++this.version;
this.last_info.sides = sides;
this.last_info.radius = radius;
}
LiteGraph.registerNodeType( "geometry/polygon", LGraphGeometryPolygon );
function LGraphGeometryExtrude() {
this.addInput("", "geometry");
this.addOutput("", "geometry");
this.properties = { top_cap: true, bottom_cap: true, offset: [0,100,0] };
this.version = -1;
this._last_geo_version = -1;
this._must_update = true;
}
LGraphGeometryExtrude.title = "extrude";
LGraphGeometryExtrude.prototype.onPropertyChanged = function(name, value)
{
this._must_update = true;
}
LGraphGeometryExtrude.prototype.onExecute = function()
{
var geo = this.getInputData(0);
if( !geo || !this.isOutputConnected(0) )
return;
if(geo.version != this._last_geo_version || this._must_update)
{
this._geo = this.extrudeGeometry( geo, this._geo );
if(this._geo)
this._geo.version = this.version++;
this._must_update = false;
}
this.setOutputData(0, this._geo);
}
LGraphGeometryExtrude.prototype.extrudeGeometry = function( geo )
{
//for every pair of vertices
var vertices = geo.vertices;
var num_points = vertices.length / 3;
var tempA = vec3.create();
var tempB = vec3.create();
var tempC = vec3.create();
var tempD = vec3.create();
var offset = new Float32Array( this.properties.offset );
if(geo.type == "line_loop")
{
var new_vertices = new Float32Array( num_points * 6 * 3 ); //every points become 6 ( caps not included )
var npos = 0;
for(var i = 0, l = vertices.length; i < l; i += 3)
{
tempA[0] = vertices[i]; tempA[1] = vertices[i+1]; tempA[2] = vertices[i+2];
if( i+3 < l ) //loop
{
tempB[0] = vertices[i+3]; tempB[1] = vertices[i+4]; tempB[2] = vertices[i+5];
}
else
{
tempB[0] = vertices[0]; tempB[1] = vertices[1]; tempB[2] = vertices[2];
}
vec3.add( tempC, tempA, offset );
vec3.add( tempD, tempB, offset );
new_vertices.set( tempA, npos ); npos += 3;
new_vertices.set( tempB, npos ); npos += 3;
new_vertices.set( tempC, npos ); npos += 3;
new_vertices.set( tempB, npos ); npos += 3;
new_vertices.set( tempD, npos ); npos += 3;
new_vertices.set( tempC, npos ); npos += 3;
}
}
var out_geo = {
_id: generateGeometryId(),
type: "triangles",
vertices: new_vertices
};
return out_geo;
}
LiteGraph.registerNodeType( "geometry/extrude", LGraphGeometryExtrude );
function LGraphGeometryEval() {
this.addInput("in", "geometry");
this.addOutput("out", "geometry");
this.properties = {
code: "V[1] += 0.01 * Math.sin(I + T*0.001);",
execute_every_frame: false
};
this.geometry = null;
this.geometry_id = -1;
this.version = -1;
this.must_update = true;
this.vertices = null;
this.func = null;
}
LGraphGeometryEval.title = "geoeval";
LGraphGeometryEval.desc = "eval code";
LGraphGeometryEval.widgets_info = {
code: { widget: "code" }
};
LGraphGeometryEval.prototype.onConfigure = function(o)
{
this.compileCode();
}
LGraphGeometryEval.prototype.compileCode = function()
{
if(!this.properties.code)
return;
try
{
this.func = new Function("V","I","T", this.properties.code);
this.boxcolor = "#AFA";
this.must_update = true;
}
catch (err)
{
this.boxcolor = "red";
}
}
LGraphGeometryEval.prototype.onPropertyChanged = function(name, value)
{
if(name == "code")
{
this.properties.code = value;
this.compileCode();
}
}
LGraphGeometryEval.prototype.onExecute = function() {
var geometry = this.getInputData(0);
if(!geometry)
return;
if(!this.func)
{
this.setOutputData(0,geometry);
return;
}
if( this.geometry_id != geometry._id || this.version != geometry._version || this.must_update || this.properties.execute_every_frame )
{
this.must_update = false;
this.geometry_id = geometry._id;
if(this.properties.execute_every_frame)
this.version++;
else
this.version = geometry._version;
var func = this.func;
var T = getTime();
//clone
if(!this.geometry)
this.geometry = {};
for(var i in geometry)
{
if(geometry[i] == null)
continue;
if( geometry[i].constructor == Float32Array )
this.geometry[i] = new Float32Array( geometry[i] );
else
this.geometry[i] = geometry[i];
}
this.geometry._id = geometry._id;
if(this.properties.execute_every_frame)
this.geometry._version = this.version;
else
this.geometry._version = geometry._version + 1;
var V = vec3.create();
var vertices = this.vertices;
if(!vertices || this.vertices.length != geometry.vertices.length)
vertices = this.vertices = new Float32Array( geometry.vertices );
else
vertices.set( geometry.vertices );
for(var i = 0; i < vertices.length; i+=3)
{
V[0] = vertices[i];
V[1] = vertices[i+1];
V[2] = vertices[i+2];
func(V,i/3,T);
vertices[i] = V[0];
vertices[i+1] = V[1];
vertices[i+2] = V[2];
}
this.geometry.vertices = vertices;
}
this.setOutputData(0,this.geometry);
}
LiteGraph.registerNodeType( "geometry/eval", LGraphGeometryEval );
/*
function LGraphGeometryDisplace() {
this.addInput("in", "geometry");
this.addInput("img", "image");
this.addOutput("out", "geometry");
this.properties = {
grid_size: 1
};
this.geometry = null;
this.geometry_id = -1;
this.version = -1;
this.must_update = true;
this.vertices = null;
}
LGraphGeometryDisplace.title = "displace";
LGraphGeometryDisplace.desc = "displace points";
LGraphGeometryDisplace.prototype.onExecute = function() {
var geometry = this.getInputData(0);
var image = this.getInputData(1);
if(!geometry)
return;
if(!image)
{
this.setOutputData(0,geometry);
return;
}
if( this.geometry_id != geometry._id || this.version != geometry._version || this.must_update )
{
this.must_update = false;
this.geometry_id = geometry._id;
this.version = geometry._version;
//copy
this.geometry = {};
for(var i in geometry)
this.geometry[i] = geometry[i];
this.geometry._id = geometry._id;
this.geometry._version = geometry._version + 1;
var grid_size = this.properties.grid_size;
if(grid_size != 0)
{
var vertices = this.vertices;
if(!vertices || this.vertices.length != this.geometry.vertices.length)
vertices = this.vertices = new Float32Array( this.geometry.vertices );
for(var i = 0; i < vertices.length; i+=3)
{
vertices[i] = Math.round(vertices[i]/grid_size) * grid_size;
vertices[i+1] = Math.round(vertices[i+1]/grid_size) * grid_size;
vertices[i+2] = Math.round(vertices[i+2]/grid_size) * grid_size;
}
this.geometry.vertices = vertices;
}
}
this.setOutputData(0,this.geometry);
}
LiteGraph.registerNodeType( "geometry/displace", LGraphGeometryDisplace );
*/
function LGraphConnectPoints() {
this.addInput("in", "geometry");
this.addOutput("out", "geometry");
this.properties = {
min_dist: 0.4,
max_dist: 0.5,
max_connections: 0,
probability: 1
};
this.geometry_id = -1;
this.version = -1;
this.my_version = 1;
this.must_update = true;
}
LGraphConnectPoints.title = "connect points";
LGraphConnectPoints.desc = "adds indices between near points";
LGraphConnectPoints.prototype.onPropertyChanged = function(name,value)
{
this.must_update = true;
}
LGraphConnectPoints.prototype.onExecute = function() {
var geometry = this.getInputData(0);
if(!geometry)
return;
if( this.geometry_id != geometry._id || this.version != geometry._version || this.must_update )
{
this.must_update = false;
this.geometry_id = geometry._id;
this.version = geometry._version;
//copy
this.geometry = {};
for(var i in geometry)
this.geometry[i] = geometry[i];
this.geometry._id = generateGeometryId();
this.geometry._version = this.my_version++;
var vertices = geometry.vertices;
var l = vertices.length;
var min_dist = this.properties.min_dist;
var max_dist = this.properties.max_dist;
var probability = this.properties.probability;
var max_connections = this.properties.max_connections;
var indices = [];
for(var i = 0; i < l; i+=3)
{
var x = vertices[i];
var y = vertices[i+1];
var z = vertices[i+2];
var connections = 0;
for(var j = i+3; j < l; j+=3)
{
var x2 = vertices[j];
var y2 = vertices[j+1];
var z2 = vertices[j+2];
var dist = Math.sqrt( (x-x2)*(x-x2) + (y-y2)*(y-y2) + (z-z2)*(z-z2));
if(dist > max_dist || dist < min_dist || (probability < 1 && probability < Math.random()) )
continue;
indices.push(i/3,j/3);
connections += 1;
if(max_connections && connections > max_connections)
break;
}
}
this.geometry.indices = this.indices = new Uint32Array(indices);
}
if(this.indices && this.indices.length)
{
this.geometry.indices = this.indices;
this.setOutputData( 0, this.geometry );
}
else
this.setOutputData( 0, null );
}
LiteGraph.registerNodeType( "geometry/connectPoints", LGraphConnectPoints );
//Works with Litegl.js to create WebGL nodes
if (typeof GL == "undefined") //LiteGL RELATED **********************************************
return;
function LGraphToGeometry() {
this.addInput("mesh", "mesh");
this.addOutput("out", "geometry");
this.geometry = {};
this.last_mesh = null;
}
LGraphToGeometry.title = "to geometry";
LGraphToGeometry.desc = "converts a mesh to geometry";
LGraphToGeometry.prototype.onExecute = function() {
var mesh = this.getInputData(0);
if(!mesh)
return;
if(mesh != this.last_mesh)
{
this.last_mesh = mesh;
for(i in mesh.vertexBuffers)
{
var buffer = mesh.vertexBuffers[i];
this.geometry[i] = buffer.data
}
if(mesh.indexBuffers["triangles"])
this.geometry.indices = mesh.indexBuffers["triangles"].data;
this.geometry._id = generateGeometryId();
this.geometry._version = 0;
}
this.setOutputData(0,this.geometry);
if(this.geometry)
this.setOutputData(1,this.geometry.vertices);
}
LiteGraph.registerNodeType( "geometry/toGeometry", LGraphToGeometry );
function LGraphGeometryToMesh() {
this.addInput("in", "geometry");
this.addOutput("mesh", "mesh");
this.properties = {};
this.version = -1;
this.mesh = null;
}
LGraphGeometryToMesh.title = "Geo to Mesh";
LGraphGeometryToMesh.prototype.updateMesh = function(geometry)
{
if(!this.mesh)
this.mesh = new GL.Mesh();
for(var i in geometry)
{
if(i[0] == "_")
continue;
var buffer_data = geometry[i];
var info = GL.Mesh.common_buffers[i];
if(!info && i != "indices") //unknown buffer
continue;
var spacing = info ? info.spacing : 3;
var mesh_buffer = this.mesh.vertexBuffers[i];
if(!mesh_buffer || mesh_buffer.data.length != buffer_data.length)
{
mesh_buffer = new GL.Buffer( i == "indices" ? GL.ELEMENT_ARRAY_BUFFER : GL.ARRAY_BUFFER, buffer_data, spacing, GL.DYNAMIC_DRAW );
}
else
{
mesh_buffer.data.set( buffer_data );
mesh_buffer.upload(GL.DYNAMIC_DRAW);
}
this.mesh.addBuffer( i, mesh_buffer );
}
if(this.mesh.vertexBuffers.normals &&this.mesh.vertexBuffers.normals.data.length != this.mesh.vertexBuffers.vertices.data.length )
{
var n = new Float32Array([0,1,0]);
var normals = new Float32Array( this.mesh.vertexBuffers.vertices.data.length );
for(var i = 0; i < normals.length; i+= 3)
normals.set( n, i );
mesh_buffer = new GL.Buffer( GL.ARRAY_BUFFER, normals, 3 );
this.mesh.addBuffer( "normals", mesh_buffer );
}
this.mesh.updateBoundingBox();
this.geometry_id = this.mesh.id = geometry._id;
this.version = this.mesh.version = geometry._version;
return this.mesh;
}
LGraphGeometryToMesh.prototype.onExecute = function() {
var geometry = this.getInputData(0);
if(!geometry)
return;
if( this.version != geometry._version || this.geometry_id != geometry._id )
this.updateMesh( geometry );
this.setOutputData(0, this.mesh);
}
LiteGraph.registerNodeType( "geometry/toMesh", LGraphGeometryToMesh );
function LGraphRenderMesh() {
this.addInput("mesh", "mesh");
this.addInput("mat4", "mat4");
this.addInput("tex", "texture");
this.properties = {
enabled: true,
primitive: GL.TRIANGLES,
additive: false,
color: [1,1,1],
opacity: 1
};
this.color = vec4.create([1,1,1,1]);
this.model_matrix = mat4.create();
this.uniforms = {
u_color: this.color,
u_model: this.model_matrix
};
}
LGraphRenderMesh.title = "Render Mesh";
LGraphRenderMesh.desc = "renders a mesh flat";
LGraphRenderMesh.PRIMITIVE_VALUES = { "points":GL.POINTS, "lines":GL.LINES, "line_loop":GL.LINE_LOOP,"line_strip":GL.LINE_STRIP, "triangles":GL.TRIANGLES, "triangle_fan":GL.TRIANGLE_FAN, "triangle_strip":GL.TRIANGLE_STRIP };
LGraphRenderMesh.widgets_info = {
primitive: { widget: "combo", values: LGraphRenderMesh.PRIMITIVE_VALUES },
color: { widget: "color" }
};
LGraphRenderMesh.prototype.onExecute = function() {
if(!this.properties.enabled)
return;
var mesh = this.getInputData(0);
if(!mesh)
return;
if(!LiteGraph.LGraphRender.onRequestCameraMatrices)
{
console.warn("cannot render geometry, LiteGraph.onRequestCameraMatrices is null, remember to fill this with a callback(view_matrix, projection_matrix,viewprojection_matrix) to use 3D rendering from the graph");
return;
}
LiteGraph.LGraphRender.onRequestCameraMatrices( view_matrix, projection_matrix,viewprojection_matrix );
var shader = null;
var texture = this.getInputData(2);
if(texture)
{
shader = gl.shaders["textured"];
if(!shader)
shader = gl.shaders["textured"] = new GL.Shader( LGraphRenderPoints.vertex_shader_code, LGraphRenderPoints.fragment_shader_code, { USE_TEXTURE:"" });
}
else
{
shader = gl.shaders["flat"];
if(!shader)
shader = gl.shaders["flat"] = new GL.Shader( LGraphRenderPoints.vertex_shader_code, LGraphRenderPoints.fragment_shader_code );
}
this.color.set( this.properties.color );
this.color[3] = this.properties.opacity;
var model_matrix = this.model_matrix;
var m = this.getInputData(1);
if(m)
model_matrix.set(m);
else
mat4.identity( model_matrix );
this.uniforms.u_point_size = 1;
var primitive = this.properties.primitive;
shader.uniforms( global_uniforms );
shader.uniforms( this.uniforms );
if(this.properties.opacity >= 1)
gl.disable( gl.BLEND );
else
gl.enable( gl.BLEND );
gl.enable( gl.DEPTH_TEST );
if( this.properties.additive )
{
gl.blendFunc( gl.SRC_ALPHA, gl.ONE );
gl.depthMask( false );
}
else
gl.blendFunc( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA );
var indices = "indices";
if( mesh.indexBuffers.triangles )
indices = "triangles";
shader.draw( mesh, primitive, indices );
gl.disable( gl.BLEND );
gl.depthMask( true );
}
LiteGraph.registerNodeType( "geometry/render_mesh", LGraphRenderMesh );
//**************************
function LGraphGeometryPrimitive() {
this.addInput("size", "number");
this.addOutput("out", "mesh");
this.properties = { type: 1, size: 1, subdivisions: 32 };
this.version = (Math.random() * 100000)|0;
this.last_info = { type: -1, size: -1, subdivisions: -1 };
}
LGraphGeometryPrimitive.title = "Primitive";
LGraphGeometryPrimitive.VALID = { "CUBE":1, "PLANE":2, "CYLINDER":3, "SPHERE":4, "CIRCLE":5, "HEMISPHERE":6, "ICOSAHEDRON":7, "CONE":8, "QUAD":9 };
LGraphGeometryPrimitive.widgets_info = {
type: { widget: "combo", values: LGraphGeometryPrimitive.VALID }
};
LGraphGeometryPrimitive.prototype.onExecute = function() {
if( !this.isOutputConnected(0) )
return;
var size = this.getInputOrProperty("size");
//update
if( this.last_info.type != this.properties.type || this.last_info.size != size || this.last_info.subdivisions != this.properties.subdivisions )
this.updateMesh( this.properties.type, size, this.properties.subdivisions );
this.setOutputData(0,this._mesh);
}
LGraphGeometryPrimitive.prototype.updateMesh = function(type, size, subdivisions)
{
subdivisions = Math.max(0,subdivisions)|0;
switch (type)
{
case 1: //CUBE:
this._mesh = GL.Mesh.cube({size: size, normals:true,coords:true});
break;
case 2: //PLANE:
this._mesh = GL.Mesh.plane({size: size, xz: true, detail: subdivisions, normals:true,coords:true});
break;
case 3: //CYLINDER:
this._mesh = GL.Mesh.cylinder({size: size, subdivisions: subdivisions, normals:true,coords:true});
break;
case 4: //SPHERE:
this._mesh = GL.Mesh.sphere({size: size, "long": subdivisions, lat: subdivisions, normals:true,coords:true});
break;
case 5: //CIRCLE:
this._mesh = GL.Mesh.circle({size: size, slices: subdivisions, normals:true, coords:true});
break;
case 6: //HEMISPHERE:
this._mesh = GL.Mesh.sphere({size: size, "long": subdivisions, lat: subdivisions, normals:true, coords:true, hemi: true});
break;
case 7: //ICOSAHEDRON:
this._mesh = GL.Mesh.icosahedron({size: size, subdivisions:subdivisions });
break;
case 8: //CONE:
this._mesh = GL.Mesh.cone({radius: size, height: size, subdivisions:subdivisions });
break;
case 9: //QUAD:
this._mesh = GL.Mesh.plane({size: size, xz: false, detail: subdivisions, normals:true, coords:true });
break;
}
this.last_info.type = type;
this.last_info.size = size;
this.last_info.subdivisions = subdivisions;
this._mesh.version = this.version++;
}
LiteGraph.registerNodeType( "geometry/mesh_primitive", LGraphGeometryPrimitive );
function LGraphRenderPoints() {
this.addInput("in", "geometry");
this.addInput("mat4", "mat4");
this.addInput("tex", "texture");
this.properties = {
enabled: true,
point_size: 0.1,
fixed_size: false,
additive: true,
color: [1,1,1],
opacity: 1
};
this.color = vec4.create([1,1,1,1]);
this.uniforms = {
u_point_size: 1,
u_perspective: 1,
u_point_perspective: 1,
u_color: this.color
};
this.geometry_id = -1;
this.version = -1;
this.mesh = null;
}
LGraphRenderPoints.title = "renderPoints";
LGraphRenderPoints.desc = "render points with a texture";
LGraphRenderPoints.widgets_info = {
color: { widget: "color" }
};
LGraphRenderPoints.prototype.updateMesh = function(geometry)
{
var buffer = this.buffer;
if(!this.buffer || !this.buffer.data || this.buffer.data.length != geometry.vertices.length)
this.buffer = new GL.Buffer( GL.ARRAY_BUFFER, geometry.vertices,3,GL.DYNAMIC_DRAW);
else
{
this.buffer.data.set( geometry.vertices );
this.buffer.upload(GL.DYNAMIC_DRAW);
}
if(!this.mesh)
this.mesh = new GL.Mesh();
this.mesh.addBuffer("vertices",this.buffer);
this.geometry_id = this.mesh.id = geometry._id;
this.version = this.mesh.version = geometry._version;
}
LGraphRenderPoints.prototype.onExecute = function() {
if(!this.properties.enabled)
return;
var geometry = this.getInputData(0);
if(!geometry)
return;
if(this.version != geometry._version || this.geometry_id != geometry._id )
this.updateMesh( geometry );
if(!LiteGraph.LGraphRender.onRequestCameraMatrices)
{
console.warn("cannot render geometry, LiteGraph.onRequestCameraMatrices is null, remember to fill this with a callback(view_matrix, projection_matrix,viewprojection_matrix) to use 3D rendering from the graph");
return;
}
LiteGraph.LGraphRender.onRequestCameraMatrices( view_matrix, projection_matrix,viewprojection_matrix );
var shader = null;
var texture = this.getInputData(2);
if(texture)
{
shader = gl.shaders["textured_points"];
if(!shader)
shader = gl.shaders["textured_points"] = new GL.Shader( LGraphRenderPoints.vertex_shader_code, LGraphRenderPoints.fragment_shader_code, { USE_TEXTURED_POINTS:"" });
}
else
{
shader = gl.shaders["points"];
if(!shader)
shader = gl.shaders["points"] = new GL.Shader( LGraphRenderPoints.vertex_shader_code, LGraphRenderPoints.fragment_shader_code, { USE_POINTS: "" });
}
this.color.set( this.properties.color );
this.color[3] = this.properties.opacity;
var m = this.getInputData(1);
if(m)
model_matrix.set(m);
else
mat4.identity( model_matrix );
this.uniforms.u_point_size = this.properties.point_size;
this.uniforms.u_point_perspective = this.properties.fixed_size ? 0 : 1;
this.uniforms.u_perspective = gl.viewport_data[3] * projection_matrix[5];
shader.uniforms( global_uniforms );
shader.uniforms( this.uniforms );
if(this.properties.opacity >= 1)
gl.disable( gl.BLEND );
else
gl.enable( gl.BLEND );
gl.enable( gl.DEPTH_TEST );
if( this.properties.additive )
{
gl.blendFunc( gl.SRC_ALPHA, gl.ONE );
gl.depthMask( false );
}
else
gl.blendFunc( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA );
shader.draw( this.mesh, GL.POINTS );
gl.disable( gl.BLEND );
gl.depthMask( true );
}
LiteGraph.registerNodeType( "geometry/render_points", LGraphRenderPoints );
LGraphRenderPoints.vertex_shader_code = '\
precision mediump float;\n\
attribute vec3 a_vertex;\n\
varying vec3 v_vertex;\n\
attribute vec3 a_normal;\n\
varying vec3 v_normal;\n\
#ifdef USE_COLOR\n\
attribute vec4 a_color;\n\
varying vec4 v_color;\n\
#endif\n\
attribute vec2 a_coord;\n\
varying vec2 v_coord;\n\
#ifdef USE_SIZE\n\
attribute float a_extra;\n\
#endif\n\
#ifdef USE_INSTANCING\n\
attribute mat4 u_model;\n\
#else\n\
uniform mat4 u_model;\n\
#endif\n\
uniform mat4 u_viewprojection;\n\
uniform float u_point_size;\n\
uniform float u_perspective;\n\
uniform float u_point_perspective;\n\
float computePointSize(float radius, float w)\n\
{\n\
if(radius < 0.0)\n\
return -radius;\n\
return u_perspective * radius / w;\n\
}\n\
void main() {\n\
v_coord = a_coord;\n\
#ifdef USE_COLOR\n\
v_color = a_color;\n\
#endif\n\
v_vertex = ( u_model * vec4( a_vertex, 1.0 )).xyz;\n\
v_normal = ( u_model * vec4( a_normal, 0.0 )).xyz;\n\
gl_Position = u_viewprojection * vec4(v_vertex,1.0);\n\
gl_PointSize = u_point_size;\n\
#ifdef USE_SIZE\n\
gl_PointSize = a_extra;\n\
#endif\n\
if(u_point_perspective != 0.0)\n\
gl_PointSize = computePointSize( gl_PointSize, gl_Position.w );\n\
}\
';
LGraphRenderPoints.fragment_shader_code = '\
precision mediump float;\n\
uniform vec4 u_color;\n\
#ifdef USE_COLOR\n\
varying vec4 v_color;\n\
#endif\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
void main() {\n\
vec4 color = u_color;\n\
#ifdef USE_TEXTURED_POINTS\n\
color *= texture2D(u_texture, gl_PointCoord.xy);\n\
#else\n\
#ifdef USE_TEXTURE\n\
color *= texture2D(u_texture, v_coord);\n\
if(color.a < 0.1)\n\
discard;\n\
#endif\n\
#ifdef USE_POINTS\n\
float dist = length( gl_PointCoord.xy - vec2(0.5) );\n\
if( dist > 0.45 )\n\
discard;\n\
#endif\n\
#endif\n\
#ifdef USE_COLOR\n\
color *= v_color;\n\
#endif\n\
gl_FragColor = color;\n\
}\
';
//based on https://inconvergent.net/2019/depth-of-field/
/*
function LGraphRenderGeometryDOF() {
this.addInput("in", "geometry");
this.addInput("mat4", "mat4");
this.addInput("tex", "texture");
this.properties = {
enabled: true,
lines: true,
point_size: 0.1,
fixed_size: false,
additive: true,
color: [1,1,1],
opacity: 1
};
this.color = vec4.create([1,1,1,1]);
this.uniforms = {
u_point_size: 1,
u_perspective: 1,
u_point_perspective: 1,
u_color: this.color
};
this.geometry_id = -1;
this.version = -1;
this.mesh = null;
}
LGraphRenderGeometryDOF.widgets_info = {
color: { widget: "color" }
};
LGraphRenderGeometryDOF.prototype.updateMesh = function(geometry)
{
var buffer = this.buffer;
if(!this.buffer || this.buffer.data.length != geometry.vertices.length)
this.buffer = new GL.Buffer( GL.ARRAY_BUFFER, geometry.vertices,3,GL.DYNAMIC_DRAW);
else
{
this.buffer.data.set( geometry.vertices );
this.buffer.upload(GL.DYNAMIC_DRAW);
}
if(!this.mesh)
this.mesh = new GL.Mesh();
this.mesh.addBuffer("vertices",this.buffer);
this.geometry_id = this.mesh.id = geometry._id;
this.version = this.mesh.version = geometry._version;
}
LGraphRenderGeometryDOF.prototype.onExecute = function() {
if(!this.properties.enabled)
return;
var geometry = this.getInputData(0);
if(!geometry)
return;
if(this.version != geometry._version || this.geometry_id != geometry._id )
this.updateMesh( geometry );
if(!LiteGraph.LGraphRender.onRequestCameraMatrices)
{
console.warn("cannot render geometry, LiteGraph.onRequestCameraMatrices is null, remember to fill this with a callback(view_matrix, projection_matrix,viewprojection_matrix) to use 3D rendering from the graph");
return;
}
LiteGraph.LGraphRender.onRequestCameraMatrices( view_matrix, projection_matrix,viewprojection_matrix );
var shader = null;
var texture = this.getInputData(2);
if(texture)
{
shader = gl.shaders["textured_points"];
if(!shader)
shader = gl.shaders["textured_points"] = new GL.Shader( LGraphRenderGeometryDOF.vertex_shader_code, LGraphRenderGeometryDOF.fragment_shader_code, { USE_TEXTURED_POINTS:"" });
}
else
{
shader = gl.shaders["points"];
if(!shader)
shader = gl.shaders["points"] = new GL.Shader( LGraphRenderGeometryDOF.vertex_shader_code, LGraphRenderGeometryDOF.fragment_shader_code, { USE_POINTS: "" });
}
this.color.set( this.properties.color );
this.color[3] = this.properties.opacity;
var m = this.getInputData(1);
if(m)
model_matrix.set(m);
else
mat4.identity( model_matrix );
this.uniforms.u_point_size = this.properties.point_size;
this.uniforms.u_point_perspective = this.properties.fixed_size ? 0 : 1;
this.uniforms.u_perspective = gl.viewport_data[3] * projection_matrix[5];
shader.uniforms( global_uniforms );
shader.uniforms( this.uniforms );
if(this.properties.opacity >= 1)
gl.disable( gl.BLEND );
else
gl.enable( gl.BLEND );
gl.enable( gl.DEPTH_TEST );
if( this.properties.additive )
{
gl.blendFunc( gl.SRC_ALPHA, gl.ONE );
gl.depthMask( false );
}
else
gl.blendFunc( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA );
shader.draw( this.mesh, GL.POINTS );
gl.disable( gl.BLEND );
gl.depthMask( true );
}
LiteGraph.registerNodeType( "geometry/render_dof", LGraphRenderGeometryDOF );
LGraphRenderGeometryDOF.vertex_shader_code = '\
precision mediump float;\n\
attribute vec3 a_vertex;\n\
varying vec3 v_vertex;\n\
attribute vec3 a_normal;\n\
varying vec3 v_normal;\n\
#ifdef USE_COLOR\n\
attribute vec4 a_color;\n\
varying vec4 v_color;\n\
#endif\n\
attribute vec2 a_coord;\n\
varying vec2 v_coord;\n\
#ifdef USE_SIZE\n\
attribute float a_extra;\n\
#endif\n\
#ifdef USE_INSTANCING\n\
attribute mat4 u_model;\n\
#else\n\
uniform mat4 u_model;\n\
#endif\n\
uniform mat4 u_viewprojection;\n\
uniform float u_point_size;\n\
uniform float u_perspective;\n\
uniform float u_point_perspective;\n\
float computePointSize(float radius, float w)\n\
{\n\
if(radius < 0.0)\n\
return -radius;\n\
return u_perspective * radius / w;\n\
}\n\
void main() {\n\
v_coord = a_coord;\n\
#ifdef USE_COLOR\n\
v_color = a_color;\n\
#endif\n\
v_vertex = ( u_model * vec4( a_vertex, 1.0 )).xyz;\n\
v_normal = ( u_model * vec4( a_normal, 0.0 )).xyz;\n\
gl_Position = u_viewprojection * vec4(v_vertex,1.0);\n\
gl_PointSize = u_point_size;\n\
#ifdef USE_SIZE\n\
gl_PointSize = a_extra;\n\
#endif\n\
if(u_point_perspective != 0.0)\n\
gl_PointSize = computePointSize( gl_PointSize, gl_Position.w );\n\
}\
';
LGraphRenderGeometryDOF.fragment_shader_code = '\
precision mediump float;\n\
uniform vec4 u_color;\n\
#ifdef USE_COLOR\n\
varying vec4 v_color;\n\
#endif\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
void main() {\n\
vec4 color = u_color;\n\
#ifdef USE_TEXTURED_POINTS\n\
color *= texture2D(u_texture, gl_PointCoord.xy);\n\
#else\n\
#ifdef USE_TEXTURE\n\
color *= texture2D(u_texture, v_coord);\n\
if(color.a < 0.1)\n\
discard;\n\
#endif\n\
#ifdef USE_POINTS\n\
float dist = length( gl_PointCoord.xy - vec2(0.5) );\n\
if( dist > 0.45 )\n\
discard;\n\
#endif\n\
#endif\n\
#ifdef USE_COLOR\n\
color *= v_color;\n\
#endif\n\
gl_FragColor = color;\n\
}\
';
*/
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
var LGraphTexture = global.LGraphTexture;
//Works with Litegl.js to create WebGL nodes
if (typeof GL != "undefined") {
// Texture Lens *****************************************
function LGraphFXLens() {
this.addInput("Texture", "Texture");
this.addInput("Aberration", "number");
this.addInput("Distortion", "number");
this.addInput("Blur", "number");
this.addOutput("Texture", "Texture");
this.properties = {
aberration: 1.0,
distortion: 1.0,
blur: 1.0,
precision: LGraphTexture.DEFAULT
};
if (!LGraphFXLens._shader) {
LGraphFXLens._shader = new GL.Shader(
GL.Shader.SCREEN_VERTEX_SHADER,
LGraphFXLens.pixel_shader
);
LGraphFXLens._texture = new GL.Texture(3, 1, {
format: gl.RGB,
wrap: gl.CLAMP_TO_EDGE,
magFilter: gl.LINEAR,
minFilter: gl.LINEAR,
pixel_data: [255, 0, 0, 0, 255, 0, 0, 0, 255]
});
}
}
LGraphFXLens.title = "Lens";
LGraphFXLens.desc = "Camera Lens distortion";
LGraphFXLens.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphFXLens.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (this.properties.precision === LGraphTexture.PASS_THROUGH) {
this.setOutputData(0, tex);
return;
}
if (!tex) {
return;
}
this._tex = LGraphTexture.getTargetTexture(
tex,
this._tex,
this.properties.precision
);
var aberration = this.properties.aberration;
if (this.isInputConnected(1)) {
aberration = this.getInputData(1);
this.properties.aberration = aberration;
}
var distortion = this.properties.distortion;
if (this.isInputConnected(2)) {
distortion = this.getInputData(2);
this.properties.distortion = distortion;
}
var blur = this.properties.blur;
if (this.isInputConnected(3)) {
blur = this.getInputData(3);
this.properties.blur = blur;
}
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = Mesh.getScreenQuad();
var shader = LGraphFXLens._shader;
//var camera = LS.Renderer._current_camera;
this._tex.drawTo(function() {
tex.bind(0);
shader
.uniforms({
u_texture: 0,
u_aberration: aberration,
u_distortion: distortion,
u_blur: blur
})
.draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphFXLens.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_camera_planes;\n\
uniform float u_aberration;\n\
uniform float u_distortion;\n\
uniform float u_blur;\n\
\n\
void main() {\n\
vec2 coord = v_coord;\n\
float dist = distance(vec2(0.5), coord);\n\
vec2 dist_coord = coord - vec2(0.5);\n\
float percent = 1.0 + ((0.5 - dist) / 0.5) * u_distortion;\n\
dist_coord *= percent;\n\
coord = dist_coord + vec2(0.5);\n\
vec4 color = texture2D(u_texture,coord, u_blur * dist);\n\
color.r = texture2D(u_texture,vec2(0.5) + dist_coord * (1.0+0.01*u_aberration), u_blur * dist ).r;\n\
color.b = texture2D(u_texture,vec2(0.5) + dist_coord * (1.0-0.01*u_aberration), u_blur * dist ).b;\n\
gl_FragColor = color;\n\
}\n\
";
/*
float normalized_tunable_sigmoid(float xs, float k)\n\
{\n\
xs = xs * 2.0 - 1.0;\n\
float signx = sign(xs);\n\
float absx = abs(xs);\n\
return signx * ((-k - 1.0)*absx)/(2.0*(-2.0*k*absx+k-1.0)) + 0.5;\n\
}\n\
*/
LiteGraph.registerNodeType("fx/lens", LGraphFXLens);
global.LGraphFXLens = LGraphFXLens;
/* not working yet
function LGraphDepthOfField()
{
this.addInput("Color","Texture");
this.addInput("Linear Depth","Texture");
this.addInput("Camera","camera");
this.addOutput("Texture","Texture");
this.properties = { high_precision: false };
}
LGraphDepthOfField.title = "Depth Of Field";
LGraphDepthOfField.desc = "Applies a depth of field effect";
LGraphDepthOfField.prototype.onExecute = function()
{
var tex = this.getInputData(0);
var depth = this.getInputData(1);
var camera = this.getInputData(2);
if(!tex || !depth || !camera)
{
this.setOutputData(0, tex);
return;
}
var precision = gl.UNSIGNED_BYTE;
if(this.properties.high_precision)
precision = gl.half_float_ext ? gl.HALF_FLOAT_OES : gl.FLOAT;
if(!this._temp_texture || this._temp_texture.type != precision ||
this._temp_texture.width != tex.width || this._temp_texture.height != tex.height)
this._temp_texture = new GL.Texture( tex.width, tex.height, { type: precision, format: gl.RGBA, filter: gl.LINEAR });
var shader = LGraphDepthOfField._shader = new GL.Shader( GL.Shader.SCREEN_VERTEX_SHADER, LGraphDepthOfField._pixel_shader );
var screen_mesh = Mesh.getScreenQuad();
gl.disable( gl.DEPTH_TEST );
gl.disable( gl.BLEND );
var camera_position = camera.getEye();
var focus_point = camera.getCenter();
var distance = vec3.distance( camera_position, focus_point );
var far = camera.far;
var focus_range = distance * 0.5;
this._temp_texture.drawTo( function() {
tex.bind(0);
depth.bind(1);
shader.uniforms({u_texture:0, u_depth_texture:1, u_resolution: [1/tex.width, 1/tex.height], u_far: far, u_focus_point: distance, u_focus_scale: focus_range }).draw(screen_mesh);
});
this.setOutputData(0, this._temp_texture);
}
//from http://tuxedolabs.blogspot.com.es/2018/05/bokeh-depth-of-field-in-single-pass.html
LGraphDepthOfField._pixel_shader = "\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture; //Image to be processed\n\
uniform sampler2D u_depth_texture; //Linear depth, where 1.0 == far plane\n\
uniform vec2 u_iresolution; //The size of a pixel: vec2(1.0/width, 1.0/height)\n\
uniform float u_far; // Far plane\n\
uniform float u_focus_point;\n\
uniform float u_focus_scale;\n\
\n\
const float GOLDEN_ANGLE = 2.39996323;\n\
const float MAX_BLUR_SIZE = 20.0;\n\
const float RAD_SCALE = 0.5; // Smaller = nicer blur, larger = faster\n\
\n\
float getBlurSize(float depth, float focusPoint, float focusScale)\n\
{\n\
float coc = clamp((1.0 / focusPoint - 1.0 / depth)*focusScale, -1.0, 1.0);\n\
return abs(coc) * MAX_BLUR_SIZE;\n\
}\n\
\n\
vec3 depthOfField(vec2 texCoord, float focusPoint, float focusScale)\n\
{\n\
float centerDepth = texture2D(u_depth_texture, texCoord).r * u_far;\n\
float centerSize = getBlurSize(centerDepth, focusPoint, focusScale);\n\
vec3 color = texture2D(u_texture, v_coord).rgb;\n\
float tot = 1.0;\n\
\n\
float radius = RAD_SCALE;\n\
for (float ang = 0.0; ang < 100.0; ang += GOLDEN_ANGLE)\n\
{\n\
vec2 tc = texCoord + vec2(cos(ang), sin(ang)) * u_iresolution * radius;\n\
\n\
vec3 sampleColor = texture2D(u_texture, tc).rgb;\n\
float sampleDepth = texture2D(u_depth_texture, tc).r * u_far;\n\
float sampleSize = getBlurSize( sampleDepth, focusPoint, focusScale );\n\
if (sampleDepth > centerDepth)\n\
sampleSize = clamp(sampleSize, 0.0, centerSize*2.0);\n\
\n\
float m = smoothstep(radius-0.5, radius+0.5, sampleSize);\n\
color += mix(color/tot, sampleColor, m);\n\
tot += 1.0;\n\
radius += RAD_SCALE/radius;\n\
if(radius>=MAX_BLUR_SIZE)\n\
return color / tot;\n\
}\n\
return color / tot;\n\
}\n\
void main()\n\
{\n\
gl_FragColor = vec4( depthOfField( v_coord, u_focus_point, u_focus_scale ), 1.0 );\n\
//gl_FragColor = vec4( texture2D(u_depth_texture, v_coord).r );\n\
}\n\
";
LiteGraph.registerNodeType("fx/DOF", LGraphDepthOfField );
global.LGraphDepthOfField = LGraphDepthOfField;
*/
//*******************************************************
function LGraphFXBokeh() {
this.addInput("Texture", "Texture");
this.addInput("Blurred", "Texture");
this.addInput("Mask", "Texture");
this.addInput("Threshold", "number");
this.addOutput("Texture", "Texture");
this.properties = {
shape: "",
size: 10,
alpha: 1.0,
threshold: 1.0,
high_precision: false
};
}
LGraphFXBokeh.title = "Bokeh";
LGraphFXBokeh.desc = "applies an Bokeh effect";
LGraphFXBokeh.widgets_info = { shape: { widget: "texture" } };
LGraphFXBokeh.prototype.onExecute = function() {
var tex = this.getInputData(0);
var blurred_tex = this.getInputData(1);
var mask_tex = this.getInputData(2);
if (!tex || !mask_tex || !this.properties.shape) {
this.setOutputData(0, tex);
return;
}
if (!blurred_tex) {
blurred_tex = tex;
}
var shape_tex = LGraphTexture.getTexture(this.properties.shape);
if (!shape_tex) {
return;
}
var threshold = this.properties.threshold;
if (this.isInputConnected(3)) {
threshold = this.getInputData(3);
this.properties.threshold = threshold;
}
var precision = gl.UNSIGNED_BYTE;
if (this.properties.high_precision) {
precision = gl.half_float_ext ? gl.HALF_FLOAT_OES : gl.FLOAT;
}
if (
!this._temp_texture ||
this._temp_texture.type != precision ||
this._temp_texture.width != tex.width ||
this._temp_texture.height != tex.height
) {
this._temp_texture = new GL.Texture(tex.width, tex.height, {
type: precision,
format: gl.RGBA,
filter: gl.LINEAR
});
}
//iterations
var size = this.properties.size;
var first_shader = LGraphFXBokeh._first_shader;
if (!first_shader) {
first_shader = LGraphFXBokeh._first_shader = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphFXBokeh._first_pixel_shader
);
}
var second_shader = LGraphFXBokeh._second_shader;
if (!second_shader) {
second_shader = LGraphFXBokeh._second_shader = new GL.Shader(
LGraphFXBokeh._second_vertex_shader,
LGraphFXBokeh._second_pixel_shader
);
}
var points_mesh = this._points_mesh;
if (
!points_mesh ||
points_mesh._width != tex.width ||
points_mesh._height != tex.height ||
points_mesh._spacing != 2
) {
points_mesh = this.createPointsMesh(tex.width, tex.height, 2);
}
var screen_mesh = Mesh.getScreenQuad();
var point_size = this.properties.size;
var min_light = this.properties.min_light;
var alpha = this.properties.alpha;
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
this._temp_texture.drawTo(function() {
tex.bind(0);
blurred_tex.bind(1);
mask_tex.bind(2);
first_shader
.uniforms({
u_texture: 0,
u_texture_blur: 1,
u_mask: 2,
u_texsize: [tex.width, tex.height]
})
.draw(screen_mesh);
});
this._temp_texture.drawTo(function() {
//clear because we use blending
//gl.clearColor(0.0,0.0,0.0,1.0);
//gl.clear( gl.COLOR_BUFFER_BIT );
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
tex.bind(0);
shape_tex.bind(3);
second_shader
.uniforms({
u_texture: 0,
u_mask: 2,
u_shape: 3,
u_alpha: alpha,
u_threshold: threshold,
u_pointSize: point_size,
u_itexsize: [1.0 / tex.width, 1.0 / tex.height]
})
.draw(points_mesh, gl.POINTS);
});
this.setOutputData(0, this._temp_texture);
};
LGraphFXBokeh.prototype.createPointsMesh = function(
width,
height,
spacing
) {
var nwidth = Math.round(width / spacing);
var nheight = Math.round(height / spacing);
var vertices = new Float32Array(nwidth * nheight * 2);
var ny = -1;
var dx = (2 / width) * spacing;
var dy = (2 / height) * spacing;
for (var y = 0; y < nheight; ++y) {
var nx = -1;
for (var x = 0; x < nwidth; ++x) {
var pos = y * nwidth * 2 + x * 2;
vertices[pos] = nx;
vertices[pos + 1] = ny;
nx += dx;
}
ny += dy;
}
this._points_mesh = GL.Mesh.load({ vertices2D: vertices });
this._points_mesh._width = width;
this._points_mesh._height = height;
this._points_mesh._spacing = spacing;
return this._points_mesh;
};
/*
LGraphTextureBokeh._pixel_shader = "precision highp float;\n\
varying vec2 a_coord;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_shape;\n\
\n\
void main() {\n\
vec4 color = texture2D( u_texture, gl_PointCoord );\n\
color *= v_color * u_alpha;\n\
gl_FragColor = color;\n\
}\n";
*/
LGraphFXBokeh._first_pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_texture_blur;\n\
uniform sampler2D u_mask;\n\
\n\
void main() {\n\
vec4 color = texture2D(u_texture, v_coord);\n\
vec4 blurred_color = texture2D(u_texture_blur, v_coord);\n\
float mask = texture2D(u_mask, v_coord).x;\n\
gl_FragColor = mix(color, blurred_color, mask);\n\
}\n\
";
LGraphFXBokeh._second_vertex_shader =
"precision highp float;\n\
attribute vec2 a_vertex2D;\n\
varying vec4 v_color;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_mask;\n\
uniform vec2 u_itexsize;\n\
uniform float u_pointSize;\n\
uniform float u_threshold;\n\
void main() {\n\
vec2 coord = a_vertex2D * 0.5 + 0.5;\n\
v_color = texture2D( u_texture, coord );\n\
v_color += texture2D( u_texture, coord + vec2(u_itexsize.x, 0.0) );\n\
v_color += texture2D( u_texture, coord + vec2(0.0, u_itexsize.y));\n\
v_color += texture2D( u_texture, coord + u_itexsize);\n\
v_color *= 0.25;\n\
float mask = texture2D(u_mask, coord).x;\n\
float luminance = length(v_color) * mask;\n\
/*luminance /= (u_pointSize*u_pointSize)*0.01 */;\n\
luminance -= u_threshold;\n\
if(luminance < 0.0)\n\
{\n\
gl_Position.x = -100.0;\n\
return;\n\
}\n\
gl_PointSize = u_pointSize;\n\
gl_Position = vec4(a_vertex2D,0.0,1.0);\n\
}\n\
";
LGraphFXBokeh._second_pixel_shader =
"precision highp float;\n\
varying vec4 v_color;\n\
uniform sampler2D u_shape;\n\
uniform float u_alpha;\n\
\n\
void main() {\n\
vec4 color = texture2D( u_shape, gl_PointCoord );\n\
color *= v_color * u_alpha;\n\
gl_FragColor = color;\n\
}\n";
LiteGraph.registerNodeType("fx/bokeh", LGraphFXBokeh);
global.LGraphFXBokeh = LGraphFXBokeh;
//************************************************
function LGraphFXGeneric() {
this.addInput("Texture", "Texture");
this.addInput("value1", "number");
this.addInput("value2", "number");
this.addOutput("Texture", "Texture");
this.properties = {
fx: "halftone",
value1: 1,
value2: 1,
precision: LGraphTexture.DEFAULT
};
}
LGraphFXGeneric.title = "FX";
LGraphFXGeneric.desc = "applies an FX from a list";
LGraphFXGeneric.widgets_info = {
fx: {
widget: "combo",
values: ["halftone", "pixelate", "lowpalette", "noise", "gamma"]
},
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphFXGeneric.shaders = {};
LGraphFXGeneric.prototype.onExecute = function() {
if (!this.isOutputConnected(0)) {
return;
} //saves work
var tex = this.getInputData(0);
if (this.properties.precision === LGraphTexture.PASS_THROUGH) {
this.setOutputData(0, tex);
return;
}
if (!tex) {
return;
}
this._tex = LGraphTexture.getTargetTexture(
tex,
this._tex,
this.properties.precision
);
//iterations
var value1 = this.properties.value1;
if (this.isInputConnected(1)) {
value1 = this.getInputData(1);
this.properties.value1 = value1;
}
var value2 = this.properties.value2;
if (this.isInputConnected(2)) {
value2 = this.getInputData(2);
this.properties.value2 = value2;
}
var fx = this.properties.fx;
var shader = LGraphFXGeneric.shaders[fx];
if (!shader) {
var pixel_shader_code = LGraphFXGeneric["pixel_shader_" + fx];
if (!pixel_shader_code) {
return;
}
shader = LGraphFXGeneric.shaders[fx] = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
pixel_shader_code
);
}
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = Mesh.getScreenQuad();
var camera = global.LS ? LS.Renderer._current_camera : null;
var camera_planes;
if (camera) {
camera_planes = [
LS.Renderer._current_camera.near,
LS.Renderer._current_camera.far
];
} else {
camera_planes = [1, 100];
}
var noise = null;
if (fx == "noise") {
noise = LGraphTexture.getNoiseTexture();
}
this._tex.drawTo(function() {
tex.bind(0);
if (fx == "noise") {
noise.bind(1);
}
shader
.uniforms({
u_texture: 0,
u_noise: 1,
u_size: [tex.width, tex.height],
u_rand: [Math.random(), Math.random()],
u_value1: value1,
u_value2: value2,
u_camera_planes: camera_planes
})
.draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphFXGeneric.pixel_shader_halftone =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_camera_planes;\n\
uniform vec2 u_size;\n\
uniform float u_value1;\n\
uniform float u_value2;\n\
\n\
float pattern() {\n\
float s = sin(u_value1 * 3.1415), c = cos(u_value1 * 3.1415);\n\
vec2 tex = v_coord * u_size.xy;\n\
vec2 point = vec2(\n\
c * tex.x - s * tex.y ,\n\
s * tex.x + c * tex.y \n\
) * u_value2;\n\
return (sin(point.x) * sin(point.y)) * 4.0;\n\
}\n\
void main() {\n\
vec4 color = texture2D(u_texture, v_coord);\n\
float average = (color.r + color.g + color.b) / 3.0;\n\
gl_FragColor = vec4(vec3(average * 10.0 - 5.0 + pattern()), color.a);\n\
}\n";
LGraphFXGeneric.pixel_shader_pixelate =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_camera_planes;\n\
uniform vec2 u_size;\n\
uniform float u_value1;\n\
uniform float u_value2;\n\
\n\
void main() {\n\
vec2 coord = vec2( floor(v_coord.x * u_value1) / u_value1, floor(v_coord.y * u_value2) / u_value2 );\n\
vec4 color = texture2D(u_texture, coord);\n\
gl_FragColor = color;\n\
}\n";
LGraphFXGeneric.pixel_shader_lowpalette =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform vec2 u_camera_planes;\n\
uniform vec2 u_size;\n\
uniform float u_value1;\n\
uniform float u_value2;\n\
\n\
void main() {\n\
vec4 color = texture2D(u_texture, v_coord);\n\
gl_FragColor = floor(color * u_value1) / u_value1;\n\
}\n";
LGraphFXGeneric.pixel_shader_noise =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform sampler2D u_noise;\n\
uniform vec2 u_size;\n\
uniform float u_value1;\n\
uniform float u_value2;\n\
uniform vec2 u_rand;\n\
\n\
void main() {\n\
vec4 color = texture2D(u_texture, v_coord);\n\
vec3 noise = texture2D(u_noise, v_coord * vec2(u_size.x / 512.0, u_size.y / 512.0) + u_rand).xyz - vec3(0.5);\n\
gl_FragColor = vec4( color.xyz + noise * u_value1, color.a );\n\
}\n";
LGraphFXGeneric.pixel_shader_gamma =
"precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform float u_value1;\n\
\n\
void main() {\n\
vec4 color = texture2D(u_texture, v_coord);\n\
float gamma = 1.0 / u_value1;\n\
gl_FragColor = vec4( pow( color.xyz, vec3(gamma) ), color.a );\n\
}\n";
LiteGraph.registerNodeType("fx/generic", LGraphFXGeneric);
global.LGraphFXGeneric = LGraphFXGeneric;
// Vigneting ************************************
function LGraphFXVigneting() {
this.addInput("Tex.", "Texture");
this.addInput("intensity", "number");
this.addOutput("Texture", "Texture");
this.properties = {
intensity: 1,
invert: false,
precision: LGraphTexture.DEFAULT
};
if (!LGraphFXVigneting._shader) {
LGraphFXVigneting._shader = new GL.Shader(
Shader.SCREEN_VERTEX_SHADER,
LGraphFXVigneting.pixel_shader
);
}
}
LGraphFXVigneting.title = "Vigneting";
LGraphFXVigneting.desc = "Vigneting";
LGraphFXVigneting.widgets_info = {
precision: { widget: "combo", values: LGraphTexture.MODE_VALUES }
};
LGraphFXVigneting.prototype.onExecute = function() {
var tex = this.getInputData(0);
if (this.properties.precision === LGraphTexture.PASS_THROUGH) {
this.setOutputData(0, tex);
return;
}
if (!tex) {
return;
}
this._tex = LGraphTexture.getTargetTexture(
tex,
this._tex,
this.properties.precision
);
var intensity = this.properties.intensity;
if (this.isInputConnected(1)) {
intensity = this.getInputData(1);
this.properties.intensity = intensity;
}
gl.disable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
var mesh = Mesh.getScreenQuad();
var shader = LGraphFXVigneting._shader;
var invert = this.properties.invert;
this._tex.drawTo(function() {
tex.bind(0);
shader
.uniforms({
u_texture: 0,
u_intensity: intensity,
u_isize: [1 / tex.width, 1 / tex.height],
u_invert: invert ? 1 : 0
})
.draw(mesh);
});
this.setOutputData(0, this._tex);
};
LGraphFXVigneting.pixel_shader =
"precision highp float;\n\
precision highp float;\n\
varying vec2 v_coord;\n\
uniform sampler2D u_texture;\n\
uniform float u_intensity;\n\
uniform int u_invert;\n\
\n\
void main() {\n\
float luminance = 1.0 - length( v_coord - vec2(0.5) ) * 1.414;\n\
vec4 color = texture2D(u_texture, v_coord);\n\
if(u_invert == 1)\n\
luminance = 1.0 - luminance;\n\
luminance = mix(1.0, luminance, u_intensity);\n\
gl_FragColor = vec4( luminance * color.xyz, color.a);\n\
}\n\
";
LiteGraph.registerNodeType("fx/vigneting", LGraphFXVigneting);
global.LGraphFXVigneting = LGraphFXVigneting;
}
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
var MIDI_COLOR = "#243";
function MIDIEvent(data) {
this.channel = 0;
this.cmd = 0;
this.data = new Uint32Array(3);
if (data) {
this.setup(data);
}
}
LiteGraph.MIDIEvent = MIDIEvent;
MIDIEvent.prototype.fromJSON = function(o) {
this.setup(o.data);
};
MIDIEvent.prototype.setup = function(data) {
var raw_data = data;
if (data.constructor === Object) {
raw_data = data.data;
}
this.data.set(raw_data);
var midiStatus = raw_data[0];
this.status = midiStatus;
var midiCommand = midiStatus & 0xf0;
if (midiStatus >= 0xf0) {
this.cmd = midiStatus;
} else {
this.cmd = midiCommand;
}
if (this.cmd == MIDIEvent.NOTEON && this.velocity == 0) {
this.cmd = MIDIEvent.NOTEOFF;
}
this.cmd_str = MIDIEvent.commands[this.cmd] || "";
if (
midiCommand >= MIDIEvent.NOTEON ||
midiCommand <= MIDIEvent.NOTEOFF
) {
this.channel = midiStatus & 0x0f;
}
};
Object.defineProperty(MIDIEvent.prototype, "velocity", {
get: function() {
if (this.cmd == MIDIEvent.NOTEON) {
return this.data[2];
}
return -1;
},
set: function(v) {
this.data[2] = v; // v / 127;
},
enumerable: true
});
MIDIEvent.notes = [
"A",
"A#",
"B",
"C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#"
];
MIDIEvent.note_to_index = {
A: 0,
"A#": 1,
B: 2,
C: 3,
"C#": 4,
D: 5,
"D#": 6,
E: 7,
F: 8,
"F#": 9,
G: 10,
"G#": 11
};
Object.defineProperty(MIDIEvent.prototype, "note", {
get: function() {
if (this.cmd != MIDIEvent.NOTEON) {
return -1;
}
return MIDIEvent.toNoteString(this.data[1], true);
},
set: function(v) {
throw "notes cannot be assigned this way, must modify the data[1]";
},
enumerable: true
});
Object.defineProperty(MIDIEvent.prototype, "octave", {
get: function() {
if (this.cmd != MIDIEvent.NOTEON) {
return -1;
}
var octave = this.data[1] - 24;
return Math.floor(octave / 12 + 1);
},
set: function(v) {
throw "octave cannot be assigned this way, must modify the data[1]";
},
enumerable: true
});
//returns HZs
MIDIEvent.prototype.getPitch = function() {
return Math.pow(2, (this.data[1] - 69) / 12) * 440;
};
MIDIEvent.computePitch = function(note) {
return Math.pow(2, (note - 69) / 12) * 440;
};
MIDIEvent.prototype.getCC = function() {
return this.data[1];
};
MIDIEvent.prototype.getCCValue = function() {
return this.data[2];
};
//not tested, there is a formula missing here
MIDIEvent.prototype.getPitchBend = function() {
return this.data[1] + (this.data[2] << 7) - 8192;
};
MIDIEvent.computePitchBend = function(v1, v2) {
return v1 + (v2 << 7) - 8192;
};
MIDIEvent.prototype.setCommandFromString = function(str) {
this.cmd = MIDIEvent.computeCommandFromString(str);
};
MIDIEvent.computeCommandFromString = function(str) {
if (!str) {
return 0;
}
if (str && str.constructor === Number) {
return str;
}
str = str.toUpperCase();
switch (str) {
case "NOTE ON":
case "NOTEON":
return MIDIEvent.NOTEON;
break;
case "NOTE OFF":
case "NOTEOFF":
return MIDIEvent.NOTEON;
break;
case "KEY PRESSURE":
case "KEYPRESSURE":
return MIDIEvent.KEYPRESSURE;
break;
case "CONTROLLER CHANGE":
case "CONTROLLERCHANGE":
case "CC":
return MIDIEvent.CONTROLLERCHANGE;
break;
case "PROGRAM CHANGE":
case "PROGRAMCHANGE":
case "PC":
return MIDIEvent.PROGRAMCHANGE;
break;
case "CHANNEL PRESSURE":
case "CHANNELPRESSURE":
return MIDIEvent.CHANNELPRESSURE;
break;
case "PITCH BEND":
case "PITCHBEND":
return MIDIEvent.PITCHBEND;
break;
case "TIME TICK":
case "TIMETICK":
return MIDIEvent.TIMETICK;
break;
default:
return Number(str); //assume its a hex code
}
};
//transform from a pitch number to string like "C4"
MIDIEvent.toNoteString = function(d, skip_octave) {
d = Math.round(d); //in case it has decimals
var note = d - 21;
var octave = Math.floor((d - 24) / 12 + 1);
note = note % 12;
if (note < 0) {
note = 12 + note;
}
return MIDIEvent.notes[note] + (skip_octave ? "" : octave);
};
MIDIEvent.NoteStringToPitch = function(str) {
str = str.toUpperCase();
var note = str[0];
var octave = 4;
if (str[1] == "#") {
note += "#";
if (str.length > 2) {
octave = Number(str[2]);
}
} else {
if (str.length > 1) {
octave = Number(str[1]);
}
}
var pitch = MIDIEvent.note_to_index[note];
if (pitch == null) {
return null;
}
return (octave - 1) * 12 + pitch + 21;
};
MIDIEvent.prototype.toString = function() {
var str = "" + this.channel + ". ";
switch (this.cmd) {
case MIDIEvent.NOTEON:
str += "NOTEON " + MIDIEvent.toNoteString(this.data[1]);
break;
case MIDIEvent.NOTEOFF:
str += "NOTEOFF " + MIDIEvent.toNoteString(this.data[1]);
break;
case MIDIEvent.CONTROLLERCHANGE:
str += "CC " + this.data[1] + " " + this.data[2];
break;
case MIDIEvent.PROGRAMCHANGE:
str += "PC " + this.data[1];
break;
case MIDIEvent.PITCHBEND:
str += "PITCHBEND " + this.getPitchBend();
break;
case MIDIEvent.KEYPRESSURE:
str += "KEYPRESS " + this.data[1];
break;
}
return str;
};
MIDIEvent.prototype.toHexString = function() {
var str = "";
for (var i = 0; i < this.data.length; i++) {
str += this.data[i].toString(16) + " ";
}
};
MIDIEvent.prototype.toJSON = function() {
return {
data: [this.data[0], this.data[1], this.data[2]],
object_class: "MIDIEvent"
};
};
MIDIEvent.NOTEOFF = 0x80;
MIDIEvent.NOTEON = 0x90;
MIDIEvent.KEYPRESSURE = 0xa0;
MIDIEvent.CONTROLLERCHANGE = 0xb0;
MIDIEvent.PROGRAMCHANGE = 0xc0;
MIDIEvent.CHANNELPRESSURE = 0xd0;
MIDIEvent.PITCHBEND = 0xe0;
MIDIEvent.TIMETICK = 0xf8;
MIDIEvent.commands = {
0x80: "note off",
0x90: "note on",
0xa0: "key pressure",
0xb0: "controller change",
0xc0: "program change",
0xd0: "channel pressure",
0xe0: "pitch bend",
0xf0: "system",
0xf2: "Song pos",
0xf3: "Song select",
0xf6: "Tune request",
0xf8: "time tick",
0xfa: "Start Song",
0xfb: "Continue Song",
0xfc: "Stop Song",
0xfe: "Sensing",
0xff: "Reset"
};
MIDIEvent.commands_short = {
0x80: "NOTEOFF",
0x90: "NOTEOFF",
0xa0: "KEYP",
0xb0: "CC",
0xc0: "PC",
0xd0: "CP",
0xe0: "PB",
0xf0: "SYS",
0xf2: "POS",
0xf3: "SELECT",
0xf6: "TUNEREQ",
0xf8: "TT",
0xfa: "START",
0xfb: "CONTINUE",
0xfc: "STOP",
0xfe: "SENS",
0xff: "RESET"
};
MIDIEvent.commands_reversed = {};
for (var i in MIDIEvent.commands) {
MIDIEvent.commands_reversed[MIDIEvent.commands[i]] = i;
}
//MIDI wrapper, instantiate by MIDIIn and MIDIOut
function MIDIInterface(on_ready, on_error) {
if (!navigator.requestMIDIAccess) {
this.error = "not suppoorted";
if (on_error) {
on_error("Not supported");
} else {
console.error("MIDI NOT SUPPORTED, enable by chrome://flags");
}
return;
}
this.on_ready = on_ready;
this.state = {
note: [],
cc: []
};
this.input_ports = null;
this.input_ports_info = [];
this.output_ports = null;
this.output_ports_info = [];
navigator.requestMIDIAccess().then(this.onMIDISuccess.bind(this), this.onMIDIFailure.bind(this));
}
MIDIInterface.input = null;
MIDIInterface.MIDIEvent = MIDIEvent;
MIDIInterface.prototype.onMIDISuccess = function(midiAccess) {
console.log("MIDI ready!");
console.log(midiAccess);
this.midi = midiAccess; // store in the global (in real usage, would probably keep in an object instance)
this.updatePorts();
if (this.on_ready) {
this.on_ready(this);
}
};
MIDIInterface.prototype.updatePorts = function() {
var midi = this.midi;
this.input_ports = midi.inputs;
this.input_ports_info = [];
this.output_ports = midi.outputs;
this.output_ports_info = [];
var num = 0;
var it = this.input_ports.values();
var it_value = it.next();
while (it_value && it_value.done === false) {
var port_info = it_value.value;
this.input_ports_info.push(port_info);
console.log( "Input port [type:'" + port_info.type + "'] id:'" + port_info.id + "' manufacturer:'" + port_info.manufacturer + "' name:'" + port_info.name + "' version:'" + port_info.version + "'" );
num++;
it_value = it.next();
}
this.num_input_ports = num;
num = 0;
var it = this.output_ports.values();
var it_value = it.next();
while (it_value && it_value.done === false) {
var port_info = it_value.value;
this.output_ports_info.push(port_info);
console.log( "Output port [type:'" + port_info.type + "'] id:'" + port_info.id + "' manufacturer:'" + port_info.manufacturer + "' name:'" + port_info.name + "' version:'" + port_info.version + "'" );
num++;
it_value = it.next();
}
this.num_output_ports = num;
};
MIDIInterface.prototype.onMIDIFailure = function(msg) {
console.error("Failed to get MIDI access - " + msg);
};
MIDIInterface.prototype.openInputPort = function(port, callback) {
var input_port = this.input_ports.get("input-" + port);
if (!input_port) {
return false;
}
MIDIInterface.input = this;
var that = this;
input_port.onmidimessage = function(a) {
var midi_event = new MIDIEvent(a.data);
that.updateState(midi_event);
if (callback) {
callback(a.data, midi_event);
}
if (MIDIInterface.on_message) {
MIDIInterface.on_message(a.data, midi_event);
}
};
console.log("port open: ", input_port);
return true;
};
MIDIInterface.parseMsg = function(data) {};
MIDIInterface.prototype.updateState = function(midi_event) {
switch (midi_event.cmd) {
case MIDIEvent.NOTEON:
this.state.note[midi_event.value1 | 0] = midi_event.value2;
break;
case MIDIEvent.NOTEOFF:
this.state.note[midi_event.value1 | 0] = 0;
break;
case MIDIEvent.CONTROLLERCHANGE:
this.state.cc[midi_event.getCC()] = midi_event.getCCValue();
break;
}
};
MIDIInterface.prototype.sendMIDI = function(port, midi_data) {
if (!midi_data) {
return;
}
var output_port = this.output_ports_info[port];//this.output_ports.get("output-" + port);
if (!output_port) {
return;
}
MIDIInterface.output = this;
if (midi_data.constructor === MIDIEvent) {
output_port.send(midi_data.data);
} else {
output_port.send(midi_data);
}
};
function LGMIDIIn() {
this.addOutput("on_midi", LiteGraph.EVENT);
this.addOutput("out", "midi");
this.properties = { port: 0 };
this._last_midi_event = null;
this._current_midi_event = null;
this.boxcolor = "#AAA";
this._last_time = 0;
var that = this;
new MIDIInterface(function(midi) {
//open
that._midi = midi;
if (that._waiting) {
that.onStart();
}
that._waiting = false;
});
}
LGMIDIIn.MIDIInterface = MIDIInterface;
LGMIDIIn.title = "MIDI Input";
LGMIDIIn.desc = "Reads MIDI from a input port";
LGMIDIIn.color = MIDI_COLOR;
LGMIDIIn.prototype.getPropertyInfo = function(name) {
if (!this._midi) {
return;
}
if (name == "port") {
var values = {};
for (var i = 0; i < this._midi.input_ports_info.length; ++i) {
var input = this._midi.input_ports_info[i];
values[i] = i + ".- " + input.name + " version:" + input.version;
}
return { type: "enum", values: values };
}
};
LGMIDIIn.prototype.onStart = function() {
if (this._midi) {
this._midi.openInputPort(
this.properties.port,
this.onMIDIEvent.bind(this)
);
} else {
this._waiting = true;
}
};
LGMIDIIn.prototype.onMIDIEvent = function(data, midi_event) {
this._last_midi_event = midi_event;
this.boxcolor = "#AFA";
this._last_time = LiteGraph.getTime();
this.trigger("on_midi", midi_event);
if (midi_event.cmd == MIDIEvent.NOTEON) {
this.trigger("on_noteon", midi_event);
} else if (midi_event.cmd == MIDIEvent.NOTEOFF) {
this.trigger("on_noteoff", midi_event);
} else if (midi_event.cmd == MIDIEvent.CONTROLLERCHANGE) {
this.trigger("on_cc", midi_event);
} else if (midi_event.cmd == MIDIEvent.PROGRAMCHANGE) {
this.trigger("on_pc", midi_event);
} else if (midi_event.cmd == MIDIEvent.PITCHBEND) {
this.trigger("on_pitchbend", midi_event);
}
};
LGMIDIIn.prototype.onDrawBackground = function(ctx) {
this.boxcolor = "#AAA";
if (!this.flags.collapsed && this._last_midi_event) {
ctx.fillStyle = "white";
var now = LiteGraph.getTime();
var f = 1.0 - Math.max(0, (now - this._last_time) * 0.001);
if (f > 0) {
var t = ctx.globalAlpha;
ctx.globalAlpha *= f;
ctx.font = "12px Tahoma";
ctx.fillText(
this._last_midi_event.toString(),
2,
this.size[1] * 0.5 + 3
);
//ctx.fillRect(0,0,this.size[0],this.size[1]);
ctx.globalAlpha = t;
}
}
};
LGMIDIIn.prototype.onExecute = function() {
if (this.outputs) {
var last = this._last_midi_event;
for (var i = 0; i < this.outputs.length; ++i) {
var output = this.outputs[i];
var v = null;
switch (output.name) {
case "midi":
v = this._midi;
break;
case "last_midi":
v = last;
break;
default:
continue;
}
this.setOutputData(i, v);
}
}
};
LGMIDIIn.prototype.onGetOutputs = function() {
return [
["last_midi", "midi"],
["on_midi", LiteGraph.EVENT],
["on_noteon", LiteGraph.EVENT],
["on_noteoff", LiteGraph.EVENT],
["on_cc", LiteGraph.EVENT],
["on_pc", LiteGraph.EVENT],
["on_pitchbend", LiteGraph.EVENT]
];
};
LiteGraph.registerNodeType("midi/input", LGMIDIIn);
function LGMIDIOut() {
this.addInput("send", LiteGraph.EVENT);
this.properties = { port: 0 };
var that = this;
new MIDIInterface(function(midi) {
that._midi = midi;
that.widget.options.values = that.getMIDIOutputs();
});
this.widget = this.addWidget("combo","Device",this.properties.port,{ property: "port", values: this.getMIDIOutputs.bind(this) });
this.size = [340,60];
}
LGMIDIOut.MIDIInterface = MIDIInterface;
LGMIDIOut.title = "MIDI Output";
LGMIDIOut.desc = "Sends MIDI to output channel";
LGMIDIOut.color = MIDI_COLOR;
LGMIDIOut.prototype.onGetPropertyInfo = function(name) {
if (!this._midi) {
return;
}
if (name == "port") {
var values = this.getMIDIOutputs();
return { type: "enum", values: values };
}
};
LGMIDIOut.default_ports = {0:"unknown"};
LGMIDIOut.prototype.getMIDIOutputs = function()
{
var values = {};
if(!this._midi)
return LGMIDIOut.default_ports;
if(this._midi.output_ports_info)
for (var i = 0; i < this._midi.output_ports_info.length; ++i) {
var output = this._midi.output_ports_info[i];
if(!output)
continue;
var name = i + ".- " + output.name + " version:" + output.version;
values[i] = name;
}
return values;
}
LGMIDIOut.prototype.onAction = function(event, midi_event) {
//console.log(midi_event);
if (!this._midi) {
return;
}
if (event == "send") {
this._midi.sendMIDI(this.properties.port, midi_event);
}
this.trigger("midi", midi_event);
};
LGMIDIOut.prototype.onGetInputs = function() {
return [["send", LiteGraph.ACTION]];
};
LGMIDIOut.prototype.onGetOutputs = function() {
return [["on_midi", LiteGraph.EVENT]];
};
LiteGraph.registerNodeType("midi/output", LGMIDIOut);
function LGMIDIShow() {
this.addInput("on_midi", LiteGraph.EVENT);
this._str = "";
this.size = [200, 40];
}
LGMIDIShow.title = "MIDI Show";
LGMIDIShow.desc = "Shows MIDI in the graph";
LGMIDIShow.color = MIDI_COLOR;
LGMIDIShow.prototype.getTitle = function() {
if (this.flags.collapsed) {
return this._str;
}
return this.title;
};
LGMIDIShow.prototype.onAction = function(event, midi_event) {
if (!midi_event) {
return;
}
if (midi_event.constructor === MIDIEvent) {
this._str = midi_event.toString();
} else {
this._str = "???";
}
};
LGMIDIShow.prototype.onDrawForeground = function(ctx) {
if (!this._str || this.flags.collapsed) {
return;
}
ctx.font = "30px Arial";
ctx.fillText(this._str, 10, this.size[1] * 0.8);
};
LGMIDIShow.prototype.onGetInputs = function() {
return [["in", LiteGraph.ACTION]];
};
LGMIDIShow.prototype.onGetOutputs = function() {
return [["on_midi", LiteGraph.EVENT]];
};
LiteGraph.registerNodeType("midi/show", LGMIDIShow);
function LGMIDIFilter() {
this.properties = {
channel: -1,
cmd: -1,
min_value: -1,
max_value: -1
};
var that = this;
this._learning = false;
this.addWidget("button", "Learn", "", function() {
that._learning = true;
that.boxcolor = "#FA3";
});
this.addInput("in", LiteGraph.EVENT);
this.addOutput("on_midi", LiteGraph.EVENT);
this.boxcolor = "#AAA";
}
LGMIDIFilter.title = "MIDI Filter";
LGMIDIFilter.desc = "Filters MIDI messages";
LGMIDIFilter.color = MIDI_COLOR;
LGMIDIFilter["@cmd"] = {
type: "enum",
title: "Command",
values: MIDIEvent.commands_reversed
};
LGMIDIFilter.prototype.getTitle = function() {
var str = null;
if (this.properties.cmd == -1) {
str = "Nothing";
} else {
str = MIDIEvent.commands_short[this.properties.cmd] || "Unknown";
}
if (
this.properties.min_value != -1 &&
this.properties.max_value != -1
) {
str +=
" " +
(this.properties.min_value == this.properties.max_value
? this.properties.max_value
: this.properties.min_value +
".." +
this.properties.max_value);
}
return "Filter: " + str;
};
LGMIDIFilter.prototype.onPropertyChanged = function(name, value) {
if (name == "cmd") {
var num = Number(value);
if (isNaN(num)) {
num = MIDIEvent.commands[value] || 0;
}
this.properties.cmd = num;
}
};
LGMIDIFilter.prototype.onAction = function(event, midi_event) {
if (!midi_event || midi_event.constructor !== MIDIEvent) {
return;
}
if (this._learning) {
this._learning = false;
this.boxcolor = "#AAA";
this.properties.channel = midi_event.channel;
this.properties.cmd = midi_event.cmd;
this.properties.min_value = this.properties.max_value =
midi_event.data[1];
} else {
if (
this.properties.channel != -1 &&
midi_event.channel != this.properties.channel
) {
return;
}
if (
this.properties.cmd != -1 &&
midi_event.cmd != this.properties.cmd
) {
return;
}
if (
this.properties.min_value != -1 &&
midi_event.data[1] < this.properties.min_value
) {
return;
}
if (
this.properties.max_value != -1 &&
midi_event.data[1] > this.properties.max_value
) {
return;
}
}
this.trigger("on_midi", midi_event);
};
LiteGraph.registerNodeType("midi/filter", LGMIDIFilter);
function LGMIDIEvent() {
this.properties = {
channel: 0,
cmd: 144, //0x90
value1: 1,
value2: 1
};
this.addInput("send", LiteGraph.EVENT);
this.addInput("assign", LiteGraph.EVENT);
this.addOutput("on_midi", LiteGraph.EVENT);
this.midi_event = new MIDIEvent();
this.gate = false;
}
LGMIDIEvent.title = "MIDIEvent";
LGMIDIEvent.desc = "Create a MIDI Event";
LGMIDIEvent.color = MIDI_COLOR;
LGMIDIEvent.prototype.onAction = function(event, midi_event) {
if (event == "assign") {
this.properties.channel = midi_event.channel;
this.properties.cmd = midi_event.cmd;
this.properties.value1 = midi_event.data[1];
this.properties.value2 = midi_event.data[2];
if (midi_event.cmd == MIDIEvent.NOTEON) {
this.gate = true;
} else if (midi_event.cmd == MIDIEvent.NOTEOFF) {
this.gate = false;
}
return;
}
//send
var midi_event = this.midi_event;
midi_event.channel = this.properties.channel;
if (this.properties.cmd && this.properties.cmd.constructor === String) {
midi_event.setCommandFromString(this.properties.cmd);
} else {
midi_event.cmd = this.properties.cmd;
}
midi_event.data[0] = midi_event.cmd | midi_event.channel;
midi_event.data[1] = Number(this.properties.value1);
midi_event.data[2] = Number(this.properties.value2);
this.trigger("on_midi", midi_event);
};
LGMIDIEvent.prototype.onExecute = function() {
var props = this.properties;
if (this.inputs) {
for (var i = 0; i < this.inputs.length; ++i) {
var input = this.inputs[i];
if (input.link == -1) {
continue;
}
switch (input.name) {
case "note":
var v = this.getInputData(i);
if (v != null) {
if (v.constructor === String) {
v = MIDIEvent.NoteStringToPitch(v);
}
this.properties.value1 = (v | 0) % 255;
}
break;
case "cmd":
var v = this.getInputData(i);
if (v != null) {
this.properties.cmd = v;
}
break;
case "value1":
var v = this.getInputData(i);
if (v != null) {
this.properties.value1 = clamp(v|0,0,127);
}
break;
case "value2":
var v = this.getInputData(i);
if (v != null) {
this.properties.value2 = clamp(v|0,0,127);
}
break;
}
}
}
if (this.outputs) {
for (var i = 0; i < this.outputs.length; ++i) {
var output = this.outputs[i];
var v = null;
switch (output.name) {
case "midi":
v = new MIDIEvent();
v.setup([props.cmd, props.value1, props.value2]);
v.channel = props.channel;
break;
case "command":
v = props.cmd;
break;
case "cc":
v = props.value1;
break;
case "cc_value":
v = props.value2;
break;
case "note":
v =
props.cmd == MIDIEvent.NOTEON ||
props.cmd == MIDIEvent.NOTEOFF
? props.value1
: null;
break;
case "velocity":
v = props.cmd == MIDIEvent.NOTEON ? props.value2 : null;
break;
case "pitch":
v =
props.cmd == MIDIEvent.NOTEON
? MIDIEvent.computePitch(props.value1)
: null;
break;
case "pitchbend":
v =
props.cmd == MIDIEvent.PITCHBEND
? MIDIEvent.computePitchBend(
props.value1,
props.value2
)
: null;
break;
case "gate":
v = this.gate;
break;
default:
continue;
}
if (v !== null) {
this.setOutputData(i, v);
}
}
}
};
LGMIDIEvent.prototype.onPropertyChanged = function(name, value) {
if (name == "cmd") {
this.properties.cmd = MIDIEvent.computeCommandFromString(value);
}
};
LGMIDIEvent.prototype.onGetInputs = function() {
return [["cmd", "number"],["note", "number"],["value1", "number"],["value2", "number"]];
};
LGMIDIEvent.prototype.onGetOutputs = function() {
return [
["midi", "midi"],
["on_midi", LiteGraph.EVENT],
["command", "number"],
["note", "number"],
["velocity", "number"],
["cc", "number"],
["cc_value", "number"],
["pitch", "number"],
["gate", "bool"],
["pitchbend", "number"]
];
};
LiteGraph.registerNodeType("midi/event", LGMIDIEvent);
function LGMIDICC() {
this.properties = {
// channel: 0,
cc: 1,
value: 0
};
this.addOutput("value", "number");
}
LGMIDICC.title = "MIDICC";
LGMIDICC.desc = "gets a Controller Change";
LGMIDICC.color = MIDI_COLOR;
LGMIDICC.prototype.onExecute = function() {
var props = this.properties;
if (MIDIInterface.input) {
this.properties.value =
MIDIInterface.input.state.cc[this.properties.cc];
}
this.setOutputData(0, this.properties.value);
};
LiteGraph.registerNodeType("midi/cc", LGMIDICC);
function LGMIDIGenerator() {
this.addInput("generate", LiteGraph.ACTION);
this.addInput("scale", "string");
this.addInput("octave", "number");
this.addOutput("note", LiteGraph.EVENT);
this.properties = {
notes: "A,A#,B,C,C#,D,D#,E,F,F#,G,G#",
octave: 2,
duration: 0.5,
mode: "sequence"
};
this.notes_pitches = LGMIDIGenerator.processScale(
this.properties.notes
);
this.sequence_index = 0;
}
LGMIDIGenerator.title = "MIDI Generator";
LGMIDIGenerator.desc = "Generates a random MIDI note";
LGMIDIGenerator.color = MIDI_COLOR;
LGMIDIGenerator.processScale = function(scale) {
var notes = scale.split(",");
for (var i = 0; i < notes.length; ++i) {
var n = notes[i];
if ((n.length == 2 && n[1] != "#") || n.length > 2) {
notes[i] = -LiteGraph.MIDIEvent.NoteStringToPitch(n);
} else {
notes[i] = MIDIEvent.note_to_index[n] || 0;
}
}
return notes;
};
LGMIDIGenerator.prototype.onPropertyChanged = function(name, value) {
if (name == "notes") {
this.notes_pitches = LGMIDIGenerator.processScale(value);
}
};
LGMIDIGenerator.prototype.onExecute = function() {
var octave = this.getInputData(2);
if (octave != null) {
this.properties.octave = octave;
}
var scale = this.getInputData(1);
if (scale) {
this.notes_pitches = LGMIDIGenerator.processScale(scale);
}
};
LGMIDIGenerator.prototype.onAction = function(event, midi_event) {
//var range = this.properties.max - this.properties.min;
//var pitch = this.properties.min + ((Math.random() * range)|0);
var pitch = 0;
var range = this.notes_pitches.length;
var index = 0;
if (this.properties.mode == "sequence") {
index = this.sequence_index = (this.sequence_index + 1) % range;
} else if (this.properties.mode == "random") {
index = Math.floor(Math.random() * range);
}
var note = this.notes_pitches[index];
if (note >= 0) {
pitch = note + (this.properties.octave - 1) * 12 + 33;
} else {
pitch = -note;
}
var midi_event = new MIDIEvent();
midi_event.setup([MIDIEvent.NOTEON, pitch, 10]);
var duration = this.properties.duration || 1;
this.trigger("note", midi_event);
//noteoff
setTimeout(
function() {
var midi_event = new MIDIEvent();
midi_event.setup([MIDIEvent.NOTEOFF, pitch, 0]);
this.trigger("note", midi_event);
}.bind(this),
duration * 1000
);
};
LiteGraph.registerNodeType("midi/generator", LGMIDIGenerator);
function LGMIDITranspose() {
this.properties = {
amount: 0
};
this.addInput("in", LiteGraph.ACTION);
this.addInput("amount", "number");
this.addOutput("out", LiteGraph.EVENT);
this.midi_event = new MIDIEvent();
}
LGMIDITranspose.title = "MIDI Transpose";
LGMIDITranspose.desc = "Transpose a MIDI note";
LGMIDITranspose.color = MIDI_COLOR;
LGMIDITranspose.prototype.onAction = function(event, midi_event) {
if (!midi_event || midi_event.constructor !== MIDIEvent) {
return;
}
if (
midi_event.data[0] == MIDIEvent.NOTEON ||
midi_event.data[0] == MIDIEvent.NOTEOFF
) {
this.midi_event = new MIDIEvent();
this.midi_event.setup(midi_event.data);
this.midi_event.data[1] = Math.round(
this.midi_event.data[1] + this.properties.amount
);
this.trigger("out", this.midi_event);
} else {
this.trigger("out", midi_event);
}
};
LGMIDITranspose.prototype.onExecute = function() {
var amount = this.getInputData(1);
if (amount != null) {
this.properties.amount = amount;
}
};
LiteGraph.registerNodeType("midi/transpose", LGMIDITranspose);
function LGMIDIQuantize() {
this.properties = {
scale: "A,A#,B,C,C#,D,D#,E,F,F#,G,G#"
};
this.addInput("note", LiteGraph.ACTION);
this.addInput("scale", "string");
this.addOutput("out", LiteGraph.EVENT);
this.valid_notes = new Array(12);
this.offset_notes = new Array(12);
this.processScale(this.properties.scale);
}
LGMIDIQuantize.title = "MIDI Quantize Pitch";
LGMIDIQuantize.desc = "Transpose a MIDI note tp fit an scale";
LGMIDIQuantize.color = MIDI_COLOR;
LGMIDIQuantize.prototype.onPropertyChanged = function(name, value) {
if (name == "scale") {
this.processScale(value);
}
};
LGMIDIQuantize.prototype.processScale = function(scale) {
this._current_scale = scale;
this.notes_pitches = LGMIDIGenerator.processScale(scale);
for (var i = 0; i < 12; ++i) {
this.valid_notes[i] = this.notes_pitches.indexOf(i) != -1;
}
for (var i = 0; i < 12; ++i) {
if (this.valid_notes[i]) {
this.offset_notes[i] = 0;
continue;
}
for (var j = 1; j < 12; ++j) {
if (this.valid_notes[(i - j) % 12]) {
this.offset_notes[i] = -j;
break;
}
if (this.valid_notes[(i + j) % 12]) {
this.offset_notes[i] = j;
break;
}
}
}
};
LGMIDIQuantize.prototype.onAction = function(event, midi_event) {
if (!midi_event || midi_event.constructor !== MIDIEvent) {
return;
}
if (
midi_event.data[0] == MIDIEvent.NOTEON ||
midi_event.data[0] == MIDIEvent.NOTEOFF
) {
this.midi_event = new MIDIEvent();
this.midi_event.setup(midi_event.data);
var note = midi_event.note;
var index = MIDIEvent.note_to_index[note];
var offset = this.offset_notes[index];
this.midi_event.data[1] += offset;
this.trigger("out", this.midi_event);
} else {
this.trigger("out", midi_event);
}
};
LGMIDIQuantize.prototype.onExecute = function() {
var scale = this.getInputData(1);
if (scale != null && scale != this._current_scale) {
this.processScale(scale);
}
};
LiteGraph.registerNodeType("midi/quantize", LGMIDIQuantize);
function LGMIDIFromFile() {
this.properties = {
url: "",
autoplay: true
};
this.addInput("play", LiteGraph.ACTION);
this.addInput("pause", LiteGraph.ACTION);
this.addOutput("note", LiteGraph.EVENT);
this._midi = null;
this._current_time = 0;
this._playing = false;
if (typeof MidiParser == "undefined") {
console.error(
"midi-parser.js not included, LGMidiPlay requires that library: https://raw.githubusercontent.com/colxi/midi-parser-js/master/src/main.js"
);
this.boxcolor = "red";
}
}
LGMIDIFromFile.title = "MIDI fromFile";
LGMIDIFromFile.desc = "Plays a MIDI file";
LGMIDIFromFile.color = MIDI_COLOR;
LGMIDIFromFile.prototype.onAction = function( name )
{
if(name == "play")
this.play();
else if(name == "pause")
this._playing = !this._playing;
}
LGMIDIFromFile.prototype.onPropertyChanged = function(name,value)
{
if(name == "url")
this.loadMIDIFile(value);
}
LGMIDIFromFile.prototype.onExecute = function() {
if(!this._midi)
return;
if(!this._playing)
return;
this._current_time += this.graph.elapsed_time;
var current_time = this._current_time * 100;
for(var i = 0; i < this._midi.tracks; ++i)
{
var track = this._midi.track[i];
if(!track._last_pos)
{
track._last_pos = 0;
track._time = 0;
}
var elem = track.event[ track._last_pos ];
if(elem && (track._time + elem.deltaTime) <= current_time )
{
track._last_pos++;
track._time += elem.deltaTime;
if(elem.data)
{
var midi_cmd = elem.type << 4 + elem.channel;
var midi_event = new MIDIEvent();
midi_event.setup([midi_cmd, elem.data[0], elem.data[1]]);
this.trigger("note", midi_event);
}
}
}
};
LGMIDIFromFile.prototype.play = function()
{
this._playing = true;
this._current_time = 0;
if(!this._midi)
return;
for(var i = 0; i < this._midi.tracks; ++i)
{
var track = this._midi.track[i];
track._last_pos = 0;
track._time = 0;
}
}
LGMIDIFromFile.prototype.loadMIDIFile = function(url)
{
var that = this;
LiteGraph.fetchFile( url, "arraybuffer", function(data)
{
that.boxcolor = "#AFA";
that._midi = MidiParser.parse( new Uint8Array(data) );
if(that.properties.autoplay)
that.play();
}, function(err){
that.boxcolor = "#FAA";
that._midi = null;
});
}
LGMIDIFromFile.prototype.onDropFile = function(file)
{
this.properties.url = "";
this.loadMIDIFile( file );
}
LiteGraph.registerNodeType("midi/fromFile", LGMIDIFromFile);
function LGMIDIPlay() {
this.properties = {
volume: 0.5,
duration: 1
};
this.addInput("note", LiteGraph.ACTION);
this.addInput("volume", "number");
this.addInput("duration", "number");
this.addOutput("note", LiteGraph.EVENT);
if (typeof AudioSynth == "undefined") {
console.error(
"Audiosynth.js not included, LGMidiPlay requires that library"
);
this.boxcolor = "red";
} else {
var Synth = (this.synth = new AudioSynth());
this.instrument = Synth.createInstrument("piano");
}
}
LGMIDIPlay.title = "MIDI Play";
LGMIDIPlay.desc = "Plays a MIDI note";
LGMIDIPlay.color = MIDI_COLOR;
LGMIDIPlay.prototype.onAction = function(event, midi_event) {
if (!midi_event || midi_event.constructor !== MIDIEvent) {
return;
}
if (this.instrument && midi_event.data[0] == MIDIEvent.NOTEON) {
var note = midi_event.note; //C#
if (!note || note == "undefined" || note.constructor !== String) {
return;
}
this.instrument.play(
note,
midi_event.octave,
this.properties.duration,
this.properties.volume
);
}
this.trigger("note", midi_event);
};
LGMIDIPlay.prototype.onExecute = function() {
var volume = this.getInputData(1);
if (volume != null) {
this.properties.volume = volume;
}
var duration = this.getInputData(2);
if (duration != null) {
this.properties.duration = duration;
}
};
LiteGraph.registerNodeType("midi/play", LGMIDIPlay);
function LGMIDIKeys() {
this.properties = {
num_octaves: 2,
start_octave: 2
};
this.addInput("note", LiteGraph.ACTION);
this.addInput("reset", LiteGraph.ACTION);
this.addOutput("note", LiteGraph.EVENT);
this.size = [400, 100];
this.keys = [];
this._last_key = -1;
}
LGMIDIKeys.title = "MIDI Keys";
LGMIDIKeys.desc = "Keyboard to play notes";
LGMIDIKeys.color = MIDI_COLOR;
LGMIDIKeys.keys = [
{ x: 0, w: 1, h: 1, t: 0 },
{ x: 0.75, w: 0.5, h: 0.6, t: 1 },
{ x: 1, w: 1, h: 1, t: 0 },
{ x: 1.75, w: 0.5, h: 0.6, t: 1 },
{ x: 2, w: 1, h: 1, t: 0 },
{ x: 2.75, w: 0.5, h: 0.6, t: 1 },
{ x: 3, w: 1, h: 1, t: 0 },
{ x: 4, w: 1, h: 1, t: 0 },
{ x: 4.75, w: 0.5, h: 0.6, t: 1 },
{ x: 5, w: 1, h: 1, t: 0 },
{ x: 5.75, w: 0.5, h: 0.6, t: 1 },
{ x: 6, w: 1, h: 1, t: 0 }
];
LGMIDIKeys.prototype.onDrawForeground = function(ctx) {
if (this.flags.collapsed) {
return;
}
var num_keys = this.properties.num_octaves * 12;
this.keys.length = num_keys;
var key_width = this.size[0] / (this.properties.num_octaves * 7);
var key_height = this.size[1];
ctx.globalAlpha = 1;
for (
var k = 0;
k < 2;
k++ //draw first whites (0) then blacks (1)
) {
for (var i = 0; i < num_keys; ++i) {
var key_info = LGMIDIKeys.keys[i % 12];
if (key_info.t != k) {
continue;
}
var octave = Math.floor(i / 12);
var x = octave * 7 * key_width + key_info.x * key_width;
if (k == 0) {
ctx.fillStyle = this.keys[i] ? "#CCC" : "white";
} else {
ctx.fillStyle = this.keys[i] ? "#333" : "black";
}
ctx.fillRect(
x + 1,
0,
key_width * key_info.w - 2,
key_height * key_info.h
);
}
}
};
LGMIDIKeys.prototype.getKeyIndex = function(pos) {
var num_keys = this.properties.num_octaves * 12;
var key_width = this.size[0] / (this.properties.num_octaves * 7);
var key_height = this.size[1];
for (
var k = 1;
k >= 0;
k-- //test blacks first (1) then whites (0)
) {
for (var i = 0; i < this.keys.length; ++i) {
var key_info = LGMIDIKeys.keys[i % 12];
if (key_info.t != k) {
continue;
}
var octave = Math.floor(i / 12);
var x = octave * 7 * key_width + key_info.x * key_width;
var w = key_width * key_info.w;
var h = key_height * key_info.h;
if (pos[0] < x || pos[0] > x + w || pos[1] > h) {
continue;
}
return i;
}
}
return -1;
};
LGMIDIKeys.prototype.onAction = function(event, params) {
if (event == "reset") {
for (var i = 0; i < this.keys.length; ++i) {
this.keys[i] = false;
}
return;
}
if (!params || params.constructor !== MIDIEvent) {
return;
}
var midi_event = params;
var start_note = (this.properties.start_octave - 1) * 12 + 29;
var index = midi_event.data[1] - start_note;
if (index >= 0 && index < this.keys.length) {
if (midi_event.data[0] == MIDIEvent.NOTEON) {
this.keys[index] = true;
} else if (midi_event.data[0] == MIDIEvent.NOTEOFF) {
this.keys[index] = false;
}
}
this.trigger("note", midi_event);
};
LGMIDIKeys.prototype.onMouseDown = function(e, pos) {
if (pos[1] < 0) {
return;
}
var index = this.getKeyIndex(pos);
this.keys[index] = true;
this._last_key = index;
var pitch = (this.properties.start_octave - 1) * 12 + 29 + index;
var midi_event = new MIDIEvent();
midi_event.setup([MIDIEvent.NOTEON, pitch, 100]);
this.trigger("note", midi_event);
return true;
};
LGMIDIKeys.prototype.onMouseMove = function(e, pos) {
if (pos[1] < 0 || this._last_key == -1) {
return;
}
this.setDirtyCanvas(true);
var index = this.getKeyIndex(pos);
if (this._last_key == index) {
return true;
}
this.keys[this._last_key] = false;
var pitch =
(this.properties.start_octave - 1) * 12 + 29 + this._last_key;
var midi_event = new MIDIEvent();
midi_event.setup([MIDIEvent.NOTEOFF, pitch, 100]);
this.trigger("note", midi_event);
this.keys[index] = true;
var pitch = (this.properties.start_octave - 1) * 12 + 29 + index;
var midi_event = new MIDIEvent();
midi_event.setup([MIDIEvent.NOTEON, pitch, 100]);
this.trigger("note", midi_event);
this._last_key = index;
return true;
};
LGMIDIKeys.prototype.onMouseUp = function(e, pos) {
if (pos[1] < 0) {
return;
}
var index = this.getKeyIndex(pos);
this.keys[index] = false;
this._last_key = -1;
var pitch = (this.properties.start_octave - 1) * 12 + 29 + index;
var midi_event = new MIDIEvent();
midi_event.setup([MIDIEvent.NOTEOFF, pitch, 100]);
this.trigger("note", midi_event);
return true;
};
LiteGraph.registerNodeType("midi/keys", LGMIDIKeys);
function now() {
return window.performance.now();
}
})(this);
(function(global) {
var LiteGraph = global.LiteGraph;
var LGAudio = {};
global.LGAudio = LGAudio;
LGAudio.getAudioContext = function() {
if (!this._audio_context) {
window.AudioContext =
window.AudioContext || window.webkitAudioContext;
if (!window.AudioContext) {
console.error("AudioContext not supported by browser");
return null;
}
this._audio_context = new AudioContext();
this._audio_context.onmessage = function(msg) {
console.log("msg", msg);
};
this._audio_context.onended = function(msg) {
console.log("ended", msg);
};
this._audio_context.oncomplete = function(msg) {
console.log("complete", msg);
};
}
//in case it crashes
//if(this._audio_context.state == "suspended")
// this._audio_context.resume();
return this._audio_context;
};
LGAudio.connect = function(audionodeA, audionodeB) {
try {
audionodeA.connect(audionodeB);
} catch (err) {
console.warn("LGraphAudio:", err);
}
};
LGAudio.disconnect = function(audionodeA, audionodeB) {
try {
audionodeA.disconnect(audionodeB);
} catch (err) {
console.warn("LGraphAudio:", err);
}
};
LGAudio.changeAllAudiosConnections = function(node, connect) {
if (node.inputs) {
for (var i = 0; i < node.inputs.length; ++i) {
var input = node.inputs[i];
var link_info = node.graph.links[input.link];
if (!link_info) {
continue;
}
var origin_node = node.graph.getNodeById(link_info.origin_id);
var origin_audionode = null;
if (origin_node.getAudioNodeInOutputSlot) {
origin_audionode = origin_node.getAudioNodeInOutputSlot(
link_info.origin_slot
);
} else {
origin_audionode = origin_node.audionode;
}
var target_audionode = null;
if (node.getAudioNodeInInputSlot) {
target_audionode = node.getAudioNodeInInputSlot(i);
} else {
target_audionode = node.audionode;
}
if (connect) {
LGAudio.connect(origin_audionode, target_audionode);
} else {
LGAudio.disconnect(origin_audionode, target_audionode);
}
}
}
if (node.outputs) {
for (var i = 0; i < node.outputs.length; ++i) {
var output = node.outputs[i];
for (var j = 0; j < output.links.length; ++j) {
var link_info = node.graph.links[output.links[j]];
if (!link_info) {
continue;
}
var origin_audionode = null;
if (node.getAudioNodeInOutputSlot) {
origin_audionode = node.getAudioNodeInOutputSlot(i);
} else {
origin_audionode = node.audionode;
}
var target_node = node.graph.getNodeById(
link_info.target_id
);
var target_audionode = null;
if (target_node.getAudioNodeInInputSlot) {
target_audionode = target_node.getAudioNodeInInputSlot(
link_info.target_slot
);
} else {
target_audionode = target_node.audionode;
}
if (connect) {
LGAudio.connect(origin_audionode, target_audionode);
} else {
LGAudio.disconnect(origin_audionode, target_audionode);
}
}
}
}
};
//used by many nodes
LGAudio.onConnectionsChange = function(
connection,
slot,
connected,
link_info
) {
//only process the outputs events
if (connection != LiteGraph.OUTPUT) {
return;
}
var target_node = null;
if (link_info) {
target_node = this.graph.getNodeById(link_info.target_id);
}
if (!target_node) {
return;
}
//get origin audionode
var local_audionode = null;
if (this.getAudioNodeInOutputSlot) {
local_audionode = this.getAudioNodeInOutputSlot(slot);
} else {
local_audionode = this.audionode;
}
//get target audionode
var target_audionode = null;
if (target_node.getAudioNodeInInputSlot) {
target_audionode = target_node.getAudioNodeInInputSlot(
link_info.target_slot
);
} else {
target_audionode = target_node.audionode;
}
//do the connection/disconnection
if (connected) {
LGAudio.connect(local_audionode, target_audionode);
} else {
LGAudio.disconnect(local_audionode, target_audionode);
}
};
//this function helps creating wrappers to existing classes
LGAudio.createAudioNodeWrapper = function(class_object) {
var old_func = class_object.prototype.onPropertyChanged;
class_object.prototype.onPropertyChanged = function(name, value) {
if (old_func) {
old_func.call(this, name, value);
}
if (!this.audionode) {
return;
}
if (this.audionode[name] === undefined) {
return;
}
if (this.audionode[name].value !== undefined) {
this.audionode[name].value = value;
} else {
this.audionode[name] = value;
}
};
class_object.prototype.onConnectionsChange =
LGAudio.onConnectionsChange;
};
//contains the samples decoded of the loaded audios in AudioBuffer format
LGAudio.cached_audios = {};
LGAudio.loadSound = function(url, on_complete, on_error) {
if (LGAudio.cached_audios[url] && url.indexOf("blob:") == -1) {
if (on_complete) {
on_complete(LGAudio.cached_audios[url]);
}
return;
}
if (LGAudio.onProcessAudioURL) {
url = LGAudio.onProcessAudioURL(url);
}
//load new sample
var request = new XMLHttpRequest();
request.open("GET", url, true);
request.responseType = "arraybuffer";
var context = LGAudio.getAudioContext();
// Decode asynchronously
request.onload = function() {
console.log("AudioSource loaded");
context.decodeAudioData(
request.response,
function(buffer) {
console.log("AudioSource decoded");
LGAudio.cached_audios[url] = buffer;
if (on_complete) {
on_complete(buffer);
}
},
onError
);
};
request.send();
function onError(err) {
console.log("Audio loading sample error:", err);
if (on_error) {
on_error(err);
}
}
return request;
};
//****************************************************
function LGAudioSource() {
this.properties = {
src: "",
gain: 0.5,
loop: true,
autoplay: true,
playbackRate: 1
};
this._loading_audio = false;
this._audiobuffer = null; //points to AudioBuffer with the audio samples decoded
this._audionodes = [];
this._last_sourcenode = null; //the last AudioBufferSourceNode (there could be more if there are several sounds playing)
this.addOutput("out", "audio");
this.addInput("gain", "number");
//init context
var context = LGAudio.getAudioContext();
//create gain node to control volume
this.audionode = context.createGain();
this.audionode.graphnode = this;
this.audionode.gain.value = this.properties.gain;
//debug
if (this.properties.src) {
this.loadSound(this.properties.src);
}
}
LGAudioSource.desc = "Plays an audio file";
LGAudioSource["@src"] = { widget: "resource" };
LGAudioSource.supported_extensions = ["wav", "ogg", "mp3"];
LGAudioSource.prototype.onAdded = function(graph) {
if (graph.status === LGraph.STATUS_RUNNING) {
this.onStart();
}
};
LGAudioSource.prototype.onStart = function() {
if (!this._audiobuffer) {
return;
}
if (this.properties.autoplay) {
this.playBuffer(this._audiobuffer);
}
};
LGAudioSource.prototype.onStop = function() {
this.stopAllSounds();
};
LGAudioSource.prototype.onPause = function() {
this.pauseAllSounds();
};
LGAudioSource.prototype.onUnpause = function() {
this.unpauseAllSounds();
//this.onStart();
};
LGAudioSource.prototype.onRemoved = function() {
this.stopAllSounds();
if (this._dropped_url) {
URL.revokeObjectURL(this._url);
}
};
LGAudioSource.prototype.stopAllSounds = function() {
//iterate and stop
for (var i = 0; i < this._audionodes.length; ++i) {
if (this._audionodes[i].started) {
this._audionodes[i].started = false;
this._audionodes[i].stop();
}
//this._audionodes[i].disconnect( this.audionode );
}
this._audionodes.length = 0;
};
LGAudioSource.prototype.pauseAllSounds = function() {
LGAudio.getAudioContext().suspend();
};
LGAudioSource.prototype.unpauseAllSounds = function() {
LGAudio.getAudioContext().resume();
};
LGAudioSource.prototype.onExecute = function() {
if (this.inputs) {
for (var i = 0; i < this.inputs.length; ++i) {
var input = this.inputs[i];
if (input.link == null) {
continue;
}
var v = this.getInputData(i);
if (v === undefined) {
continue;
}
if (input.name == "gain")
this.audionode.gain.value = v;
else if (input.name == "src") {
this.setProperty("src",v);
} else if (input.name == "playbackRate") {
this.properties.playbackRate = v;
for (var j = 0; j < this._audionodes.length; ++j) {
this._audionodes[j].playbackRate.value = v;
}
}
}
}
if (this.outputs) {
for (var i = 0; i < this.outputs.length; ++i) {
var output = this.outputs[i];
if (output.name == "buffer" && this._audiobuffer) {
this.setOutputData(i, this._audiobuffer);
}
}
}
};
LGAudioSource.prototype.onAction = function(event) {
if (this._audiobuffer) {
if (event == "Play") {
this.playBuffer(this._audiobuffer);
} else if (event == "Stop") {
this.stopAllSounds();
}
}
};
LGAudioSource.prototype.onPropertyChanged = function(name, value) {
if (name == "src") {
this.loadSound(value);
} else if (name == "gain") {
this.audionode.gain.value = value;
} else if (name == "playbackRate") {
for (var j = 0; j < this._audionodes.length; ++j) {
this._audionodes[j].playbackRate.value = value;
}
}
};
LGAudioSource.prototype.playBuffer = function(buffer) {
var that = this;
var context = LGAudio.getAudioContext();
//create a new audionode (this is mandatory, AudioAPI doesnt like to reuse old ones)
var audionode = context.createBufferSource(); //create a AudioBufferSourceNode
this._last_sourcenode = audionode;
audionode.graphnode = this;
audionode.buffer = buffer;
audionode.loop = this.properties.loop;
audionode.playbackRate.value = this.properties.playbackRate;
this._audionodes.push(audionode);
audionode.connect(this.audionode); //connect to gain
this._audionodes.push(audionode);
this.trigger("start");
audionode.onended = function() {
//console.log("ended!");
that.trigger("ended");
//remove
var index = that._audionodes.indexOf(audionode);
if (index != -1) {
that._audionodes.splice(index, 1);
}
};
if (!audionode.started) {
audionode.started = true;
audionode.start();
}
return audionode;
};
LGAudioSource.prototype.loadSound = function(url) {
var that = this;
//kill previous load
if (this._request) {
this._request.abort();
this._request = null;
}
this._audiobuffer = null; //points to the audiobuffer once the audio is loaded
this._loading_audio = false;
if (!url) {
return;
}
this._request = LGAudio.loadSound(url, inner);
this._loading_audio = true;
this.boxcolor = "#AA4";
function inner(buffer) {
this.boxcolor = LiteGraph.NODE_DEFAULT_BOXCOLOR;
that._audiobuffer = buffer;
that._loading_audio = false;
//if is playing, then play it
if (that.graph && that.graph.status === LGraph.STATUS_RUNNING) {
that.onStart();
} //this controls the autoplay already
}
};
//Helps connect/disconnect AudioNodes when new connections are made in the node
LGAudioSource.prototype.onConnectionsChange = LGAudio.onConnectionsChange;
LGAudioSource.prototype.onGetInputs = function() {
return [
["playbackRate", "number"],
["src","string"],
["Play", LiteGraph.ACTION],
["Stop", LiteGraph.ACTION]
];
};
LGAudioSource.prototype.onGetOutputs = function() {
return [["buffer", "audiobuffer"], ["start", LiteGraph.EVENT], ["ended", LiteGraph.EVENT]];
};
LGAudioSource.prototype.onDropFile = function(file) {
if (this._dropped_url) {
URL.revokeObjectURL(this._dropped_url);
}
var url = URL.createObjectURL(file);
this.properties.src = url;
this.loadSound(url);
this._dropped_url = url;
};
LGAudioSource.title = "Source";
LGAudioSource.desc = "Plays audio";
LiteGraph.registerNodeType("audio/source", LGAudioSource);
//****************************************************
function LGAudioMediaSource() {
this.properties = {
gain: 0.5
};
this._audionodes = [];
this._media_stream = null;
this.addOutput("out", "audio");
this.addInput("gain", "number");
//create gain node to control volume
var context = LGAudio.getAudioContext();
this.audionode = context.createGain();
this.audionode.graphnode = this;
this.audionode.gain.value = this.properties.gain;
}
LGAudioMediaSource.prototype.onAdded = function(graph) {
if (graph.status === LGraph.STATUS_RUNNING) {
this.onStart();
}
};
LGAudioMediaSource.prototype.onStart = function() {
if (this._media_stream == null && !this._waiting_confirmation) {
this.openStream();
}
};
LGAudioMediaSource.prototype.onStop = function() {
this.audionode.gain.value = 0;
};
LGAudioMediaSource.prototype.onPause = function() {
this.audionode.gain.value = 0;
};
LGAudioMediaSource.prototype.onUnpause = function() {
this.audionode.gain.value = this.properties.gain;
};
LGAudioMediaSource.prototype.onRemoved = function() {
this.audionode.gain.value = 0;
if (this.audiosource_node) {
this.audiosource_node.disconnect(this.audionode);
this.audiosource_node = null;
}
if (this._media_stream) {
var tracks = this._media_stream.getTracks();
if (tracks.length) {
tracks[0].stop();
}
}
};
LGAudioMediaSource.prototype.openStream = function() {
if (!navigator.mediaDevices) {
console.log(
"getUserMedia() is not supported in your browser, use chrome and enable WebRTC from about://flags"
);
return;
}
this._waiting_confirmation = true;
// Not showing vendor prefixes.
navigator.mediaDevices
.getUserMedia({ audio: true, video: false })
.then(this.streamReady.bind(this))
.catch(onFailSoHard);
var that = this;
function onFailSoHard(err) {
console.log("Media rejected", err);
that._media_stream = false;
that.boxcolor = "red";
}
};
LGAudioMediaSource.prototype.streamReady = function(localMediaStream) {
this._media_stream = localMediaStream;
//this._waiting_confirmation = false;
//init context
if (this.audiosource_node) {
this.audiosource_node.disconnect(this.audionode);
}
var context = LGAudio.getAudioContext();
this.audiosource_node = context.createMediaStreamSource(
localMediaStream
);
this.audiosource_node.graphnode = this;
this.audiosource_node.connect(this.audionode);
this.boxcolor = "white";
};
LGAudioMediaSource.prototype.onExecute = function() {
if (this._media_stream == null && !this._waiting_confirmation) {
this.openStream();
}
if (this.inputs) {
for (var i = 0; i < this.inputs.length; ++i) {
var input = this.inputs[i];
if (input.link == null) {
continue;
}
var v = this.getInputData(i);
if (v === undefined) {
continue;
}
if (input.name == "gain") {
this.audionode.gain.value = this.properties.gain = v;
}
}
}
};
LGAudioMediaSource.prototype.onAction = function(event) {
if (event == "Play") {
this.audionode.gain.value = this.properties.gain;
} else if (event == "Stop") {
this.audionode.gain.value = 0;
}
};
LGAudioMediaSource.prototype.onPropertyChanged = function(name, value) {
if (name == "gain") {
this.audionode.gain.value = value;
}
};
//Helps connect/disconnect AudioNodes when new connections are made in the node
LGAudioMediaSource.prototype.onConnectionsChange =
LGAudio.onConnectionsChange;
LGAudioMediaSource.prototype.onGetInputs = function() {
return [
["playbackRate", "number"],
["Play", LiteGraph.ACTION],
["Stop", LiteGraph.ACTION]
];
};
LGAudioMediaSource.title = "MediaSource";
LGAudioMediaSource.desc = "Plays microphone";
LiteGraph.registerNodeType("audio/media_source", LGAudioMediaSource);
//*****************************************************
function LGAudioAnalyser() {
this.properties = {
fftSize: 2048,
minDecibels: -100,
maxDecibels: -10,
smoothingTimeConstant: 0.5
};
var context = LGAudio.getAudioContext();
this.audionode = context.createAnalyser();
this.audionode.graphnode = this;
this.audionode.fftSize = this.properties.fftSize;
this.audionode.minDecibels = this.properties.minDecibels;
this.audionode.maxDecibels = this.properties.maxDecibels;
this.audionode.smoothingTimeConstant = this.properties.smoothingTimeConstant;
this.addInput("in", "audio");
this.addOutput("freqs", "array");
this.addOutput("samples", "array");
this._freq_bin = null;
this._time_bin = null;
}
LGAudioAnalyser.prototype.onPropertyChanged = function(name, value) {
this.audionode[name] = value;
};
LGAudioAnalyser.prototype.onExecute = function() {
if (this.isOutputConnected(0)) {
//send FFT
var bufferLength = this.audionode.frequencyBinCount;
if (!this._freq_bin || this._freq_bin.length != bufferLength) {
this._freq_bin = new Uint8Array(bufferLength);
}
this.audionode.getByteFrequencyData(this._freq_bin);
this.setOutputData(0, this._freq_bin);
}
//send analyzer
if (this.isOutputConnected(1)) {
//send Samples
var bufferLength = this.audionode.frequencyBinCount;
if (!this._time_bin || this._time_bin.length != bufferLength) {
this._time_bin = new Uint8Array(bufferLength);
}
this.audionode.getByteTimeDomainData(this._time_bin);
this.setOutputData(1, this._time_bin);
}
//properties
for (var i = 1; i < this.inputs.length; ++i) {
var input = this.inputs[i];
if (input.link == null) {
continue;
}
var v = this.getInputData(i);
if (v !== undefined) {
this.audionode[input.name].value = v;
}
}
//time domain
//this.audionode.getFloatTimeDomainData( dataArray );
};
LGAudioAnalyser.prototype.onGetInputs = function() {
return [
["minDecibels", "number"],
["maxDecibels", "number"],
["smoothingTimeConstant", "number"]
];
};
LGAudioAnalyser.prototype.onGetOutputs = function() {
return [["freqs", "array"], ["samples", "array"]];
};
LGAudioAnalyser.title = "Analyser";
LGAudioAnalyser.desc = "Audio Analyser";
LiteGraph.registerNodeType("audio/analyser", LGAudioAnalyser);
//*****************************************************
function LGAudioGain() {
//default
this.properties = {
gain: 1
};
this.audionode = LGAudio.getAudioContext().createGain();
this.addInput("in", "audio");
this.addInput("gain", "number");
this.addOutput("out", "audio");
}
LGAudioGain.prototype.onExecute = function() {
if (!this.inputs || !this.inputs.length) {
return;
}
for (var i = 1; i < this.inputs.length; ++i) {
var input = this.inputs[i];
var v = this.getInputData(i);
if (v !== undefined) {
this.audionode[input.name].value = v;
}
}
};
LGAudio.createAudioNodeWrapper(LGAudioGain);
LGAudioGain.title = "Gain";
LGAudioGain.desc = "Audio gain";
LiteGraph.registerNodeType("audio/gain", LGAudioGain);
function LGAudioConvolver() {
//default
this.properties = {
impulse_src: "",
normalize: true
};
this.audionode = LGAudio.getAudioContext().createConvolver();
this.addInput("in", "audio");
this.addOutput("out", "audio");
}
LGAudio.createAudioNodeWrapper(LGAudioConvolver);
LGAudioConvolver.prototype.onRemove = function() {
if (this._dropped_url) {
URL.revokeObjectURL(this._dropped_url);
}
};
LGAudioConvolver.prototype.onPropertyChanged = function(name, value) {
if (name == "impulse_src") {
this.loadImpulse(value);
} else if (name == "normalize") {
this.audionode.normalize = value;
}
};
LGAudioConvolver.prototype.onDropFile = function(file) {
if (this._dropped_url) {
URL.revokeObjectURL(this._dropped_url);
}
this._dropped_url = URL.createObjectURL(file);
this.properties.impulse_src = this._dropped_url;
this.loadImpulse(this._dropped_url);
};
LGAudioConvolver.prototype.loadImpulse = function(url) {
var that = this;
//kill previous load
if (this._request) {
this._request.abort();
this._request = null;
}
this._impulse_buffer = null;
this._loading_impulse = false;
if (!url) {
return;
}
//load new sample
this._request = LGAudio.loadSound(url, inner);
this._loading_impulse = true;
// Decode asynchronously
function inner(buffer) {
that._impulse_buffer = buffer;
that.audionode.buffer = buffer;
console.log("Impulse signal set");
that._loading_impulse = false;
}
};
LGAudioConvolver.title = "Convolver";
LGAudioConvolver.desc = "Convolves the signal (used for reverb)";
LiteGraph.registerNodeType("audio/convolver", LGAudioConvolver);
function LGAudioDynamicsCompressor() {
//default
this.properties = {
threshold: -50,
knee: 40,
ratio: 12,
reduction: -20,
attack: 0,
release: 0.25
};
this.audionode = LGAudio.getAudioContext().createDynamicsCompressor();
this.addInput("in", "audio");
this.addOutput("out", "audio");
}
LGAudio.createAudioNodeWrapper(LGAudioDynamicsCompressor);
LGAudioDynamicsCompressor.prototype.onExecute = function() {
if (!this.inputs || !this.inputs.length) {
return;
}
for (var i = 1; i < this.inputs.length; ++i) {
var input = this.inputs[i];
if (input.link == null) {
continue;
}
var v = this.getInputData(i);
if (v !== undefined) {
this.audionode[input.name].value = v;
}
}
};
LGAudioDynamicsCompressor.prototype.onGetInputs = function() {
return [
["threshold", "number"],
["knee", "number"],
["ratio", "number"],
["reduction", "number"],
["attack", "number"],
["release", "number"]
];
};
LGAudioDynamicsCompressor.title = "DynamicsCompressor";
LGAudioDynamicsCompressor.desc = "Dynamics Compressor";
LiteGraph.registerNodeType(
"audio/dynamicsCompressor",
LGAudioDynamicsCompressor
);
function LGAudioWaveShaper() {
//default
this.properties = {};
this.audionode = LGAudio.getAudioContext().createWaveShaper();
this.addInput("in", "audio");
this.addInput("shape", "waveshape");
this.addOutput("out", "audio");
}
LGAudioWaveShaper.prototype.onExecute = function() {
if (!this.inputs || !this.inputs.length) {
return;
}
var v = this.getInputData(1);
if (v === undefined) {
return;
}
this.audionode.curve = v;
};
LGAudioWaveShaper.prototype.setWaveShape = function(shape) {
this.audionode.curve = shape;
};
LGAudio.createAudioNodeWrapper(LGAudioWaveShaper);
/* disabled till I dont find a way to do a wave shape
LGAudioWaveShaper.title = "WaveShaper";
LGAudioWaveShaper.desc = "Distortion using wave shape";
LiteGraph.registerNodeType("audio/waveShaper", LGAudioWaveShaper);
*/
function LGAudioMixer() {
//default
this.properties = {
gain1: 0.5,
gain2: 0.5
};
this.audionode = LGAudio.getAudioContext().createGain();
this.audionode1 = LGAudio.getAudioContext().createGain();
this.audionode1.gain.value = this.properties.gain1;
this.audionode2 = LGAudio.getAudioContext().createGain();
this.audionode2.gain.value = this.properties.gain2;
this.audionode1.connect(this.audionode);
this.audionode2.connect(this.audionode);
this.addInput("in1", "audio");
this.addInput("in1 gain", "number");
this.addInput("in2", "audio");
this.addInput("in2 gain", "number");
this.addOutput("out", "audio");
}
LGAudioMixer.prototype.getAudioNodeInInputSlot = function(slot) {
if (slot == 0) {
return this.audionode1;
} else if (slot == 2) {
return this.audionode2;
}
};
LGAudioMixer.prototype.onPropertyChanged = function(name, value) {
if (name == "gain1") {
this.audionode1.gain.value = value;
} else if (name == "gain2") {
this.audionode2.gain.value = value;
}
};
LGAudioMixer.prototype.onExecute = function() {
if (!this.inputs || !this.inputs.length) {
return;
}
for (var i = 1; i < this.inputs.length; ++i) {
var input = this.inputs[i];
if (input.link == null || input.type == "audio") {
continue;
}
var v = this.getInputData(i);
if (v === undefined) {
continue;
}
if (i == 1) {
this.audionode1.gain.value = v;
} else if (i == 3) {
this.audionode2.gain.value = v;
}
}
};
LGAudio.createAudioNodeWrapper(LGAudioMixer);
LGAudioMixer.title = "Mixer";
LGAudioMixer.desc = "Audio mixer";
LiteGraph.registerNodeType("audio/mixer", LGAudioMixer);
function LGAudioADSR() {
//default
this.properties = {
A: 0.1,
D: 0.1,
S: 0.1,
R: 0.1
};
this.audionode = LGAudio.getAudioContext().createGain();
this.audionode.gain.value = 0;
this.addInput("in", "audio");
this.addInput("gate", "boolean");
this.addOutput("out", "audio");
this.gate = false;
}
LGAudioADSR.prototype.onExecute = function() {
var audioContext = LGAudio.getAudioContext();
var now = audioContext.currentTime;
var node = this.audionode;
var gain = node.gain;
var current_gate = this.getInputData(1);
var A = this.getInputOrProperty("A");
var D = this.getInputOrProperty("D");
var S = this.getInputOrProperty("S");
var R = this.getInputOrProperty("R");
if (!this.gate && current_gate) {
gain.cancelScheduledValues(0);
gain.setValueAtTime(0, now);
gain.linearRampToValueAtTime(1, now + A);
gain.linearRampToValueAtTime(S, now + A + D);
} else if (this.gate && !current_gate) {
gain.cancelScheduledValues(0);
gain.setValueAtTime(gain.value, now);
gain.linearRampToValueAtTime(0, now + R);
}
this.gate = current_gate;
};
LGAudioADSR.prototype.onGetInputs = function() {
return [
["A", "number"],
["D", "number"],
["S", "number"],
["R", "number"]
];
};
LGAudio.createAudioNodeWrapper(LGAudioADSR);
LGAudioADSR.title = "ADSR";
LGAudioADSR.desc = "Audio envelope";
LiteGraph.registerNodeType("audio/adsr", LGAudioADSR);
function LGAudioDelay() {
//default
this.properties = {
delayTime: 0.5
};
this.audionode = LGAudio.getAudioContext().createDelay(10);
this.audionode.delayTime.value = this.properties.delayTime;
this.addInput("in", "audio");
this.addInput("time", "number");
this.addOutput("out", "audio");
}
LGAudio.createAudioNodeWrapper(LGAudioDelay);
LGAudioDelay.prototype.onExecute = function() {
var v = this.getInputData(1);
if (v !== undefined) {
this.audionode.delayTime.value = v;
}
};
LGAudioDelay.title = "Delay";
LGAudioDelay.desc = "Audio delay";
LiteGraph.registerNodeType("audio/delay", LGAudioDelay);
function LGAudioBiquadFilter() {
//default
this.properties = {
frequency: 350,
detune: 0,
Q: 1
};
this.addProperty("type", "lowpass", "enum", {
values: [
"lowpass",
"highpass",
"bandpass",
"lowshelf",
"highshelf",
"peaking",
"notch",
"allpass"
]
});
//create node
this.audionode = LGAudio.getAudioContext().createBiquadFilter();
//slots
this.addInput("in", "audio");
this.addOutput("out", "audio");
}
LGAudioBiquadFilter.prototype.onExecute = function() {
if (!this.inputs || !this.inputs.length) {
return;
}
for (var i = 1; i < this.inputs.length; ++i) {
var input = this.inputs[i];
if (input.link == null) {
continue;
}
var v = this.getInputData(i);
if (v !== undefined) {
this.audionode[input.name].value = v;
}
}
};
LGAudioBiquadFilter.prototype.onGetInputs = function() {
return [["frequency", "number"], ["detune", "number"], ["Q", "number"]];
};
LGAudio.createAudioNodeWrapper(LGAudioBiquadFilter);
LGAudioBiquadFilter.title = "BiquadFilter";
LGAudioBiquadFilter.desc = "Audio filter";
LiteGraph.registerNodeType("audio/biquadfilter", LGAudioBiquadFilter);
function LGAudioOscillatorNode() {
//default
this.properties = {
frequency: 440,
detune: 0,
type: "sine"
};
this.addProperty("type", "sine", "enum", {
values: ["sine", "square", "sawtooth", "triangle", "custom"]
});
//create node
this.audionode = LGAudio.getAudioContext().createOscillator();
//slots
this.addOutput("out", "audio");
}
LGAudioOscillatorNode.prototype.onStart = function() {
if (!this.audionode.started) {
this.audionode.started = true;
try {
this.audionode.start();
} catch (err) {}
}
};
LGAudioOscillatorNode.prototype.onStop = function() {
if (this.audionode.started) {
this.audionode.started = false;
this.audionode.stop();
}
};
LGAudioOscillatorNode.prototype.onPause = function() {
this.onStop();
};
LGAudioOscillatorNode.prototype.onUnpause = function() {
this.onStart();
};
LGAudioOscillatorNode.prototype.onExecute = function() {
if (!this.inputs || !this.inputs.length) {
return;
}
for (var i = 0; i < this.inputs.length; ++i) {
var input = this.inputs[i];
if (input.link == null) {
continue;
}
var v = this.getInputData(i);
if (v !== undefined) {
this.audionode[input.name].value = v;
}
}
};
LGAudioOscillatorNode.prototype.onGetInputs = function() {
return [
["frequency", "number"],
["detune", "number"],
["type", "string"]
];
};
LGAudio.createAudioNodeWrapper(LGAudioOscillatorNode);
LGAudioOscillatorNode.title = "Oscillator";
LGAudioOscillatorNode.desc = "Oscillator";
LiteGraph.registerNodeType("audio/oscillator", LGAudioOscillatorNode);
//*****************************************************
//EXTRA
function LGAudioVisualization() {
this.properties = {
continuous: true,
mark: -1
};
this.addInput("data", "array");
this.addInput("mark", "number");
this.size = [300, 200];
this._last_buffer = null;
}
LGAudioVisualization.prototype.onExecute = function() {
this._last_buffer = this.getInputData(0);
var v = this.getInputData(1);
if (v !== undefined) {
this.properties.mark = v;
}
this.setDirtyCanvas(true, false);
};
LGAudioVisualization.prototype.onDrawForeground = function(ctx) {
if (!this._last_buffer) {
return;
}
var buffer = this._last_buffer;
//delta represents how many samples we advance per pixel
var delta = buffer.length / this.size[0];
var h = this.size[1];
ctx.fillStyle = "black";
ctx.fillRect(0, 0, this.size[0], this.size[1]);
ctx.strokeStyle = "white";
ctx.beginPath();
var x = 0;
if (this.properties.continuous) {
ctx.moveTo(x, h);
for (var i = 0; i < buffer.length; i += delta) {
ctx.lineTo(x, h - (buffer[i | 0] / 255) * h);
x++;
}
} else {
for (var i = 0; i < buffer.length; i += delta) {
ctx.moveTo(x + 0.5, h);
ctx.lineTo(x + 0.5, h - (buffer[i | 0] / 255) * h);
x++;
}
}
ctx.stroke();
if (this.properties.mark >= 0) {
var samplerate = LGAudio.getAudioContext().sampleRate;
var binfreq = samplerate / buffer.length;
var x = (2 * (this.properties.mark / binfreq)) / delta;
if (x >= this.size[0]) {
x = this.size[0] - 1;
}
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.moveTo(x, h);
ctx.lineTo(x, 0);
ctx.stroke();
}
};
LGAudioVisualization.title = "Visualization";
LGAudioVisualization.desc = "Audio Visualization";
LiteGraph.registerNodeType("audio/visualization", LGAudioVisualization);
function LGAudioBandSignal() {
//default
this.properties = {
band: 440,
amplitude: 1
};
this.addInput("freqs", "array");
this.addOutput("signal", "number");
}
LGAudioBandSignal.prototype.onExecute = function() {
this._freqs = this.getInputData(0);
if (!this._freqs) {
return;
}
var band = this.properties.band;
var v = this.getInputData(1);
if (v !== undefined) {
band = v;
}
var samplerate = LGAudio.getAudioContext().sampleRate;
var binfreq = samplerate / this._freqs.length;
var index = 2 * (band / binfreq);
var v = 0;
if (index < 0) {
v = this._freqs[0];
}
if (index >= this._freqs.length) {
v = this._freqs[this._freqs.length - 1];
} else {
var pos = index | 0;
var v0 = this._freqs[pos];
var v1 = this._freqs[pos + 1];
var f = index - pos;
v = v0 * (1 - f) + v1 * f;
}
this.setOutputData(0, (v / 255) * this.properties.amplitude);
};
LGAudioBandSignal.prototype.onGetInputs = function() {
return [["band", "number"]];
};
LGAudioBandSignal.title = "Signal";
LGAudioBandSignal.desc = "extract the signal of some frequency";
LiteGraph.registerNodeType("audio/signal", LGAudioBandSignal);
function LGAudioScript() {
if (!LGAudioScript.default_code) {
var code = LGAudioScript.default_function.toString();
var index = code.indexOf("{") + 1;
var index2 = code.lastIndexOf("}");
LGAudioScript.default_code = code.substr(index, index2 - index);
}
//default
this.properties = {
code: LGAudioScript.default_code
};
//create node
var ctx = LGAudio.getAudioContext();
if (ctx.createScriptProcessor) {
this.audionode = ctx.createScriptProcessor(4096, 1, 1);
}
//buffer size, input channels, output channels
else {
console.warn("ScriptProcessorNode deprecated");
this.audionode = ctx.createGain(); //bypass audio
}
this.processCode();
if (!LGAudioScript._bypass_function) {
LGAudioScript._bypass_function = this.audionode.onaudioprocess;
}
//slots
this.addInput("in", "audio");
this.addOutput("out", "audio");
}
LGAudioScript.prototype.onAdded = function(graph) {
if (graph.status == LGraph.STATUS_RUNNING) {
this.audionode.onaudioprocess = this._callback;
}
};
LGAudioScript["@code"] = { widget: "code", type: "code" };
LGAudioScript.prototype.onStart = function() {
this.audionode.onaudioprocess = this._callback;
};
LGAudioScript.prototype.onStop = function() {
this.audionode.onaudioprocess = LGAudioScript._bypass_function;
};
LGAudioScript.prototype.onPause = function() {
this.audionode.onaudioprocess = LGAudioScript._bypass_function;
};
LGAudioScript.prototype.onUnpause = function() {
this.audionode.onaudioprocess = this._callback;
};
LGAudioScript.prototype.onExecute = function() {
//nothing! because we need an onExecute to receive onStart... fix that
};
LGAudioScript.prototype.onRemoved = function() {
this.audionode.onaudioprocess = LGAudioScript._bypass_function;
};
LGAudioScript.prototype.processCode = function() {
try {
var func = new Function("properties", this.properties.code);
this._script = new func(this.properties);
this._old_code = this.properties.code;
this._callback = this._script.onaudioprocess;
} catch (err) {
console.error("Error in onaudioprocess code", err);
this._callback = LGAudioScript._bypass_function;
this.audionode.onaudioprocess = this._callback;
}
};
LGAudioScript.prototype.onPropertyChanged = function(name, value) {
if (name == "code") {
this.properties.code = value;
this.processCode();
if (this.graph && this.graph.status == LGraph.STATUS_RUNNING) {
this.audionode.onaudioprocess = this._callback;
}
}
};
LGAudioScript.default_function = function() {
this.onaudioprocess = function(audioProcessingEvent) {
// The input buffer is the song we loaded earlier
var inputBuffer = audioProcessingEvent.inputBuffer;
// The output buffer contains the samples that will be modified and played
var outputBuffer = audioProcessingEvent.outputBuffer;
// Loop through the output channels (in this case there is only one)
for (
var channel = 0;
channel < outputBuffer.numberOfChannels;
channel++
) {
var inputData = inputBuffer.getChannelData(channel);
var outputData = outputBuffer.getChannelData(channel);
// Loop through the 4096 samples
for (var sample = 0; sample < inputBuffer.length; sample++) {
// make output equal to the same as the input
outputData[sample] = inputData[sample];
}
}
};
};
LGAudio.createAudioNodeWrapper(LGAudioScript);
LGAudioScript.title = "Script";
LGAudioScript.desc = "apply script to signal";
LiteGraph.registerNodeType("audio/script", LGAudioScript);
function LGAudioDestination() {
this.audionode = LGAudio.getAudioContext().destination;
this.addInput("in", "audio");
}
LGAudioDestination.title = "Destination";
LGAudioDestination.desc = "Audio output";
LiteGraph.registerNodeType("audio/destination", LGAudioDestination);
})(this);
//event related nodes
(function(global) {
var LiteGraph = global.LiteGraph;
function LGWebSocket() {
this.size = [60, 20];
this.addInput("send", LiteGraph.ACTION);
this.addOutput("received", LiteGraph.EVENT);
this.addInput("in", 0);
this.addOutput("out", 0);
this.properties = {
url: "",
room: "lgraph", //allows to filter messages,
only_send_changes: true
};
this._ws = null;
this._last_sent_data = [];
this._last_received_data = [];
}
LGWebSocket.title = "WebSocket";
LGWebSocket.desc = "Send data through a websocket";
LGWebSocket.prototype.onPropertyChanged = function(name, value) {
if (name == "url") {
this.connectSocket();
}
};
LGWebSocket.prototype.onExecute = function() {
if (!this._ws && this.properties.url) {
this.connectSocket();
}
if (!this._ws || this._ws.readyState != WebSocket.OPEN) {
return;
}
var room = this.properties.room;
var only_changes = this.properties.only_send_changes;
for (var i = 1; i < this.inputs.length; ++i) {
var data = this.getInputData(i);
if (data == null) {
continue;
}
var json;
try {
json = JSON.stringify({
type: 0,
room: room,
channel: i,
data: data
});
} catch (err) {
continue;
}
if (only_changes && this._last_sent_data[i] == json) {
continue;
}
this._last_sent_data[i] = json;
this._ws.send(json);
}
for (var i = 1; i < this.outputs.length; ++i) {
this.setOutputData(i, this._last_received_data[i]);
}
if (this.boxcolor == "#AFA") {
this.boxcolor = "#6C6";
}
};
LGWebSocket.prototype.connectSocket = function() {
var that = this;
var url = this.properties.url;
if (url.substr(0, 2) != "ws") {
url = "ws://" + url;
}
this._ws = new WebSocket(url);
this._ws.onopen = function() {
console.log("ready");
that.boxcolor = "#6C6";
};
this._ws.onmessage = function(e) {
that.boxcolor = "#AFA";
var data = JSON.parse(e.data);
if (data.room && data.room != that.properties.room) {
return;
}
if (data.type == 1) {
if (
data.data.object_class &&
LiteGraph[data.data.object_class]
) {
var obj = null;
try {
obj = new LiteGraph[data.data.object_class](data.data);
that.triggerSlot(0, obj);
} catch (err) {
return;
}
} else {
that.triggerSlot(0, data.data);
}
} else {
that._last_received_data[data.channel || 0] = data.data;
}
};
this._ws.onerror = function(e) {
console.log("couldnt connect to websocket");
that.boxcolor = "#E88";
};
this._ws.onclose = function(e) {
console.log("connection closed");
that.boxcolor = "#000";
};
};
LGWebSocket.prototype.send = function(data) {
if (!this._ws || this._ws.readyState != WebSocket.OPEN) {
return;
}
this._ws.send(JSON.stringify({ type: 1, msg: data }));
};
LGWebSocket.prototype.onAction = function(action, param) {
if (!this._ws || this._ws.readyState != WebSocket.OPEN) {
return;
}
this._ws.send({
type: 1,
room: this.properties.room,
action: action,
data: param
});
};
LGWebSocket.prototype.onGetInputs = function() {
return [["in", 0]];
};
LGWebSocket.prototype.onGetOutputs = function() {
return [["out", 0]];
};
LiteGraph.registerNodeType("network/websocket", LGWebSocket);
//It is like a websocket but using the SillyServer.js server that bounces packets back to all clients connected:
//For more information: https://github.com/jagenjo/SillyServer.js
function LGSillyClient() {
//this.size = [60,20];
this.room_widget = this.addWidget(
"text",
"Room",
"lgraph",
this.setRoom.bind(this)
);
this.addWidget(
"button",
"Reconnect",
null,
this.connectSocket.bind(this)
);
this.addInput("send", LiteGraph.ACTION);
this.addOutput("received", LiteGraph.EVENT);
this.addInput("in", 0);
this.addOutput("out", 0);
this.properties = {
url: "tamats.com:55000",
room: "lgraph",
only_send_changes: true
};
this._server = null;
this.connectSocket();
this._last_sent_data = [];
this._last_received_data = [];
if(typeof(SillyClient) == "undefined")
console.warn("remember to add SillyClient.js to your project: https://tamats.com/projects/sillyserver/src/sillyclient.js");
}
LGSillyClient.title = "SillyClient";
LGSillyClient.desc = "Connects to SillyServer to broadcast messages";
LGSillyClient.prototype.onPropertyChanged = function(name, value) {
if (name == "room") {
this.room_widget.value = value;
}
this.connectSocket();
};
LGSillyClient.prototype.setRoom = function(room_name) {
this.properties.room = room_name;
this.room_widget.value = room_name;
this.connectSocket();
};
//force label names
LGSillyClient.prototype.onDrawForeground = function() {
for (var i = 1; i < this.inputs.length; ++i) {
var slot = this.inputs[i];
slot.label = "in_" + i;
}
for (var i = 1; i < this.outputs.length; ++i) {
var slot = this.outputs[i];
slot.label = "out_" + i;
}
};
LGSillyClient.prototype.onExecute = function() {
if (!this._server || !this._server.is_connected) {
return;
}
var only_send_changes = this.properties.only_send_changes;
for (var i = 1; i < this.inputs.length; ++i) {
var data = this.getInputData(i);
var prev_data = this._last_sent_data[i];
if (data != null) {
if (only_send_changes)
{
var is_equal = true;
if( data && data.length && prev_data && prev_data.length == data.length && data.constructor !== String)
{
for(var j = 0; j < data.length; ++j)
if( prev_data[j] != data[j] )
{
is_equal = false;
break;
}
}
else if(this._last_sent_data[i] != data)
is_equal = false;
if(is_equal)
continue;
}
this._server.sendMessage({ type: 0, channel: i, data: data });
if( data.length && data.constructor !== String )
{
if( this._last_sent_data[i] )
{
this._last_sent_data[i].length = data.length;
for(var j = 0; j < data.length; ++j)
this._last_sent_data[i][j] = data[j];
}
else //create
{
if(data.constructor === Array)
this._last_sent_data[i] = data.concat();
else
this._last_sent_data[i] = new data.constructor( data );
}
}
else
this._last_sent_data[i] = data; //should be cloned
}
}
for (var i = 1; i < this.outputs.length; ++i) {
this.setOutputData(i, this._last_received_data[i]);
}
if (this.boxcolor == "#AFA") {
this.boxcolor = "#6C6";
}
};
LGSillyClient.prototype.connectSocket = function() {
var that = this;
if (typeof SillyClient == "undefined") {
if (!this._error) {
console.error(
"SillyClient node cannot be used, you must include SillyServer.js"
);
}
this._error = true;
return;
}
this._server = new SillyClient();
this._server.on_ready = function() {
console.log("ready");
that.boxcolor = "#6C6";
};
this._server.on_message = function(id, msg) {
var data = null;
try {
data = JSON.parse(msg);
} catch (err) {
return;
}
if (data.type == 1) {
//EVENT slot
if (
data.data.object_class &&
LiteGraph[data.data.object_class]
) {
var obj = null;
try {
obj = new LiteGraph[data.data.object_class](data.data);
that.triggerSlot(0, obj);
} catch (err) {
return;
}
} else {
that.triggerSlot(0, data.data);
}
} //for FLOW slots
else {
that._last_received_data[data.channel || 0] = data.data;
}
that.boxcolor = "#AFA";
};
this._server.on_error = function(e) {
console.log("couldnt connect to websocket");
that.boxcolor = "#E88";
};
this._server.on_close = function(e) {
console.log("connection closed");
that.boxcolor = "#000";
};
if (this.properties.url && this.properties.room) {
try {
this._server.connect(this.properties.url, this.properties.room);
} catch (err) {
console.error("SillyServer error: " + err);
this._server = null;
return;
}
this._final_url = this.properties.url + "/" + this.properties.room;
}
};
LGSillyClient.prototype.send = function(data) {
if (!this._server || !this._server.is_connected) {
return;
}
this._server.sendMessage({ type: 1, data: data });
};
LGSillyClient.prototype.onAction = function(action, param) {
if (!this._server || !this._server.is_connected) {
return;
}
this._server.sendMessage({ type: 1, action: action, data: param });
};
LGSillyClient.prototype.onGetInputs = function() {
return [["in", 0]];
};
LGSillyClient.prototype.onGetOutputs = function() {
return [["out", 0]];
};
LiteGraph.registerNodeType("network/sillyclient", LGSillyClient);
})(this);