Repository: jagenjo/litegraph.js Branch: master Commit: 0555a2f2a3df Files: 106 Total size: 4.5 MB Directory structure: gitextract_a35_aqba/ ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode/ │ └── extensions.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build/ │ ├── litegraph.core.js │ ├── litegraph.js │ └── litegraph_mini.js ├── csharp/ │ ├── LiteGraph.cs │ ├── LiteGraphNodes.cs │ ├── LiteGraphTest.cs │ ├── SimpleJSON.cs │ ├── graph.JSON │ └── readme.md ├── css/ │ ├── litegraph-editor.css │ └── litegraph.css ├── doc/ │ ├── api.js │ ├── assets/ │ │ ├── css/ │ │ │ └── main.css │ │ ├── index.html │ │ ├── js/ │ │ │ ├── api-filter.js │ │ │ ├── api-list.js │ │ │ ├── api-search.js │ │ │ ├── apidocs.js │ │ │ └── yui-prettify.js │ │ └── vendor/ │ │ └── prettify/ │ │ ├── CHANGES.html │ │ ├── COPYING │ │ ├── README.html │ │ ├── prettify-min.css │ │ └── prettify-min.js │ ├── classes/ │ │ ├── ContextMenu.html │ │ ├── LGraph.html │ │ ├── LGraphCanvas.html │ │ ├── LGraphNode.html │ │ ├── LiteGraph.html │ │ └── index.html │ ├── data.json │ ├── elements/ │ │ └── index.html │ ├── files/ │ │ ├── .._src_litegraph.js.html │ │ └── index.html │ ├── index.html │ └── modules/ │ └── index.html ├── editor/ │ ├── demodata/ │ │ └── video.webm │ ├── editor_mobile.html │ ├── examples/ │ │ ├── audio.json │ │ ├── audio_delay.json │ │ ├── audio_reverb.json │ │ ├── benchmark.json │ │ ├── copypaste.json │ │ ├── features.json │ │ ├── midi_generation.json │ │ └── subgraph.json │ ├── index.html │ ├── js/ │ │ ├── code.js │ │ ├── defaults.js │ │ ├── defaults_mobile.js │ │ ├── demos.js │ │ └── libs/ │ │ ├── audiosynth.js │ │ ├── gl-matrix-min.js │ │ ├── litegl.js │ │ └── midi-parser.js │ └── style.css ├── external/ │ ├── Basica.otf │ ├── Criticized.otf │ ├── DS-Digital.otf │ └── beat.otf ├── gruntfile.js ├── guides/ │ └── README.md ├── index.html ├── jest.config.js ├── package.json ├── src/ │ ├── litegraph-editor.js │ ├── litegraph.d.ts │ ├── litegraph.js │ ├── litegraph.test.js │ └── nodes/ │ ├── audio.js │ ├── base.js │ ├── events.js │ ├── geometry.js │ ├── glfx.js │ ├── glshaders.js │ ├── gltextures.js │ ├── graphics.js │ ├── input.js │ ├── interface.js │ ├── logic.js │ ├── math.js │ ├── math3d.js │ ├── midi.js │ ├── network.js │ ├── others.js │ └── strings.js ├── style.css └── utils/ ├── build.sh ├── builder.py ├── deploy_files.txt ├── deploy_files_core.txt ├── deploy_files_mini.txt ├── generate_doc.sh ├── pack.sh ├── server.js ├── temp.js └── test.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ /build ================================================ FILE: .eslintrc.js ================================================ module.exports = { "env": { "browser": true, "es2021": true, "node": true, "jest/globals": true }, "extends": "eslint:recommended", "overrides": [ ], "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "plugins": ["jest"], "globals": { "gl": true, "GL": true, "LS": true, "Uint8Array": true, "Uint32Array": true, "Float32Array": true, "LGraphCanvas": true, "LGraph": true, "LGraphNode": true, "LiteGraph": true, "LGraphTexture": true, "Mesh": true, "Shader": true, "enableWebGLCanvas": true, "vec2": true, "vec3": true, "vec4": true, "DEG2RAD": true, "isPowerOfTwo": true, "cloneCanvas": true, "createCanvas": true, "hex2num": true, "colorToString": true, "showElement": true, "quat": true, "AudioSynth": true, "SillyClient": true }, "rules": { "no-console": "off", "no-empty": "warn", "no-redeclare": "warn", "no-inner-declarations": "warn", "no-constant-condition": "warn", "no-unused-vars": "warn", "no-mixed-spaces-and-tabs": "warn", "no-unreachable": "warn", "curly": ["warn", "all"] } } ================================================ FILE: .gitignore ================================================ node_modules/ node_modules/* npm-debug.log temp/ temp/* coverage/ # Editors /.vscode/* !/.vscode/extensions.json *.bak .project ================================================ FILE: .npmrc ================================================ package-lock=false ================================================ FILE: .prettierrc ================================================ { "singleQuote": false, "semi": true, "tabWidth": 4 } ================================================ FILE: .vscode/extensions.json ================================================ { // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp // List of extensions which should be recommended for users of this workspace. "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] } ================================================ FILE: CONTRIBUTING.md ================================================ # Contribution Rules There are some simple rules that everyone should follow: ### Do not commit files from build folder > I usually have horrible merge conflicts when I upload the build version that take me too much time to solve, but I want to keep the build version in the repo, so I guess it would be better if only one of us does the built, which would be me. > https://github.com/jagenjo/litegraph.js/pull/155#issuecomment-656602861 Those files will be updated by owner. ================================================ FILE: LICENSE ================================================ Copyright (C) 2013 by Javi Agenjo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # litegraph.js A library in Javascript to create graphs in the browser similar to Unreal Blueprints. Nodes can be programmed easily and it includes an editor to construct and tests the graphs. It can be integrated easily in any existing web applications and graphs can be run without the need of the editor. Try it in the [demo site](https://tamats.com/projects/litegraph/editor). ![Node Graph](imgs/node_graph_example.png "WebGLStudio") ## Features - Renders on Canvas2D (zoom in/out and panning, easy to render complex interfaces, can be used inside a WebGLTexture) - Easy to use editor (searchbox, keyboard shortcuts, multiple selection, context menu, ...) - Optimized to support hundreds of nodes per graph (on editor but also on execution) - Customizable theme (colors, shapes, background) - Callbacks to personalize every action/drawing/event of nodes - Subgraphs (nodes that contain graphs themselves) - Live mode system (hides the graph but calls nodes to render whatever they want, useful to create UIs) - Graphs can be executed in NodeJS - Highly customizable nodes (color, shape, slots vertical or horizontal, widgets, custom rendering) - Easy to integrate in any JS application (one single file, no dependencies) - Typescript support ## Nodes provided Although it is easy to create new node types, LiteGraph comes with some default nodes that could be useful for many cases: - Interface (Widgets) - Math (trigonometry, math operations) - Audio (AudioAPI and MIDI) - 3D Graphics (Postprocessing in WebGL) - Input (read Gamepad) ## Installation You can install it using npm ``` npm install litegraph.js ``` Or downloading the ```build/litegraph.js``` and ```css/litegraph.css``` version from this repository. ## First project ## ```html ``` ## How to code a new Node type Here is an example of how to build a node that sums two inputs: ```javascript //node constructor class function MyAddNode() { this.addInput("A","number"); this.addInput("B","number"); this.addOutput("A+B","number"); this.properties = { precision: 1 }; } //name to show MyAddNode.title = "Sum"; //function to call when the node is executed MyAddNode.prototype.onExecute = function() { var A = this.getInputData(0); if( A === undefined ) A = 0; var B = this.getInputData(1); if( B === undefined ) B = 0; this.setOutputData( 0, A + B ); } //register in the system LiteGraph.registerNodeType("basic/sum", MyAddNode ); ``` or you can wrap an existing function: ```js function sum(a,b) { return a+b; } LiteGraph.wrapFunctionAsNode("math/sum",sum, ["Number","Number"],"Number"); ``` ## Server side It also works server-side using NodeJS although some nodes do not work in server (audio, graphics, input, etc). ```js var LiteGraph = require("./litegraph.js").LiteGraph; var graph = new LiteGraph.LGraph(); var node_time = LiteGraph.createNode("basic/time"); graph.add(node_time); var node_console = LiteGraph.createNode("basic/console"); node_console.mode = LiteGraph.ALWAYS; graph.add(node_console); node_time.connect( 0, node_console, 1 ); graph.start() ``` ## Projects using it ### [comfyUI](https://github.com/comfyanonymous/ComfyUI) ![screenshot](https://github.com/comfyanonymous/ComfyUI/blob/6efe561c2a7321501b1b27f47039c7616dda1860/comfyui_screenshot.png) ### [webglstudio.org](http://webglstudio.org) ![WebGLStudio](imgs/webglstudio.gif "WebGLStudio") ### [MOI Elephant](http://moiscript.weebly.com/elephant-systegraveme-nodal.html) ![MOI Elephant](imgs/elephant.gif "MOI Elephant") ### Mynodes ![MyNodes](imgs/mynodes.png "MyNodes") ## Utils ----- It includes several commands in the utils folder to generate doc, check errors and build minifyed version. ## Demo ----- The demo includes some examples of graphs. In order to try them you can visit [demo site](http://tamats.com/projects/litegraph/editor) or install it on your local computer, to do so you need `git`, `node` and `npm`. Given those dependencies are installed, run the following commands to try it out: ```sh $ git clone https://github.com/jagenjo/litegraph.js.git $ cd litegraph.js $ npm install $ node utils/server.js Example app listening on port 80! ``` Open your browser and point it to http://localhost:8000/. You can select a demo from the dropdown at the top of the page. ## Feedback -------- You can write any feedback to javi.agenjo@gmail.com ## Contributors - atlasan - kriffe - rappestad - InventivetalentDev - NateScarlet - coderofsalvation - ilyabesk - gausszhou ================================================ FILE: build/litegraph.core.js ================================================ //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 use_deferred_actions: true, //executes actions during the graph execution flow 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 an object with some properties * like onCreate, inputs:Array, outputs:Array, properties, onExecute * @method buildNodeClassFromObject * @param {String} name node name with namespace (p.e.: 'math/sum') * @param {Object} object methods expected onCreate, inputs, outputs, properties, onExecute */ buildNodeClassFromObject: function( name, object ) { var ctor_code = ""; if(object.inputs) for(var i=0; i < object.inputs.length; ++i) { var _name = object.inputs[i][0]; var _type = object.inputs[i][1]; if(_type && _type.constructor === String) _type = '"'+_type+'"'; ctor_code += "this.addInput('"+_name+"',"+_type+");\n"; } if(object.outputs) for(var i=0; i < object.outputs.length; ++i) { var _name = object.outputs[i][0]; var _type = object.outputs[i][1]; if(_type && _type.constructor === String) _type = '"'+_type+'"'; ctor_code += "this.addOutput('"+_name+"',"+_type+");\n"; } if(object.properties) for(var i in object.properties) { var prop = object.properties[i]; if(prop && prop.constructor === String) prop = '"'+prop+'"'; ctor_code += "this.addProperty('"+i+"',"+prop+");\n"; } ctor_code += "if(this.onCreate)this.onCreate()"; var classobj = Function(ctor_code); for(var i in object) if(i!="inputs" && i!="outputs" && i!="properties") classobj.prototype[i] = object[i]; classobj.title = object.title || name.split("/").pop(); classobj.desc = object.desc || "Generated from object"; this.registerNodeType(name, classobj); return classobj; }, /** * 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 = ""; if(param_types !== null) //null means no inputs { var names = LiteGraph.getParameterNames(func); for (var i = 0; i < names.length; ++i) { var type = 0; if(param_types) { //type = param_types[i] != null ? "'" + param_types[i] + "'" : "0"; if( param_types[i] != null && param_types[i].constructor === String ) type = "'" + param_types[i] + "'" ; else if( param_types[i] != null ) type = param_types[i]; } code += "this.addInput('" + names[i] + "'," + type + ");\n"; } } if(return_type !== null) //null means no output code += "this.addOutput('out'," + (return_type != null ? (return_type.constructor === String ? "'" + 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); return 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); //not optimal: executes possible pending actions in node, problem is it is not optimized //it is done here as if it was done in the later loop it wont be called in the node missed the onExecute //from now on it will iterate only on executable nodes which is faster 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(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length) node.executePendingActions(); 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 { //catch errors try { //iterations for (var i = 0; i < num; i++) { for (var j = 0; j < limit; ++j) { var node = nodes[j]; if(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length) node.executePendingActions(); 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 ] != undefined)) 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 execution of actions that were deferred when the action was triggered * @method executePendingActions */ LGraphNode.prototype.executePendingActions = function() { if(!this._waiting_actions || !this._waiting_actions.length) return; for(var i = 0; i < this._waiting_actions.length;++i) { var p = this._waiting_actions[i]; this.onAction(p[0],p[1],p[2],p[3],p[4]); } this._waiting_actions.length = 0; } /** * Triggers the node code execution, place a boolean/counter to mark the node as being executed * @method doExecute * @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; } } else { } 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, action_slot ) { 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, action_slot); 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]; //instead of executing them now, it will be executed in the next graph loop, to ensure data flow if(LiteGraph.use_deferred_actions && node.onExecute) { if(!node._waiting_actions) node._waiting_actions = []; node._waiting_actions.push([target_connection.name, param, options, link_info.target_slot]); } else { // wrap node.onAction(target_connection.name, param); node.actionDo( target_connection.name, param, options, link_info.target_slot ); } } } }; /** * 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 * @method getBounding * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] */ LGraphNode.prototype.getBounding = function(out, compute_outer) { out = out || new Float32Array(4); const nodePos = this.pos; const isCollapsed = this.flags.collapsed; const nodeSize = this.size; let left_offset = 0; // 1 offset due to how nodes are rendered let right_offset = 1 ; let top_offset = 0; let bottom_offset = 0; if (compute_outer) { // 4 offset for collapsed node connection points left_offset = 4; // 6 offset for right shadow and collapsed node connection points right_offset = 6 + left_offset; // 4 offset for collapsed nodes top connection points top_offset = 4; // 5 offset for bottom shadow and collapsed node connection points bottom_offset = 5 + top_offset; } out[0] = nodePos[0] - left_offset; out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset; out[2] = isCollapsed ? (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset : nodeSize[0] + right_offset; out[3] = isCollapsed ? LiteGraph.NODE_TITLE_HEIGHT + bottom_offset : nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset; 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= 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 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.setAttribute("tabindex",1); //otherwise key events are ignored 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]; if(node.clonable === false) continue; 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, true))) { 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 deltaX = event.deltaX || event.deltax || 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(deltaX) w.value += 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: "" + (info.label ? info.label : i) + "" + "" + value + "", 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 = ""; 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 = " "; else dialog.innerHTML = " "; 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 = "Search "; if (options.do_type_filter){ dialog.innerHTML += ""; dialog.innerHTML += ""; } dialog.innerHTML += "
"; 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) { if(that.search_box) 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) { refreshHelper(); 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 (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 = ""; } else if ( (type == "enum" || type == "combo") && info.values) { input_html = ""; } else if (type == "boolean" || type == "toggle") { input_html = ""; } else { console.warn("unknown type: " + type); return; } var dialog = this.createDialog( "" + (info.label ? info.label : property) + "" + input_html + "", 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 = "
"; 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 = "✕"; 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 = ""; 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(""+node.type+""+(node.constructor.desc || "")+""); panel.addHTML("

Properties

"); 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 = "
"; 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 = ""; 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 = " "; 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 = " + NameType"; 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 = " "; 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 = " + NameType"; 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: "No color" }); for (var i in LGraphCanvas.node_colors) { var color = LGraphCanvas.node_colors[i]; var value = { value: i, content: "" + i + "" }; 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( "Name", 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 = "
" //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; } ================================================ FILE: build/litegraph.js ================================================ //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 use_deferred_actions: true, //executes actions during the graph execution flow 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 an object with some properties * like onCreate, inputs:Array, outputs:Array, properties, onExecute * @method buildNodeClassFromObject * @param {String} name node name with namespace (p.e.: 'math/sum') * @param {Object} object methods expected onCreate, inputs, outputs, properties, onExecute */ buildNodeClassFromObject: function( name, object ) { var ctor_code = ""; if(object.inputs) for(var i=0; i < object.inputs.length; ++i) { var _name = object.inputs[i][0]; var _type = object.inputs[i][1]; if(_type && _type.constructor === String) _type = '"'+_type+'"'; ctor_code += "this.addInput('"+_name+"',"+_type+");\n"; } if(object.outputs) for(var i=0; i < object.outputs.length; ++i) { var _name = object.outputs[i][0]; var _type = object.outputs[i][1]; if(_type && _type.constructor === String) _type = '"'+_type+'"'; ctor_code += "this.addOutput('"+_name+"',"+_type+");\n"; } if(object.properties) for(var i in object.properties) { var prop = object.properties[i]; if(prop && prop.constructor === String) prop = '"'+prop+'"'; ctor_code += "this.addProperty('"+i+"',"+prop+");\n"; } ctor_code += "if(this.onCreate)this.onCreate()"; var classobj = Function(ctor_code); for(var i in object) if(i!="inputs" && i!="outputs" && i!="properties") classobj.prototype[i] = object[i]; classobj.title = object.title || name.split("/").pop(); classobj.desc = object.desc || "Generated from object"; this.registerNodeType(name, classobj); return classobj; }, /** * 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 = ""; if(param_types !== null) //null means no inputs { var names = LiteGraph.getParameterNames(func); for (var i = 0; i < names.length; ++i) { var type = 0; if(param_types) { //type = param_types[i] != null ? "'" + param_types[i] + "'" : "0"; if( param_types[i] != null && param_types[i].constructor === String ) type = "'" + param_types[i] + "'" ; else if( param_types[i] != null ) type = param_types[i]; } code += "this.addInput('" + names[i] + "'," + type + ");\n"; } } if(return_type !== null) //null means no output code += "this.addOutput('out'," + (return_type != null ? (return_type.constructor === String ? "'" + 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); return 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); //not optimal: executes possible pending actions in node, problem is it is not optimized //it is done here as if it was done in the later loop it wont be called in the node missed the onExecute //from now on it will iterate only on executable nodes which is faster 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(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length) node.executePendingActions(); 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 { //catch errors try { //iterations for (var i = 0; i < num; i++) { for (var j = 0; j < limit; ++j) { var node = nodes[j]; if(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length) node.executePendingActions(); 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 ] != undefined)) 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 execution of actions that were deferred when the action was triggered * @method executePendingActions */ LGraphNode.prototype.executePendingActions = function() { if(!this._waiting_actions || !this._waiting_actions.length) return; for(var i = 0; i < this._waiting_actions.length;++i) { var p = this._waiting_actions[i]; this.onAction(p[0],p[1],p[2],p[3],p[4]); } this._waiting_actions.length = 0; } /** * Triggers the node code execution, place a boolean/counter to mark the node as being executed * @method doExecute * @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; } } else { } 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, action_slot ) { 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, action_slot); 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]; //instead of executing them now, it will be executed in the next graph loop, to ensure data flow if(LiteGraph.use_deferred_actions && node.onExecute) { if(!node._waiting_actions) node._waiting_actions = []; node._waiting_actions.push([target_connection.name, param, options, link_info.target_slot]); } else { // wrap node.onAction(target_connection.name, param); node.actionDo( target_connection.name, param, options, link_info.target_slot ); } } } }; /** * 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 * @method getBounding * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] */ LGraphNode.prototype.getBounding = function(out, compute_outer) { out = out || new Float32Array(4); const nodePos = this.pos; const isCollapsed = this.flags.collapsed; const nodeSize = this.size; let left_offset = 0; // 1 offset due to how nodes are rendered let right_offset = 1 ; let top_offset = 0; let bottom_offset = 0; if (compute_outer) { // 4 offset for collapsed node connection points left_offset = 4; // 6 offset for right shadow and collapsed node connection points right_offset = 6 + left_offset; // 4 offset for collapsed nodes top connection points top_offset = 4; // 5 offset for bottom shadow and collapsed node connection points bottom_offset = 5 + top_offset; } out[0] = nodePos[0] - left_offset; out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset; out[2] = isCollapsed ? (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset : nodeSize[0] + right_offset; out[3] = isCollapsed ? LiteGraph.NODE_TITLE_HEIGHT + bottom_offset : nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset; 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= 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 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.setAttribute("tabindex",1); //otherwise key events are ignored 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]; if(node.clonable === false) continue; 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, true))) { 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 deltaX = event.deltaX || event.deltax || 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(deltaX) w.value += 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: "" + (info.label ? info.label : i) + "" + "" + value + "", 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 = ""; 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 = " "; else dialog.innerHTML = " "; 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 = "Search "; if (options.do_type_filter){ dialog.innerHTML += ""; dialog.innerHTML += ""; } dialog.innerHTML += "
"; 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) { if(that.search_box) 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) { refreshHelper(); 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 (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 = ""; } else if ( (type == "enum" || type == "combo") && info.values) { input_html = ""; } else if (type == "boolean" || type == "toggle") { input_html = ""; } else { console.warn("unknown type: " + type); return; } var dialog = this.createDialog( "" + (info.label ? info.label : property) + "" + input_html + "", 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 = "
"; 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 = "✕"; 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 = ""; 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(""+node.type+""+(node.constructor.desc || "")+""); panel.addHTML("

Properties

"); 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 = "
"; 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 = ""; 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 = " "; 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 = " + NameType"; 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 = " "; 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 = " + NameType"; 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: "No color" }); for (var i in LGraphCanvas.node_colors) { var color = LGraphCanvas.node_colors[i]; var value = { value: i, content: "" + i + "" }; 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( "Name", 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 = "
" //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 JSONParse() { this.addInput("parse", LiteGraph.ACTION); this.addInput("json", "string"); this.addOutput("done", LiteGraph.EVENT); this.addOutput("object", "object"); this.widget = this.addWidget("button","parse","",this.parse.bind(this)); this._str = null; this._obj = null; } JSONParse.title = "JSON Parse"; JSONParse.desc = "Parses JSON String into object"; JSONParse.prototype.parse = function() { if(!this._str) return; try { this._str = this.getInputData(1); this._obj = JSON.parse(this._str); this.boxcolor = "#AEA"; this.triggerSlot(0); } catch (err) { this.boxcolor = "red"; } } JSONParse.prototype.onExecute = function() { this._str = this.getInputData(1); this.setOutputData(1, this._obj); }; JSONParse.prototype.onAction = function(name) { if(name == "parse") this.parse(); } LiteGraph.registerNodeType("basic/jsonparse", JSONParse); //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); //Sequence of events function WaitAll() { var that = this; this.addInput("", LiteGraph.ACTION); this.addInput("", LiteGraph.ACTION); this.addOutput("", LiteGraph.EVENT); this.addWidget("button","+",null,function(){ that.addInput("", LiteGraph.ACTION); that.size[0] = 90; }); this.size = [90, 70]; this.ready = []; } WaitAll.title = "WaitAll"; WaitAll.desc = "Wait until all input events arrive then triggers output"; WaitAll.prototype.getTitle = function() { return ""; }; WaitAll.prototype.onDrawBackground = function(ctx) { if (this.flags.collapsed) { return; } for(var i = 0; i < this.inputs.length; ++i) { var y = i * LiteGraph.NODE_SLOT_HEIGHT + 10; ctx.fillStyle = this.ready[i] ? "#AFB" : "#000"; ctx.fillRect(20, y, 10, 10); } } WaitAll.prototype.onAction = function(action, param, options, slot_index) { if(slot_index == null) return; //check all this.ready.length = this.outputs.length; this.ready[slot_index] = true; for(var i = 0; i < this.ready.length;++i) if(!this.ready[i]) return; //pass this.reset(); this.triggerSlot(0); }; WaitAll.prototype.reset = function() { this.ready.length = 0; } LiteGraph.registerNodeType("events/waitAll", WaitAll); //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; } this.setOutputData(i, value); } }; MathCompare.prototype.onGetOutputs = function() { return [ ["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=", { 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 = "

pixelcode must be vec3, uvcode must be vec2, is optional

\

uv: tex. coords

color: texture colorB: textureB

time: scene time value: input value

For multiline you must type: result = ...

"; 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 (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 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); //HTTP Request function HTTPRequestNode() { var that = this; this.addInput("request", LiteGraph.ACTION); this.addInput("url", "string"); this.addProperty("url", ""); this.addOutput("ready", LiteGraph.EVENT); this.addOutput("data", "string"); this.addWidget("button", "Fetch", null, this.fetch.bind(this)); this._data = null; this._fetching = null; } HTTPRequestNode.title = "HTTP Request"; HTTPRequestNode.desc = "Fetch data through HTTP"; HTTPRequestNode.prototype.fetch = function() { var url = this.properties.url; if(!url) return; this.boxcolor = "#FF0"; var that = this; this._fetching = fetch(url) .then(resp=>{ if(!resp.ok) { this.boxcolor = "#F00"; that.trigger("error"); } else { this.boxcolor = "#0F0"; return resp.text(); } }) .then(data=>{ that._data = data; that._fetching = null; that.trigger("ready"); }); } HTTPRequestNode.prototype.onAction = function(evt) { if(evt == "request") this.fetch(); } HTTPRequestNode.prototype.onExecute = function() { this.setOutputData(1, this._data); }; HTTPRequestNode.prototype.onGetOutputs = function() { return [["error",LiteGraph.EVENT]]; } LiteGraph.registerNodeType("network/httprequest", HTTPRequestNode); })(this); ================================================ FILE: build/litegraph_mini.js ================================================ //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 use_deferred_actions: true, //executes actions during the graph execution flow 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 an object with some properties * like onCreate, inputs:Array, outputs:Array, properties, onExecute * @method buildNodeClassFromObject * @param {String} name node name with namespace (p.e.: 'math/sum') * @param {Object} object methods expected onCreate, inputs, outputs, properties, onExecute */ buildNodeClassFromObject: function( name, object ) { var ctor_code = ""; if(object.inputs) for(var i=0; i < object.inputs.length; ++i) { var _name = object.inputs[i][0]; var _type = object.inputs[i][1]; if(_type && _type.constructor === String) _type = '"'+_type+'"'; ctor_code += "this.addInput('"+_name+"',"+_type+");\n"; } if(object.outputs) for(var i=0; i < object.outputs.length; ++i) { var _name = object.outputs[i][0]; var _type = object.outputs[i][1]; if(_type && _type.constructor === String) _type = '"'+_type+'"'; ctor_code += "this.addOutput('"+_name+"',"+_type+");\n"; } if(object.properties) for(var i in object.properties) { var prop = object.properties[i]; if(prop && prop.constructor === String) prop = '"'+prop+'"'; ctor_code += "this.addProperty('"+i+"',"+prop+");\n"; } ctor_code += "if(this.onCreate)this.onCreate()"; var classobj = Function(ctor_code); for(var i in object) if(i!="inputs" && i!="outputs" && i!="properties") classobj.prototype[i] = object[i]; classobj.title = object.title || name.split("/").pop(); classobj.desc = object.desc || "Generated from object"; this.registerNodeType(name, classobj); return classobj; }, /** * 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 = ""; if(param_types !== null) //null means no inputs { var names = LiteGraph.getParameterNames(func); for (var i = 0; i < names.length; ++i) { var type = 0; if(param_types) { //type = param_types[i] != null ? "'" + param_types[i] + "'" : "0"; if( param_types[i] != null && param_types[i].constructor === String ) type = "'" + param_types[i] + "'" ; else if( param_types[i] != null ) type = param_types[i]; } code += "this.addInput('" + names[i] + "'," + type + ");\n"; } } if(return_type !== null) //null means no output code += "this.addOutput('out'," + (return_type != null ? (return_type.constructor === String ? "'" + 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); return 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); //not optimal: executes possible pending actions in node, problem is it is not optimized //it is done here as if it was done in the later loop it wont be called in the node missed the onExecute //from now on it will iterate only on executable nodes which is faster 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(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length) node.executePendingActions(); 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 { //catch errors try { //iterations for (var i = 0; i < num; i++) { for (var j = 0; j < limit; ++j) { var node = nodes[j]; if(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length) node.executePendingActions(); 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 ] != undefined)) 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 execution of actions that were deferred when the action was triggered * @method executePendingActions */ LGraphNode.prototype.executePendingActions = function() { if(!this._waiting_actions || !this._waiting_actions.length) return; for(var i = 0; i < this._waiting_actions.length;++i) { var p = this._waiting_actions[i]; this.onAction(p[0],p[1],p[2],p[3],p[4]); } this._waiting_actions.length = 0; } /** * Triggers the node code execution, place a boolean/counter to mark the node as being executed * @method doExecute * @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; } } else { } 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, action_slot ) { 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, action_slot); 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]; //instead of executing them now, it will be executed in the next graph loop, to ensure data flow if(LiteGraph.use_deferred_actions && node.onExecute) { if(!node._waiting_actions) node._waiting_actions = []; node._waiting_actions.push([target_connection.name, param, options, link_info.target_slot]); } else { // wrap node.onAction(target_connection.name, param); node.actionDo( target_connection.name, param, options, link_info.target_slot ); } } } }; /** * 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 * @method getBounding * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] */ LGraphNode.prototype.getBounding = function(out, compute_outer) { out = out || new Float32Array(4); const nodePos = this.pos; const isCollapsed = this.flags.collapsed; const nodeSize = this.size; let left_offset = 0; // 1 offset due to how nodes are rendered let right_offset = 1 ; let top_offset = 0; let bottom_offset = 0; if (compute_outer) { // 4 offset for collapsed node connection points left_offset = 4; // 6 offset for right shadow and collapsed node connection points right_offset = 6 + left_offset; // 4 offset for collapsed nodes top connection points top_offset = 4; // 5 offset for bottom shadow and collapsed node connection points bottom_offset = 5 + top_offset; } out[0] = nodePos[0] - left_offset; out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset; out[2] = isCollapsed ? (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset : nodeSize[0] + right_offset; out[3] = isCollapsed ? LiteGraph.NODE_TITLE_HEIGHT + bottom_offset : nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset; 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= 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 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.setAttribute("tabindex",1); //otherwise key events are ignored 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]; if(node.clonable === false) continue; 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, true))) { 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 deltaX = event.deltaX || event.deltax || 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(deltaX) w.value += 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: "" + (info.label ? info.label : i) + "" + "" + value + "", 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 = ""; 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 = " "; else dialog.innerHTML = " "; 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 = "Search "; if (options.do_type_filter){ dialog.innerHTML += ""; dialog.innerHTML += ""; } dialog.innerHTML += "
"; 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) { if(that.search_box) 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) { refreshHelper(); 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 (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 = ""; } else if ( (type == "enum" || type == "combo") && info.values) { input_html = ""; } else if (type == "boolean" || type == "toggle") { input_html = ""; } else { console.warn("unknown type: " + type); return; } var dialog = this.createDialog( "" + (info.label ? info.label : property) + "" + input_html + "", 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 = "
"; 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 = "✕"; 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 = ""; 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(""+node.type+""+(node.constructor.desc || "")+""); panel.addHTML("

Properties

"); 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 = "
"; 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 = ""; 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 = " "; 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 = " + NameType"; 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 = " "; 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 = " + NameType"; 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: "No color" }); for (var i in LGraphCanvas.node_colors) { var color = LGraphCanvas.node_colors[i]; var value = { value: i, content: "" + i + "" }; 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( "Name", 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 = "
" //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 JSONParse() { this.addInput("parse", LiteGraph.ACTION); this.addInput("json", "string"); this.addOutput("done", LiteGraph.EVENT); this.addOutput("object", "object"); this.widget = this.addWidget("button","parse","",this.parse.bind(this)); this._str = null; this._obj = null; } JSONParse.title = "JSON Parse"; JSONParse.desc = "Parses JSON String into object"; JSONParse.prototype.parse = function() { if(!this._str) return; try { this._str = this.getInputData(1); this._obj = JSON.parse(this._str); this.boxcolor = "#AEA"; this.triggerSlot(0); } catch (err) { this.boxcolor = "red"; } } JSONParse.prototype.onExecute = function() { this._str = this.getInputData(1); this.setOutputData(1, this._obj); }; JSONParse.prototype.onAction = function(name) { if(name == "parse") this.parse(); } LiteGraph.registerNodeType("basic/jsonparse", JSONParse); //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); //Sequence of events function WaitAll() { var that = this; this.addInput("", LiteGraph.ACTION); this.addInput("", LiteGraph.ACTION); this.addOutput("", LiteGraph.EVENT); this.addWidget("button","+",null,function(){ that.addInput("", LiteGraph.ACTION); that.size[0] = 90; }); this.size = [90, 70]; this.ready = []; } WaitAll.title = "WaitAll"; WaitAll.desc = "Wait until all input events arrive then triggers output"; WaitAll.prototype.getTitle = function() { return ""; }; WaitAll.prototype.onDrawBackground = function(ctx) { if (this.flags.collapsed) { return; } for(var i = 0; i < this.inputs.length; ++i) { var y = i * LiteGraph.NODE_SLOT_HEIGHT + 10; ctx.fillStyle = this.ready[i] ? "#AFB" : "#000"; ctx.fillRect(20, y, 10, 10); } } WaitAll.prototype.onAction = function(action, param, options, slot_index) { if(slot_index == null) return; //check all this.ready.length = this.outputs.length; this.ready[slot_index] = true; for(var i = 0; i < this.ready.length;++i) if(!this.ready[i]) return; //pass this.reset(); this.triggerSlot(0); }; WaitAll.prototype.reset = function() { this.ready.length = 0; } LiteGraph.registerNodeType("events/waitAll", WaitAll); //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); (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; } this.setOutputData(i, value); } }; MathCompare.prototype.onGetOutputs = function() { return [ ["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=", { 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); //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); //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); //HTTP Request function HTTPRequestNode() { var that = this; this.addInput("request", LiteGraph.ACTION); this.addInput("url", "string"); this.addProperty("url", ""); this.addOutput("ready", LiteGraph.EVENT); this.addOutput("data", "string"); this.addWidget("button", "Fetch", null, this.fetch.bind(this)); this._data = null; this._fetching = null; } HTTPRequestNode.title = "HTTP Request"; HTTPRequestNode.desc = "Fetch data through HTTP"; HTTPRequestNode.prototype.fetch = function() { var url = this.properties.url; if(!url) return; this.boxcolor = "#FF0"; var that = this; this._fetching = fetch(url) .then(resp=>{ if(!resp.ok) { this.boxcolor = "#F00"; that.trigger("error"); } else { this.boxcolor = "#0F0"; return resp.text(); } }) .then(data=>{ that._data = data; that._fetching = null; that.trigger("ready"); }); } HTTPRequestNode.prototype.onAction = function(evt) { if(evt == "request") this.fetch(); } HTTPRequestNode.prototype.onExecute = function() { this.setOutputData(1, this._data); }; HTTPRequestNode.prototype.onGetOutputs = function() { return [["error",LiteGraph.EVENT]]; } LiteGraph.registerNodeType("network/httprequest", HTTPRequestNode); })(this); ================================================ FILE: csharp/LiteGraph.cs ================================================ using System; using System.Collections; using System.Collections.Generic; //using System.Diagnostics; using UnityEngine; //for debug messages using SimpleJSON; //to parse the JSON namespace LiteGraph { public enum DataType { NONE, ENUM, NUMBER, STRING, BOOL, VEC2, VEC3 }; public struct vec2 { float x; float y; }; public struct vec3 { float x; float y; float z; }; //not used yet... public class MutableType { public DataType type; public bool data_bool; public float data_number; public string data_string; public vec2 data_vec2; public vec3 data_vec3; public bool AsBool { get { return data_bool; } } public int AsEnum { get { return (int)data_number; } } public float AsFloat { get { return data_number; } } public string AsString { get { return data_string; } } public vec2 AsVec2 { get { return data_vec2; } } public vec3 AsVec3 { get { return data_vec3; } } public void Set(bool v) { type = DataType.BOOL; data_bool = v; } public void Set(int v) { type = DataType.ENUM; data_number = v; } public void Set(float v) { type = DataType.NUMBER; data_number = v; } public void Set(string v) { type = DataType.STRING; data_string = v; } public void Set(vec2 v) { type = DataType.VEC2; data_vec2 = v; } public void Set(vec3 v) { type = DataType.VEC3; data_vec3 = v; } public override string ToString() { switch (type) { case DataType.NONE: return ""; case DataType.ENUM: return data_number.ToString(); case DataType.BOOL: return data_bool.ToString(); case DataType.NUMBER: return data_number.ToString(); case DataType.STRING: return data_string; case DataType.VEC2: return data_vec2.ToString(); case DataType.VEC3: return data_vec3.ToString(); } return ""; } }; //to store connection info public class LLink { public int id; public int origin_id; public int origin_slot; public int target_id; public int target_slot; public DataType data_type; public bool data_bool; public float data_float; public string data_string; public vec2 data_vec2; public vec3 data_vec3; public LLink(int id, DataType type, int origin_id, int origin_slot, int target_id, int target_slot) { this.id = id; this.data_type = type; this.origin_id = origin_id; this.origin_slot = origin_slot; this.target_id = target_id; this.target_slot = target_slot; } public void setData(bool data) { data_type = DataType.BOOL; data_bool = data; } public void setData(float data) { data_type = DataType.NUMBER; data_float = data; } public void setData(string data) { data_type = DataType.STRING; data_string = data; } public void setData(vec2 data) { data_type = DataType.VEC2; data_vec2 = data; } public void setData(vec3 data) { data_type = DataType.VEC3; data_vec3 = data; } } //to store slot info public class LSlot { public int num; public string name; public DataType type; public LLink link = null; //for input slots public List links = new List(); //for output slots public LSlot(string name, DataType type) { this.name = name; this.type = type; } } //the node base class public class LGraphNode { public int id = -1; public LGraph graph = null; public int order = -1; public List inputs = new List(); public List outputs = new List(); public LGraphNode() { } public virtual LSlot addInput(string name, DataType type = DataType.NONE) { LSlot slot = new LSlot(name, type); slot.num = inputs.Count; inputs.Add(slot); return slot; } public virtual LSlot addOutput(string name, DataType type = DataType.NONE) { LSlot slot = new LSlot(name, type); slot.num = outputs.Count; outputs.Add(slot); return slot; } public virtual bool getInputData(int slot_num, bool default_value) { LLink link = inputs[slot_num].link; if (link != null) return link.data_bool; return default_value; } public virtual float getInputData(int slot_num, float default_value ) { LLink link = inputs[slot_num].link; if (link != null) return link.data_float; return default_value; } public virtual string getInputData(int slot_num, string default_value) { LLink link = inputs[slot_num].link; if (link != null) return link.data_string; return default_value; } public virtual vec2 getInputData(int slot_num, vec2 default_value) { LLink link = inputs[slot_num].link; if (link != null) return link.data_vec2; return default_value; } public virtual vec3 getInputData(int slot_num, vec3 default_value) { LLink link = inputs[slot_num].link; if (link != null) return link.data_vec3; return default_value; } public virtual void setOutputData(int slot_num, bool v) { if (inputs.Count <= slot_num) return; LSlot slot = outputs[slot_num]; if (slot == null) return; for (int i = 0; i < slot.links.Count; ++i) { LLink link = slot.links[i]; if (link == null) return; link.setData(v); } } public virtual void setOutputData(int slot_num, float v) { if (outputs.Count <= slot_num) return; LSlot slot = outputs[slot_num]; if (slot == null) return; for (int i = 0; i < slot.links.Count; ++i) { LLink link = slot.links[i]; if (link == null) return; link.setData(v); } } public virtual void setOutputData(int slot_num, string v) { if (outputs.Count <= slot_num) return; LSlot slot = outputs[slot_num]; if (slot == null) return; for (int i = 0; i < slot.links.Count; ++i) { LLink link = slot.links[i]; if (link == null) return; link.setData(v); } } public virtual void setOutputData(int slot_num, vec2 v) { if (outputs.Count <= slot_num) return; LSlot slot = outputs[slot_num]; if (slot == null) return; for (int i = 0; i < slot.links.Count; ++i) { LLink link = slot.links[i]; if (link == null) return; link.setData(v); } } public virtual void setOutputData(int slot_num, vec3 v) { if (outputs.Count <= slot_num) return; LSlot slot = outputs[slot_num]; if (slot == null) return; for (int i = 0; i < slot.links.Count; ++i) { LLink link = slot.links[i]; if (link == null) return; link.setData(v); } } public virtual void transferData(int input_slot_num, int output_slot_num) { LLink input_link = inputs[input_slot_num].link; if (input_link == null) return; if (outputs.Count <= output_slot_num) return; LSlot slot = outputs[output_slot_num]; if (slot == null) return; for (int i = 0; i < slot.links.Count; ++i) { LLink link = slot.links[i]; if (link == null) return; switch(input_link.data_type) { case DataType.BOOL: link.setData(input_link.data_bool); break; case DataType.NUMBER: link.setData(input_link.data_float); break; case DataType.STRING: link.setData(input_link.data_string); break; case DataType.VEC2: link.setData(input_link.data_vec2); break; case DataType.VEC3: link.setData(input_link.data_vec3); break; } } } public virtual bool connect(int origin_slot, LGraphNode target, int target_slot) { if(graph == null) throw (new Exception("node does not belong to a graph")); if (graph != target.graph) throw (new Exception("nodes do not belong to same graph") ); LSlot origin_slot_info = this.outputs[origin_slot]; LSlot target_slot_info = target.inputs[target_slot]; if (origin_slot_info == null || target_slot_info == null) return false; if(origin_slot_info.type != target_slot_info.type && (origin_slot_info.type != DataType.NONE && target_slot_info.type != DataType.NONE) ) throw (new Exception("connecting incompatible types")); int id = graph.last_link_id++; LLink link = new LLink(id, origin_slot_info.type, this.id, origin_slot, target.id, target_slot); graph.links.Add(link); origin_slot_info.links.Add(link); target_slot_info.link = link; graph.sortByExecutionOrder(); return true; } public virtual void onExecute() { } public virtual void configure(JSONNode json_node) { this.id = json_node["id"].AsInt; this.order = json_node["order"].AsInt; //inputs var json_inputs = json_node["inputs"]; if (json_inputs != null) { JSONNode.Enumerator it = json_inputs.GetEnumerator(); int i = 0; while(it.MoveNext()) { JSONNode json_slot = it.Current; string str_type = json_slot["type"]; DataType type = DataType.NONE; if (str_type != null && Globals.stringToDataType.ContainsKey(str_type)) type = Globals.stringToDataType[str_type]; LSlot slot = null; if (inputs.Count > i) slot = inputs[i]; if(slot == null) slot = this.addInput( json_slot["name"], type ); JSONNode json_link = json_slot["link"]; if (json_link != null) slot.link = graph.links_by_id[json_link.AsInt]; ++i; } } //outputs var json_outputs = json_node["outputs"]; if (json_outputs != null) { JSONNode.Enumerator it = json_outputs.GetEnumerator(); int i = 0; while (it.MoveNext()) { JSONNode json_slot = it.Current; string str_type = json_slot["type"]; DataType type = DataType.NONE; if (str_type != null && Globals.stringToDataType.ContainsKey(str_type)) type = Globals.stringToDataType[str_type]; LSlot slot = null; if (outputs.Count > i) slot = outputs[i]; if (slot == null) slot = this.addOutput(json_slot["name"], type); JSONNode json_links = json_slot["links"]; if(json_links != null) { JSONNode.Enumerator it2 = json_links.GetEnumerator(); while (it2.MoveNext()) { JSONNode json_link_id = it2.Current; LLink link = graph.links_by_id[json_link_id.AsInt]; if (link != null) slot.links.Add(link); else Debug.LogError("Link ID not found!: " + json_link_id); } } ++i; } } //custom data (properties) this.onConfigure(json_node); } public virtual void onConfigure(JSONNode json_node) { //overwrite this one } } //namespace to store global litegraph data public class Globals { static public Dictionary stringToDataType = new Dictionary { { "NONE", DataType.NONE }, { "", DataType.NONE }, { "ENUM", DataType.ENUM }, {"NUMBER",DataType.NUMBER }, { "number", DataType.NUMBER }, { "BOOLEAN", DataType.BOOL }, { "boolean", DataType.BOOL }, { "STRING", DataType.STRING }, { "string", DataType.STRING }, { "VEC2", DataType.VEC2 } }; static public Dictionary> node_types = new Dictionary>(); static public void registerType(string name, Func ctor ) { node_types.Add(name, ctor); } static public LGraphNode createNodeType(string name) { if (!node_types.ContainsKey(name)) { Debug.Log("Node type not found: " + name); return null; } Func ctor = node_types[name]; return ctor(); } }; //one graph public class LGraph { public List nodes = new List(); public Dictionary nodes_by_id = new Dictionary(); public List nodes_in_execution_order = new List(); public List links = new List(); public Dictionary links_by_id = new Dictionary(); public bool has_errors = false; public int last_node_id = 0; public int last_link_id = 0; public double time = 0; //time in seconds public Dictionary outputs = new Dictionary(); // Start is called before the first frame update public LGraph() { } public void add(LGraphNode node) { if (node.graph != null) throw ( new Exception("already has graph") ); node.graph = this; node.id = last_node_id++; node.order = node.id; nodes.Add(node); nodes_by_id.Add(node.id, node); } public void clear() { has_errors = false; nodes.Clear(); nodes_by_id.Clear(); links.Clear(); links_by_id.Clear(); nodes_in_execution_order.Clear(); last_link_id = 0; last_node_id = 0; outputs.Clear(); } // Update is called once per frame public void runStep(float dt = 0) { for (int i = 0; i < nodes_in_execution_order.Count; ++i) { LGraphNode node = nodes_in_execution_order[i]; node.onExecute(); } time += dt; } public void configure(string data) { sortByExecutionOrder(); } public void sortByExecutionOrder() { nodes_in_execution_order = nodes.GetRange(0, nodes.Count); nodes_in_execution_order.Sort(delegate (LGraphNode a, LGraphNode b) { return a.order - b.order; }); } public void fromJSONText(string text) { clear(); var root = JSON.Parse(text); last_node_id = root["last_node_id"].AsInt; last_link_id = root["last_link_id"].AsInt; var json_links = root["links"]; for (int i = 0; i < json_links.Count; ++i) { var json_node = json_links[i]; int id = json_node[0].AsInt; int origin_id = json_node[1].AsInt; int origin_slot = json_node[2].AsInt; int target_id = json_node[3].AsInt; int target_slot = json_node[4].AsInt; JSONNode json_type = json_node[5]; DataType type = DataType.NONE; if(json_type != null && json_type.Value != "0" && Globals.stringToDataType.ContainsKey(json_type) ) type = Globals.stringToDataType[ json_type ]; LLink link = new LLink(id, type, origin_id, origin_slot, target_id, target_slot); links.Add(link); links_by_id[link.id] = link; } var json_nodes = root["nodes"]; for (int i = 0; i < json_nodes.Count; ++i) { var json_node = json_nodes[i]; string node_type = json_node["type"]; Debug.Log(node_type); LGraphNode node = LiteGraph.Globals.createNodeType(node_type); if (node == null) { Debug.Log("Error: node type not found: " + node_type); has_errors = true; continue; } node.graph = this; nodes.Add(node); node.configure(json_node); } sortByExecutionOrder(); } public float getOutput(string name, float def_value) { if (!outputs.ContainsKey(name)) return def_value; return outputs[name]; } } public class Main { public static void Init() { Main.loadNodes(); } public static void loadNodes() { Globals.registerType(WatchNode.type, () => new WatchNode() ); Globals.registerType(RandomNumberNode.type, () => new RandomNumberNode()); Globals.registerType(ConstNumberNode.type, () => new ConstNumberNode()); Globals.registerType(TimeNode.type, () => new TimeNode()); Globals.registerType(ConditionNode.type, () => new ConditionNode()); Globals.registerType(GraphOutputNode.type, () => new GraphOutputNode()); Globals.registerType(GateNode.type, () => new GateNode()); } public static void test() { Debug.Log("Testing Graph..."); LGraph graph = new LGraph(); LGraphNode node1 = LiteGraph.Globals.createNodeType("math/rand"); graph.add(node1); LGraphNode node2 = LiteGraph.Globals.createNodeType("basic/watch"); graph.add(node2); node1.connect(0,node2,0); for(int i = 0; i < 100; ++i) graph.runStep(); } } } ================================================ FILE: csharp/LiteGraphNodes.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using SimpleJSON; namespace LiteGraph { public class ConstNumberNode : LGraphNode { public static string type = "basic/const"; public float value = 0; public ConstNumberNode() { this.addOutput("out", DataType.NUMBER ); } override public void onExecute() { this.setOutputData(0, value ); } override public void onConfigure( JSONNode o) { JSONNode json_properties = o["properties"]; value = json_properties["value"].AsFloat; } } public class RandomNumberNode : LGraphNode { public static string type = "math/rand"; float min_value = 0; float max_value = 1; System.Random random = new System.Random(); public RandomNumberNode() { this.addOutput("out", DataType.NUMBER ); } override public void onExecute() { this.setOutputData(0, (float)(min_value + random.NextDouble() * (max_value - min_value)) ); } override public void onConfigure(JSONNode o) { JSONNode json_properties = o["properties"]; min_value = json_properties["min"].AsFloat; max_value = json_properties["max"].AsFloat; } } public class GraphOutputNode : LGraphNode { public static string type = "graph/output"; string name = ""; DataType datatype = DataType.NUMBER; public GraphOutputNode() { this.addInput("out", datatype); } override public void onExecute() { switch (datatype) { case DataType.NUMBER: float v = this.getInputData(0, 0); graph.outputs[name] = v; break; } } override public void onConfigure(JSONNode o) { JSONNode json_properties = o["properties"]; name = json_properties["name"]; if (json_properties.HasKey("type")) { string str_type = json_properties["type"]; if (Globals.stringToDataType.ContainsKey(str_type)) { datatype = Globals.stringToDataType[str_type]; this.inputs[0].type = datatype; } } } } public class ConditionNode : LGraphNode { public static string type = "math/condition"; public enum OPERATION { NONE,GREATER,LOWER,EQUAL,NEQUAL,GEQUAL,LEQUAL,OR,AND }; static public Dictionary strToOperation = new Dictionary { { "NONE", OPERATION.NONE }, { ">", OPERATION.GREATER }, { "<", OPERATION.LOWER }, { "==", OPERATION.EQUAL }, { "!=", OPERATION.NEQUAL }, { "<=", OPERATION.LEQUAL }, { ">=", OPERATION.GEQUAL }, { "&&", OPERATION.AND }, { "||", OPERATION.OR } }; public float A = 0; public float B = 0; public OPERATION OP = OPERATION.EQUAL; public ConditionNode() { this.addOutput("A", DataType.NUMBER ); this.addOutput("B", DataType.NUMBER ); this.addOutput("out", DataType.BOOL ); } override public void onExecute() { float A = this.getInputData(0,this.A); float B = this.getInputData(1,this.B); bool v = false; switch(OP) { case OPERATION.NONE: v = false; break; case OPERATION.GREATER: v = A > B; break; case OPERATION.LOWER: v = A < B; break; case OPERATION.EQUAL: v = A == B; break; case OPERATION.NEQUAL: v = A != B; break; case OPERATION.GEQUAL: v = A >= B; break; case OPERATION.LEQUAL: v = A <= B; break; case OPERATION.OR: v = (A != 0) || (B != 0); break; case OPERATION.AND: v = (A != 0) && (B != 0); break; } this.setOutputData(0, v); } override public void onConfigure(JSONNode o) { JSONNode json_properties = o["properties"]; A = json_properties["A"].AsFloat; B = json_properties["B"].AsFloat; string op = json_properties["OP"]; if (strToOperation.ContainsKey(op)) OP = strToOperation[op]; else Debug.Log("Wrong operation type: " + op); } } public class GateNode : LGraphNode { public static string type = "math/gate"; public GateNode() { this.addInput("v", DataType.BOOL); this.addInput("A"); this.addInput("B"); this.addOutput("out"); } override public void onExecute() { bool v = this.getInputData(0, true); this.transferData(v ? 1 : 2, 0); } } public class TimeNode : LGraphNode { public static string type = "basic/time"; public TimeNode() { this.addOutput("in ms", DataType.NUMBER ); this.addOutput("in sec", DataType.NUMBER); } override public void onExecute() { this.setOutputData(0, (float)(graph.time * 1000)); this.setOutputData(1, (float)graph.time); } } public class WatchNode : LGraphNode { public static string type = "basic/watch"; public WatchNode() { this.addInput("in", DataType.NUMBER); } override public void onExecute() { float v = this.getInputData(0, 0); //Debug.Log("Watch: " + v.ToString()); } } } ================================================ FILE: csharp/LiteGraphTest.cs ================================================ using System.Collections; using System.Collections.Generic; using UnityEngine; using LiteGraph; public class LiteGraphTest : MonoBehaviour { public TextAsset graph_file = null; private LGraph graph = null; public bool graph_has_errors = false; public float output_value = 0; // Start is called before the first frame update void Start() { System.Diagnostics.Debug.WriteLine("Test!"); LiteGraph.Main.Init(); graph = new LGraph(); if (!graph_file) { Debug.Log("Testing Base Graph..."); LGraphNode node1 = LiteGraph.Globals.createNodeType("math/rand"); graph.add(node1); LGraphNode node2 = LiteGraph.Globals.createNodeType("basic/watch"); graph.add(node2); node1.connect(0, node2, 0); } else { Debug.Log("Testing File Graph..."); string text = graph_file.text; graph.fromJSONText(text); } graph_has_errors = graph.has_errors; } // Update is called once per frame void Update() { if(graph != null) graph.runStep( Time.deltaTime ); output_value = graph.getOutput("output",0); } } ================================================ FILE: csharp/SimpleJSON.cs ================================================ /* * * * * * A simple JSON Parser / builder * ------------------------------ * * It mainly has been written as a simple JSON parser. It can build a JSON string * from the node-tree, or generate a node tree from any valid JSON string. * * Written by Bunny83 * 2012-06-09 * * [2012-06-09 First Version] * - provides strongly typed node classes and lists / dictionaries * - provides easy access to class members / array items / data values * - the parser now properly identifies types. So generating JSON with this framework should work. * - only double quotes (") are used for quoting strings. * - provides "casting" properties to easily convert to / from those types: * int / float / double / bool * - provides a common interface for each node so no explicit casting is required. * - the parser tries to avoid errors, but if malformed JSON is parsed the result is more or less undefined * - It can serialize/deserialize a node tree into/from an experimental compact binary format. It might * be handy if you want to store things in a file and don't want it to be easily modifiable * * [2012-12-17 Update] * - Added internal JSONLazyCreator class which simplifies the construction of a JSON tree * Now you can simple reference any item that doesn't exist yet and it will return a JSONLazyCreator * The class determines the required type by it's further use, creates the type and removes itself. * - Added binary serialization / deserialization. * - Added support for BZip2 zipped binary format. Requires the SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ ) * The usage of the SharpZipLib library can be disabled by removing or commenting out the USE_SharpZipLib define at the top * - The serializer uses different types when it comes to store the values. Since my data values * are all of type string, the serializer will "try" which format fits best. The order is: int, float, double, bool, string. * It's not the most efficient way but for a moderate amount of data it should work on all platforms. * * [2017-03-08 Update] * - Optimised parsing by using a StringBuilder for token. This prevents performance issues when large * string data fields are contained in the json data. * - Finally refactored the badly named JSONClass into JSONObject. * - Replaced the old JSONData class by distict typed classes ( JSONString, JSONNumber, JSONBool, JSONNull ) this * allows to propertly convert the node tree back to json without type information loss. The actual value * parsing now happens at parsing time and not when you actually access one of the casting properties. * * [2017-04-11 Update] * - Fixed parsing bug where empty string values have been ignored. * - Optimised "ToString" by using a StringBuilder internally. This should heavily improve performance for large files * - Changed the overload of "ToString(string aIndent)" to "ToString(int aIndent)" * * [2017-11-29 Update] * - Removed the IEnumerator implementations on JSONArray & JSONObject and replaced it with a common * struct Enumerator in JSONNode that should avoid garbage generation. The enumerator always works * on KeyValuePair, even for JSONArray. * - Added two wrapper Enumerators that allows for easy key or value enumeration. A JSONNode now has * a "Keys" and a "Values" enumerable property. Those are also struct enumerators / enumerables * - A KeyValuePair can now be implicitly converted into a JSONNode. This allows * a foreach loop over a JSONNode to directly access the values only. Since KeyValuePair as well as * all the Enumerators are structs, no garbage is allocated. * - To add Linq support another "LinqEnumerator" is available through the "Linq" property. This * enumerator does implement the generic IEnumerable interface so most Linq extensions can be used * on this enumerable object. This one does allocate memory as it's a wrapper class. * - The Escape method now escapes all control characters (# < 32) in strings as uncode characters * (\uXXXX) and if the static bool JSONNode.forceASCII is set to true it will also escape all * characters # > 127. This might be useful if you require an ASCII output. Though keep in mind * when your strings contain many non-ascii characters the strings become much longer (x6) and are * no longer human readable. * - The node types JSONObject and JSONArray now have an "Inline" boolean switch which will default to * false. It can be used to serialize this element inline even you serialize with an indented format * This is useful for arrays containing numbers so it doesn't place every number on a new line * - Extracted the binary serialization code into a seperate extension file. All classes are now declared * as "partial" so an extension file can even add a new virtual or abstract method / interface to * JSONNode and override it in the concrete type classes. It's of course a hacky approach which is * generally not recommended, but i wanted to keep everything tightly packed. * - Added a static CreateOrGet method to the JSONNull class. Since this class is immutable it could * be reused without major problems. If you have a lot null fields in your data it will help reduce * the memory / garbage overhead. I also added a static setting (reuseSameInstance) to JSONNull * (default is true) which will change the behaviour of "CreateOrGet". If you set this to false * CreateOrGet will not reuse the cached instance but instead create a new JSONNull instance each time. * I made the JSONNull constructor private so if you need to create an instance manually use * JSONNull.CreateOrGet() * * [2018-01-09 Update] * - Changed all double.TryParse and double.ToString uses to use the invariant culture to avoid problems * on systems with a culture that uses a comma as decimal point. * * [2018-01-26 Update] * - Added AsLong. Note that a JSONNumber is stored as double and can't represent all long values. However * storing it as string would work. * - Added static setting "JSONNode.longAsString" which controls the default type that is used by the * LazyCreator when using AsLong * * [2018-04-25 Update] * - Added support for parsing single values (JSONBool, JSONString, JSONNumber, JSONNull) as top level value. * * [2019-02-18 Update] * - Added HasKey(key) and GetValueOrDefault(key, default) to the JSONNode class to provide way to read * values conditionally without creating a LazyCreator * * [2019-03-25 Update] * - Added static setting "allowLineComments" to the JSONNode class which is true by default. This allows * "//" line comments when parsing json text as long as it's not within quoted text. All text after // up * to the end of the line is completely ignored / skipped. This makes it easier to create human readable * and editable files. Note that stripped comments are not read, processed or preserved in any way. So * this feature is only relevant for human created files. * - Explicitly strip BOM (Byte Order Mark) when parsing to avoid getting it leaked into a single primitive * value. That's a rare case but better safe than sorry. * - Allowing adding the empty string as key * * The MIT License (MIT) * * Copyright (c) 2012-2017 Markus Göbel (Bunny83) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * * * * */ using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; namespace SimpleJSON { public enum JSONNodeType { Array = 1, Object = 2, String = 3, Number = 4, NullValue = 5, Boolean = 6, None = 7, Custom = 0xFF, } public enum JSONTextMode { Compact, Indent } public abstract partial class JSONNode { #region Enumerators public struct Enumerator { private enum Type { None, Array, Object } private Type type; private Dictionary.Enumerator m_Object; private List.Enumerator m_Array; public bool IsValid { get { return type != Type.None; } } public Enumerator(List.Enumerator aArrayEnum) { type = Type.Array; m_Object = default(Dictionary.Enumerator); m_Array = aArrayEnum; } public Enumerator(Dictionary.Enumerator aDictEnum) { type = Type.Object; m_Object = aDictEnum; m_Array = default(List.Enumerator); } public KeyValuePair Current { get { if (type == Type.Array) return new KeyValuePair(string.Empty, m_Array.Current); else if (type == Type.Object) return m_Object.Current; return new KeyValuePair(string.Empty, null); } } public bool MoveNext() { if (type == Type.Array) return m_Array.MoveNext(); else if (type == Type.Object) return m_Object.MoveNext(); return false; } } public struct ValueEnumerator { private Enumerator m_Enumerator; public ValueEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } public ValueEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } public ValueEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } public JSONNode Current { get { return m_Enumerator.Current.Value; } } public bool MoveNext() { return m_Enumerator.MoveNext(); } public ValueEnumerator GetEnumerator() { return this; } } public struct KeyEnumerator { private Enumerator m_Enumerator; public KeyEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } public KeyEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } public KeyEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } public string Current { get { return m_Enumerator.Current.Key; } } public bool MoveNext() { return m_Enumerator.MoveNext(); } public KeyEnumerator GetEnumerator() { return this; } } public class LinqEnumerator : IEnumerator>, IEnumerable> { private JSONNode m_Node; private Enumerator m_Enumerator; internal LinqEnumerator(JSONNode aNode) { m_Node = aNode; if (m_Node != null) m_Enumerator = m_Node.GetEnumerator(); } public KeyValuePair Current { get { return m_Enumerator.Current; } } object IEnumerator.Current { get { return m_Enumerator.Current; } } public bool MoveNext() { return m_Enumerator.MoveNext(); } public void Dispose() { m_Node = null; m_Enumerator = new Enumerator(); } public IEnumerator> GetEnumerator() { return new LinqEnumerator(m_Node); } public void Reset() { if (m_Node != null) m_Enumerator = m_Node.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return new LinqEnumerator(m_Node); } } #endregion Enumerators #region common interface public static bool forceASCII = false; // Use Unicode by default public static bool longAsString = false; // lazy creator creates a JSONString instead of JSONNumber public static bool allowLineComments = true; // allow "//"-style comments at the end of a line public abstract JSONNodeType Tag { get; } public virtual JSONNode this[int aIndex] { get { return null; } set { } } public virtual JSONNode this[string aKey] { get { return null; } set { } } public virtual string Value { get { return ""; } set { } } public virtual int Count { get { return 0; } } public virtual bool IsNumber { get { return false; } } public virtual bool IsString { get { return false; } } public virtual bool IsBoolean { get { return false; } } public virtual bool IsNull { get { return false; } } public virtual bool IsArray { get { return false; } } public virtual bool IsObject { get { return false; } } public virtual bool Inline { get { return false; } set { } } public virtual void Add(string aKey, JSONNode aItem) { } public virtual void Add(JSONNode aItem) { Add("", aItem); } public virtual JSONNode Remove(string aKey) { return null; } public virtual JSONNode Remove(int aIndex) { return null; } public virtual JSONNode Remove(JSONNode aNode) { return aNode; } public virtual IEnumerable Children { get { yield break; } } public IEnumerable DeepChildren { get { foreach (var C in Children) foreach (var D in C.DeepChildren) yield return D; } } public virtual bool HasKey(string aKey) { return false; } public virtual JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) { return aDefault; } public override string ToString() { StringBuilder sb = new StringBuilder(); WriteToStringBuilder(sb, 0, 0, JSONTextMode.Compact); return sb.ToString(); } public virtual string ToString(int aIndent) { StringBuilder sb = new StringBuilder(); WriteToStringBuilder(sb, 0, aIndent, JSONTextMode.Indent); return sb.ToString(); } internal abstract void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode); public abstract Enumerator GetEnumerator(); public IEnumerable> Linq { get { return new LinqEnumerator(this); } } public KeyEnumerator Keys { get { return new KeyEnumerator(GetEnumerator()); } } public ValueEnumerator Values { get { return new ValueEnumerator(GetEnumerator()); } } #endregion common interface #region typecasting properties public virtual double AsDouble { get { double v = 0.0; if (double.TryParse(Value,NumberStyles.Float, CultureInfo.InvariantCulture, out v)) return v; return 0.0; } set { Value = value.ToString(CultureInfo.InvariantCulture); } } public virtual int AsInt { get { return (int)AsDouble; } set { AsDouble = value; } } public virtual float AsFloat { get { return (float)AsDouble; } set { AsDouble = value; } } public virtual bool AsBool { get { bool v = false; if (bool.TryParse(Value, out v)) return v; return !string.IsNullOrEmpty(Value); } set { Value = (value) ? "true" : "false"; } } public virtual long AsLong { get { long val = 0; if (long.TryParse(Value, out val)) return val; return 0L; } set { Value = value.ToString(); } } public virtual JSONArray AsArray { get { return this as JSONArray; } } public virtual JSONObject AsObject { get { return this as JSONObject; } } #endregion typecasting properties #region operators public static implicit operator JSONNode(string s) { return new JSONString(s); } public static implicit operator string(JSONNode d) { return (d == null) ? null : d.Value; } public static implicit operator JSONNode(double n) { return new JSONNumber(n); } public static implicit operator double(JSONNode d) { return (d == null) ? 0 : d.AsDouble; } public static implicit operator JSONNode(float n) { return new JSONNumber(n); } public static implicit operator float(JSONNode d) { return (d == null) ? 0 : d.AsFloat; } public static implicit operator JSONNode(int n) { return new JSONNumber(n); } public static implicit operator int(JSONNode d) { return (d == null) ? 0 : d.AsInt; } public static implicit operator JSONNode(long n) { if (longAsString) return new JSONString(n.ToString()); return new JSONNumber(n); } public static implicit operator long(JSONNode d) { return (d == null) ? 0L : d.AsLong; } public static implicit operator JSONNode(bool b) { return new JSONBool(b); } public static implicit operator bool(JSONNode d) { return (d == null) ? false : d.AsBool; } public static implicit operator JSONNode(KeyValuePair aKeyValue) { return aKeyValue.Value; } public static bool operator ==(JSONNode a, object b) { if (ReferenceEquals(a, b)) return true; bool aIsNull = a is JSONNull || ReferenceEquals(a, null) || a is JSONLazyCreator; bool bIsNull = b is JSONNull || ReferenceEquals(b, null) || b is JSONLazyCreator; if (aIsNull && bIsNull) return true; return !aIsNull && a.Equals(b); } public static bool operator !=(JSONNode a, object b) { return !(a == b); } public override bool Equals(object obj) { return ReferenceEquals(this, obj); } public override int GetHashCode() { return base.GetHashCode(); } #endregion operators [ThreadStatic] private static StringBuilder m_EscapeBuilder; internal static StringBuilder EscapeBuilder { get { if (m_EscapeBuilder == null) m_EscapeBuilder = new StringBuilder(); return m_EscapeBuilder; } } internal static string Escape(string aText) { var sb = EscapeBuilder; sb.Length = 0; if (sb.Capacity < aText.Length + aText.Length / 10) sb.Capacity = aText.Length + aText.Length / 10; foreach (char c in aText) { switch (c) { case '\\': sb.Append("\\\\"); break; case '\"': sb.Append("\\\""); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; case '\b': sb.Append("\\b"); break; case '\f': sb.Append("\\f"); break; default: if (c < ' ' || (forceASCII && c > 127)) { ushort val = c; sb.Append("\\u").Append(val.ToString("X4")); } else sb.Append(c); break; } } string result = sb.ToString(); sb.Length = 0; return result; } private static JSONNode ParseElement(string token, bool quoted) { if (quoted) return token; string tmp = token.ToLower(); if (tmp == "false" || tmp == "true") return tmp == "true"; if (tmp == "null") return JSONNull.CreateOrGet(); double val; if (double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out val)) return val; else return token; } public static JSONNode Parse(string aJSON) { Stack stack = new Stack(); JSONNode ctx = null; int i = 0; StringBuilder Token = new StringBuilder(); string TokenName = ""; bool QuoteMode = false; bool TokenIsQuoted = false; while (i < aJSON.Length) { switch (aJSON[i]) { case '{': if (QuoteMode) { Token.Append(aJSON[i]); break; } stack.Push(new JSONObject()); if (ctx != null) { ctx.Add(TokenName, stack.Peek()); } TokenName = ""; Token.Length = 0; ctx = stack.Peek(); break; case '[': if (QuoteMode) { Token.Append(aJSON[i]); break; } stack.Push(new JSONArray()); if (ctx != null) { ctx.Add(TokenName, stack.Peek()); } TokenName = ""; Token.Length = 0; ctx = stack.Peek(); break; case '}': case ']': if (QuoteMode) { Token.Append(aJSON[i]); break; } if (stack.Count == 0) throw new Exception("JSON Parse: Too many closing brackets"); stack.Pop(); if (Token.Length > 0 || TokenIsQuoted) ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); TokenIsQuoted = false; TokenName = ""; Token.Length = 0; if (stack.Count > 0) ctx = stack.Peek(); break; case ':': if (QuoteMode) { Token.Append(aJSON[i]); break; } TokenName = Token.ToString(); Token.Length = 0; TokenIsQuoted = false; break; case '"': QuoteMode ^= true; TokenIsQuoted |= QuoteMode; break; case ',': if (QuoteMode) { Token.Append(aJSON[i]); break; } if (Token.Length > 0 || TokenIsQuoted) ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); TokenIsQuoted = false; TokenName = ""; Token.Length = 0; TokenIsQuoted = false; break; case '\r': case '\n': break; case ' ': case '\t': if (QuoteMode) Token.Append(aJSON[i]); break; case '\\': ++i; if (QuoteMode) { char C = aJSON[i]; switch (C) { case 't': Token.Append('\t'); break; case 'r': Token.Append('\r'); break; case 'n': Token.Append('\n'); break; case 'b': Token.Append('\b'); break; case 'f': Token.Append('\f'); break; case 'u': { string s = aJSON.Substring(i + 1, 4); Token.Append((char)int.Parse( s, System.Globalization.NumberStyles.AllowHexSpecifier)); i += 4; break; } default: Token.Append(C); break; } } break; case '/': if (allowLineComments && !QuoteMode && i + 1 < aJSON.Length && aJSON[i+1] == '/') { while (++i < aJSON.Length && aJSON[i] != '\n' && aJSON[i] != '\r') ; break; } Token.Append(aJSON[i]); break; case '\uFEFF': // remove / ignore BOM (Byte Order Mark) break; default: Token.Append(aJSON[i]); break; } ++i; } if (QuoteMode) { throw new Exception("JSON Parse: Quotation marks seems to be messed up."); } if (ctx == null) return ParseElement(Token.ToString(), TokenIsQuoted); return ctx; } } // End of JSONNode public partial class JSONArray : JSONNode { private List m_List = new List(); private bool inline = false; public override bool Inline { get { return inline; } set { inline = value; } } public override JSONNodeType Tag { get { return JSONNodeType.Array; } } public override bool IsArray { get { return true; } } public override Enumerator GetEnumerator() { return new Enumerator(m_List.GetEnumerator()); } public override JSONNode this[int aIndex] { get { if (aIndex < 0 || aIndex >= m_List.Count) return new JSONLazyCreator(this); return m_List[aIndex]; } set { if (value == null) value = JSONNull.CreateOrGet(); if (aIndex < 0 || aIndex >= m_List.Count) m_List.Add(value); else m_List[aIndex] = value; } } public override JSONNode this[string aKey] { get { return new JSONLazyCreator(this); } set { if (value == null) value = JSONNull.CreateOrGet(); m_List.Add(value); } } public override int Count { get { return m_List.Count; } } public override void Add(string aKey, JSONNode aItem) { if (aItem == null) aItem = JSONNull.CreateOrGet(); m_List.Add(aItem); } public override JSONNode Remove(int aIndex) { if (aIndex < 0 || aIndex >= m_List.Count) return null; JSONNode tmp = m_List[aIndex]; m_List.RemoveAt(aIndex); return tmp; } public override JSONNode Remove(JSONNode aNode) { m_List.Remove(aNode); return aNode; } public override IEnumerable Children { get { foreach (JSONNode N in m_List) yield return N; } } internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) { aSB.Append('['); int count = m_List.Count; if (inline) aMode = JSONTextMode.Compact; for (int i = 0; i < count; i++) { if (i > 0) aSB.Append(','); if (aMode == JSONTextMode.Indent) aSB.AppendLine(); if (aMode == JSONTextMode.Indent) aSB.Append(' ', aIndent + aIndentInc); m_List[i].WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); } if (aMode == JSONTextMode.Indent) aSB.AppendLine().Append(' ', aIndent); aSB.Append(']'); } } // End of JSONArray public partial class JSONObject : JSONNode { public Dictionary m_Dict = new Dictionary(); private bool inline = false; public override bool Inline { get { return inline; } set { inline = value; } } public override JSONNodeType Tag { get { return JSONNodeType.Object; } } public override bool IsObject { get { return true; } } public override Enumerator GetEnumerator() { return new Enumerator(m_Dict.GetEnumerator()); } public override JSONNode this[string aKey] { get { if (m_Dict.ContainsKey(aKey)) return m_Dict[aKey]; else return new JSONLazyCreator(this, aKey); } set { if (value == null) value = JSONNull.CreateOrGet(); if (m_Dict.ContainsKey(aKey)) m_Dict[aKey] = value; else m_Dict.Add(aKey, value); } } public override JSONNode this[int aIndex] { get { if (aIndex < 0 || aIndex >= m_Dict.Count) return null; return m_Dict.ElementAt(aIndex).Value; } set { if (value == null) value = JSONNull.CreateOrGet(); if (aIndex < 0 || aIndex >= m_Dict.Count) return; string key = m_Dict.ElementAt(aIndex).Key; m_Dict[key] = value; } } public override int Count { get { return m_Dict.Count; } } public override void Add(string aKey, JSONNode aItem) { if (aItem == null) aItem = JSONNull.CreateOrGet(); if (aKey != null) { if (m_Dict.ContainsKey(aKey)) m_Dict[aKey] = aItem; else m_Dict.Add(aKey, aItem); } else m_Dict.Add(Guid.NewGuid().ToString(), aItem); } public override JSONNode Remove(string aKey) { if (!m_Dict.ContainsKey(aKey)) return null; JSONNode tmp = m_Dict[aKey]; m_Dict.Remove(aKey); return tmp; } public override JSONNode Remove(int aIndex) { if (aIndex < 0 || aIndex >= m_Dict.Count) return null; var item = m_Dict.ElementAt(aIndex); m_Dict.Remove(item.Key); return item.Value; } public override JSONNode Remove(JSONNode aNode) { try { var item = m_Dict.Where(k => k.Value == aNode).First(); m_Dict.Remove(item.Key); return aNode; } catch { return null; } } public override bool HasKey(string aKey) { return m_Dict.ContainsKey(aKey); } public override JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) { JSONNode res; if (m_Dict.TryGetValue(aKey, out res)) return res; return aDefault; } public override IEnumerable Children { get { foreach (KeyValuePair N in m_Dict) yield return N.Value; } } internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) { aSB.Append('{'); bool first = true; if (inline) aMode = JSONTextMode.Compact; foreach (var k in m_Dict) { if (!first) aSB.Append(','); first = false; if (aMode == JSONTextMode.Indent) aSB.AppendLine(); if (aMode == JSONTextMode.Indent) aSB.Append(' ', aIndent + aIndentInc); aSB.Append('\"').Append(Escape(k.Key)).Append('\"'); if (aMode == JSONTextMode.Compact) aSB.Append(':'); else aSB.Append(" : "); k.Value.WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); } if (aMode == JSONTextMode.Indent) aSB.AppendLine().Append(' ', aIndent); aSB.Append('}'); } } // End of JSONObject public partial class JSONString : JSONNode { private string m_Data; public override JSONNodeType Tag { get { return JSONNodeType.String; } } public override bool IsString { get { return true; } } public override Enumerator GetEnumerator() { return new Enumerator(); } public override string Value { get { return m_Data; } set { m_Data = value; } } public JSONString(string aData) { m_Data = aData; } internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) { aSB.Append('\"').Append(Escape(m_Data)).Append('\"'); } public override bool Equals(object obj) { if (base.Equals(obj)) return true; string s = obj as string; if (s != null) return m_Data == s; JSONString s2 = obj as JSONString; if (s2 != null) return m_Data == s2.m_Data; return false; } public override int GetHashCode() { return m_Data.GetHashCode(); } } // End of JSONString public partial class JSONNumber : JSONNode { private double m_Data; public override JSONNodeType Tag { get { return JSONNodeType.Number; } } public override bool IsNumber { get { return true; } } public override Enumerator GetEnumerator() { return new Enumerator(); } public override string Value { get { return m_Data.ToString(CultureInfo.InvariantCulture); } set { double v; if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out v)) m_Data = v; } } public override double AsDouble { get { return m_Data; } set { m_Data = value; } } public override long AsLong { get { return (long)m_Data; } set { m_Data = value; } } public JSONNumber(double aData) { m_Data = aData; } public JSONNumber(string aData) { Value = aData; } internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) { aSB.Append(Value); } private static bool IsNumeric(object value) { return value is int || value is uint || value is float || value is double || value is decimal || value is long || value is ulong || value is short || value is ushort || value is sbyte || value is byte; } public override bool Equals(object obj) { if (obj == null) return false; if (base.Equals(obj)) return true; JSONNumber s2 = obj as JSONNumber; if (s2 != null) return m_Data == s2.m_Data; if (IsNumeric(obj)) return Convert.ToDouble(obj) == m_Data; return false; } public override int GetHashCode() { return m_Data.GetHashCode(); } } // End of JSONNumber public partial class JSONBool : JSONNode { private bool m_Data; public override JSONNodeType Tag { get { return JSONNodeType.Boolean; } } public override bool IsBoolean { get { return true; } } public override Enumerator GetEnumerator() { return new Enumerator(); } public override string Value { get { return m_Data.ToString(); } set { bool v; if (bool.TryParse(value, out v)) m_Data = v; } } public override bool AsBool { get { return m_Data; } set { m_Data = value; } } public JSONBool(bool aData) { m_Data = aData; } public JSONBool(string aData) { Value = aData; } internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) { aSB.Append((m_Data) ? "true" : "false"); } public override bool Equals(object obj) { if (obj == null) return false; if (obj is bool) return m_Data == (bool)obj; return false; } public override int GetHashCode() { return m_Data.GetHashCode(); } } // End of JSONBool public partial class JSONNull : JSONNode { static JSONNull m_StaticInstance = new JSONNull(); public static bool reuseSameInstance = true; public static JSONNull CreateOrGet() { if (reuseSameInstance) return m_StaticInstance; return new JSONNull(); } private JSONNull() { } public override JSONNodeType Tag { get { return JSONNodeType.NullValue; } } public override bool IsNull { get { return true; } } public override Enumerator GetEnumerator() { return new Enumerator(); } public override string Value { get { return "null"; } set { } } public override bool AsBool { get { return false; } set { } } public override bool Equals(object obj) { if (object.ReferenceEquals(this, obj)) return true; return (obj is JSONNull); } public override int GetHashCode() { return 0; } internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) { aSB.Append("null"); } } // End of JSONNull internal partial class JSONLazyCreator : JSONNode { private JSONNode m_Node = null; private string m_Key = null; public override JSONNodeType Tag { get { return JSONNodeType.None; } } public override Enumerator GetEnumerator() { return new Enumerator(); } public JSONLazyCreator(JSONNode aNode) { m_Node = aNode; m_Key = null; } public JSONLazyCreator(JSONNode aNode, string aKey) { m_Node = aNode; m_Key = aKey; } private T Set(T aVal) where T : JSONNode { if (m_Key == null) m_Node.Add(aVal); else m_Node.Add(m_Key, aVal); m_Node = null; // Be GC friendly. return aVal; } public override JSONNode this[int aIndex] { get { return new JSONLazyCreator(this); } set { Set(new JSONArray()).Add(value); } } public override JSONNode this[string aKey] { get { return new JSONLazyCreator(this, aKey); } set { Set(new JSONObject()).Add(aKey, value); } } public override void Add(JSONNode aItem) { Set(new JSONArray()).Add(aItem); } public override void Add(string aKey, JSONNode aItem) { Set(new JSONObject()).Add(aKey, aItem); } public static bool operator ==(JSONLazyCreator a, object b) { if (b == null) return true; return System.Object.ReferenceEquals(a, b); } public static bool operator !=(JSONLazyCreator a, object b) { return !(a == b); } public override bool Equals(object obj) { if (obj == null) return true; return System.Object.ReferenceEquals(this, obj); } public override int GetHashCode() { return 0; } public override int AsInt { get { Set(new JSONNumber(0)); return 0; } set { Set(new JSONNumber(value)); } } public override float AsFloat { get { Set(new JSONNumber(0.0f)); return 0.0f; } set { Set(new JSONNumber(value)); } } public override double AsDouble { get { Set(new JSONNumber(0.0)); return 0.0; } set { Set(new JSONNumber(value)); } } public override long AsLong { get { if (longAsString) Set(new JSONString("0")); else Set(new JSONNumber(0.0)); return 0L; } set { if (longAsString) Set(new JSONString(value.ToString())); else Set(new JSONNumber(value)); } } public override bool AsBool { get { Set(new JSONBool(false)); return false; } set { Set(new JSONBool(value)); } } public override JSONArray AsArray { get { return Set(new JSONArray()); } } public override JSONObject AsObject { get { return Set(new JSONObject()); } } internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) { aSB.Append("null"); } } // End of JSONLazyCreator public static class JSON { public static JSONNode Parse(string aJSON) { return JSONNode.Parse(aJSON); } } } ================================================ FILE: csharp/graph.JSON ================================================ {"last_node_id":15,"last_link_id":26,"nodes":[{"id":5,"type":"graph/output","pos":[1265,331],"size":[180,60],"flags":{"collapsed":false},"order":4,"mode":0,"inputs":[{"name":"","type":"number","link":24}],"properties":{"name":"output","type":"number"}},{"id":13,"type":"basic/watch","pos":[1228,213],"size":{"0":140,"1":26},"flags":{},"order":5,"mode":0,"inputs":[{"name":"value","type":0,"link":25,"label":"0.000"}],"properties":{}},{"id":9,"type":"math/condition","pos":[754,261],"size":[180,61],"flags":{},"order":2,"mode":0,"inputs":[{"name":"A","type":"number","link":9},{"name":"B","type":"number","link":10}],"outputs":[{"name":"true","type":"boolean","links":[21]},{"name":"false","type":"boolean","links":null}],"properties":{"A":6.957165000028908,"B":4,"OP":"<"}},{"id":7,"type":"basic/time","pos":[406,262],"size":{"0":140,"1":46},"flags":{},"order":0,"mode":0,"outputs":[{"name":"in ms","type":"number","links":null},{"name":"in sec","type":"number","links":[9,22]}],"properties":{}},{"id":10,"type":"basic/const","pos":[298,560],"size":{"0":140,"1":26},"flags":{},"order":1,"mode":0,"outputs":[{"name":"value","type":"number","links":[10,23],"label":"4.000"}],"properties":{"value":4}},{"id":15,"type":"math/gate","pos":[961,483],"size":{"0":140,"1":66},"flags":{},"order":3,"mode":0,"inputs":[{"name":"v","type":"boolean","link":21},{"name":"A","type":0,"link":22},{"name":"B","type":0,"link":23}],"outputs":[{"name":"out","links":[24,25]}],"properties":{}}],"links":[[9,7,1,9,0,"number"],[10,10,0,9,1,"number"],[21,9,0,15,0,"boolean"],[22,7,1,15,1,0],[23,10,0,15,2,0],[24,15,0,5,0,"number"],[25,15,0,13,0,0]],"groups":[],"config":{},"version":0.4} ================================================ FILE: csharp/readme.md ================================================ # C SHARP This code allows to execute a subset of nodes of LiteGraph directly in Unity. Still a work in progress. ================================================ FILE: css/litegraph-editor.css ================================================ .litegraph-editor { width: 100%; height: 100%; margin: 0; padding: 0; background-color: #333; color: #eee; font: 14px Tahoma; position: relative; } .litegraph-editor h1 { font-family: "Metro Light", Tahoma; color: #ddd; font-size: 28px; padding-left: 10px; /*text-shadow: 0 1px 1px #333, 0 -1px 1px #777;*/ margin: 0; font-weight: normal; } .litegraph-editor h1 span { font-family: "Arial"; font-size: 14px; font-weight: normal; color: #aaa; } .litegraph-editor h2 { font-family: "Metro Light"; padding: 5px; margin-left: 10px; } .litegraph-editor * { box-sizing: border-box; -moz-box-sizing: border-box; } .litegraph-editor .content { position: relative; width: 100%; height: calc(100% - 80px); background-color: #1a1a1a; } .litegraph-editor .header, .litegraph-editor .footer { position: relative; height: 40px; background-color: #333; /*border-radius: 10px 10px 0 0;*/ } .litegraph-editor .tools, .litegraph-editor .tools-left, .litegraph-editor .tools-right { position: absolute; top: 2px; right: 0px; vertical-align: top; margin: 2px 5px 0 0px; } .litegraph-editor .tools-left { right: auto; left: 4px; } .litegraph-editor .footer { height: 40px; position: relative; /*border-radius: 0 0 10px 10px;*/ } .litegraph-editor .miniwindow { background-color: #333; border: 1px solid #111; } .litegraph-editor .miniwindow .corner-button { position: absolute; top: 2px; right: 2px; font-family: "Tahoma"; font-size: 14px; color: #aaa; cursor: pointer; } /* BUTTONS **********************/ .litegraph-editor .btn { /*font-family: "Metro Light";*/ color: #ccc; font-size: 20px; min-width: 30px; /*border-radius: 0.3em;*/ border: 0 solid #666; background-color: #3f3f3f; /*box-shadow: 0 0 3px black;*/ padding: 4px 10px; cursor: pointer; transition: all 1s; -moz-transition: all 1s; -webkit-transition: all 0.4s; } .litegraph-editor button:hover { background-color: #999; color: #fff; transition: all 1s; -moz-transition: all 1s; -webkit-transition: all 0.4s; } .litegraph-editor button:active { background-color: white; } .litegraph-editor button.fixed { position: absolute; top: 5px; right: 5px; font-size: 1.2em; } .litegraph-editor button img { margin: -4px; vertical-align: top; opacity: 0.8; transition: all 1s; } .litegraph-editor button:hover img { opacity: 1; } .litegraph-editor .header button { height: 32px; vertical-align: top; } .litegraph-editor .footer button { /*font-size: 16px;*/ } .litegraph-editor .toolbar-widget { display: inline-block; } .litegraph-editor .editor-area { width: 100%; height: 100%; } /* METER *********************/ .litegraph-editor .loadmeter { font-family: "Tahoma"; color: #aaa; font-size: 12px; border-radius: 2px; width: 130px; vertical-align: top; } .litegraph-editor .strong { vertical-align: top; padding: 3px; width: 30px; display: inline-block; line-height: 8px; } .litegraph-editor .cpuload .bgload, .litegraph-editor .gpuload .bgload { display: inline-block; width: 90px; height: 15px; background-image: url("../editor/imgs/load-progress-empty.png"); } .litegraph-editor .cpuload .fgload, .litegraph-editor .gpuload .fgload { display: inline-block; width: 4px; height: 15px; max-width: 90px; background-image: url("../editor/imgs/load-progress-full.png"); } .litegraph-editor textarea.code, .litegraph-editor div.code { height: 100%; width: 100%; background-color: black; padding: 4px; font: 16px monospace; overflow: auto; resize: none; outline: none; color: #DDD; } .litegraph-editor .codeflask { background-color: #2a2a2a; } .litegraph-editor .codeflask textarea { opacity: 0; } ================================================ FILE: css/litegraph.css ================================================ /* this CSS contains only the basic CSS needed to run the app and use it */ .lgraphcanvas { /*cursor: crosshair;*/ user-select: none; -moz-user-select: none; -webkit-user-select: none; outline: none; font-family: Tahoma, sans-serif; } .lgraphcanvas * { box-sizing: border-box; } .litegraph.litecontextmenu { font-family: Tahoma, sans-serif; position: fixed; top: 100px; left: 100px; min-width: 100px; color: #aaf; padding: 0; box-shadow: 0 0 10px black !important; background-color: #2e2e2e !important; z-index: 10; } .litegraph.litecontextmenu.dark { background-color: #000 !important; } .litegraph.litecontextmenu .litemenu-title img { margin-top: 2px; margin-left: 2px; margin-right: 4px; } .litegraph.litecontextmenu .litemenu-entry { margin: 2px; padding: 2px; } .litegraph.litecontextmenu .litemenu-entry.submenu { background-color: #2e2e2e !important; } .litegraph.litecontextmenu.dark .litemenu-entry.submenu { background-color: #000 !important; } .litegraph .litemenubar ul { font-family: Tahoma, sans-serif; margin: 0; padding: 0; } .litegraph .litemenubar li { font-size: 14px; color: #999; display: inline-block; min-width: 50px; padding-left: 10px; padding-right: 10px; user-select: none; -moz-user-select: none; -webkit-user-select: none; cursor: pointer; } .litegraph .litemenubar li:hover { background-color: #777; color: #eee; } .litegraph .litegraph .litemenubar-panel { position: absolute; top: 5px; left: 5px; min-width: 100px; background-color: #444; box-shadow: 0 0 3px black; padding: 4px; border-bottom: 2px solid #aaf; z-index: 10; } .litegraph .litemenu-entry, .litemenu-title { font-size: 12px; color: #aaa; padding: 0 0 0 4px; margin: 2px; padding-left: 2px; -moz-user-select: none; -webkit-user-select: none; user-select: none; cursor: pointer; } .litegraph .litemenu-entry .icon { display: inline-block; width: 12px; height: 12px; margin: 2px; vertical-align: top; } .litegraph .litemenu-entry.checked .icon { background-color: #aaf; } .litegraph .litemenu-entry .more { float: right; padding-right: 5px; } .litegraph .litemenu-entry.disabled { opacity: 0.5; cursor: default; } .litegraph .litemenu-entry.separator { display: block; border-top: 1px solid #333; border-bottom: 1px solid #666; width: 100%; height: 0px; margin: 3px 0 2px 0; background-color: transparent; padding: 0 !important; cursor: default !important; } .litegraph .litemenu-entry.has_submenu { border-right: 2px solid cyan; } .litegraph .litemenu-title { color: #dde; background-color: #111; margin: 0; padding: 2px; cursor: default; } .litegraph .litemenu-entry:hover:not(.disabled):not(.separator) { background-color: #444 !important; color: #eee; transition: all 0.2s; } .litegraph .litemenu-entry .property_name { display: inline-block; text-align: left; min-width: 80px; min-height: 1.2em; } .litegraph .litemenu-entry .property_value { display: inline-block; background-color: rgba(0, 0, 0, 0.5); text-align: right; min-width: 80px; min-height: 1.2em; vertical-align: middle; padding-right: 10px; } .litegraph.litesearchbox { font-family: Tahoma, sans-serif; position: absolute; background-color: rgba(0, 0, 0, 0.5); padding-top: 4px; } .litegraph.litesearchbox input, .litegraph.litesearchbox select { margin-top: 3px; min-width: 60px; min-height: 1.5em; background-color: black; border: 0; color: white; padding-left: 10px; margin-right: 5px; } .litegraph.litesearchbox .name { display: inline-block; min-width: 60px; min-height: 1.5em; padding-left: 10px; } .litegraph.litesearchbox .helper { overflow: auto; max-height: 200px; margin-top: 2px; } .litegraph.lite-search-item { font-family: Tahoma, sans-serif; background-color: rgba(0, 0, 0, 0.5); color: white; padding-top: 2px; } .litegraph.lite-search-item.not_in_filter{ /*background-color: rgba(50, 50, 50, 0.5);*/ /*color: #999;*/ color: #B99; font-style: italic; } .litegraph.lite-search-item.generic_type{ /*background-color: rgba(50, 50, 50, 0.5);*/ /*color: #DD9;*/ color: #999; font-style: italic; } .litegraph.lite-search-item:hover, .litegraph.lite-search-item.selected { cursor: pointer; background-color: white; color: black; } /* DIALOGs ******/ .litegraph .dialog { position: absolute; top: 50%; left: 50%; margin-top: -150px; margin-left: -200px; background-color: #2A2A2A; min-width: 400px; min-height: 200px; box-shadow: 0 0 4px #111; border-radius: 6px; } .litegraph .dialog.settings { left: 10px; top: 10px; height: calc( 100% - 20px ); margin: auto; max-width: 50%; } .litegraph .dialog.centered { top: 50px; left: 50%; position: absolute; transform: translateX(-50%); min-width: 600px; min-height: 300px; height: calc( 100% - 100px ); margin: auto; } .litegraph .dialog .close { float: right; margin: 4px; margin-right: 10px; cursor: pointer; font-size: 1.4em; } .litegraph .dialog .close:hover { color: white; } .litegraph .dialog .dialog-header { color: #AAA; border-bottom: 1px solid #161616; } .litegraph .dialog .dialog-header { height: 40px; } .litegraph .dialog .dialog-footer { height: 50px; padding: 10px; border-top: 1px solid #1a1a1a;} .litegraph .dialog .dialog-header .dialog-title { font: 20px "Arial"; margin: 4px; padding: 4px 10px; display: inline-block; } .litegraph .dialog .dialog-content, .litegraph .dialog .dialog-alt-content { height: calc(100% - 90px); width: 100%; min-height: 100px; display: inline-block; color: #AAA; /*background-color: black;*/ overflow: auto; } .litegraph .dialog .dialog-content h3 { margin: 10px; } .litegraph .dialog .dialog-content .connections { flex-direction: row; } .litegraph .dialog .dialog-content .connections .connections_side { width: calc(50% - 5px); min-height: 100px; background-color: black; display: flex; } .litegraph .dialog .node_type { font-size: 1.2em; display: block; margin: 10px; } .litegraph .dialog .node_desc { opacity: 0.5; display: block; margin: 10px; } .litegraph .dialog .separator { display: block; width: calc( 100% - 4px ); height: 1px; border-top: 1px solid #000; border-bottom: 1px solid #333; margin: 10px 2px; padding: 0; } .litegraph .dialog .property { margin-bottom: 2px; padding: 4px; } .litegraph .dialog .property:hover { background: #545454; } .litegraph .dialog .property_name { color: #737373; display: inline-block; text-align: left; vertical-align: top; width: 160px; padding-left: 4px; overflow: hidden; margin-right: 6px; } .litegraph .dialog .property:hover .property_name { color: white; } .litegraph .dialog .property_value { display: inline-block; text-align: right; color: #AAA; background-color: #1A1A1A; /*width: calc( 100% - 122px );*/ max-width: calc( 100% - 162px ); min-width: 200px; max-height: 300px; min-height: 20px; padding: 4px; padding-right: 12px; overflow: hidden; cursor: pointer; border-radius: 3px; } .litegraph .dialog .property_value:hover { color: white; } .litegraph .dialog .property.boolean .property_value { padding-right: 30px; color: #A88; /*width: auto; float: right;*/ } .litegraph .dialog .property.boolean.bool-on .property_name{ color: #8A8; } .litegraph .dialog .property.boolean.bool-on .property_value{ color: #8A8; } .litegraph .dialog .btn { border: 0; border-radius: 4px; padding: 4px 20px; margin-left: 0px; background-color: #060606; color: #8e8e8e; } .litegraph .dialog .btn:hover { background-color: #111; color: #FFF; } .litegraph .dialog .btn.delete:hover { background-color: #F33; color: black; } .litegraph .subgraph_property { padding: 4px; } .litegraph .subgraph_property:hover { background-color: #333; } .litegraph .subgraph_property.extra { margin-top: 8px; } .litegraph .subgraph_property span.name { font-size: 1.3em; padding-left: 4px; } .litegraph .subgraph_property span.type { opacity: 0.5; margin-right: 20px; padding-left: 4px; } .litegraph .subgraph_property span.label { display: inline-block; width: 60px; padding: 0px 10px; } .litegraph .subgraph_property input { width: 140px; color: #999; background-color: #1A1A1A; border-radius: 4px; border: 0; margin-right: 10px; padding: 4px; padding-left: 10px; } .litegraph .subgraph_property button { background-color: #1c1c1c; color: #aaa; border: 0; border-radius: 2px; padding: 4px 10px; cursor: pointer; } .litegraph .subgraph_property.extra { color: #ccc; } .litegraph .subgraph_property.extra input { background-color: #111; } .litegraph .bullet_icon { margin-left: 10px; border-radius: 10px; width: 12px; height: 12px; background-color: #666; display: inline-block; margin-top: 2px; margin-right: 4px; transition: background-color 0.1s ease 0s; -moz-transition: background-color 0.1s ease 0s; } .litegraph .bullet_icon:hover { background-color: #698; cursor: pointer; } /* OLD */ .graphcontextmenu { padding: 4px; min-width: 100px; } .graphcontextmenu-title { color: #dde; background-color: #222; margin: 0; padding: 2px; cursor: default; } .graphmenu-entry { box-sizing: border-box; margin: 2px; padding-left: 20px; user-select: none; -moz-user-select: none; -webkit-user-select: none; transition: all linear 0.3s; } .graphmenu-entry.event, .litemenu-entry.event { border-left: 8px solid orange; padding-left: 12px; } .graphmenu-entry.disabled { opacity: 0.3; } .graphmenu-entry.submenu { border-right: 2px solid #eee; } .graphmenu-entry:hover { background-color: #555; } .graphmenu-entry.separator { background-color: #111; border-bottom: 1px solid #666; height: 1px; width: calc(100% - 20px); -moz-width: calc(100% - 20px); -webkit-width: calc(100% - 20px); } .graphmenu-entry .property_name { display: inline-block; text-align: left; min-width: 80px; min-height: 1.2em; } .graphmenu-entry .property_value, .litemenu-entry .property_value { display: inline-block; background-color: rgba(0, 0, 0, 0.5); text-align: right; min-width: 80px; min-height: 1.2em; vertical-align: middle; padding-right: 10px; } .graphdialog { position: absolute; top: 10px; left: 10px; min-height: 2em; background-color: #333; font-size: 1.2em; box-shadow: 0 0 10px black !important; z-index: 10; } .graphdialog.rounded { border-radius: 12px; padding-right: 2px; } .graphdialog .name { display: inline-block; min-width: 60px; min-height: 1.5em; padding-left: 10px; } .graphdialog input, .graphdialog textarea, .graphdialog select { margin: 3px; min-width: 60px; min-height: 1.5em; background-color: black; border: 0; color: white; padding-left: 10px; outline: none; } .graphdialog textarea { min-height: 150px; } .graphdialog button { margin-top: 3px; vertical-align: top; background-color: #999; border: 0; } .graphdialog button.rounded, .graphdialog input.rounded { border-radius: 0 12px 12px 0; } .graphdialog .helper { overflow: auto; max-height: 200px; } .graphdialog .help-item { padding-left: 10px; } .graphdialog .help-item:hover, .graphdialog .help-item.selected { cursor: pointer; background-color: white; color: black; } .litegraph .dialog { min-height: 0; } .litegraph .dialog .dialog-content { display: block; } .litegraph .dialog .dialog-content .subgraph_property { padding: 5px; } .litegraph .dialog .dialog-footer { margin: 0; } .litegraph .dialog .dialog-footer .subgraph_property { margin-top: 0; display: flex; align-items: center; padding: 5px; } .litegraph .dialog .dialog-footer .subgraph_property .name { flex: 1; } .litegraph .graphdialog { display: flex; align-items: center; border-radius: 20px; padding: 4px 10px; position: fixed; } .litegraph .graphdialog .name { padding: 0; min-height: 0; font-size: 16px; vertical-align: middle; } .litegraph .graphdialog .value { font-size: 16px; min-height: 0; margin: 0 10px; padding: 2px 5px; } .litegraph .graphdialog input[type="checkbox"] { width: 16px; height: 16px; } .litegraph .graphdialog button { padding: 4px 18px; border-radius: 20px; cursor: pointer; } ================================================ FILE: doc/api.js ================================================ YUI.add("yuidoc-meta", function(Y) { Y.YUIDoc = { meta: { "classes": [ "ContextMenu", "LGraph", "LGraphCanvas", "LGraphNode", "LiteGraph" ], "modules": [], "allModules": [], "elements": [] } }; }); ================================================ FILE: doc/assets/css/main.css ================================================ /* Font sizes for all selectors other than the body are given in percentages, with 100% equal to 13px. To calculate a font size percentage, multiply the desired size in pixels by 7.6923076923. Here's a quick lookup table: 10px - 76.923% 11px - 84.615% 12px - 92.308% 13px - 100% 14px - 107.692% 15px - 115.385% 16px - 123.077% 17px - 130.769% 18px - 138.462% 19px - 146.154% 20px - 153.846% */ html { background: #fff; color: #333; overflow-y: scroll; } body { /*font: 13px/1.4 'Lucida Grande', 'Lucida Sans Unicode', 'DejaVu Sans', 'Bitstream Vera Sans', 'Helvetica', 'Arial', sans-serif;*/ font: 13px/1.4 'Helvetica', 'Arial', sans-serif; margin: 0; padding: 0; } /* -- Links ----------------------------------------------------------------- */ a { color: #356de4; text-decoration: none; } .hidden { display: none; } a:hover { text-decoration: underline; } /* "Jump to Table of Contents" link is shown to assistive tools, but hidden from sight until it's focused. */ .jump { position: absolute; padding: 3px 6px; left: -99999px; top: 0; } .jump:focus { left: 40%; } /* -- Paragraphs ------------------------------------------------------------ */ p { margin: 1.3em 0; } dd p, td p { margin-bottom: 0; } dd p:first-child, td p:first-child { margin-top: 0; } /* -- Headings -------------------------------------------------------------- */ h1, h2, h3, h4, h5, h6 { color: #D98527;/*was #f80*/ font-family: 'Trebuchet MS', sans-serif; font-weight: bold; line-height: 1.1; margin: 1.1em 0 0.5em; } h1 { font-size: 184.6%; color: #30418C; margin: 0.75em 0 0.5em; } h2 { font-size: 153.846%; color: #E48A2B; } h3 { font-size: 138.462%; } h4 { border-bottom: 1px solid #DBDFEA; color: #E48A2B; font-size: 115.385%; font-weight: normal; padding-bottom: 2px; } h5, h6 { font-size: 107.692%; } /* -- Code and examples ----------------------------------------------------- */ code, kbd, pre, samp { font-family: Menlo, Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace; font-size: 92.308%; line-height: 1.35; } p code, p kbd, p samp, li code { background: #FCFBFA; border: 1px solid #EFEEED; padding: 0 3px; } a code, a kbd, a samp, pre code, pre kbd, pre samp, table code, table kbd, table samp, .intro code, .intro kbd, .intro samp, .toc code, .toc kbd, .toc samp { background: none; border: none; padding: 0; } pre.code, pre.terminal, pre.cmd { overflow-x: auto; *overflow-x: scroll; padding: 0.3em 0.6em; } pre.code { background: #FCFBFA; border: 1px solid #EFEEED; border-left-width: 5px; } pre.terminal, pre.cmd { background: #F0EFFC; border: 1px solid #D0CBFB; border-left: 5px solid #D0CBFB; } /* Don't reduce the font size of // elements inside
   blocks. */
pre code, pre kbd, pre samp { font-size: 100%; }

/* Used to denote text that shouldn't be selectable, such as line numbers or
   shell prompts. Guess which browser this doesn't work in. */
.noselect {
    -moz-user-select: -moz-none;
    -khtml-user-select: none;
    -webkit-user-select: none;
    -o-user-select: none;
    user-select: none;
}

/* -- Lists ----------------------------------------------------------------- */
dd { margin: 0.2em 0 0.7em 1em; }
dl { margin: 1em 0; }
dt { font-weight: bold; }

/* -- Tables ---------------------------------------------------------------- */
caption, th { text-align: left; }

table {
    border-collapse: collapse;
    width: 100%;
}

td, th {
    border: 1px solid #fff;
    padding: 5px 12px;
    vertical-align: top;
}

td { background: #E6E9F5; }
td dl { margin: 0; }
td dl dl { margin: 1em 0; }
td pre:first-child { margin-top: 0; }

th {
    background: #D2D7E6;/*#97A0BF*/
    border-bottom: none;
    border-top: none;
    color: #000;/*#FFF1D5*/
    font-family: 'Trebuchet MS', sans-serif;
    font-weight: bold;
    line-height: 1.3;
    white-space: nowrap;
}


/* -- Layout and Content ---------------------------------------------------- */
#doc {
    margin: auto;
    min-width: 1024px;
}

.content { padding: 0 20px 0 25px; }

.sidebar {
    padding: 0 15px 0 10px;
}
#bd {
    padding: 7px 0 130px;
    position: relative;
    width: 99%;
}

/* -- Table of Contents ----------------------------------------------------- */

/* The #toc id refers to the single global table of contents, while the .toc
   class refers to generic TOC lists that could be used throughout the page. */

.toc code, .toc kbd, .toc samp { font-size: 100%; }
.toc li { font-weight: bold; }
.toc li li { font-weight: normal; }

/* -- Intro and Example Boxes ----------------------------------------------- */
/*
.intro, .example { margin-bottom: 2em; }
.example {
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
    border-radius: 4px;
    -moz-box-shadow: 0 0 5px #bfbfbf;
    -webkit-box-shadow: 0 0 5px #bfbfbf;
    box-shadow: 0 0 5px #bfbfbf;
    padding: 1em;
}
.intro {
    background: none repeat scroll 0 0 #F0F1F8; border: 1px solid #D4D8EB; padding: 0 1em;
}
*/

/* -- Other Styles ---------------------------------------------------------- */

/* These are probably YUI-specific, and should be moved out of Selleck's default
   theme. */

.button {
    border: 1px solid #dadada;
    -moz-border-radius: 3px;
    -webkit-border-radius: 3px;
    border-radius: 3px;
    color: #444;
    display: inline-block;
    font-family: Helvetica, Arial, sans-serif;
    font-size: 92.308%;
    font-weight: bold;
    padding: 4px 13px 3px;
    -moz-text-shadow: 1px 1px 0 #fff;
    -webkit-text-shadow: 1px 1px 0 #fff;
    text-shadow: 1px 1px 0 #fff;
    white-space: nowrap;

    background: #EFEFEF; /* old browsers */
    background: -moz-linear-gradient(top, #f5f5f5 0%, #efefef 50%, #e5e5e5 51%, #dfdfdf 100%); /* firefox */
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f5f5f5), color-stop(50%,#efefef), color-stop(51%,#e5e5e5), color-stop(100%,#dfdfdf)); /* webkit */
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f5f5f5', endColorstr='#dfdfdf',GradientType=0 ); /* ie */
}

.button:hover {
    border-color: #466899;
    color: #fff;
    text-decoration: none;
    -moz-text-shadow: 1px 1px 0 #222;
    -webkit-text-shadow: 1px 1px 0 #222;
    text-shadow: 1px 1px 0 #222;

    background: #6396D8; /* old browsers */
    background: -moz-linear-gradient(top, #6396D8 0%, #5A83BC 50%, #547AB7 51%, #466899 100%); /* firefox */
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#6396D8), color-stop(50%,#5A83BC), color-stop(51%,#547AB7), color-stop(100%,#466899)); /* webkit */
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#6396D8', endColorstr='#466899',GradientType=0 ); /* ie */
}

.newwindow { text-align: center; }

.header .version em {
    display: block;
    text-align: right;
}


#classdocs .item {
    border-bottom: 1px solid #466899;
    margin: 1em 0;
    padding: 1.5em;
}

#classdocs .item .params p,
    #classdocs .item .returns p,{
    display: inline;
}

#classdocs .item em code, #classdocs .item em.comment {
    color: green;
}

#classdocs .item em.comment a {
    color: green;
    text-decoration: underline;
}

#classdocs .foundat {
    font-size: 11px;
    font-style: normal;
}

.attrs .emits {
    margin-left: 2em;
    padding: .5em;
    border-left: 1px dashed #ccc;
}

abbr {
    border-bottom: 1px dashed #ccc;
    font-size: 80%;
    cursor: help;
}

.prettyprint li.L0, 
.prettyprint li.L1, 
.prettyprint li.L2, 
.prettyprint li.L3, 
.prettyprint li.L5, 
.prettyprint li.L6, 
.prettyprint li.L7, 
.prettyprint li.L8 {
    list-style: decimal;
}

ul li p {
    margin-top: 0;
}

.method .name {
    font-size: 110%;
}

.apidocs .methods .extends .method,
.apidocs .properties .extends .property,
.apidocs .attrs .extends .attr,
.apidocs .events .extends .event {
    font-weight: bold;
}

.apidocs .methods .extends .inherited,
.apidocs .properties .extends .inherited,
.apidocs .attrs .extends .inherited,
.apidocs .events .extends .inherited {
    font-weight: normal;
}

#hd {
    background: whiteSmoke;
    background: -moz-linear-gradient(top,#DCDBD9 0,#F6F5F3 100%);
    background: -webkit-gradient(linear,left top,left bottom,color-stop(0%,#DCDBD9),color-stop(100%,#F6F5F3));
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#dcdbd9',endColorstr='#F6F5F3',GradientType=0);
    border-bottom: 1px solid #DFDFDF;
    padding: 0 15px 1px 20px;
    margin-bottom: 15px;
}

#hd img {
    margin-right: 10px;
    vertical-align: middle;
}


/* -- API Docs CSS ---------------------------------------------------------- */

/*
This file is organized so that more generic styles are nearer the top, and more
specific styles are nearer the bottom of the file. This allows us to take full
advantage of the cascade to avoid redundant style rules. Please respect this
convention when making changes.
*/

/* -- Generic TabView styles ------------------------------------------------ */

/*
These styles apply to all API doc tabviews. To change styles only for a
specific tabview, see the other sections below.
*/

.yui3-js-enabled .apidocs .tabview {
    visibility: hidden; /* Hide until the TabView finishes rendering. */
    _visibility: visible;
}

.apidocs .tabview.yui3-tabview-content { visibility: visible; }
.apidocs .tabview .yui3-tabview-panel { background: #fff; }

/* -- Generic Content Styles ------------------------------------------------ */

/* Headings */
h2, h3, h4, h5, h6 {
    border: none;
    color: #30418C;
    font-weight: bold;
    text-decoration: none;
}

.link-docs {
    float: right;
    font-size: 15px;
    margin: 4px 4px 6px;
    padding: 6px 30px 5px;
}

.apidocs { zoom: 1; }

/* Generic box styles. */
.apidocs .box {
    border: 1px solid;
    border-radius: 3px;
    margin: 1em 0;
    padding: 0 1em;
}

/* A flag is a compact, capsule-like indicator of some kind. It's used to
   indicate private and protected items, item return types, etc. in an
   attractive and unobtrusive way. */
.apidocs .flag {
    background: #bababa;
    border-radius: 3px;
    color: #fff;
    font-size: 11px;
    margin: 0 0.5em;
    padding: 2px 4px 1px;
}

/* Class/module metadata such as "Uses", "Extends", "Defined in", etc. */
.apidocs .meta {
    background: #f9f9f9;
    border-color: #efefef;
    color: #555;
    font-size: 11px;
    padding: 3px 6px;
}

.apidocs .meta p { margin: 0; }

/* Deprecation warning. */
.apidocs .box.deprecated,
.apidocs .flag.deprecated {
    background: #fdac9f;
    border: 1px solid #fd7775;
}

.apidocs .box.deprecated p { margin: 0.5em 0; }
.apidocs .flag.deprecated { color: #333; }

/* Module/Class intro description. */
.apidocs .intro {
    background: #f0f1f8;
    border-color: #d4d8eb;
}

/* Loading spinners. */
#bd.loading .apidocs,
#api-list.loading .yui3-tabview-panel {
    background: #fff url(../img/spinner.gif) no-repeat center 70px;
    min-height: 150px;
}

#bd.loading .apidocs .content,
#api-list.loading .yui3-tabview-panel .apis {
    display: none;
}

.apidocs .no-visible-items { color: #666; }

/* Generic inline list. */
.apidocs ul.inline {
    display: inline;
    list-style: none;
    margin: 0;
    padding: 0;
}

.apidocs ul.inline li { display: inline; }

/* Comma-separated list. */
.apidocs ul.commas li:after { content: ','; }
.apidocs ul.commas li:last-child:after { content: ''; }

/* Keyboard shortcuts. */
kbd .cmd { font-family: Monaco, Helvetica; }

/* -- Generic Access Level styles ------------------------------------------- */
.apidocs .item.protected,
.apidocs .item.private,
.apidocs .index-item.protected,
.apidocs .index-item.deprecated,
.apidocs .index-item.private {
    display: none;
}

.show-deprecated .item.deprecated,
.show-deprecated .index-item.deprecated,
.show-protected .item.protected,
.show-protected .index-item.protected,
.show-private .item.private,
.show-private .index-item.private {
    display: block;
}

.hide-inherited .item.inherited,
.hide-inherited .index-item.inherited {
    display: none;
}

/* -- Generic Item Index styles --------------------------------------------- */
.apidocs .index { margin: 1.5em 0 3em; }

.apidocs .index h3 {
    border-bottom: 1px solid #efefef;
    color: #333;
    font-size: 13px;
    margin: 2em 0 0.6em;
    padding-bottom: 2px;
}

.apidocs .index .no-visible-items { margin-top: 2em; }

.apidocs .index-list {
    border-color: #efefef;
    font-size: 12px;
    list-style: none;
    margin: 0;
    padding: 0;
    -moz-column-count: 4;
    -moz-column-gap: 10px;
    -moz-column-width: 170px;
    -ms-column-count: 4;
    -ms-column-gap: 10px;
    -ms-column-width: 170px;
    -o-column-count: 4;
    -o-column-gap: 10px;
    -o-column-width: 170px;
    -webkit-column-count: 4;
    -webkit-column-gap: 10px;
    -webkit-column-width: 170px;
    column-count: 4;
    column-gap: 10px;
    column-width: 170px;
}

.apidocs .no-columns .index-list {
    -moz-column-count: 1;
    -ms-column-count: 1;
    -o-column-count: 1;
    -webkit-column-count: 1;
    column-count: 1;
}

.apidocs .index-item { white-space: nowrap; }

.apidocs .index-item .flag {
    background: none;
    border: none;
    color: #afafaf;
    display: inline;
    margin: 0 0 0 0.2em;
    padding: 0;
}

/* -- Generic API item styles ----------------------------------------------- */
.apidocs .args {
    display: inline;
    margin: 0 0.5em;
}

.apidocs .flag.chainable { background: #46ca3b; }
.apidocs .flag.protected { background: #9b86fc; }
.apidocs .flag.private { background: #fd6b1b; }
.apidocs .flag.async { background: #356de4; }
.apidocs .flag.required { background: #e60923; }

.apidocs .item {
    border-bottom: 1px solid #efefef;
    margin: 1.5em 0 2em;
    padding-bottom: 2em;
}

.apidocs .item h4,
.apidocs .item h5,
.apidocs .item h6 {
    color: #333;
    font-family: inherit;
    font-size: 100%;
}

.apidocs .item .description p,
.apidocs .item pre.code {
    margin: 1em 0 0;
}

.apidocs .item .meta {
    background: none;
    border: none;
    padding: 0;
}

.apidocs .item .name {
    display: inline;
    font-size: 14px;
}

.apidocs .item .type,
.apidocs .item .type a,
.apidocs .returns-inline {
    color: #555;
}

.apidocs .item .type,
.apidocs .returns-inline {
    font-size: 11px;
    margin: 0 0 0 0;
}

.apidocs .item .type a { border-bottom: 1px dotted #afafaf; }
.apidocs .item .type a:hover { border: none; }

/* -- Item Parameter List --------------------------------------------------- */
.apidocs .params-list {
    list-style: square;
    margin: 1em 0 0 2em;
    padding: 0;
}

.apidocs .param { margin-bottom: 1em; }

.apidocs .param .type,
.apidocs .param .type a {
    color: #666;
}

.apidocs .param .type {
    margin: 0 0 0 0.5em;
    *margin-left: 0.5em;
}

.apidocs .param-name { font-weight: bold; }

/* -- Item "Emits" block ---------------------------------------------------- */
.apidocs .item .emits {
    background: #f9f9f9;
    border-color: #eaeaea;
}

/* -- Item "Returns" block -------------------------------------------------- */
.apidocs .item .returns .type,
.apidocs .item .returns .type a {
    font-size: 100%;
    margin: 0;
}

/* -- Class Constructor block ----------------------------------------------- */
.apidocs .constructor .item {
    border: none;
    padding-bottom: 0;
}

/* -- File Source View ------------------------------------------------------ */
.apidocs .file pre.code,
#doc .apidocs .file pre.prettyprint {
    background: inherit;
    border: none;
    overflow: visible;
    padding: 0;
}

.apidocs .L0,
.apidocs .L1,
.apidocs .L2,
.apidocs .L3,
.apidocs .L4,
.apidocs .L5,
.apidocs .L6,
.apidocs .L7,
.apidocs .L8,
.apidocs .L9 {
    background: inherit;
}

/* -- Submodule List -------------------------------------------------------- */
.apidocs .module-submodule-description {
    font-size: 12px;
    margin: 0.3em 0 1em;
}

.apidocs .module-submodule-description p:first-child { margin-top: 0; }

/* -- Sidebar TabView ------------------------------------------------------- */
#api-tabview { margin-top: 0.6em; }

#api-tabview-filter,
#api-tabview-panel {
    border: 1px solid #dfdfdf;
}

#api-tabview-filter {
    border-bottom: none;
    border-top: none;
    padding: 0.6em 10px 0 10px;
}

#api-tabview-panel { border-top: none; }
#api-filter { width: 97%; }

/* -- Content TabView ------------------------------------------------------- */
#classdocs .yui3-tabview-panel { border: none; }

/* -- Source File Contents -------------------------------------------------- */
.prettyprint li.L0,
.prettyprint li.L1,
.prettyprint li.L2,
.prettyprint li.L3,
.prettyprint li.L5,
.prettyprint li.L6,
.prettyprint li.L7,
.prettyprint li.L8 {
    list-style: decimal;
}

/* -- API options ----------------------------------------------------------- */
#api-options {
    font-size: 11px;
    margin-top: 2.2em;
    position: absolute;
    right: 1.5em;
}

/*#api-options label { margin-right: 0.6em; }*/

/* -- API list -------------------------------------------------------------- */
#api-list {
    margin-top: 1.5em;
    *zoom: 1;
}

.apis {
    font-size: 12px;
    line-height: 1.4;
    list-style: none;
    margin: 0;
    padding: 0.5em 0 0.5em 0.4em;
}

.apis a {
    border: 1px solid transparent;
    display: block;
    margin: 0 0 0 -4px;
    padding: 1px 4px 0;
    text-decoration: none;
    _border: none;
    _display: inline;
}

.apis a:hover,
.apis a:focus {
    background: #E8EDFC;
    background: -moz-linear-gradient(top, #e8edfc 0%, #becef7 100%);
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#E8EDFC), color-stop(100%,#BECEF7));
    border-color: #AAC0FA;
    border-radius: 3px;
    color: #333;
    outline: none;
}

.api-list-item a:hover,
.api-list-item a:focus {
    font-weight: bold;
    text-shadow: 1px 1px 1px #fff;
}

.apis .message { color: #888; }
.apis .result a { padding: 3px 5px 2px; }

.apis .result .type {
    right: 4px;
    top: 7px;
}

.api-list-item .yui3-highlight {
    font-weight: bold;
}



================================================
FILE: doc/assets/index.html
================================================


    
        Redirector
        
    
    
        Click here to redirect
    



================================================
FILE: doc/assets/js/api-filter.js
================================================
YUI.add('api-filter', function (Y) {

Y.APIFilter = Y.Base.create('apiFilter', Y.Base, [Y.AutoCompleteBase], {
    // -- Initializer ----------------------------------------------------------
    initializer: function () {
        this._bindUIACBase();
        this._syncUIACBase();
    },
    getDisplayName: function(name) {

        Y.each(Y.YUIDoc.meta.allModules, function(i) {
            if (i.name === name && i.displayName) {
                name = i.displayName;
            }
        });

        if (this.get('queryType') === 'elements') {
            name = '<' + name + '>';
        }

        return name;
    }

}, {
    // -- Attributes -----------------------------------------------------------
    ATTRS: {
        resultHighlighter: {
            value: 'phraseMatch'
        },

        // May be set to "classes", "elements" or "modules".
        queryType: {
            value: 'classes'
        },

        source: {
            valueFn: function() {
                var self = this;
                return function(q) {
                    var data = Y.YUIDoc.meta[self.get('queryType')],
                        out = [];
                    Y.each(data, function(v) {
                        if (v.toLowerCase().indexOf(q.toLowerCase()) > -1) {
                            out.push(v);
                        }
                    });
                    return out;
                };
            }
        }
    }
});

}, '3.4.0', {requires: [
    'autocomplete-base', 'autocomplete-highlighters', 'autocomplete-sources'
]});


================================================
FILE: doc/assets/js/api-list.js
================================================
YUI.add('api-list', function (Y) {

var Lang   = Y.Lang,
    YArray = Y.Array,

    APIList = Y.namespace('APIList'),

    classesNode    = Y.one('#api-classes'),
    elementsNode   = Y.one('#api-elements'),
    inputNode      = Y.one('#api-filter'),
    modulesNode    = Y.one('#api-modules'),
    tabviewNode    = Y.one('#api-tabview'),

    tabs = APIList.tabs = {},

    filter = APIList.filter = new Y.APIFilter({
        inputNode : inputNode,
        maxResults: 1000,

        on: {
            results: onFilterResults
        }
    }),

    search = APIList.search = new Y.APISearch({
        inputNode : inputNode,
        maxResults: 100,

        on: {
            clear  : onSearchClear,
            results: onSearchResults
        }
    }),

    tabview = APIList.tabview = new Y.TabView({
        srcNode  : tabviewNode,
        panelNode: '#api-tabview-panel',
        render   : true,

        on: {
            selectionChange: onTabSelectionChange
        }
    }),

    focusManager = APIList.focusManager = tabviewNode.plug(Y.Plugin.NodeFocusManager, {
        circular   : true,
        descendants: '#api-filter, .yui3-tab-panel-selected .api-list-item a, .yui3-tab-panel-selected .result a',
        keys       : {next: 'down:40', previous: 'down:38'}
    }).focusManager,

    LIST_ITEM_TEMPLATE =
        '
  • ' + '{displayName}' + '
  • '; // -- Init --------------------------------------------------------------------- // Duckpunch FocusManager's key event handling to prevent it from handling key // events when a modifier is pressed. Y.before(function (e, activeDescendant) { if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return new Y.Do.Prevent(); } }, focusManager, '_focusPrevious', focusManager); Y.before(function (e, activeDescendant) { if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return new Y.Do.Prevent(); } }, focusManager, '_focusNext', focusManager); // Create a mapping of tabs in the tabview so we can refer to them easily later. tabview.each(function (tab, index) { var name = tab.get('label').toLowerCase(); tabs[name] = { index: index, name : name, tab : tab }; }); // Switch tabs on Ctrl/Cmd-Left/Right arrows. tabviewNode.on('key', onTabSwitchKey, 'down:37,39'); // Focus the filter input when the `/` key is pressed. Y.one(Y.config.doc).on('key', onSearchKey, 'down:83'); // Keep the Focus Manager up to date. inputNode.on('focus', function () { focusManager.set('activeDescendant', inputNode); }); // Update all tabview links to resolved URLs. tabview.get('panelNode').all('a').each(function (link) { link.setAttribute('href', link.get('href')); }); // -- Private Functions -------------------------------------------------------- function getFilterResultNode() { var queryType = filter.get('queryType'); return queryType === 'classes' ? classesNode : queryType === 'elements' ? elementsNode : modulesNode; } // -- Event Handlers ----------------------------------------------------------- function onFilterResults(e) { var frag = Y.one(Y.config.doc.createDocumentFragment()), resultNode = getFilterResultNode(), typePlural = filter.get('queryType'), typeSingular = typePlural === 'classes' ? 'class' : typePlural === 'elements' ? 'element' : 'module'; if (e.results.length) { YArray.each(e.results, function (result) { frag.append(Lang.sub(LIST_ITEM_TEMPLATE, { rootPath : APIList.rootPath, displayName : filter.getDisplayName(result.highlighted), name : result.text, typePlural : typePlural, typeSingular: typeSingular })); }); } else { frag.append( '
  • ' + 'No ' + typePlural + ' found.' + '
  • ' ); } resultNode.empty(true); resultNode.append(frag); focusManager.refresh(); } function onSearchClear(e) { focusManager.refresh(); } function onSearchKey(e) { var target = e.target; if (target.test('input,select,textarea') || target.get('isContentEditable')) { return; } e.preventDefault(); inputNode.focus(); focusManager.refresh(); } function onSearchResults(e) { var frag = Y.one(Y.config.doc.createDocumentFragment()); if (e.results.length) { YArray.each(e.results, function (result) { frag.append(result.display); }); } else { frag.append( '
  • ' + 'No results found. Maybe you\'ll have better luck with a ' + 'different query?' + '
  • ' ); } focusManager.refresh(); } function onTabSelectionChange(e) { var tab = e.newVal, name = tab.get('label').toLowerCase(); tabs.selected = { index: tab.get('index'), name : name, tab : tab }; switch (name) { case 'elements':// fallthru case 'classes': // fallthru case 'modules': filter.setAttrs({ minQueryLength: 0, queryType : name }); search.set('minQueryLength', -1); // Only send a request if this isn't the initially-selected tab. if (e.prevVal) { filter.sendRequest(filter.get('value')); } break; case 'everything': filter.set('minQueryLength', -1); search.set('minQueryLength', 1); if (search.get('value')) { search.sendRequest(search.get('value')); } else { inputNode.focus(); } break; default: // WTF? We shouldn't be here! filter.set('minQueryLength', -1); search.set('minQueryLength', -1); } if (focusManager) { setTimeout(function () { focusManager.refresh(); }, 1); } } function onTabSwitchKey(e) { var currentTabIndex = tabs.selected.index; if (!(e.ctrlKey || e.metaKey)) { return; } e.preventDefault(); switch (e.keyCode) { case 37: // left arrow if (currentTabIndex > 0) { tabview.selectChild(currentTabIndex - 1); inputNode.focus(); } break; case 39: // right arrow if (currentTabIndex < (Y.Object.size(tabs) - 2)) { tabview.selectChild(currentTabIndex + 1); inputNode.focus(); } break; } } }, '3.4.0', {requires: [ 'api-filter', 'api-search', 'event-key', 'node-focusmanager', 'tabview' ]}); ================================================ FILE: doc/assets/js/api-search.js ================================================ YUI.add('api-search', function (Y) { var Lang = Y.Lang, Node = Y.Node, YArray = Y.Array; Y.APISearch = Y.Base.create('apiSearch', Y.Base, [Y.AutoCompleteBase], { // -- Public Properties ---------------------------------------------------- RESULT_TEMPLATE: '
  • ' + '' + '

    {name}

    ' + '{resultType}' + '
    {description}
    ' + '{class}' + '
    ' + '
  • ', // -- Initializer ---------------------------------------------------------- initializer: function () { this._bindUIACBase(); this._syncUIACBase(); }, // -- Protected Methods ---------------------------------------------------- _apiResultFilter: function (query, results) { // Filter components out of the results. return YArray.filter(results, function (result) { return result.raw.resultType === 'component' ? false : result; }); }, _apiResultFormatter: function (query, results) { return YArray.map(results, function (result) { var raw = Y.merge(result.raw), // create a copy desc = raw.description || ''; // Convert description to text and truncate it if necessary. desc = Node.create('
    ' + desc + '
    ').get('text'); if (desc.length > 65) { desc = Y.Escape.html(desc.substr(0, 65)) + ' …'; } else { desc = Y.Escape.html(desc); } raw['class'] || (raw['class'] = ''); raw.description = desc; // Use the highlighted result name. raw.name = result.highlighted; return Lang.sub(this.RESULT_TEMPLATE, raw); }, this); }, _apiTextLocator: function (result) { return result.displayName || result.name; } }, { // -- Attributes ----------------------------------------------------------- ATTRS: { resultFormatter: { valueFn: function () { return this._apiResultFormatter; } }, resultFilters: { valueFn: function () { return this._apiResultFilter; } }, resultHighlighter: { value: 'phraseMatch' }, resultListLocator: { value: 'data.results' }, resultTextLocator: { valueFn: function () { return this._apiTextLocator; } }, source: { value: '/api/v1/search?q={query}&count={maxResults}' } } }); }, '3.4.0', {requires: [ 'autocomplete-base', 'autocomplete-highlighters', 'autocomplete-sources', 'escape' ]}); ================================================ FILE: doc/assets/js/apidocs.js ================================================ YUI().use( 'yuidoc-meta', 'api-list', 'history-hash', 'node-screen', 'node-style', 'pjax', function (Y) { var win = Y.config.win, localStorage = win.localStorage, bdNode = Y.one('#bd'), pjax, defaultRoute, classTabView, selectedTab; // Kill pjax functionality unless serving over HTTP. if (!Y.getLocation().protocol.match(/^https?\:/)) { Y.Router.html5 = false; } // Create the default route with middleware which enables syntax highlighting // on the loaded content. defaultRoute = Y.Pjax.defaultRoute.concat(function (req, res, next) { prettyPrint(); bdNode.removeClass('loading'); next(); }); pjax = new Y.Pjax({ container : '#docs-main', contentSelector: '#docs-main > .content', linkSelector : '#bd a', titleSelector : '#xhr-title', navigateOnHash: true, root : '/', routes : [ // -- / ---------------------------------------------------------------- { path : '/(index.html)?', callbacks: defaultRoute }, // -- /elements/* ------------------------------------------------------- { path : '/elements/:element.html*', callbacks: defaultRoute }, // -- /classes/* ------------------------------------------------------- { path : '/classes/:class.html*', callbacks: [defaultRoute, 'handleClasses'] }, // -- /files/* --------------------------------------------------------- { path : '/files/*file', callbacks: [defaultRoute, 'handleFiles'] }, // -- /modules/* ------------------------------------------------------- { path : '/modules/:module.html*', callbacks: defaultRoute } ] }); // -- Utility Functions -------------------------------------------------------- pjax.checkVisibility = function (tab) { tab || (tab = selectedTab); if (!tab) { return; } var panelNode = tab.get('panelNode'), visibleItems; // If no items are visible in the tab panel due to the current visibility // settings, display a message to that effect. visibleItems = panelNode.all('.item,.index-item').some(function (itemNode) { if (itemNode.getComputedStyle('display') !== 'none') { return true; } }); panelNode.all('.no-visible-items').remove(); if (!visibleItems) { if (Y.one('#index .index-item')) { panelNode.append( '
    ' + '

    ' + 'Some items are not shown due to the current visibility ' + 'settings. Use the checkboxes at the upper right of this ' + 'page to change the visibility settings.' + '

    ' + '
    ' ); } else { panelNode.append( '
    ' + '

    ' + 'This class doesn\'t provide any methods, properties, ' + 'attributes, or events.' + '

    ' + '
    ' ); } } // Hide index sections without any visible items. Y.all('.index-section').each(function (section) { var items = 0, visibleItems = 0; section.all('.index-item').each(function (itemNode) { items += 1; if (itemNode.getComputedStyle('display') !== 'none') { visibleItems += 1; } }); section.toggleClass('hidden', !visibleItems); section.toggleClass('no-columns', visibleItems < 4); }); }; pjax.initClassTabView = function () { if (!Y.all('#classdocs .api-class-tab').size()) { return; } if (classTabView) { classTabView.destroy(); selectedTab = null; } classTabView = new Y.TabView({ srcNode: '#classdocs', on: { selectionChange: pjax.onTabSelectionChange } }); pjax.updateTabState(); classTabView.render(); }; pjax.initLineNumbers = function () { var hash = win.location.hash.substring(1), container = pjax.get('container'), hasLines, node; // Add ids for each line number in the file source view. container.all('.linenums>li').each(function (lineNode, index) { lineNode.set('id', 'l' + (index + 1)); lineNode.addClass('file-line'); hasLines = true; }); // Scroll to the desired line. if (hasLines && /^l\d+$/.test(hash)) { if ((node = container.getById(hash))) { win.scroll(0, node.getY()); } } }; pjax.initRoot = function () { var terminators = /^(?:classes|files|elements|modules)$/, parts = pjax._getPathRoot().split('/'), root = [], i, len, part; for (i = 0, len = parts.length; i < len; i += 1) { part = parts[i]; if (part.match(terminators)) { // Makes sure the path will end with a "/". root.push(''); break; } root.push(part); } pjax.set('root', root.join('/')); }; pjax.updateTabState = function (src) { var hash = win.location.hash.substring(1), defaultTab, node, tab, tabPanel; function scrollToNode() { if (node.hasClass('protected')) { Y.one('#api-show-protected').set('checked', true); pjax.updateVisibility(); } if (node.hasClass('private')) { Y.one('#api-show-private').set('checked', true); pjax.updateVisibility(); } setTimeout(function () { // For some reason, unless we re-get the node instance here, // getY() always returns 0. var node = Y.one('#classdocs').getById(hash); win.scrollTo(0, node.getY() - 70); }, 1); } if (!classTabView) { return; } if (src === 'hashchange' && !hash) { defaultTab = 'index'; } else { if (localStorage) { defaultTab = localStorage.getItem('tab_' + pjax.getPath()) || 'index'; } else { defaultTab = 'index'; } } if (hash && (node = Y.one('#classdocs').getById(hash))) { if ((tabPanel = node.ancestor('.api-class-tabpanel', true))) { if ((tab = Y.one('#classdocs .api-class-tab.' + tabPanel.get('id')))) { if (classTabView.get('rendered')) { Y.Widget.getByNode(tab).set('selected', 1); } else { tab.addClass('yui3-tab-selected'); } } } // Scroll to the desired element if this is a hash URL. if (node) { if (classTabView.get('rendered')) { scrollToNode(); } else { classTabView.once('renderedChange', scrollToNode); } } } else { tab = Y.one('#classdocs .api-class-tab.' + defaultTab); // When the `defaultTab` node isn't found, `localStorage` is stale. if (!tab && defaultTab !== 'index') { tab = Y.one('#classdocs .api-class-tab.index'); } if (classTabView.get('rendered')) { Y.Widget.getByNode(tab).set('selected', 1); } else { tab.addClass('yui3-tab-selected'); } } }; pjax.updateVisibility = function () { var container = pjax.get('container'); container.toggleClass('hide-inherited', !Y.one('#api-show-inherited').get('checked')); container.toggleClass('show-deprecated', Y.one('#api-show-deprecated').get('checked')); container.toggleClass('show-protected', Y.one('#api-show-protected').get('checked')); container.toggleClass('show-private', Y.one('#api-show-private').get('checked')); pjax.checkVisibility(); }; // -- Route Handlers ----------------------------------------------------------- pjax.handleClasses = function (req, res, next) { var status = res.ioResponse.status; // Handles success and local filesystem XHRs. if (res.ioResponse.readyState === 4 && (!status || (status >= 200 && status < 300))) { pjax.initClassTabView(); } next(); }; pjax.handleFiles = function (req, res, next) { var status = res.ioResponse.status; // Handles success and local filesystem XHRs. if (res.ioResponse.readyState === 4 && (!status || (status >= 200 && status < 300))) { pjax.initLineNumbers(); } next(); }; // -- Event Handlers ----------------------------------------------------------- pjax.onNavigate = function (e) { var hash = e.hash, originTarget = e.originEvent && e.originEvent.target, tab; if (hash) { tab = originTarget && originTarget.ancestor('.yui3-tab', true); if (hash === win.location.hash) { pjax.updateTabState('hashchange'); } else if (!tab) { win.location.hash = hash; } e.preventDefault(); return; } // Only scroll to the top of the page when the URL doesn't have a hash. this.set('scrollToTop', !e.url.match(/#.+$/)); bdNode.addClass('loading'); }; pjax.onOptionClick = function (e) { pjax.updateVisibility(); }; pjax.onTabSelectionChange = function (e) { var tab = e.newVal, tabId = tab.get('contentBox').getAttribute('href').substring(1); selectedTab = tab; // If switching from a previous tab (i.e., this is not the default tab), // replace the history entry with a hash URL that will cause this tab to // be selected if the user navigates away and then returns using the back // or forward buttons. if (e.prevVal && localStorage) { localStorage.setItem('tab_' + pjax.getPath(), tabId); } pjax.checkVisibility(tab); }; // -- Init --------------------------------------------------------------------- pjax.on('navigate', pjax.onNavigate); pjax.initRoot(); pjax.upgrade(); pjax.initClassTabView(); pjax.initLineNumbers(); pjax.updateVisibility(); Y.APIList.rootPath = pjax.get('root'); Y.one('#api-options').delegate('click', pjax.onOptionClick, 'input'); Y.on('hashchange', function (e) { pjax.updateTabState('hashchange'); }, win); }); ================================================ FILE: doc/assets/js/yui-prettify.js ================================================ YUI().use('node', function(Y) { var code = Y.all('.prettyprint.linenums'); if (code.size()) { code.each(function(c) { var lis = c.all('ol li'), l = 1; lis.each(function(n) { n.prepend(''); l++; }); }); var h = location.hash; location.hash = ''; h = h.replace('LINE_', 'LINENUM_'); location.hash = h; } }); ================================================ FILE: doc/assets/vendor/prettify/CHANGES.html ================================================ Change Log README

    Known Issues

    • Perl formatting is really crappy. Partly because the author is lazy and partly because Perl is hard to parse.
    • On some browsers, <code> elements with newlines in the text which use CSS to specify white-space:pre will have the newlines improperly stripped if the element is not attached to the document at the time the stripping is done. Also, on IE 6, all newlines will be stripped from <code> elements because of the way IE6 produces innerHTML. Workaround: use <pre> for code with newlines.

    Change Log

    29 March 2007

    • Added tests for PHP support to address issue 3.
    • Fixed bug: prettyPrintOne was not halting. This was not reachable through the normal entry point.
    • Fixed bug: recursing into a script block or PHP tag that was not properly closed would not silently drop the content. (test)
    • Fixed bug: was eating tabs (test)
    • Fixed entity handling so that the caveat

      Caveats: please properly escape less-thans. x&lt;y instead of x<y, and use " instead of &quot; for string delimiters.

      is no longer applicable.
    • Added noisefree's C# patch
    • Added a distribution that has comments and whitespace removed to reduce download size from 45.5kB to 12.8kB.

    4 Jul 2008

    • Added language specific formatters that are triggered by the presence of a lang-<language-file-extension>
    • Fixed bug: python handling of '''string'''
    • Fixed bug: / in regex [charsets] should not end regex

    5 Jul 2008

    • Defined language extensions for Lisp and Lua

    14 Jul 2008

    • Language handlers for F#, OCAML, SQL
    • Support for nocode spans to allow embedding of line numbers and code annotations which should not be styled or otherwise affect the tokenization of prettified code. See the issue 22 testcase.

    6 Jan 2009

    • Language handlers for Visual Basic, Haskell, CSS, and WikiText
    • Added .mxml extension to the markup style handler for Flex MXML files. See issue 37.
    • Added .m extension to the C style handler so that Objective C source files properly highlight. See issue 58.
    • Changed HTML lexer to use the same embedded source mechanism as the wiki language handler, and changed to use the registered CSS handler for STYLE element content.

    21 May 2009

    • Rewrote to improve performance on large files. See benchmarks.
    • Fixed bugs with highlighting of Haskell line comments, Lisp number literals, Lua strings, C preprocessor directives, newlines in Wiki code on Windows, and newlines in IE6.

    14 August 2009

    • Fixed prettifying of <code> blocks with embedded newlines.

    3 October 2009

    • Fixed prettifying of XML/HTML tags that contain uppercase letters.

    19 July 2010

    • Added support for line numbers. Bug 22
    • Added YAML support. Bug 123
    • Added VHDL support courtesy Le Poussin.
    • IE performance improvements. Bug 102 courtesy jacobly.
    • A variety of markup formatting fixes courtesy smain and thezbyg.
    • Fixed copy and paste in IE[678].
    • Changed output to use &#160; instead of &nbsp; so that the output works when embedded in XML. Bug 108.
    ================================================ FILE: doc/assets/vendor/prettify/COPYING ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: doc/assets/vendor/prettify/README.html ================================================ Javascript code prettifier Languages : CH

    Javascript code prettifier

    Setup

    1. Download a distribution
    2. Include the script and stylesheets in your document (you will need to make sure the css and js file are on your server, and adjust the paths in the script and link tag)
      <link href="prettify.css" type="text/css" rel="stylesheet" />
      <script type="text/javascript" src="prettify.js"></script>
    3. Add onload="prettyPrint()" to your document's body tag.
    4. Modify the stylesheet to get the coloring you prefer

    Usage

    Put code snippets in <pre class="prettyprint">...</pre> or <code class="prettyprint">...</code> and it will automatically be pretty printed.
    The original Prettier
    class Voila {
    public:
      // Voila
      static const string VOILA = "Voila";
    
      // will not interfere with embedded tags.
    }
    class Voila {
    public:
      // Voila
      static const string VOILA = "Voila";
    
      // will not interfere with embedded tags.
    }

    FAQ

    Which languages does it work for?

    The comments in prettify.js are authoritative but the lexer should work on a number of languages including C and friends, Java, Python, Bash, SQL, HTML, XML, CSS, Javascript, and Makefiles. It works passably on Ruby, PHP, VB, and Awk and a decent subset of Perl and Ruby, but, because of commenting conventions, doesn't work on Smalltalk, or CAML-like languages.

    LISPy languages are supported via an extension: lang-lisp.js.

    And similarly for CSS, Haskell, Lua, OCAML, SML, F#, Visual Basic, SQL, Protocol Buffers, and WikiText..

    If you'd like to add an extension for your favorite language, please look at src/lang-lisp.js and file an issue including your language extension, and a testcase.

    How do I specify which language my code is in?

    You don't need to specify the language since prettyprint() will guess. You can specify a language by specifying the language extension along with the prettyprint class like so:

    <pre class="prettyprint lang-html">
      The lang-* class specifies the language file extensions.
      File extensions supported by default include
        "bsh", "c", "cc", "cpp", "cs", "csh", "cyc", "cv", "htm", "html",
        "java", "js", "m", "mxml", "perl", "pl", "pm", "py", "rb", "sh",
        "xhtml", "xml", "xsl".
    </pre>

    It doesn't work on <obfuscated code sample>?

    Yes. Prettifying obfuscated code is like putting lipstick on a pig — i.e. outside the scope of this tool.

    Which browsers does it work with?

    It's been tested with IE 6, Firefox 1.5 & 2, and Safari 2.0.4. Look at the test page to see if it works in your browser.

    What's changed?

    See the change log

    Why doesn't Prettyprinting of strings work on WordPress?

    Apparently wordpress does "smart quoting" which changes close quotes. This causes end quotes to not match up with open quotes.

    This breaks prettifying as well as copying and pasting of code samples. See WordPress's help center for info on how to stop smart quoting of code snippets.

    How do I put line numbers in my code?

    You can use the linenums class to turn on line numbering. If your code doesn't start at line number 1, you can add a colon and a line number to the end of that class as in linenums:52.

    For example

    <pre class="prettyprint linenums:4"
    >// This is line 4.
    foo();
    bar();
    baz();
    boo();
    far();
    faz();
    <pre>
    produces
    // This is line 4.
    foo();
    bar();
    baz();
    boo();
    far();
    faz();
    

    How do I prevent a portion of markup from being marked as code?

    You can use the nocode class to identify a span of markup that is not code.

    <pre class=prettyprint>
    int x = foo();  /* This is a comment  <span class="nocode">This is not code</span>
      Continuation of comment */
    int y = bar();
    </pre>
    produces
    int x = foo();  /* This is a comment  This is not code
      Continuation of comment */
    int y = bar();
    

    For a more complete example see the issue22 testcase.

    I get an error message "a is not a function" or "opt_whenDone is not a function"

    If you are calling prettyPrint via an event handler, wrap it in a function. Instead of doing

    addEventListener('load', prettyPrint, false);
    wrap it in a closure like
    addEventListener('load', function (event) { prettyPrint() }, false);
    so that the browser does not pass an event object to prettyPrint which will confuse it.


    ================================================ FILE: doc/assets/vendor/prettify/prettify-min.css ================================================ .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} ================================================ FILE: doc/assets/vendor/prettify/prettify-min.js ================================================ window.PR_SHOULD_USE_CONTINUATION=true;var prettyPrintOne;var prettyPrint;(function(){var O=window;var j=["break,continue,do,else,for,if,return,while"];var v=[j,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var q=[v,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var m=[q,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var y=[q,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var T=[y,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"];var s="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes";var x=[q,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var t="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var J=[j,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var g=[j,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var I=[j,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var B=[m,T,x,t+J,g,I];var f=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/;var D="str";var A="kwd";var k="com";var Q="typ";var H="lit";var M="pun";var G="pln";var n="tag";var F="dec";var K="src";var R="atn";var o="atv";var P="nocode";var N="(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function l(ab){var af=0;var U=false;var ae=false;for(var X=0,W=ab.length;X122)){if(!(am<65||ai>90)){ah.push([Math.max(65,ai)|32,Math.min(am,90)|32])}if(!(am<97||ai>122)){ah.push([Math.max(97,ai)&~32,Math.min(am,122)&~32])}}}}ah.sort(function(aw,av){return(aw[0]-av[0])||(av[1]-aw[1])});var ak=[];var aq=[];for(var at=0;atau[0]){if(au[1]+1>au[0]){ao.push("-")}ao.push(V(au[1]))}}ao.push("]");return ao.join("")}function Y(an){var al=an.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var aj=al.length;var ap=[];for(var am=0,ao=0;am=2&&ak==="["){al[am]=Z(ai)}else{if(ak!=="\\"){al[am]=ai.replace(/[a-zA-Z]/g,function(aq){var ar=aq.charCodeAt(0);return"["+String.fromCharCode(ar&~32,ar|32)+"]"})}}}}return al.join("")}var ac=[];for(var X=0,W=ab.length;X=0;){U[ae.charAt(ag)]=aa}}var ah=aa[1];var ac=""+ah;if(!ai.hasOwnProperty(ac)){aj.push(ah);ai[ac]=null}}aj.push(/[\0-\uffff]/);X=l(aj)})();var Z=V.length;var Y=function(aj){var ab=aj.sourceCode,aa=aj.basePos;var af=[aa,G];var ah=0;var ap=ab.match(X)||[];var al={};for(var ag=0,at=ap.length;ag=5&&"lang-"===ar.substring(0,5);if(ao&&!(ak&&typeof ak[1]==="string")){ao=false;ar=K}if(!ao){al[ai]=ar}}var ad=ah;ah+=ai.length;if(!ao){af.push(aa+ad,ar)}else{var an=ak[1];var am=ai.indexOf(an);var ae=am+an.length;if(ak[2]){ae=ai.length-ak[2].length;am=ae-an.length}var au=ar.substring(5);C(aa+ad,ai.substring(0,am),Y,af);C(aa+ad+am,an,r(au,an),af);C(aa+ad+ae,ai.substring(ae),Y,af)}}aj.decorations=af};return Y}function i(V){var Y=[],U=[];if(V.tripleQuotedStrings){Y.push([D,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(V.multiLineStrings){Y.push([D,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{Y.push([D,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(V.verbatimStrings){U.push([D,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var ab=V.hashComments;if(ab){if(V.cStyleComments){if(ab>1){Y.push([k,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{Y.push([k,/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}U.push([D,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,null])}else{Y.push([k,/^#[^\r\n]*/,null,"#"])}}if(V.cStyleComments){U.push([k,/^\/\/[^\r\n]*/,null]);U.push([k,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(V.regexLiterals){var aa=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");U.push(["lang-regex",new RegExp("^"+N+"("+aa+")")])}var X=V.types;if(X){U.push([Q,X])}var W=(""+V.keywords).replace(/^ | $/g,"");if(W.length){U.push([A,new RegExp("^(?:"+W.replace(/[\s,]+/g,"|")+")\\b"),null])}Y.push([G,/^\s+/,null," \r\n\t\xA0"]);var Z=/^.[^\s\w\.$@\'\"\`\/\\]*/;U.push([H,/^@[a-z_$][a-z_$@0-9]*/i,null],[Q,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[G,/^[a-z_$][a-z_$@0-9]*/i,null],[H,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[G,/^\\[\s\S]?/,null],[M,Z,null]);return h(Y,U)}var L=i({keywords:B,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function S(W,ah,aa){var V=/(?:^|\s)nocode(?:\s|$)/;var ac=/\r\n?|\n/;var ad=W.ownerDocument;var ag=ad.createElement("li");while(W.firstChild){ag.appendChild(W.firstChild)}var X=[ag];function af(am){switch(am.nodeType){case 1:if(V.test(am.className)){break}if("br"===am.nodeName){ae(am);if(am.parentNode){am.parentNode.removeChild(am)}}else{for(var ao=am.firstChild;ao;ao=ao.nextSibling){af(ao)}}break;case 3:case 4:if(aa){var an=am.nodeValue;var ak=an.match(ac);if(ak){var aj=an.substring(0,ak.index);am.nodeValue=aj;var ai=an.substring(ak.index+ak[0].length);if(ai){var al=am.parentNode;al.insertBefore(ad.createTextNode(ai),am.nextSibling)}ae(am);if(!aj){am.parentNode.removeChild(am)}}}break}}function ae(al){while(!al.nextSibling){al=al.parentNode;if(!al){return}}function aj(am,at){var ar=at?am.cloneNode(false):am;var ap=am.parentNode;if(ap){var aq=aj(ap,1);var ao=am.nextSibling;aq.appendChild(ar);for(var an=ao;an;an=ao){ao=an.nextSibling;aq.appendChild(an)}}return ar}var ai=aj(al.nextSibling,0);for(var ak;(ak=ai.parentNode)&&ak.nodeType===1;){ai=ak}X.push(ai)}for(var Z=0;Z=U){aj+=2}if(Y>=ar){ac+=2}}}finally{if(au){au.style.display=ak}}}var u={};function d(W,X){for(var U=X.length;--U>=0;){var V=X[U];if(!u.hasOwnProperty(V)){u[V]=W}else{if(O.console){console.warn("cannot override language handler %s",V)}}}}function r(V,U){if(!(V&&u.hasOwnProperty(V))){V=/^\s*]*(?:>|$)/],[k,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[M,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);d(h([[G,/^[\s]+/,null," \t\r\n"],[o,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[n,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[R,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[M,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);d(h([],[[o,/^[\s\S]+/]]),["uq.val"]);d(i({keywords:m,hashComments:true,cStyleComments:true,types:f}),["c","cc","cpp","cxx","cyc","m"]);d(i({keywords:"null,true,false"}),["json"]);d(i({keywords:T,hashComments:true,cStyleComments:true,verbatimStrings:true,types:f}),["cs"]);d(i({keywords:y,cStyleComments:true}),["java"]);d(i({keywords:I,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);d(i({keywords:J,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);d(i({keywords:t,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);d(i({keywords:g,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);d(i({keywords:x,cStyleComments:true,regexLiterals:true}),["js"]);d(i({keywords:s,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);d(h([],[[D,/^[\s\S]+/]]),["regex"]);function e(X){var W=X.langExtension;try{var U=b(X.sourceNode,X.pre);var V=U.sourceCode;X.sourceCode=V;X.spans=U.spans;X.basePos=0;r(W,V)(X);E(X)}catch(Y){if(O.console){console.log(Y&&Y.stack?Y.stack:Y)}}}function z(Y,X,W){var U=document.createElement("pre");U.innerHTML=Y;if(W){S(U,W,true)}var V={langExtension:X,numberLines:W,sourceNode:U,pre:1};e(V);return U.innerHTML}function c(aj){function ab(al){return document.getElementsByTagName(al)}var ah=[ab("pre"),ab("code"),ab("xmp")];var V=[];for(var ae=0;ae]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); ================================================ FILE: doc/classes/ContextMenu.html ================================================ ContextMenu

    API Docs for:
    Show:

    ContextMenu Class

    ContextMenu from LiteGUI

    Constructor

    ContextMenu

    (
    • values
    • options
    )

    Parameters:

    • values Array

      (allows object { title: "Nice text", callback: function ... })

    • options Object

      [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

    Item Index

    ================================================ FILE: doc/classes/LGraph.html ================================================ LGraph

    API Docs for:
    Show:

    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.

    Constructor

    LGraph

    (
    • o
    )

    Parameters:

    • o Object

      data from previous serialization [optional]

    Methods

    add

    (
    • node
    )

    Adds a new node instasnce to this graph

    Parameters:

    addGlobalInput

    (
    • name
    • type
    • value
    )

    Tell this graph it has a global graph input of this type

    Parameters:

    • name String
    • type String
    • value

      [optional]

    addOutput

    (
    • name
    • type
    • value
    )

    Creates a global graph output

    Parameters:

    • name String
    • type String
    • value

    arrange

    ()

    Positions every node in a more readable manner

    attachCanvas

    (
    • graph_canvas
    )

    Attach Canvas to this graph

    Parameters:

    • graph_canvas GraphCanvas

    changeInputType

    (
    • name
    • type
    )

    Changes the type of a global graph input

    Parameters:

    • name String
    • type String

    changeOutputType

    (
    • name
    • type
    )

    Changes the type of a global graph output

    Parameters:

    • name String
    • type String

    clear

    ()

    Removes all nodes from this graph

    clearTriggeredSlots

    ()

    clears the triggered slot animation in all links (stop visual animation)

    configure

    (
    • str
    • returns
    )

    Configure a graph from a JSON string

    Parameters:

    • str String

      configure a graph from a JSON string

    • returns Boolean

      if there was any error parsing

    detachCanvas

    (
    • graph_canvas
    )

    Detach Canvas from this graph

    Parameters:

    • graph_canvas GraphCanvas

    findNodesByClass

    (
    • classObject
    )
    Array

    Returns a list of nodes that matches a class

    Parameters:

    • classObject Class

      the class itself (not an string)

    Returns:

    Array:

    a list with all the nodes of this type

    findNodesByTitle

    (
    • name
    )
    Array

    Returns a list of nodes that matches a name

    Parameters:

    • name String

      the name of the node to search

    Returns:

    Array:

    a list with all the nodes with this name

    findNodesByType

    (
    • type
    )
    Array

    Returns a list of nodes that matches a type

    Parameters:

    • type String

      the name of the node type

    Returns:

    Array:

    a list with all the nodes of this type

    getAncestors

    () Array

    Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. It doesnt include the node itself

    Returns:

    Array:

    an array with all the LGraphNodes that affect this node, in order of execution

    getElapsedTime

    () Number

    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

    Returns:

    Number:

    number of milliseconds it took the last cycle

    getFixedTime

    () Number

    Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant

    Returns:

    Number:

    number of milliseconds the graph has been running

    getGroupOnPos

    (
    • x
    • y
    )
    LGraphGroup

    Returns the top-most group in that position

    Parameters:

    • x Number

      the x coordinate in canvas space

    • y Number

      the y coordinate in canvas space

    Returns:

    LGraphGroup:

    the group or null

    getInputData

    (
    • name
    )

    Returns the current value of a global graph input

    Parameters:

    • name String

    Returns:

    :

    the data

    getNodeById

    (
    • id
    )

    Returns a node by its id.

    Parameters:

    • id Number

    getNodeOnPos

    (
    • x
    • y
    • nodes_list
    )
    LGraphNode

    Returns the top-most node in this position of the canvas

    Parameters:

    • x Number

      the x coordinate in canvas space

    • y Number

      the y coordinate in canvas space

    • nodes_list Array

      a list with all the nodes to search from, by default is all the nodes in the graph

    Returns:

    LGraphNode:

    the node at this position or null

    getOutputData

    (
    • name
    )

    Returns the current value of a global graph output

    Parameters:

    • name String

    Returns:

    :

    the data

    getTime

    () Number

    Returns the amount of time the graph has been running in milliseconds

    Returns:

    Number:

    number of milliseconds the graph has been running

    isLive

    ()

    returns if the graph is in live mode

    remove

    (
    • node
    )

    Removes a node from the graph

    Parameters:

    removeInput

    (
    • name
    • type
    )

    Removes a global graph input

    Parameters:

    • name String
    • type String

    removeOutput

    (
    • name
    )

    Removes a global graph output

    Parameters:

    • name String

    renameInput

    (
    • old_name
    • new_name
    )

    Changes the name of a global graph input

    Parameters:

    • old_name String
    • new_name String

    renameOutput

    (
    • old_name
    • new_name
    )

    Renames a global graph output

    Parameters:

    • old_name String
    • new_name String

    runStep

    (
    • num
    )

    Run N steps (cycles) of the graph

    Parameters:

    • num Number

      number of steps to run, default is 1

    sendEventToAllNodes

    (
    • eventname
    • params
    )

    Sends an event to all the nodes, useful to trigger stuff

    Parameters:

    • eventname String

      the name of the event (function to be called)

    • params Array

      parameters in array format

    serialize

    () Object

    Creates a Object containing all the info about this graph, it can be serialized

    Returns:

    Object:

    value of the node

    setGlobalInputData

    (
    • name
    • data
    )

    Assign a data to the global graph input

    Parameters:

    • name String
    • data

    setOutputData

    (
    • name
    • value
    )

    Assign a data to the global output

    Parameters:

    • name String
    • value String

    start

    (
    • interval
    )

    Starts running this graph every interval milliseconds.

    Parameters:

    • interval Number

      amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate

    stop execution

    ()

    Stops the execution loop of the graph

    updateExecutionOrder

    ()

    Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than nodes with only inputs.

    ================================================ FILE: doc/classes/LGraphCanvas.html ================================================ LGraphCanvas

    API Docs for:
    Show:

    LGraphCanvas Class

    marks as dirty the canvas, this way it will be rendered again

    Constructor

    LGraphCanvas

    (
    • canvas
    • graph
    • options
    )

    Parameters:

    • canvas HTMLCanvas

      the canvas where you want to render (it accepts a selector in string format or the canvas element itself)

    • graph LGraph

      [optional]

    • options Object

      [optional] { skip_rendering, autoresize }

    Methods

    adjustMouseEvent

    ()

    adds some useful properties to a mouse event, like the position in graph coordinates

    bindEvents

    ()

    binds mouse, keyboard, touch and drag events to the canvas

    bringToFront

    ()

    brings a node to front (above all other nodes)

    centerOnNode

    ()

    centers the camera on a given node

    clear

    ()

    clears all the data inside

    closeSubgraph

    (
    • assigns
    )

    closes a subgraph contained inside a node

    Parameters:

    computeVisibleNodes

    ()

    checks which nodes are visible (inside the camera area)

    convertCanvasToOffset

    ()

    converts a coordinate from Canvas2D coordinates to graph space

    convertOffsetToCanvas

    ()

    converts a coordinate from graph coordinates to canvas2D coordinates

    deleteSelectedNodes

    ()

    deletes all nodes in the current selection from the graph

    deselectAllNodes

    ()

    removes all nodes from the current selection

    deselectNode

    ()

    removes a node from the current selection

    draw

    ()

    renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes)

    drawBackCanvas

    ()

    draws the back canvas (the one containing the background and the connections)

    drawConnections

    ()

    draws every connection visible in the canvas OPTIMIZE THIS: precatch connections position instead of recomputing them every time

    drawFrontCanvas

    ()

    draws the front canvas (the one containing all the nodes)

    drawGroups

    ()

    draws every group area in the background

    drawNode

    ()

    draws the given node inside the canvas

    drawNodeShape

    ()

    draws the shape of the given node in the canvas

    drawNodeWidgets

    ()

    draws the widgets stored inside a node

    enableWebGL

    ()

    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

    getCanvasWindow

    () Window

    Used to attach the canvas in a popup

    Returns:

    Window:

    returns the window where the canvas is attached (the DOM root node)

    isOverNodeBox

    ()

    retuns true if a position (in graph space) is on top of a node little corner box

    isOverNodeInput

    ()

    retuns true if a position (in graph space) is on top of a node input slot

    openSubgraph

    (
    • graph
    )

    opens a graph contained inside a node in the current graph

    Parameters:

    processDrop

    ()

    process a item drop event on top the canvas

    processKey

    ()

    process a key event

    processMouseMove

    ()

    Called when a mouse move event has to be processed

    processMouseUp

    ()

    Called when a mouse up event has to be processed

    processMouseWheel

    ()

    Called when a mouse wheel event has to be processed

    processNodeWidgets

    ()

    process an event on widgets

    renderInfo

    ()

    draws some useful stats in the corner of the canvas

    resize

    ()

    resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode

    selectNode

    ()

    selects a given node (or adds it to the current selection)

    selectNodes

    ()

    selects several nodes (or adds them to the current selection)

    sendToBack

    ()

    sends a node to the back (below all other nodes)

    setCanvas

    (
    • assigns
    )

    assigns a canvas

    Parameters:

    • assigns Canvas

      a canvas (also accepts the ID of the element (not a selector)

    setGraph

    (
    • graph
    )

    assigns a graph, you can reasign graphs to the same canvas

    Parameters:

    setZoom

    ()

    changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom

    startRendering

    ()

    starts rendering the content of the canvas when needed

    stopRendering

    ()

    stops rendering the content of the canvas (to save resources)

    switchLiveMode

    ()

    switches to live mode (node shapes are not rendered, only the content) this feature was designed when graphs where meant to create user interfaces

    unbindEvents

    ()

    unbinds mouse events from the canvas

    ================================================ FILE: doc/classes/LGraphNode.html ================================================ LGraphNode

    API Docs for:
    Show:

    LGraphNode Class

    Base Class for all the node type classes

    Methods

    addConnection

    (
    • name
    • type
    • pos
    • direction
    )

    add an special connection to this node (used for special kinds of graphs)

    Parameters:

    • name String
    • type String

      string defining the input type ("vec3","number",...)

    • pos x,y

      position of the connection inside the node

    • direction String

      if is input or output

    addInput

    (
    • name
    • type
    • extra_info
    )

    add a new input slot to use in this node

    Parameters:

    • name String
    • type String

      string defining the input type ("vec3","number",...), it its a generic one use 0

    • extra_info Object

      this can be used to have special properties of an input (label, color, position, etc)

    addInputs

    (
    • array
    )

    add several new input slots in this node

    Parameters:

    • array Array

      of triplets like [[name,type,extra_info],[...]]

    addOutput

    (
    • name
    • type
    • extra_info
    )

    add a new output slot to use in this node

    Parameters:

    • name String
    • type String

      string defining the output type ("vec3","number",...)

    • extra_info Object

      this can be used to have special properties of an output (label, special color, position, etc)

    addOutputs

    (
    • array
    )

    add a new output slot to use in this node

    Parameters:

    • array Array

      of triplets like [[name,type,extra_info],[...]]

    addProperty

    (
    • name
    • default_value
    • type
    • extra_info
    )

    add a new property to this node

    Parameters:

    • name String
    • default_value
    • type String

      string defining the output type ("vec3","number",...)

    • extra_info Object

      this can be used to have special properties of the property (like values, etc)

    addWidget

    () Object

    Allows to pass

    Returns:

    Object:

    the created widget

    clearTriggeredSlot

    (
    • slot
    • link_id
    )

    clears the trigger slot animation

    Parameters:

    • slot Number

      the index of the output slot

    • link_id Number

      [optional] in case you want to trigger and specific output link in a slot

    collapse

    ()

    Collapse the node to make it smaller on the canvas

    computeSize

    (
    • minHeight
    )
    Number

    computes the size of a node according to its inputs and output slots

    Parameters:

    • minHeight Number

    Returns:

    Number:

    the total size

    configure

    ()

    configure a node from an object containing the serialized info

    connect

    (
    • slot
    • node
    • target_slot
    )
    Object

    connect this node output to the input of another node

    Parameters:

    • slot Number_or_string

      (could be the number of the slot or the string with the name of the slot)

    • node LGraphNode

      the target node

    • target_slot Number_or_string

      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)

    Returns:

    Object:

    the link_info is created, otherwise null

    disconnectInput

    (
    • slot
    )
    Boolean

    disconnect one input

    Parameters:

    • slot Number_or_string

      (could be the number of the slot or the string with the name of the slot)

    Returns:

    Boolean:

    if it was disconnected succesfully

    disconnectOutput

    (
    • slot
    • target_node
    )
    Boolean

    disconnect one output to an specific node

    Parameters:

    • slot Number_or_string

      (could be the number of the slot or the string with the name of the slot)

    • target_node LGraphNode

      the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected]

    Returns:

    Boolean:

    if it was disconnected succesfully

    findInputSlot

    (
    • name
    )
    Number

    returns the input slot with a given name (used for dynamic slots), -1 if not found

    Parameters:

    • name String

      the name of the slot

    Returns:

    Number:

    the slot (-1 if not found)

    findOutputSlot

    (
    • name
    )
    Number

    returns the output slot with a given name (used for dynamic slots), -1 if not found

    Parameters:

    • name String

      the name of the slot

    Returns:

    Number:

    the slot (-1 if not found)

    getBounding

    () Float32Array4

    returns the bounding of the object, used for rendering purposes bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Returns:

    Float32Array4:

    the total size

    getConnectionPos

    (
    • is_input
    • slot
    • out
    )
    x,y

    returns the center of a connection point in canvas coords

    Parameters:

    • is_input Boolean

      true if if a input slot, false if it is an output

    • slot Number_or_string

      (could be the number of the slot or the string with the name of the slot)

    • out Vec2

      [optional] a place to store the output, to free garbage

    Returns:

    x,y:

    the position

    getInputData

    (
    • slot
    • force_update
    )

    Retrieves the input data (data traveling through the connection) from one slot

    Parameters:

    • slot Number
    • force_update Boolean

      if set to true it will force the connected node of this slot to output data into this link

    Returns:

    :

    data or if it is not connected returns undefined

    getInputDataByName

    (
    • slot_name
    • force_update
    )

    Retrieves the input data from one slot using its name instead of slot number

    Parameters:

    • slot_name String
    • force_update Boolean

      if set to true it will force the connected node of this slot to output data into this link

    Returns:

    :

    data or if it is not connected returns null

    getInputDataType

    (
    • slot
    )
    String

    Retrieves the input data type (in case this supports multiple input types)

    Parameters:

    • slot Number

    Returns:

    String:

    datatype in string format

    getInputInfo

    (
    • slot
    )
    Object

    tells you info about an input connection (which node, type, etc)

    Parameters:

    • slot Number

    Returns:

    Object:

    object or null { link: id, name: string, type: string or 0 }

    getInputNode

    (
    • slot
    )
    LGraphNode

    returns the node connected in the input slot

    Parameters:

    • slot Number

    Returns:

    LGraphNode:

    node or null

    getInputOrProperty

    (
    • name
    )

    returns the value of an input with this name, otherwise checks if there is a property with that name

    Parameters:

    • name String

    Returns:

    :

    value

    getOutputData

    (
    • slot
    )
    Object

    tells you the last output data that went in that slot

    Parameters:

    • slot Number

    Returns:

    Object:

    object or null

    getOutputInfo

    (
    • slot
    )
    Object

    tells you info about an output connection (which node, type, etc)

    Parameters:

    • slot Number

    Returns:

    Object:

    object or null { name: string, type: string, links: [ ids of links in number ] }

    getOutputNodes

    (
    • slot
    )
    Array

    retrieves all the nodes connected to this output slot

    Parameters:

    • slot Number

    Returns:

    Array:

    getSlotInPosition

    (
    • x
    • y
    )
    Object

    checks if a point is inside a node slot, and returns info about which slot

    Parameters:

    • x Number
    • y Number

    Returns:

    Object:

    if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] }

    getTitle

    ()

    get the title string

    isAnyOutputConnected

    () Boolean

    tells you if there is any connection in the output slots

    Returns:

    Boolean:

    isInputConnected

    (
    • slot
    )
    Boolean

    tells you if there is a connection in one input slot

    Parameters:

    • slot Number

    Returns:

    Boolean:

    isOutputConnected

    (
    • slot
    )
    Boolean

    tells you if there is a connection in one output slot

    Parameters:

    • slot Number

    Returns:

    Boolean:

    isPointInside

    (
    • x
    • y
    )
    Boolean

    checks if a point is inside the shape of a node

    Parameters:

    • x Number
    • y Number

    Returns:

    Boolean:

    pin

    ()

    Forces the node to do not move or realign on Z

    removeInput

    (
    • slot
    )

    remove an existing input slot

    Parameters:

    • slot Number

    removeOutput

    (
    • slot
    )

    remove an existing output slot

    Parameters:

    • slot Number

    serialize

    ()

    serialize the content

    setOutputData

    (
    • slot
    • data
    )

    sets the output data

    Parameters:

    • slot Number
    • data

    setOutputDataType

    (
    • slot
    • datatype
    )

    sets the output data type, useful when you want to be able to overwrite the data type

    Parameters:

    • slot Number
    • datatype String

    toString

    ()

    serialize and stringify

    trigger

    (
    • event
    • param
    )

    Triggers an event in this node, this will trigger any output with the same name

    Parameters:

    • event String

      name ( "on_play", ... ) if action is equivalent to false then the event is send to all

    • param

    triggerSlot

    (
    • slot
    • param
    • link_id
    )

    Triggers an slot event in this node

    Parameters:

    • slot Number

      the index of the output slot

    • param
    • link_id Number

      [optional] in case you want to trigger and specific output link in a slot

    ================================================ FILE: doc/classes/LiteGraph.html ================================================ LiteGraph

    API Docs for:
    Show:

    LiteGraph Class

    Defined in: ../src/litegraph.js:6

    The Global Scope. It contains all the registered node classes.

    Constructor

    LiteGraph

    ()

    Methods

    addNodeMethod

    (
    • func
    )

    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)

    Parameters:

    • func Function

    createNode

    (
    • type
    • name
    • options
    )

    Create a node of a given type with a name. The node is not attached to any graph yet.

    Parameters:

    • type String

      full name of the node class. p.e. "math/sin"

    • name String

      a name to distinguish from other nodes

    • options Object

      to set options

    getNodeType

    (
    • type
    )
    Class

    Returns a registered node type with a given name

    Parameters:

    • type String

      full name of the node class. p.e. "math/sin"

    Returns:

    Class:

    the node class

    getNodeType

    (
    • category
    )
    Array

    Returns a list of node types matching one category

    Parameters:

    • category String

      category name

    Returns:

    Array:

    array with all the node classes

    getNodeTypesCategories

    () Array

    Returns a list with all the node type categories

    Returns:

    Array:

    array with all the names of the categories

    registerNodeType

    (
    • type
    • base_class
    )

    Register a node class so it can be listed when the user wants to create a new one

    Parameters:

    • type String

      name of the node and path

    • base_class Class

      class containing the structure of a node

    wrapFunctionAsNode

    (
    • name
    • func
    • param_types
    • return_type
    • properties
    )

    Create a new node type by passing a function, it wraps it with a propper 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.

    Parameters:

    • name String

      node name with namespace (p.e.: 'math/sum')

    • func Function
    • param_types Array

      [optional] an array containing the type of every parameter, otherwise parameters will accept any type

    • return_type String

      [optional] string with the return type, otherwise it will be generic

    • properties Object

      [optional] properties to be configurable

    ================================================ FILE: doc/classes/index.html ================================================ Redirector Click here to redirect ================================================ FILE: doc/data.json ================================================ { "project": {}, "files": { "../src/litegraph.js": { "name": "../src/litegraph.js", "modules": {}, "classes": { "LiteGraph": 1, "LGraph": 1, "LGraphNode": 1, "LGraphCanvas": 1, "ContextMenu": 1 }, "fors": {}, "namespaces": {} } }, "modules": {}, "classes": { "LiteGraph": { "name": "LiteGraph", "shortname": "LiteGraph", "classitems": [], "plugins": [], "extensions": [], "plugin_for": [], "extension_for": [], "file": "../src/litegraph.js", "line": 6, "description": "The Global Scope. It contains all the registered node classes.", "is_constructor": 1 }, "LGraph": { "name": "LGraph", "shortname": "LGraph", "classitems": [], "plugins": [], "extensions": [], "plugin_for": [], "extension_for": [], "file": "../src/litegraph.js", "line": 438, "description": "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.", "is_constructor": 1, "params": [ { "name": "o", "description": "data from previous serialization [optional]", "type": "Object" } ] }, "LGraphNode": { "name": "LGraphNode", "shortname": "LGraphNode", "classitems": [], "plugins": [], "extensions": [], "plugin_for": [], "extension_for": [], "file": "../src/litegraph.js", "line": 1830, "description": "Base Class for all the node type classes", "params": [ { "name": "name", "description": "a name for the node", "type": "String" } ] }, "LGraphCanvas": { "name": "LGraphCanvas", "shortname": "LGraphCanvas", "classitems": [], "plugins": [], "extensions": [], "plugin_for": [], "extension_for": [], "file": "../src/litegraph.js", "line": 4164, "description": "marks as dirty the canvas, this way it will be rendered again", "is_constructor": 1, "params": [ { "name": "canvas", "description": "the canvas where you want to render (it accepts a selector in string format or the canvas element itself)", "type": "HTMLCanvas" }, { "name": "graph", "description": "[optional]", "type": "LGraph" }, { "name": "options", "description": "[optional] { skip_rendering, autoresize }", "type": "Object" } ], "itemtype": "method" }, "ContextMenu": { "name": "ContextMenu", "shortname": "ContextMenu", "classitems": [], "plugins": [], "extensions": [], "plugin_for": [], "extension_for": [], "file": "../src/litegraph.js", "line": 8395, "description": "ContextMenu from LiteGUI", "is_constructor": 1, "params": [ { "name": "values", "description": "(allows object { title: \"Nice text\", callback: function ... })", "type": "Array" }, { "name": "options", "description": "[optional] Some options:\\\n- title: title to show on top of the menu\n- callback: function to call when an option is clicked, it receives the item information\n- ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback\n- event: you can pass a MouseEvent, this way the ContextMenu appears in that position", "type": "Object" } ] } }, "elements": {}, "classitems": [ { "file": "../src/litegraph.js", "line": 93, "description": "Register a node class so it can be listed when the user wants to create a new one", "itemtype": "method", "name": "registerNodeType", "params": [ { "name": "type", "description": "name of the node and path", "type": "String" }, { "name": "base_class", "description": "class containing the structure of a node", "type": "Class" } ], "class": "LiteGraph" }, { "file": "../src/litegraph.js", "line": 160, "description": "Create a new node type by passing a function, it wraps it with a propper class and generates inputs according to the parameters of the function.\nUseful to wrap simple methods that do not require properties, and that only process some input to generate an output.", "itemtype": "method", "name": "wrapFunctionAsNode", "params": [ { "name": "name", "description": "node name with namespace (p.e.: 'math/sum')", "type": "String" }, { "name": "func", "description": "", "type": "Function" }, { "name": "param_types", "description": "[optional] an array containing the type of every parameter, otherwise parameters will accept any type", "type": "Array" }, { "name": "return_type", "description": "[optional] string with the return type, otherwise it will be generic", "type": "String" }, { "name": "properties", "description": "[optional] properties to be configurable", "type": "Object" } ], "class": "LiteGraph" }, { "file": "../src/litegraph.js", "line": 193, "description": "Adds this method to all nodetypes, existing and to be created\n(You can add it to LGraphNode.prototype but then existing node types wont have it)", "itemtype": "method", "name": "addNodeMethod", "params": [ { "name": "func", "description": "", "type": "Function" } ], "class": "LiteGraph" }, { "file": "../src/litegraph.js", "line": 211, "description": "Create a node of a given type with a name. The node is not attached to any graph yet.", "itemtype": "method", "name": "createNode", "params": [ { "name": "type", "description": "full name of the node class. p.e. \"math/sin\"", "type": "String" }, { "name": "name", "description": "a name to distinguish from other nodes", "type": "String" }, { "name": "options", "description": "to set options", "type": "Object" } ], "class": "LiteGraph" }, { "file": "../src/litegraph.js", "line": 270, "description": "Returns a registered node type with a given name", "itemtype": "method", "name": "getNodeType", "params": [ { "name": "type", "description": "full name of the node class. p.e. \"math/sin\"", "type": "String" } ], "return": { "description": "the node class", "type": "Class" }, "class": "LiteGraph" }, { "file": "../src/litegraph.js", "line": 281, "description": "Returns a list of node types matching one category", "itemtype": "method", "name": "getNodeType", "params": [ { "name": "category", "description": "category name", "type": "String" } ], "return": { "description": "array with all the node classes", "type": "Array" }, "class": "LiteGraph" }, { "file": "../src/litegraph.js", "line": 309, "description": "Returns a list with all the node type categories", "itemtype": "method", "name": "getNodeTypesCategories", "return": { "description": "array with all the names of the categories", "type": "Array" }, "class": "LiteGraph" }, { "file": "../src/litegraph.js", "line": 468, "description": "Removes all nodes from this graph", "itemtype": "method", "name": "clear", "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 531, "description": "Attach Canvas to this graph", "itemtype": "method", "name": "attachCanvas", "params": [ { "name": "graph_canvas", "description": "", "type": "GraphCanvas" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 550, "description": "Detach Canvas from this graph", "itemtype": "method", "name": "detachCanvas", "params": [ { "name": "graph_canvas", "description": "", "type": "GraphCanvas" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 567, "description": "Starts running this graph every interval milliseconds.", "itemtype": "method", "name": "start", "params": [ { "name": "interval", "description": "amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate", "type": "Number" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 609, "description": "Stops the execution loop of the graph", "itemtype": "method", "name": "stop execution", "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 634, "description": "Run N steps (cycles) of the graph", "itemtype": "method", "name": "runStep", "params": [ { "name": "num", "description": "number of steps to run, default is 1", "type": "Number" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 716, "description": "Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than\nnodes with only inputs.", "itemtype": "method", "name": "updateExecutionOrder", "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 848, "description": "Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively.\nIt doesnt include the node itself", "itemtype": "method", "name": "getAncestors", "return": { "description": "an array with all the LGraphNodes that affect this node, in order of execution", "type": "Array" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 885, "description": "Positions every node in a more readable manner", "itemtype": "method", "name": "arrange", "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 929, "description": "Returns the amount of time the graph has been running in milliseconds", "itemtype": "method", "name": "getTime", "return": { "description": "number of milliseconds the graph has been running", "type": "Number" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 939, "description": "Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant", "itemtype": "method", "name": "getFixedTime", "return": { "description": "number of milliseconds the graph has been running", "type": "Number" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 950, "description": "Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct\nif the nodes are using graphical actions", "itemtype": "method", "name": "getElapsedTime", "return": { "description": "number of milliseconds it took the last cycle", "type": "Number" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 962, "description": "Sends an event to all the nodes, useful to trigger stuff", "itemtype": "method", "name": "sendEventToAllNodes", "params": [ { "name": "eventname", "description": "the name of the event (function to be called)", "type": "String" }, { "name": "params", "description": "parameters in array format", "type": "Array" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1004, "description": "Adds a new node instasnce to this graph", "itemtype": "method", "name": "add", "params": [ { "name": "node", "description": "the instance of the node", "type": "LGraphNode" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1068, "description": "Removes a node from the graph", "itemtype": "method", "name": "remove", "params": [ { "name": "node", "description": "the instance of the node", "type": "LGraphNode" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1149, "description": "Returns a node by its id.", "itemtype": "method", "name": "getNodeById", "params": [ { "name": "id", "description": "", "type": "Number" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1162, "description": "Returns a list of nodes that matches a class", "itemtype": "method", "name": "findNodesByClass", "params": [ { "name": "classObject", "description": "the class itself (not an string)", "type": "Class" } ], "return": { "description": "a list with all the nodes of this type", "type": "Array" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1178, "description": "Returns a list of nodes that matches a type", "itemtype": "method", "name": "findNodesByType", "params": [ { "name": "type", "description": "the name of the node type", "type": "String" } ], "return": { "description": "a list with all the nodes of this type", "type": "Array" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1195, "description": "Returns a list of nodes that matches a name", "itemtype": "method", "name": "findNodesByTitle", "params": [ { "name": "name", "description": "the name of the node to search", "type": "String" } ], "return": { "description": "a list with all the nodes with this name", "type": "Array" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1211, "description": "Returns the top-most node in this position of the canvas", "itemtype": "method", "name": "getNodeOnPos", "params": [ { "name": "x", "description": "the x coordinate in canvas space", "type": "Number" }, { "name": "y", "description": "the y coordinate in canvas space", "type": "Number" }, { "name": "nodes_list", "description": "a list with all the nodes to search from, by default is all the nodes in the graph", "type": "Array" } ], "return": { "description": "the node at this position or null", "type": "LGraphNode" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1231, "description": "Returns the top-most group in that position", "itemtype": "method", "name": "getGroupOnPos", "params": [ { "name": "x", "description": "the x coordinate in canvas space", "type": "Number" }, { "name": "y", "description": "the y coordinate in canvas space", "type": "Number" } ], "return": { "description": "the group or null", "type": "LGraphGroup" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1251, "description": "Tell this graph it has a global graph input of this type", "itemtype": "method", "name": "addGlobalInput", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "type", "description": "", "type": "String" }, { "name": "value", "description": "[optional]", "type": "*" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1274, "description": "Assign a data to the global graph input", "itemtype": "method", "name": "setGlobalInputData", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "data", "description": "", "type": "*" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1288, "description": "Returns the current value of a global graph input", "itemtype": "method", "name": "getInputData", "params": [ { "name": "name", "description": "", "type": "String" } ], "return": { "description": "the data", "type": "*" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1302, "description": "Changes the name of a global graph input", "itemtype": "method", "name": "renameInput", "params": [ { "name": "old_name", "description": "", "type": "String" }, { "name": "new_name", "description": "", "type": "String" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1333, "description": "Changes the type of a global graph input", "itemtype": "method", "name": "changeInputType", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "type", "description": "", "type": "String" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1353, "description": "Removes a global graph input", "itemtype": "method", "name": "removeInput", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "type", "description": "", "type": "String" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1375, "description": "Creates a global graph output", "itemtype": "method", "name": "addOutput", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "type", "description": "", "type": "String" }, { "name": "value", "description": "", "type": "*" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1394, "description": "Assign a data to the global output", "itemtype": "method", "name": "setOutputData", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "value", "description": "", "type": "String" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1408, "description": "Returns the current value of a global graph output", "itemtype": "method", "name": "getOutputData", "params": [ { "name": "name", "description": "", "type": "String" } ], "return": { "description": "the data", "type": "*" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1422, "description": "Renames a global graph output", "itemtype": "method", "name": "renameOutput", "params": [ { "name": "old_name", "description": "", "type": "String" }, { "name": "new_name", "description": "", "type": "String" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1450, "description": "Changes the type of a global graph output", "itemtype": "method", "name": "changeOutputType", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "type", "description": "", "type": "String" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1470, "description": "Removes a global graph output", "itemtype": "method", "name": "removeOutput", "params": [ { "name": "name", "description": "", "type": "String" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1514, "description": "returns if the graph is in live mode", "itemtype": "method", "name": "isLive", "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1533, "description": "clears the triggered slot animation in all links (stop visual animation)", "itemtype": "method", "name": "clearTriggeredSlots", "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1565, "description": "Destroys a link", "itemtype": "method", "name": "removeLink", "params": [ { "name": "link_id", "description": "", "type": "Number" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1582, "description": "Creates a Object containing all the info about this graph, it can be serialized", "itemtype": "method", "name": "serialize", "return": { "description": "value of the node", "type": "Object" }, "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1619, "description": "Configure a graph from a JSON string", "itemtype": "method", "name": "configure", "params": [ { "name": "str", "description": "configure a graph from a JSON string", "type": "String" }, { "name": "returns", "description": "if there was any error parsing", "type": "Boolean" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", "line": 1881, "description": "configure a node from an object containing the serialized info", "itemtype": "method", "name": "configure", "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 1949, "description": "serialize the content", "itemtype": "method", "name": "serialize", "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2039, "description": "serialize and stringify", "itemtype": "method", "name": "toString", "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2051, "description": "get the title string", "itemtype": "method", "name": "getTitle", "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2064, "description": "sets the output data", "itemtype": "method", "name": "setOutputData", "params": [ { "name": "slot", "description": "", "type": "Number" }, { "name": "data", "description": "", "type": "*" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2100, "description": "sets the output data type, useful when you want to be able to overwrite the data type", "itemtype": "method", "name": "setOutputDataType", "params": [ { "name": "slot", "description": "", "type": "Number" }, { "name": "datatype", "description": "", "type": "String" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2129, "description": "Retrieves the input data (data traveling through the connection) from one slot", "itemtype": "method", "name": "getInputData", "params": [ { "name": "slot", "description": "", "type": "Number" }, { "name": "force_update", "description": "if set to true it will force the connected node of this slot to output data into this link", "type": "Boolean" } ], "return": { "description": "data or if it is not connected returns undefined", "type": "*" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2165, "description": "Retrieves the input data type (in case this supports multiple input types)", "itemtype": "method", "name": "getInputDataType", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "return": { "description": "datatype in string format", "type": "String" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2191, "description": "Retrieves the input data from one slot using its name instead of slot number", "itemtype": "method", "name": "getInputDataByName", "params": [ { "name": "slot_name", "description": "", "type": "String" }, { "name": "force_update", "description": "if set to true it will force the connected node of this slot to output data into this link", "type": "Boolean" } ], "return": { "description": "data or if it is not connected returns null", "type": "*" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2207, "description": "tells you if there is a connection in one input slot", "itemtype": "method", "name": "isInputConnected", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "return": { "description": "", "type": "Boolean" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2220, "description": "tells you info about an input connection (which node, type, etc)", "itemtype": "method", "name": "getInputInfo", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "return": { "description": "object or null { link: id, name: string, type: string or 0 }", "type": "Object" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2235, "description": "returns the node connected in the input slot", "itemtype": "method", "name": "getInputNode", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "return": { "description": "node or null", "type": "LGraphNode" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2257, "description": "returns the value of an input with this name, otherwise checks if there is a property with that name", "itemtype": "method", "name": "getInputOrProperty", "params": [ { "name": "name", "description": "", "type": "String" } ], "return": { "description": "value", "type": "*" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2284, "description": "tells you the last output data that went in that slot", "itemtype": "method", "name": "getOutputData", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "return": { "description": "object or null", "type": "Object" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2302, "description": "tells you info about an output connection (which node, type, etc)", "itemtype": "method", "name": "getOutputInfo", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "return": { "description": "object or null { name: string, type: string, links: [ ids of links in number ] }", "type": "Object" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2318, "description": "tells you if there is a connection in one output slot", "itemtype": "method", "name": "isOutputConnected", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "return": { "description": "", "type": "Boolean" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2331, "description": "tells you if there is any connection in the output slots", "itemtype": "method", "name": "isAnyOutputConnected", "return": { "description": "", "type": "Boolean" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2347, "description": "retrieves all the nodes connected to this output slot", "itemtype": "method", "name": "getOutputNodes", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "return": { "description": "", "type": "Array" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2380, "description": "Triggers an event in this node, this will trigger any output with the same name", "itemtype": "method", "name": "trigger", "params": [ { "name": "event", "description": "name ( \"on_play\", ... ) if action is equivalent to false then the event is send to all", "type": "String" }, { "name": "param", "description": "", "type": "*" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2403, "description": "Triggers an slot event in this node", "itemtype": "method", "name": "triggerSlot", "params": [ { "name": "slot", "description": "the index of the output slot", "type": "Number" }, { "name": "param", "description": "", "type": "*" }, { "name": "link_id", "description": "[optional] in case you want to trigger and specific output link in a slot", "type": "Number" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2453, "description": "clears the trigger slot animation", "itemtype": "method", "name": "clearTriggeredSlot", "params": [ { "name": "slot", "description": "the index of the output slot", "type": "Number" }, { "name": "link_id", "description": "[optional] in case you want to trigger and specific output link in a slot", "type": "Number" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2485, "description": "add a new property to this node", "itemtype": "method", "name": "addProperty", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "default_value", "description": "", "type": "*" }, { "name": "type", "description": "string defining the output type (\"vec3\",\"number\",...)", "type": "String" }, { "name": "extra_info", "description": "this can be used to have special properties of the property (like values, etc)", "type": "Object" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2511, "description": "add a new output slot to use in this node", "itemtype": "method", "name": "addOutput", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "type", "description": "string defining the output type (\"vec3\",\"number\",...)", "type": "String" }, { "name": "extra_info", "description": "this can be used to have special properties of an output (label, special color, position, etc)", "type": "Object" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2535, "description": "add a new output slot to use in this node", "itemtype": "method", "name": "addOutputs", "params": [ { "name": "array", "description": "of triplets like [[name,type,extra_info],[...]]", "type": "Array" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2561, "description": "remove an existing output slot", "itemtype": "method", "name": "removeOutput", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2590, "description": "add a new input slot to use in this node", "itemtype": "method", "name": "addInput", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "type", "description": "string defining the input type (\"vec3\",\"number\",...), it its a generic one use 0", "type": "String" }, { "name": "extra_info", "description": "this can be used to have special properties of an input (label, color, position, etc)", "type": "Object" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2615, "description": "add several new input slots in this node", "itemtype": "method", "name": "addInputs", "params": [ { "name": "array", "description": "of triplets like [[name,type,extra_info],[...]]", "type": "Array" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2641, "description": "remove an existing input slot", "itemtype": "method", "name": "removeInput", "params": [ { "name": "slot", "description": "", "type": "Number" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2665, "description": "add an special connection to this node (used for special kinds of graphs)", "itemtype": "method", "name": "addConnection", "params": [ { "name": "name", "description": "", "type": "String" }, { "name": "type", "description": "string defining the input type (\"vec3\",\"number\",...)", "type": "String" }, { "name": "pos", "description": "position of the connection inside the node", "type": "[x,y]" }, { "name": "direction", "description": "if is input or output", "type": "String" } ], "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2686, "description": "computes the size of a node according to its inputs and output slots", "itemtype": "method", "name": "computeSize", "params": [ { "name": "minHeight", "description": "", "type": "Number" } ], "return": { "description": "the total size", "type": "Number" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2753, "description": "Allows to pass", "itemtype": "method", "name": "addWidget", "return": { "description": "the created widget", "type": "Object" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2791, "description": "returns the bounding of the object, used for rendering purposes\nbounding is: [topleft_cornerx, topleft_cornery, width, height]", "itemtype": "method", "name": "getBounding", "return": { "description": "the total size", "type": "Float32Array[4]" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2810, "description": "checks if a point is inside the shape of a node", "itemtype": "method", "name": "isPointInside", "params": [ { "name": "x", "description": "", "type": "Number" }, { "name": "y", "description": "", "type": "Number" } ], "return": { "description": "", "type": "Boolean" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2836, "description": "checks if a point is inside a node slot, and returns info about which slot", "itemtype": "method", "name": "getSlotInPosition", "params": [ { "name": "x", "description": "", "type": "Number" }, { "name": "y", "description": "", "type": "Number" } ], "return": { "description": "if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] }", "type": "Object" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2868, "description": "returns the input slot with a given name (used for dynamic slots), -1 if not found", "itemtype": "method", "name": "findInputSlot", "params": [ { "name": "name", "description": "the name of the slot", "type": "String" } ], "return": { "description": "the slot (-1 if not found)", "type": "Number" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2884, "description": "returns the output slot with a given name (used for dynamic slots), -1 if not found", "itemtype": "method", "name": "findOutputSlot", "params": [ { "name": "name", "description": "the name of the slot", "type": "String" } ], "return": { "description": "the slot (-1 if not found)", "type": "Number" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 2899, "description": "connect this node output to the input of another node", "itemtype": "method", "name": "connect", "params": [ { "name": "slot", "description": "(could be the number of the slot or the string with the name of the slot)", "type": "Number_or_string" }, { "name": "node", "description": "the target node", "type": "LGraphNode" }, { "name": "target_slot", "description": "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)", "type": "Number_or_string" } ], "return": { "description": "the link_info is created, otherwise null", "type": "Object" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 3024, "description": "disconnect one output to an specific node", "itemtype": "method", "name": "disconnectOutput", "params": [ { "name": "slot", "description": "(could be the number of the slot or the string with the name of the slot)", "type": "Number_or_string" }, { "name": "target_node", "description": "the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected]", "type": "LGraphNode" } ], "return": { "description": "if it was disconnected succesfully", "type": "Boolean" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 3132, "description": "disconnect one input", "itemtype": "method", "name": "disconnectInput", "params": [ { "name": "slot", "description": "(could be the number of the slot or the string with the name of the slot)", "type": "Number_or_string" } ], "return": { "description": "if it was disconnected succesfully", "type": "Boolean" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 3206, "description": "returns the center of a connection point in canvas coords", "itemtype": "method", "name": "getConnectionPos", "params": [ { "name": "is_input", "description": "true if if a input slot, false if it is an output", "type": "Boolean" }, { "name": "slot", "description": "(could be the number of the slot or the string with the name of the slot)", "type": "Number_or_string" }, { "name": "out", "description": "[optional] a place to store the output, to free garbage", "type": "Vec2" } ], "return": { "description": "the position", "type": "[x,y]" }, "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 3390, "description": "Collapse the node to make it smaller on the canvas", "itemtype": "method", "name": "collapse", "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 3406, "description": "Forces the node to do not move or realign on Z", "itemtype": "method", "name": "pin", "class": "LGraphNode" }, { "file": "../src/litegraph.js", "line": 3837, "description": "clears all the data inside", "itemtype": "method", "name": "clear", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 3880, "description": "assigns a graph, you can reasign graphs to the same canvas", "itemtype": "method", "name": "setGraph", "params": [ { "name": "graph", "description": "", "type": "LGraph" } ], "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 3911, "description": "opens a graph contained inside a node in the current graph", "itemtype": "method", "name": "openSubgraph", "params": [ { "name": "graph", "description": "", "type": "LGraph" } ], "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 3938, "description": "closes a subgraph contained inside a node", "itemtype": "method", "name": "closeSubgraph", "params": [ { "name": "assigns", "description": "a graph", "type": "LGraph" } ], "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 3961, "description": "assigns a canvas", "itemtype": "method", "name": "setCanvas", "params": [ { "name": "assigns", "description": "a canvas (also accepts the ID of the element (not a selector)", "type": "Canvas" } ], "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4038, "description": "binds mouse, keyboard, touch and drag events to the canvas", "itemtype": "method", "name": "bindEvents", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4090, "description": "unbinds mouse events from the canvas", "itemtype": "method", "name": "unbindEvents", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4138, "description": "this function allows to render the canvas using WebGL instead of Canvas2D\nthis 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", "itemtype": "method", "name": "enableWebGL", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4180, "description": "Used to attach the canvas in a popup", "itemtype": "method", "name": "getCanvasWindow", "return": { "description": "returns the window where the canvas is attached (the DOM root node)", "type": "Window" }, "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4194, "description": "starts rendering the content of the canvas when needed", "itemtype": "method", "name": "startRendering", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4218, "description": "stops rendering the content of the canvas (to save resources)", "itemtype": "method", "name": "stopRendering", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4501, "description": "Called when a mouse move event has to be processed", "itemtype": "method", "name": "processMouseMove", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4675, "description": "Called when a mouse up event has to be processed", "itemtype": "method", "name": "processMouseUp", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4847, "description": "Called when a mouse wheel event has to be processed", "itemtype": "method", "name": "processMouseWheel", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4876, "description": "retuns true if a position (in graph space) is on top of a node little corner box", "itemtype": "method", "name": "isOverNodeBox", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4888, "description": "retuns true if a position (in graph space) is on top of a node input slot", "itemtype": "method", "name": "isOverNodeInput", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 4917, "description": "process a key event", "itemtype": "method", "name": "processKey", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5074, "description": "process a item drop event on top the canvas", "itemtype": "method", "name": "processDrop", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5192, "description": "selects a given node (or adds it to the current selection)", "itemtype": "method", "name": "selectNode", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5204, "description": "selects several nodes (or adds them to the current selection)", "itemtype": "method", "name": "selectNodes", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5242, "description": "removes a node from the current selection", "itemtype": "method", "name": "deselectNode", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5268, "description": "removes all nodes from the current selection", "itemtype": "method", "name": "deselectAllNodes", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5291, "description": "deletes all nodes in the current selection from the graph", "itemtype": "method", "name": "deleteSelectedNodes", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5308, "description": "centers the camera on a given node", "itemtype": "method", "name": "centerOnNode", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5319, "description": "adds some useful properties to a mouse event, like the position in graph coordinates", "itemtype": "method", "name": "adjustMouseEvent", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5347, "description": "changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom", "itemtype": "method", "name": "setZoom", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5378, "description": "converts a coordinate from graph coordinates to canvas2D coordinates", "itemtype": "method", "name": "convertOffsetToCanvas", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5387, "description": "converts a coordinate from Canvas2D coordinates to graph space", "itemtype": "method", "name": "convertCanvasToOffset", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5403, "description": "brings a node to front (above all other nodes)", "itemtype": "method", "name": "bringToFront", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5416, "description": "sends a node to the back (below all other nodes)", "itemtype": "method", "name": "sendToBack", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5436, "description": "checks which nodes are visible (inside the camera area)", "itemtype": "method", "name": "computeVisibleNodes", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5461, "description": "renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes)", "itemtype": "method", "name": "draw", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5488, "description": "draws the front canvas (the one containing all the nodes)", "itemtype": "method", "name": "drawFrontCanvas", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5632, "description": "draws some useful stats in the corner of the canvas", "itemtype": "method", "name": "renderInfo", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5659, "description": "draws the back canvas (the one containing the background and the connections)", "itemtype": "method", "name": "drawBackCanvas", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 5807, "description": "draws the given node inside the canvas", "itemtype": "method", "name": "drawNode", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 6115, "description": "draws the shape of the given node in the canvas", "itemtype": "method", "name": "drawNodeShape", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 6311, "description": "draws every connection visible in the canvas\nOPTIMIZE THIS: precatch connections position instead of recomputing them every time", "itemtype": "method", "name": "drawConnections", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 6398, "description": "draws a link between two points", "itemtype": "method", "name": "renderLink", "params": [ { "name": "a", "description": "start pos", "type": "Vec2" }, { "name": "b", "description": "end pos", "type": "Vec2" }, { "name": "link", "description": "the link object with all the link info", "type": "Object" }, { "name": "skip_border", "description": "ignore the shadow of the link", "type": "Boolean" }, { "name": "flow", "description": "show flow animation (for events)", "type": "Boolean" }, { "name": "color", "description": "the color for the link", "type": "String" }, { "name": "start_dir", "description": "the direction enum", "type": "Number" }, { "name": "end_dir", "description": "the direction enum", "type": "Number" }, { "name": "num_sublines", "description": "number of sublines (useful to represent vec3 or rgb)", "type": "Number" } ], "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 6663, "description": "draws the widgets stored inside a node", "itemtype": "method", "name": "drawNodeWidgets", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 6813, "description": "process an event on widgets", "itemtype": "method", "name": "processNodeWidgets", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 6924, "description": "draws every group area in the background", "itemtype": "method", "name": "drawGroups", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 6979, "description": "resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode", "itemtype": "method", "name": "resize", "class": "LGraphCanvas" }, { "file": "../src/litegraph.js", "line": 7002, "description": "switches to live mode (node shapes are not rendered, only the content)\nthis feature was designed when graphs where meant to create user interfaces", "itemtype": "method", "name": "switchLiveMode", "class": "LGraphCanvas" } ], "warnings": [] } ================================================ FILE: doc/elements/index.html ================================================ Redirector Click here to redirect ================================================ FILE: doc/files/.._src_litegraph.js.html ================================================ ../src/litegraph.js

    API Docs for:
    Show:

    File: ../src/litegraph.js

    (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_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",
    	DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)",
    	DEFAULT_GROUP_FONT: 24,
    
    	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,
    
    	//enums
    	INPUT: 1,
    	OUTPUT: 2,
    
    	EVENT: -1, //for outputs
    	ACTION: -1, //for inputs
    
    	ALWAYS: 0,
    	ON_EVENT: 1,
    	NEVER: 2,
    	ON_TRIGGER: 3,
    
    	UP: 1,
    	DOWN:2,
    	LEFT:3,
    	RIGHT:4,
    	CENTER:5,
    
    	STRAIGHT_LINK: 0,
    	LINEAR_LINK: 1,
    	SPLINE_LINK: 2,
    
    	NORMAL_TITLE: 0,
    	NO_TITLE: 1,
    	TRANSPARENT_TITLE: 2,
    	AUTOHIDE_TITLE: 3,
    
    	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 droping files in the canvas
    	Nodes: {}, //node types by classname
    
    	searchbox_extras: {}, //used to add extra features to the search box
    
    	/**
    	* 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);
    
    		var categories = type.split("/");
    		var classname = base_class.name;
    
    		var pos = type.lastIndexOf("/");
    		base_class.category = type.substr(0,pos);
    
    		if(!base_class.title)
    			base_class.title = classname;
    		//info.name = name.substr(pos+1,name.length - pos);
    
    		//extend class
    		if(base_class.prototype) //is a class
    			for(var i in LGraphNode.prototype)
    				if(!base_class.prototype[i])
    					base_class.prototype[i] = LGraphNode.prototype[i];
    
    		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(v)
    			{
    				return this._shape;
    			},
    			enumerable: true
    		});
    
    		this.registered_node_types[ type ] = base_class;
    		if(base_class.constructor.name)
    			this.Nodes[ classname ] = base_class;
    
    		//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");
    
    		if( base_class.supported_extensions )
    		{
    			for(var i in base_class.supported_extensions )
    				this.node_types_by_file_extension[ base_class.supported_extensions[i].toLowerCase() ] = base_class;
    		}
    	},
    
    	/**
    	* Create a new node type by passing a function, it wraps it with a propper 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 );
    	},
    
    	/**
    	* 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();
    		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];
    		}
    
    		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(filter && type.filter && type.filter != filter)
    				continue;
    
    			if(category == "" )
    			{
    				if (type.category == null)
    					r.push(type);
    			}
    			else if (type.category == category)
    				r.push(type);
    		}
    
    		return r;
    	},
    
    	/**
    	* Returns a list with all the node type categories
    	* @method getNodeTypesCategories
    	* @return {Array} array with all the names of the categories
    	*/
    
    	getNodeTypesCategories: function()
    	{
    		var categories = {"":1};
    		for(var i in this.registered_node_types)
    			if(this.registered_node_types[i].category && !this.registered_node_types[i].skip_list)
    				categories[ this.registered_node_types[i].category ] = 1;
    		var result = [];
    		for(var i in categories)
    			result.push(i);
    		return result;
    	},
    
    	//debug purposes: reloads all the js scripts that matches a wilcard
    	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 in tmp)
    			script_files.push(tmp[i]);
    
    
    		var docHeadObj = document.getElementsByTagName("head")[0];
    		folder_wildcard = document.location.href + folder_wildcard;
    
    		for(var i in script_files)
    		{
    			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 doesnt 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;
    	},
    
    	isValidConnection: function( type_a, type_b )
    	{
    		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( supported_types_a[i] == supported_types_b[j] )
    					return true;
    
    		return false;
    	},
    
    	registerSearchboxExtra: function( node_type, description, data )
    	{
    		this.searchbox_extras[ description ] = { type: node_type, desc: description, data: data };
    	}
    };
    
    //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.
    *
    * @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 = 1;
    	this.last_link_id = 1;
    
    	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 that are executable sorted in execution order
    	this._nodes_executable = null; //nodes that contain onExecute
    
    	//other scene stuff
    	this._groups = [];
    
    	//links
    	this.links = {}; //container with all the links
    
    	//iterations
    	this.iteration = 0;
    
    	//custom data
    	this.config = {};
    
    	//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;
    
    	//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;
    
    	if(interval == 0 && typeof(window) != "undefined" && window.requestAnimationFrame )
    	{
    		function on_frame()
    		{
    			if(that.execution_timer_id != -1)
    				return;
    			window.requestAnimationFrame(on_frame);
    			that.runStep(1, !this.catch_errors );
    		}
    		this.execution_timer_id = -1;
    		on_frame();
    	}
    	else
    		this.execution_timer_id = setInterval( function() {
    			//execute
    			that.runStep(1, !this.catch_errors );
    		},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
    */
    
    LGraph.prototype.runStep = function( num, do_not_catch_errors )
    {
    	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;
    
    	if( do_not_catch_errors )
    	{
    		//iterations
    		for(var i = 0; i < num; i++)
    		{
    			for( var j = 0, l = nodes.length; j < l; ++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();
    	}
    	else
    	{
    		try
    		{
    			//iterations
    			for(var i = 0; i < num; i++)
    			{
    				for( var j = 0, l = nodes.length; j < l; ++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;
    }
    
    /**
    * 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 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;
    		}
    		else //num of input links
    		{
    			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)
    			return A.order - B.order;
    		return Ap - Bp;
    	});
    
    	//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 doesnt 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 )
    {
    	margin = margin || 40;
    
    	var nodes = this.computeExecutionOrder( false, true );
    	var columns = [];
    	for(var i = 0; i < nodes.length; ++i)
    	{
    		var node = nodes[i];
    		var col = node._level || 1;
    		if(!columns[col])
    			columns[col] = [];
    		columns[col].push( node );
    	}
    
    	var x = margin;
    
    	for(var i = 0; i < columns.length; ++i)
    	{
    		var column = columns[i];
    		if(!column)
    			continue;
    		var max_size = 100;
    		var y = margin;
    		for(var j = 0; j < column.length; ++j)
    		{
    			var node = column[j];
    			node.pos[0] = x;
    			node.pos[1] = y;
    			if(node.size[0] > max_size)
    				max_size = node.size[0];
    			y += node.size[1] + margin;
    		}
    		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[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 instasnce 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");
    		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(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
    
    	//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);
    
    	this.setDirtyCanvas(true,true);
    	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)
    {
    	var r = [];
    	for(var i = 0, l = this._nodes.length; i < l; ++i)
    		if(this._nodes[i].constructor === classObject)
    			r.push(this._nodes[i]);
    	return r;
    }
    
    /**
    * 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)
    {
    	var type = type.toLowerCase();
    	var r = [];
    	for(var i = 0, l = this._nodes.length; i < l; ++i)
    		if(this._nodes[i].type.toLowerCase() == type )
    			r.push(this._nodes[i]);
    	return r;
    }
    
    /**
    * 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;
    	for (var i = nodes_list.length - 1; i >= 0; i--)
    	{
    		var n = nodes_list[i];
    		if(n.isPointInside( x, y, margin ))
    			return n;
    	}
    	return null;
    }
    
    /**
    * 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;
    }
    
    // ********** GLOBALS *****************
    
    /**
    * 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.inputs[ name ] = { name: name, type: type, value: value };
    	this._version++;
    
    	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 && this.inputs[name].type.toLowerCase() == 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 && this.outputs[name].type.toLowerCase() == 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);
    }
    
    
    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];
    		links.push([ link.id, link.origin_id, link.origin_slot, link.target_id, link.target_slot, link.type ]);
    	}
    
    	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,
    		version: LiteGraph.VERSION
    	};
    
    	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];
    			var link = new LLink();
    			link.configure( link_data );
    			links[ link.id ] = link;
    		}
    		data.links = links;
    	}
    
    	//copy all stored fields
    	for (var i in data)
    		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._version++;
    	this.setDirtyCanvas(true,true);
    	return error;
    }
    
    LGraph.prototype.load = function(url)
    {
    	var that = this;
    	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);
    	}
    	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.type, this.origin_id, this.origin_slot, this.target_id, this.target_slot ];
    }
    
    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 cliped
    		+ 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_up: widgets start 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
    	});
    
    	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 dont 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]);
    		}
    		else //value
    			this[j] = info[j];
    	}
    
    	if(!info.title)
    		this.title = this.constructor.title;
    
    	if(this.onConnectionsChange)
    	{
    		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;
    			this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated
    		}
    
    		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;
    				this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated
    			}
    		}
    	}
    
    	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),
    		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( !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"];
    	//remove links
    	node.configure(data);
    
    	return node;
    }
    
    
    /**
    * serialize and stringify
    * @method toString
    */
    
    LGraphNode.prototype.toString = function()
    {
    	return JSON.stringify( this.serialize() );
    }
    //LGraphNode.prototype.unserialize = 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;
    }
    
    
    
    // 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];
    			this.graph.links[ link_id ].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 incomming 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 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;
    }
    
    /**
    * 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 )
    {
    	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 );
    	}
    }
    
    /**
    * Triggers an slot event in this node
    * @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 )
    {
    	if( !this.outputs )
    		return;
    
    	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.onAction)
    			node.onAction( target_connection.name, param );
    		else if(node.mode === LiteGraph.ON_TRIGGER)
    		{
    			if(node.onExecute)
    				node.onExecute(param);
    		}
    	}
    }
    
    /**
    * 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;
    	}
    }
    
    /**
    * 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 o = { name: name, type: type, links: null };
    	if(extra_info)
    		for(var i in extra_info)
    			o[i] = extra_info[i];
    
    	if(!this.outputs)
    		this.outputs = [];
    	this.outputs.push(o);
    	if(this.onOutputAdded)
    		this.onOutputAdded(o);
    	this.size = this.computeSize();
    	this.setDirtyCanvas(true,true);
    	return o;
    }
    
    /**
    * 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);
    	}
    
    	this.size = 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.size = 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 o = {name:name,type:type,link:null};
    	if(extra_info)
    		for(var i in extra_info)
    			o[i] = extra_info[i];
    
    	if(!this.inputs)
    		this.inputs = [];
    	this.inputs.push(o);
    	this.size = this.computeSize();
    	if(this.onInputAdded)
    		this.onInputAdded(o);
    	this.setDirtyCanvas(true,true);
    	return o;
    }
    
    /**
    * 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);
    	}
    
    	this.size = this.computeSize();
    	this.setDirtyCanvas(true,true);
    }
    
    /**
    * remove an existing input slot
    * @method removeInput
    * @param {number} slot
    */
    LGraphNode.prototype.removeInput = function(slot)
    {
    	this.disconnectInput(slot);
    	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.size = this.computeSize();
    	if(this.onInputRemoved)
    		this.onInputRemoved(slot);
    	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 size of a node according to its inputs and output slots
    * @method computeSize
    * @param {number} minHeight
    * @return {number} the total size
    */
    LGraphNode.prototype.computeSize = function( minHeight, 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
    	size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT;
    	if( this.widgets && this.widgets.length )
    		size[1] += this.widgets.length * (LiteGraph.NODE_WIDGET_HEIGHT + 4) + 8;
    
    	var font_size = 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 );
    
    	if(this.onResize)
    		this.onResize(size);
    
    	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;
    }
    
    /**
    * Allows to pass 
    * 
    * @method addWidget
    * @return {Object} the created widget
    */
    LGraphNode.prototype.addWidget = function( type, name, value, callback, options )
    {
    	if(!this.widgets)
    		this.widgets = [];
    	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 )
    		console.warn("LiteGraph addWidget('button',...) without a callback");
    	if( type == "combo" && !w.options.values )
    		throw("LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }");
    	this.widgets.push(w);
    	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.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 : 20;
    	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, locked: input.locked };
    		}
    
    	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, locked: output.locked };
    		}
    
    	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
    * @return {number} the slot (-1 if not found)
    */
    LGraphNode.prototype.findInputSlot = function(name)
    {
    	if(!this.inputs)
    		return -1;
    	for(var i = 0, l = this.inputs.length; i < l; ++i)
    		if(name == this.inputs[i].name)
    			return 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
    * @return {number} the slot (-1 if not found)
    */
    LGraphNode.prototype.findOutputSlot = function(name)
    {
    	if(!this.outputs) return -1;
    	for(var i = 0, l = this.outputs.length; i < l; ++i)
    		if(name == this.outputs[i].name)
    			return i;
    	return -1;
    }
    
    /**
    * 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 doesnt 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 )
    	{
    		//search for first slot with event?
    		/*
    		//create input for trigger
    		var input = target_node.addInput("onTrigger", LiteGraph.EVENT );
    		target_slot = target_node.inputs.length - 1; //last one is the one created
    		target_node.mode = LiteGraph.ON_TRIGGER;
    		*/
    		return null;
    	}
    	else if( !target_node.inputs || target_slot >= target_node.inputs.length )
    	{
    		if(LiteGraph.debug)
    			console.log("Connect: Error, slot number not found");
    		return null;
    	}
    
    	//if there is something already plugged there, disconnect
    	if(target_node.inputs[ target_slot ].link != null )
    		target_node.disconnectInput( target_slot );
    
    	//why here??
    	//this.setDirtyCanvas(false,true);
    	//this.graph.connectionChange( this );
    
    	var output = this.outputs[slot];
    
    	//allows nodes to block connection
    	if(target_node.onConnectInput)
    		if( target_node.onConnectInput( target_slot, output.type, output ) === false)
    			return null;
    
    	var input = target_node.inputs[target_slot];
    	var link_info = null;
    
    	if( LiteGraph.isValidConnection( output.type, input.type ) )
    	{
    		link_info = new LLink( this.graph.last_link_id++, input.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.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 succesfully
    */
    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 hasnt 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;
    			}
    		}
    	}
    	else //all the links in this output slot
    	{
    		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 hasnt 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 succesfully
    */
    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;
    	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 );
    		}
    	}
    
    	this.setDirtyCanvas(false,true);
    	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;
    	}
    
    	//hardcoded 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();
    
    	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 = o.font;
    }
    
    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: this.font
    	};
    }
    
    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);
    
    	element.addEventListener("mousedown", this._binded_mouse_callback );
    	element.addEventListener("mousemove", this._binded_mouse_callback );
    
    	element.addEventListener("mousewheel", this._binded_mouse_callback, false);
    	element.addEventListener("wheel", this._binded_mouse_callback, false);
    }
    
    DragAndScale.prototype.computeVisibleArea = function()
    {
    	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];
    	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 ignore = false;
    	if(this.onmouse)
    		ignore = this.onmouse(e);
    
    	if(e.type == "mousedown")
    	{
    		this.dragging = true;
    		canvas.removeEventListener("mousemove", this._binded_mouse_callback );
    		document.body.addEventListener("mousemove", this._binded_mouse_callback  );
    		document.body.addEventListener("mouseup", this._binded_mouse_callback );
    	}
    	else if(e.type == "mousemove")
    	{
    		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 == "mouseup")
    	{
    		this.dragging = false;
    		document.body.removeEventListener("mousemove", this._binded_mouse_callback );
    		document.body.removeEventListener("mouseup", this._binded_mouse_callback );
    		canvas.addEventListener("mousemove", this._binded_mouse_callback  );
    	}
    	else if(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;
    
    	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 }
    */
    function LGraphCanvas( canvas, graph, options )
    {
    	options = options || {};
    
    	//if(graph === undefined)
      //	throw ("No graph assigned");
    	this.background_image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
    
    	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",
    		output_off: "#778",
    		output_on: "#7F7"
    	};
    
    	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.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.allow_searchbox = true;
    	this.allow_reconnect_links = false; //allows to change a connection with having to redo it again
    
    	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.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.links_render_mode = LiteGraph.SPLINE_LINK;
    
    	this.canvas_mouse = [0,0]; //mouse in canvas graph coordinates, where 0,0 is the top-left corner of the blue rectangle
    
    	//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.connections_width = 3;
    	this.round_radius = 8;
    
    	this.current_node = null;
    	this.node_widget = null; //used for widgets
    	this.last_mouse_position = [0,0];
    	this.visible_area = this.ds.visible_area;
    	this.visible_links = [];
    
    	//link canvas and graph
    	if(graph)
    		graph.attachCanvas(this);
    
    	this.setCanvas( canvas );
    	this.clear();
    
    	if(!options.skip_render)
    		this.startRendering();
    
    	this.autoresize = options.autoresize;
    }
    
    global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas;
    
    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.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.visible_area.set([0,0,0,0]);
    
    	if(this.onClear)
    		this.onClear();
    	//this.UIinit();
    }
    
    /**
    * assigns a graph, you can reasign 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;
    	}
    
    	/*
    	if(this.graph)
    		this.graph.canvas = null; //remove old graph link to the canvas
    	this.graph = graph;
    	if(this.graph)
    		this.graph.canvas = this;
    	*/
    	graph.attachCanvas(this);
    	this.setDirty(true,true);
    }
    
    /**
    * 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.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 subraph_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( subraph_node )
    	{
    		this.centerOnNode( subraph_node );
    		this.selectNodes( [subraph_node] );
    	}
    }
    
    /**
    * 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 doesnt 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)
    	this._mousemove_callback = this.processMouseMove.bind(this);
    	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) { 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;
    	}
    
    	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);
    
    	canvas.addEventListener("mousedown", this._mousedown_callback, true ); //down do not need to store the binded
    	canvas.addEventListener("mousemove", this._mousemove_callback );
    	canvas.addEventListener("mousewheel", this._mousewheel_callback, false);
    
    	canvas.addEventListener("contextmenu", this._doNothing );
    	canvas.addEventListener("DOMMouseScroll", this._mousewheel_callback, false);
    
    	//touch events
    	//if( 'touchstart' in document.documentElement )
    	{
    		canvas.addEventListener("touchstart", this.touchHandler, true);
    		canvas.addEventListener("touchmove", this.touchHandler, true);
    		canvas.addEventListener("touchend", this.touchHandler, true);
    		canvas.addEventListener("touchcancel", this.touchHandler, 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 doesnt fire keyup
    
    	//Droping 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;
    	}
    
    	var ref_window = this.getCanvasWindow();
    	var document = ref_window.document;
    
    	this.canvas.removeEventListener( "mousedown", 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 );
    
    	this.canvas.removeEventListener("touchstart", this.touchHandler );
    	this.canvas.removeEventListener("touchmove", this.touchHandler );
    	this.canvas.removeEventListener("touchend", this.touchHandler );
    	this.canvas.removeEventListener("touchcancel", this.touchHandler );
    
    	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 */
    
    LGraphCanvas.prototype.processMouseDown = function(e)
    {
    	if(!this.graph)
    		return;
    
    	this.adjustMouseEvent(e);
    
    	var ref_window = this.getCanvasWindow();
    	var document = ref_window.document;
    	LGraphCanvas.active_canvas = this;
    	var that = this;
    
    	//move mouse move event to the window in case it drags outside of the canvas
    	this.canvas.removeEventListener("mousemove", this._mousemove_callback );
    	ref_window.document.addEventListener("mousemove", this._mousemove_callback, true ); //catch for the entire window
    	ref_window.document.addEventListener("mouseup", this._mouseup_callback, true );
    
    	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_double_click = (now - this.last_mouseclick) < 300;
    
    	this.canvas_mouse[0] = e.canvasX;
    	this.canvas_mouse[1] = e.canvasY;
    	this.canvas.focus();
    
        LiteGraph.closeAllContextMenus( ref_window );
    
    	if(this.onMouse)
    	{
    		if( this.onMouse(e) == true )
    			return;
    	}
    
    	if(e.which == 1) //left button mouse
    	{
    		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;
    		}
    
    		var clicking_canvas_bg = false;
    
    		//when clicked on top of a node
    		//and it is not interactive
    		if( node && this.allow_interaction && !skip_action )
    		{
    			if( !this.live_mode && !node.flags.pinned )
    				this.bringToFront( node ); //if it wasnt selected?
    
    			//not dragging mouse to connect two slots
    			if(!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.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_pos = node.getConnectionPos(false,i);
    								this.connecting_slot = i;
    
    								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
    									node.disconnectInput(i);
    
    									if( this.allow_reconnect_links || e.shiftKey )
    									{
    										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;
    								}
    							}
    						}
    				} //not resizing
    			}
    
    			//Search for corner for collapsing
    			/*
    			if( !skip_action && 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();
    				skip_action = true;
    			}
    			*/
    
    			//it wasnt clicked on the links boxes
    			if(!skip_action)
    			{
    				var block_drag_node = false;
    
    				//widgets
    				var widget = this.processNodeWidgets( node, this.canvas_mouse, e );
    				if(widget)
    				{
    					block_drag_node = true;
    					this.node_widget = [node, widget];
    				}
    
    				//double clicking
    				if (is_double_click && this.selected_nodes[ node.id ])
    				{
    					//double click node
    					if( node.onDblClick)
    						node.onDblClick(e,[e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this);
    					this.processNodeDblClicked( node );
    					block_drag_node = true;
    				}
    
    				//if do not capture mouse
    				if( node.onMouseDown && node.onMouseDown( e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this ) )
    				{
    					block_drag_node = true;
    				}
    				else if(this.live_mode)
    				{
    					clicking_canvas_bg = true;
    					block_drag_node = true;
    				}
    
    				if(!block_drag_node)
    				{
    					if(this.allow_dragnodes)
    						this.node_dragged = node;
    					if(!this.selected_nodes[ node.id ])
    						this.processNodeSelected( node, e );
    				}
    
    				this.dirty_canvas = true;
    			}
    		}
    		else //clicked outside of nodes
    		{
    
    			//search for link connector
    			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 );
    				break;
    			}
    
    			this.selected_group = this.graph.getGroupOnPos( e.canvasX, e.canvasY );
    			this.selected_group_resizing = false;
    			if( this.selected_group )
    			{
    				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.showSearchBox( e );
    			
    			clicking_canvas_bg = true;
    		}
    
    		if( !skip_action && clicking_canvas_bg && this.allow_dragcanvas )
    		{
    			this.dragging_canvas = true;
    		}
    	}
    	else if (e.which == 2) //middle button
    	{
    
    	}
    	else if (e.which == 3) //right button
    	{
    		this.processContextMenu( node, e );
    	}
    
    	//TODO
    	//if(this.node_selected != prev_selected)
    	//	this.onNodeSelectionChange(this.node_selected);
    
    	this.last_mouse[0] = e.localX;
    	this.last_mouse[1] = e.localY;
    	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.graph)
    		return;
    
    	LGraphCanvas.active_canvas = this;
    	this.adjustMouseEvent(e);
    	var mouse = [e.localX, e.localY];
    	var delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]];
    	this.last_mouse = mouse;
    	this.canvas_mouse[0] = e.canvasX;
    	this.canvas_mouse[1] = e.canvasY;
    	e.dragging = this.last_mouse_dragging;
    
    	if( this.node_widget )
    	{
    		this.processNodeWidgets( this.node_widget[0], this.canvas_mouse, e, this.node_widget[1] );
    		this.dirty_canvas = true;
    	}
    
    	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) //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)
    	{
    		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)
    	{
    		if(this.connecting_node)
    			this.dirty_canvas = true;
    
    		//get node over
    		var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
    
    		//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)
    		{
    			//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)
    			{
    				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, dont 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;
    					}
    					else
    						this._highlight_input = 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 if(this.canvas)
    			this.canvas.style.cursor = "";
    
    		if(this.node_capturing_input && this.node_capturing_input != node && this.node_capturing_input.onMouseMove)
    		{
    			this.node_capturing_input.onMouseMove(e);
    		}
    
    
    		if(this.node_dragged && !this.live_mode)
    		{
    			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;
    			}
    
    			this.dirty_canvas = true;
    			this.dirty_bgcanvas = true;
    		}
    
    		if(this.resizing_node && !this.live_mode)
    		{
    			//convert mouse to node space
    			this.resizing_node.size[0] = e.canvasX - this.resizing_node.pos[0];
    			this.resizing_node.size[1] = e.canvasY - this.resizing_node.pos[1];
    
    			//constraint size
    			var max_slots = Math.max( this.resizing_node.inputs ? this.resizing_node.inputs.length : 0, this.resizing_node.outputs ? this.resizing_node.outputs.length : 0);
    			var min_height = max_slots * LiteGraph.NODE_SLOT_HEIGHT + ( this.resizing_node.widgets ? this.resizing_node.widgets.length : 0 ) * (LiteGraph.NODE_WIDGET_HEIGHT + 4 ) + 4;
    			if(this.resizing_node.size[1] < min_height )
    				this.resizing_node.size[1] = min_height;
    			if(this.resizing_node.size[0] < LiteGraph.NODE_MIN_WIDTH)
    				this.resizing_node.size[0] = LiteGraph.NODE_MIN_WIDTH;
    
    			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)
    {
    	if(!this.graph)
    		return;
    
    	var window = this.getCanvasWindow();
    	var document = window.document;
    	LGraphCanvas.active_canvas = this;
    
    	//restore the mousemove event back to the canvas
    	document.removeEventListener("mousemove", this._mousemove_callback, true );
    	this.canvas.addEventListener("mousemove", this._mousemove_callback, true);
    	document.removeEventListener("mouseup", this._mouseup_callback, true );
    
    	this.adjustMouseEvent(e);
    	var now = LiteGraph.getTime();
    	e.click_time = (now - this.last_mouseclick);
    	this.last_mouse_dragging = false;
    
    	if (e.which == 1) //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;
    
    		if( this.dragging_rectangle )
    		{
    			if(this.graph)
    			{
    				var nodes = this.graph._nodes;
    				var node_bounding = new Float32Array(4);
    				this.deselectAllNodes();
    				//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 against all nodes (not visible becasue the rectangle maybe start outside
    				var to_select = [];
    				for(var i = 0; i < nodes.length; ++i)
    				{
    					var node = nodes[i];
    					node.getBounding( node_bounding );
    					if(!overlapBounding( this.dragging_rectangle, node_bounding ))
    						continue; //out of the visible area
    					to_select.push(node);
    				}
    				if(to_select.length)
    					this.selectNodes(to_select);
    			}
    			this.dragging_rectangle = null;
    		}
    		else if(this.connecting_node) //dragging a connection
    		{
    			this.dirty_canvas = true;
    			this.dirty_bgcanvas = true;
    
    			var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
    
    			//node below mouse
    			if(node)
    			{
    				if( this.connecting_output.type == LiteGraph.EVENT && this.isOverNodeBox( node, e.canvasX, e.canvasY ) )
    				{
    					this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT );
    				}
    				else
    				{
    					//slot below mouse? connect
    					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
    						var input = node.getInputInfo(0);
    						//auto connect
    						if(this.connecting_output.type == LiteGraph.EVENT)
    							this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT );
    						else
    							if(input && !input.link && LiteGraph.isValidConnection( input.type && this.connecting_output.type ) )
    								this.connecting_node.connect( this.connecting_slot, node, 0 );
    					}
    				}
    			}
    
    			this.connecting_output = 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.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.node_dragged.alignToGrid();
    			this.node_dragged = null;
    		}
    		else //no node being dragged
    		{
    			//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();
    	*/
    
    	this.graph.change();
    
    	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 scale = this.ds.scale;
    
    	if (delta > 0)
    		scale *= 1.1;
    	else if (delta < 0)
    		scale *= 1/(1.1);
    
    	//this.setZoom( scale, [ e.localX, e.localY ] );
    	this.ds.changeScale( scale, [ e.localX, e.localY ] );
    
    	this.graph.change();
    
    	e.preventDefault();
    	return false; // prevent default
    }
    
    /**
    * retuns 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;
    }
    
    /**
    * retuns true 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;
    }
    
    /**
    * 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) //esc
    		{
    			this.dragging_canvas = true;
    			block_default = true;
    		}
    
    		//select all Control A
    		if(e.keyCode == 65 && e.ctrlKey)
    		{
    			this.selectNodes();
    			block_default = true;
    		}
    
    		if(e.code == "KeyC" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //copy
    		{
    			if(this.selected_nodes)
    			{
    				this.copyToClipboard();
    				block_default = true;
    			}
    		}
    
    		if(e.code == "KeyV" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //paste
    		{
    			this.pasteFromClipboard();
    		}
    
    		//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)
    			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];
    		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];
    		clipboard_info.nodes.push( node.clone().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 || !this.selected_nodes[ target_node.id ] ) //improve this by allowing connections to non-selected nodes
    					continue; //not selected
    				clipboard_info.links.push([ target_node._relative_id, j, node._relative_id, link_info.target_slot ]);
    			}
    	}
    	localStorage.setItem( "litegrapheditor_clipboard", JSON.stringify( clipboard_info ) );
    }
    
    LGraphCanvas.prototype.pasteFromClipboard = function()
    {
    	var data = localStorage.getItem( "litegrapheditor_clipboard" );
    	if(!data)
    		return;
    
    	//create nodes
    	var clipboard_info = JSON.parse(data);
    	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);
    			node.pos[0] += 5;
    			node.pos[1] += 5;
    			this.graph.add( node );
    			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 = nodes[ link_info[0] ];
    		var target_node = nodes[ link_info[2] ];
    		origin_node.connect( link_info[1], target_node, link_info[3] );
    	}
    
    	this.selectNodes( nodes );
    }
    
    /**
    * process a item drop event on top the canvas
    * @method processDrop
    **/
    LGraphCanvas.prototype.processDrop = function(e)
    {
    	e.preventDefault();
    	this.adjustMouseEvent(e);
    
    
    	var pos = [e.canvasX,e.canvasY];
    	var node = this.graph.getNodeOnPos(pos[0],pos[1]);
    
    	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 doesnt 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)
    		{
    			var node = LiteGraph.createNode( nodetype.type );
    			node.pos = [e.canvasX, e.canvasY];
    			this.graph.add( node );
    			if( node.onDropFile )
    				node.onDropFile( file );
    		}
    	}
    }
    
    
    LGraphCanvas.prototype.processNodeDblClicked = function(n)
    {
    	if(this.onShowNodePanel)
    		this.onShowNodePanel(n);
    
    	if(this.onNodeDblClicked)
    		this.onNodeDblClicked(n);
    
    	this.setDirty(true);
    }
    
    LGraphCanvas.prototype.processNodeSelected = function(node,e)
    {
    	this.selectNode( node, e && e.shiftKey );
    	if(this.onNodeSelected)
    		this.onNodeSelected(node);
    }
    
    LGraphCanvas.prototype.processNodeDeselected = function(node)
    {
    	this.deselectNode(node);
    	if(this.onNodeDeselected)
    		this.onNodeDeselected(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;
    	for(var i = 0; i < nodes.length; ++i)
    	{
    		var node = nodes[i];
    		if(node.is_selected)
    			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;
    			}
    
    	}
    
    	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;
    
    	//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;
    	}
    	this.selected_nodes = {};
    	this.highlighted_links = {};
    	this.setDirty(true);
    }
    
    /**
    * deletes all nodes in the current selection from the graph
    * @method deleteSelectedNodes
    **/
    LGraphCanvas.prototype.deleteSelectedNodes = function()
    {
    	for(var i in this.selected_nodes)
    	{
    		var m = this.selected_nodes[i];
    		//if(m == this.node_in_panel) this.showNodePanel(null);
    		this.graph.remove(m);
    	}
    	this.selected_nodes = {};
    	this.highlighted_links = {};
    	this.setDirty(true);
    }
    
    /**
    * 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)
    {
    	if(this.canvas)
    	{
    		var b = this.canvas.getBoundingClientRect();
    		e.localX = e.clientX - b.left;
    		e.localY = e.clientY - b.top;
    	}
    	else
    	{
    		e.localX = e.clientX;
    		e.localY = e.clientY;
    	}
    
    	e.deltaX = e.localX - this.last_mouse_position[0];
    	e.deltaY = e.localY - this.last_mouse_position[1];
    
    	this.last_mouse_position[0] = e.localX;
    	this.last_mouse_position[1] = e.localY;
    
    	e.canvasX = e.localX / this.ds.scale - this.ds.offset[0];
    	e.canvasY = e.localY / this.ds.scale - this.ds.offset[1];
    }
    
    /**
    * 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)
    		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();
    
    	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;
    
    	if(ctx.start2D)
    		ctx.start2D();
    
    	var canvas = this.canvas;
    
    	//reset in case of error
    	ctx.restore();
    	ctx.setTransform(1, 0, 0, 1, 0, 0);
    
    	//clip dirty area if there is one, otherwise work in full canvas
    	if(this.dirty_area)
    	{
    		ctx.save();
    		ctx.beginPath();
    		ctx.rect(this.dirty_area[0],this.dirty_area[1],this.dirty_area[2],this.dirty_area[3]);
    		ctx.clip();
    	}
    
    	//clear
    	//canvas.width = canvas.width;
    	if(this.clear_background)
    		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);
    
    	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;
    
    			switch( this.connecting_output.type )
    			{
    				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.canvas_mouse[0], this.canvas_mouse[1] ], null, false, null, link_color, this.connecting_output.dir || (this.connecting_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT), LiteGraph.CENTER );
    
    			ctx.beginPath();
    				if( this.connecting_output.type === LiteGraph.EVENT || this.connecting_output.shape === LiteGraph.BOX_SHAPE )
    					ctx.rect( (this.connecting_pos[0] - 6) + 0.5, (this.connecting_pos[1] - 5) + 0.5,14,10);
    				else
    					ctx.arc( this.connecting_pos[0], this.connecting_pos[1],4,0,Math.PI*2);
    			ctx.fill();
    
    			ctx.fillStyle = "#ffcc00";
    			if(this._highlight_input)
    			{
    				ctx.beginPath();
    					ctx.arc( this._highlight_input[0], this._highlight_input[1],6,0,Math.PI*2);
    				ctx.fill();
    			}
    		}
    
    		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] );
    		}
    
    		if( this.onDrawForeground )
    			this.onDrawForeground( ctx, this.visible_rect );
    
    		ctx.restore();
    	}
    
    	if( this.onDrawOverlay )
    		this.onDrawOverlay( ctx );
    
    	if(this.dirty_area)
    	{
    		ctx.restore();
    		//this.dirty_area = null;
    	}
    
    	if(ctx.finish2D) //this is a function I use in webgl renderer
    		ctx.finish2D();
    }
    
    /**
    * draws some useful stats in the corner of the canvas
    * @method renderInfo
    **/
    LGraphCanvas.prototype.renderInfo = function( ctx, x, y )
    {
    	x = x || 0;
    	y = y || 0;
    
    	ctx.save();
    	ctx.translate( x, y );
    
    	ctx.font = "10px Arial";
    	ctx.fillStyle = "#888";
    	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();
    
    	//clear
    	if(this.clear_background)
    		ctx.clearRect(0,0,canvas.width, canvas.height);
    
    	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;
    		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
    	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.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.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = false;
    			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.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = true;
    		}
    
    		//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;
    
    	//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)
    	{
    		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.onDrawCollaped && 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 = this.ds.scale > 0.6;
    
    	var out_slot = this.connecting_output;
    	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];
    
    				ctx.globalAlpha = editor_alpha;
    				//change opacity of incompatible slots when dragging a connection
    				if ( this.connecting_node && 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.input_on) : (slot.color_off || 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 === 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 {
                        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
    		if(this.connecting_node)
    			ctx.globalAlpha = 0.4 * editor_alpha;
    
    		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 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.output_on) : (slot.color_off || this.default_connection_color.output_off);
    				ctx.beginPath();
    				//ctx.rect( node.size[0] - 14,i*14,10,10);
    
    				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 {
                        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();
    				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)
    		{
    			if( horizontal || node.widgets_up  )
    				max_y = 2;
    			this.drawNodeWidgets( node, max_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;
    }
    
    /**
    * 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 )
    		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], this.round_radius, shape == LiteGraph.CARD_SHAPE ? 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();
    
    		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 );
    
    	//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);
    					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, this.round_radius, node.flags.collapsed ? this.round_radius : 0);
    			ctx.fill();
    			ctx.shadowColor = "transparent";
    		}
    
    		//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 || LiteGraph.NODE_DEFAULT_BOXCOLOR;
    			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 || 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 = node.getTitle();
    			if(title)
    			{
    				if(selected)
    					ctx.fillStyle = "white";
    				else
    					ctx.fillStyle = node.constructor.title_text_color || this.node_title_color;
    				if( node.flags.collapsed )
    				{
    					ctx.textAlign =  "center";
    					var measure = ctx.measureText(title);
    					ctx.fillText( title, 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 );
    				}
    			}
    		}
    
    		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);
    		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 = "#FFF";
    		ctx.stroke();
    		ctx.strokeStyle = fgcolor;
    		ctx.globalAlpha = 1;
    	}
    }
    
    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: precatch 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 = "#666";
    	var background_color = "#222";
    	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";
    
    		switch( w.type )
    		{
    			case "button": 
    				if(w.clicked)
    				{
    					ctx.fillStyle = "#AAA";
    					w.clicked = false;
    					this.dirty_canvas = true;
    				}
    				ctx.fillRect(margin,y,width-margin*2,H);
    				ctx.strokeRect(margin,y,width-margin*2,H);
    				if(show_text)
    				{
    					ctx.textAlign = "center";
    					ctx.fillStyle = "#AAA";
    					ctx.fillText( w.name, width*0.5, y + H*0.7 );
    				}
    				break;
    			case "toggle":
    				ctx.textAlign = "left";
    				ctx.strokeStyle = outline_color;
    				ctx.fillStyle = background_color;
    				ctx.beginPath();
    				ctx.roundRect( margin, posY, width - margin*2, H,H*0.5 );
    				ctx.fill();
    				ctx.stroke();
    				ctx.fillStyle = w.value ? "#89A" : "#333";
    				ctx.beginPath();
    				ctx.arc( width - margin*2, y + H*0.5, H * 0.36, 0, Math.PI * 2 );
    				ctx.fill();
    				if(show_text)
    				{
    					ctx.fillStyle = "#999";
    					if(w.name != null)
    						ctx.fillText( w.name, margin*2, y + H*0.7 );
    					ctx.fillStyle = w.value ? "#DDD" : "#888";
    					ctx.textAlign = "right";
    					ctx.fillText( w.value ? (w.options.on || "true") : (w.options.off || "false"), width - 40, y + H*0.7 );
    				}
    				break;
    			case "slider": 
    				ctx.fillStyle = background_color;
    				ctx.fillRect(margin,y,width-margin*2,H);
    				var range = w.options.max - w.options.min;
    				var nvalue = (w.value - w.options.min) / range;
    				ctx.fillStyle = active_widget == w ? "#89A" : "#678";
    				ctx.fillRect(margin,y,nvalue*(width-margin*2),H);
    				ctx.strokeRect(margin,y,width-margin*2,H);
    				if( w.marker )
    				{
    					var marker_nvalue = (w.marker - w.options.min) / range;
    					ctx.fillStyle = "#AA9";
    					ctx.fillRect(margin + marker_nvalue*(width-margin*2),y,2,H);
    				}
    				if(show_text)
    				{
    					ctx.textAlign = "center";
    					ctx.fillStyle = "#DDD";
    					ctx.fillText( w.name + "  " + Number(w.value).toFixed(3), 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();
    				ctx.roundRect( margin, posY, width - margin*2, H,H*0.5 );
    				ctx.fill();
    				ctx.stroke();
    				if(show_text)
    				{
    					ctx.fillStyle = "#AAA";
    					ctx.beginPath();
    					ctx.moveTo( margin + 16, posY + 5 );
    					ctx.lineTo( margin + 6, posY + H*0.5 );
    					ctx.lineTo( margin + 16, posY + H - 5 );
    					ctx.moveTo( width - margin - 16, posY + 5 );
    					ctx.lineTo( width - margin - 6, posY + H*0.5 );
    					ctx.lineTo( width - margin - 16, posY + H - 5 );
    					ctx.fill();
    					ctx.fillStyle = "#999";
    					ctx.fillText( w.name,  margin*2 + 5, y + H*0.7 );
    					ctx.fillStyle = "#DDD";
    					ctx.textAlign = "right";
    					if(w.type == "number")
    						ctx.fillText( Number(w.value).toFixed( w.options.precision !== undefined ? w.options.precision : 3), width - margin*2 - 20, y + H*0.7 );
    					else
    						ctx.fillText( w.value, 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();
    				ctx.roundRect( margin, posY, width - margin*2, H,H*0.5 );
    				ctx.fill();
    				ctx.stroke();
    				if(show_text)
    				{
    					ctx.fillStyle = "#999";
    					if(w.name != null)
    						ctx.fillText( w.name, margin*2, y + H*0.7 );
    					ctx.fillStyle = "#DDD";
    					ctx.textAlign = "right";
    					ctx.fillText( w.value, width - margin*2, y + H*0.7 );
    				}
    				break;
    			default:
    				if(w.draw)
    					w.draw(ctx,node,w,y,H);
    				break;
    		}
    		posY += H + 4;
    	}
    	ctx.restore();
    }
    
    /**
    * process an event on widgets 
    * @method processNodeWidgets
    **/
    LGraphCanvas.prototype.processNodeWidgets = function( node, pos, event, active_widget )
    {
    	if(!node.widgets || !node.widgets.length)
    		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 == active_widget || (x > 6 && x < (width - 12) && y > w.last_y && y < (w.last_y + LiteGraph.NODE_WIDGET_HEIGHT)) )
    		{
    			//inside widget
    			switch( w.type )
    			{
    				case "button": 
    					if(w.callback)
    						setTimeout( function(){	w.callback( w, that, node, pos ); }, 20 );
    					w.clicked = true;
    					this.dirty_canvas = true;
    					break;
    				case "slider": 
    					var range = w.options.max - w.options.min;
    					var nvalue = Math.clamp( (x - 10) / (width - 20), 0, 1);
    					w.value = w.options.min + (w.options.max - w.options.min) * nvalue;
    					if(w.callback)
    						setTimeout( function(){	w.callback( w.value, that, node, pos ); }, 20 );
    					this.dirty_canvas = true;
    					break;
    				case "number": 
    				case "combo": 
    					if(event.type == "mousemove" && w.type == "number")
    					{
    						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 == "mousedown" )
    					{
    						var values = w.options.values;
    						if(values && values.constructor === Function)
    							values = w.options.values( w, node );
    
    						var delta = ( x < 40 ? -1 : ( x > 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)
    						{
    							var index = values.indexOf( w.value ) + delta;
    							if( index >= values.length )
    								index = 0;
    							if( index < 0 )
    								index = values.length - 1;
    							w.value = values[ index ];
    						}
    						else
    						{
    							var menu = new LiteGraph.ContextMenu( 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 )
    							{
    								this.value = v;
    								that.dirty_canvas = true;
    								return false;
    							}
    						}
    					}
    					if(w.callback)
    						setTimeout( (function(){ this.callback( this.value, that, node, pos ); }).bind(w), 20 );
    					this.dirty_canvas = true;
    					break;
    				case "toggle":
    					if( event.type == "mousedown" )
    					{
    						w.value = !w.value;
    						if(w.callback)
    							setTimeout( function(){	w.callback( w.value, that, node, pos ); }, 20 );
    					}
    					break;
    				case "string":
    				case "text":
    					if( event.type == "mousedown" )
    						this.prompt( "Value", w.value, (function(v){ this.value = v; if(w.callback) w.callback(v, that, node ); }).bind(w), event );
    					break;
    				default: 
    					if( w.mouse )
    						w.mouse( ctx, event, [x,y], node );
    					break;
    			}
    
    			return w;
    		}
    	}
    	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.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
    }
    
    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);
    
    	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 );
    }
    
    LGraphCanvas.onMenuAdd = function( node, options, e, prev_menu )
    {
    	var canvas = LGraphCanvas.active_canvas;
    	var ref_window = canvas.getCanvasWindow();
    
    	var values = LiteGraph.getNodeTypesCategories();
    	var entries = [];
    	for(var i in values)
    		if(values[i])
    			entries.push({ value: values[i], content: values[i], has_submenu: true });
    
    	//show categories
    	var menu = new LiteGraph.ContextMenu( entries, { event: e, callback: inner_clicked, parentMenu: prev_menu }, ref_window);
    
    	function inner_clicked( v, option, e )
    	{
    		var category = v.value;
    		var node_types = LiteGraph.getNodeTypesInCategory( category, canvas.filter );
    		var values = [];
    		for(var i in node_types)
    			if (!node_types[i].skip_list)
    				values.push( { content: node_types[i].title, value: node_types[i].type });
    
    		new LiteGraph.ContextMenu( values, {event: e, callback: inner_create, parentMenu: menu }, ref_window);
    		return false;
    	}
    
    	function inner_create( v, e )
    	{
    		var first_event = prev_menu.getFirstEvent();
    		var node = LiteGraph.createNode( v.value );
    		if(node)
    		{
    			node.pos = canvas.convertEventToCanvasOffset( first_event );
    			canvas.graph.add( node );
    		}
    	}
    
    	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 in options)
    		{
    			var entry = options[i];
    			if(!entry)
    			{
    				entries.push(null);
    				continue;
    			}
    			var label = entry[0];
    			if(entry[2] && entry[2].label)
    				label = entry[2].label;
    			var data = {content: label, value: entry};
    			if(entry[1] == LiteGraph.ACTION)
    				data.className = "event";
    			entries.push(data);
    		}
    
    	if(this.onMenuNodeInputs)
    		entries = this.onMenuNodeInputs( entries );
    
    	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)
    		{
    			node.addInput(v.value[0],v.value[1], v.value[2]);
    			node.setDirtyCanvas(true,true);
    		}
    	}
    
    	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 in options)
    		{
    			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].label)
    				label = entry[2].label;
    			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(!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.addOutput( v.value[0], v.value[1], v.value[2]);
    			node.setDirtyCanvas(true,true);
    		}
    
    	}
    
    	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] : " ";
    			//value could contain invalid html characters, clean that
    			value = LGraphCanvas.decodeHTML(value);
    			entries.push({content: "<span class='property_name'>" + 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.onResizeNode = function( value, options, e, menu, node )
    {
    	if(!node)
    		return;
    	node.size = node.computeSize();
    	node.setDirtyCanvas(true,true);
    }
    
    LGraphCanvas.prototype.showLinkMenu = function( link, e )
    {
    	var that = this;
    
    	new LiteGraph.ContextMenu(["Delete"], { event: e, callback: inner_clicked });
    
    	function inner_clicked(v)
    	{
    		switch(v)
    		{
    			case "Delete": that.graph.removeLink( link.id ); break;
    			default:
    		}
    	}
    
    	return false;
    }
    
    LGraphCanvas.onShowPropertyEditor = function( item, options, e, menu, node )
    {
    	var input_html = "";
    	var property = item.property || "title";
    	var value = node[ property ];
    
    	var dialog = document.createElement("div");
    	dialog.className = "graphdialog";
    	dialog.innerHTML = "<span class='name'></span><input autofocus type='text' class='value'/><button>OK</button>";
    	var title = dialog.querySelector(".name");
    	title.innerText = property;
    	var input = dialog.querySelector("input");
    	if(input)
    	{
    		input.value = value;
            input.addEventListener("blur", function(e){
                this.focus();
            });
    		input.addEventListener("keydown", function(e){
    			if(e.keyCode != 13)
    				return;
    			inner();
    			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 );
    
    	function inner()
    	{
    		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);
    	}
    }
    
    LGraphCanvas.prototype.prompt = function( title, value, callback, event )
    {
    	var that = this;
    	var input_html = "";
    	title = title || "";
    
    	var modified = false;
    
    	var dialog = document.createElement("div");
    	dialog.className = "graphdialog rounded";
    	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 );
    	}
    
    	if(this.ds.scale > 1)
    		dialog.style.transform = "scale("+this.ds.scale+")";
    
    	dialog.addEventListener("mouseleave",function(e){
    		if(!modified)
    			 dialog.close();
    	});
    
    	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 = dialog.querySelector("input");
    	input.addEventListener("keydown", function(e){
    		modified = true;
    		if(e.keyCode == 27) //ESC
    			dialog.close();
    		else if(e.keyCode == 13)
    		{
    			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 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";
    	}
    
    	canvas.parentNode.appendChild( dialog );
    	setTimeout( function(){	input.focus(); },10 );
    
    	return dialog;
    }
    
    
    LGraphCanvas.search_limit = -1;
    LGraphCanvas.prototype.showSearchBox = function(event)
    {
    	var that = this;
    	var input_html = "";
    
    	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'/><div class='helper'></div>";
    	dialog.close = function()
    	{
    		that.search_box = null;
    		document.body.focus();
    		setTimeout( function(){ that.canvas.focus(); },20 ); //important, if canvas loses focus keys wont be captured
    		if(dialog.parentNode)
    			dialog.parentNode.removeChild( dialog );
    	}
    
    	var timeout_close = null;
    
    	if(this.ds.scale > 1)
    		dialog.style.transform = "scale("+this.ds.scale+")";
    
    	dialog.addEventListener("mouseenter",function(e){
    		if(timeout_close)
    		{
    			clearTimeout(timeout_close);
    			timeout_close = null;
    		}
    	});
    
    	dialog.addEventListener("mouseleave",function(e){
    		 //dialog.close();
    		timeout_close = setTimeout(function(){
    			dialog.close();
    		},500);
    	});
    
    	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, 10 );
    				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";
    	}
    
    	canvas.parentNode.appendChild( dialog );
    	input.focus();
    
    	function select( name )
    	{
    		if(name)
    		{
    			if( that.onSearchBoxSelection )
    				that.onSearchBoxSelection( name, event, graphcanvas );
    			else
    			{
    				var extra = LiteGraph.searchbox_extras[ name ];
    				if( extra )
    					name = extra.type;
    
    				var node = LiteGraph.createNode( name );
    				if(node)
    				{
    					node.pos = graphcanvas.convertEventToCanvasOffset( event );
    					graphcanvas.graph.add( node );
    				}
    
    				if( extra && extra.data )
    				{
    					if(extra.data.properties)
    						for(var i in extra.data.properties)
    							node.addProperty( extra.data.properties[i][0], extra.data.properties[i][0] );
    					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 );
    				}
    			}
    		}
    
    		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();
    	}
    
    	function refreshHelper() {
            timeout = null;
            var str = input.value;
            first = null;
            helper.innerHTML = "";
            if (!str)
                return;
    
            if (that.onSearchBox) {
                var list = that.onSearchBox( help, str, graphcanvas );
    			if(list)
    				for( var i = 0; i < list.length; ++i )
    					addResult( list[i] );
        	} else {
                var c = 0;
           		str = str.toLowerCase();
    			//extras
    			for(var i in LiteGraph.searchbox_extras)
    			{
    				var extra = LiteGraph.searchbox_extras[i];
    				if( extra.desc.toLowerCase().indexOf(str) === -1 )
    					continue;
    				addResult( extra.desc, "searchbox_extra" );
    				if(LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit )
    					break;
    			}
    
            	if(Array.prototype.filter)//filter supported
    			{
    				//types
            		var keys = Object.keys( LiteGraph.registered_node_types );
            		var filtered = keys.filter(function (item) {
    					return item.toLowerCase().indexOf(str) !== -1;
                    });
            		for(var i = 0; i < filtered.length; i++)
    				{
                        addResult(filtered[i]);
                        if(LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit)
    						break;
    				}
    			} else {
                    for (var i in LiteGraph.registered_node_types)
    				{
                        if (i.indexOf(str) != -1) {
                            addResult(i);
                            if(LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit)
    							break;
                        }
                    }
                }
            }
    
    		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 type = "string";
    
    	if(node.properties[ property ] !== null)
    		type = typeof(node.properties[ property ]);
    
    	//for arrays
    	if(type == "object")
    	{
    		if( node.properties[ property ].length )
    			type = "array";
    	}
    
    	var info = null;
    	if(node.getPropertyInfo)
    		info = node.getPropertyInfo(property);
    	if(node.properties_info)
    	{
    		for(var i = 0; i < node.properties_info.length; ++i)
    		{
    			if( node.properties_info[i].name == property )
    			{
    				info = node.properties_info[i];
    				break;
    			}
    		}
    	}
    
    	if(info !== undefined && info !== null && info.type )
    		type = info.type;
    
    	var input_html = "";
    
    	if(type == "string" || type == "number" || type == "array")
    		input_html = "<input autofocus type='text' class='value'/>";
    	else if(type == "enum" && info.values)
    	{
    		input_html = "<select autofocus type='text' class='value'>";
    		for(var i in info.values)
    		{
    			var v = info.values.constructor === Array ? info.values[i] : i;
    			input_html += "<option value='"+v+"' "+(v == node.properties[property] ? "selected" : "")+">"+info.values[i]+"</option>";
    		}
    		input_html += "</select>";
    	}
    	else if(type == "boolean")
    	{
    		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'>" + property + "</span>"+input_html+"<button>OK</button>" , options );
    
    	if(type == "enum" && info.values)
    	{
    		var input = dialog.querySelector("select");
    		input.addEventListener("change", function(e){
    			setValue( e.target.value );
    			//var index = e.target.value;
    			//setValue( e.options[e.selectedIndex].value );
    		});
    	}
    	else if(type == "boolean")
    	{
    		var input = dialog.querySelector("input");
    		if(input)
    		{
    			input.addEventListener("click", function(e){
    				setValue( !!input.checked );
    			});
    		}
    	}
    	else
    	{
    		var input = dialog.querySelector("input");
    		if(input)
    		{
                input.addEventListener("blur", function(e){
                    this.focus();
                });
    			input.value = node.properties[ property ] !== undefined ? node.properties[ property ] : "";
    			input.addEventListener("keydown", function(e){
    				if(e.keyCode != 13)
    					return;
    				inner();
    				e.preventDefault();
    				e.stopPropagation();
    			});
    		}
    	}
    
    	var button = dialog.querySelector("button");
    	button.addEventListener("click", inner );
    
    	function inner()
    	{
    		setValue( input.value );
    	}
    
    	function setValue(value)
    	{
    		if(typeof( node.properties[ property ] ) == "number")
    			value = Number(value);
    		if(type == "array")
    			value = value.split(",").map(Number);
    		node.properties[ property ] = value;
    		if(node._graph)
    			node._graph._version++;
    		if(node.onPropertyChanged)
    			node.onPropertyChanged( property, value );
    		dialog.close();
    		node.setDirtyCanvas(true,true);
    	}
    }
    
    LGraphCanvas.prototype.createDialog = function( html, options )
    {
    	options = options || {};
    
    	var dialog = document.createElement("div");
    	dialog.className = "graphdialog";
    	dialog.innerHTML = html;
    
    	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;
    	}
    	else //centered
    	{
    		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 );
    
    	dialog.close = function()
    	{
    		if(this.parentNode)
    			this.parentNode.removeChild( this );
    	}
    
    	return dialog;
    }
    
    LGraphCanvas.onMenuNodeCollapse = function( value, options, e, menu, node )
    {
    	node.collapse();
    }
    
    LGraphCanvas.onMenuNodePin = function( value, options, e, menu, node )
    {
    	node.pin();
    }
    
    LGraphCanvas.onMenuNodeMode = function( value, options, e, menu, node )
    {
    	new LiteGraph.ContextMenu(["Always","On Event","On Trigger","Never"], {event: e, callback: inner_clicked, parentMenu: menu, node: node });
    
    	function inner_clicked(v)
    	{
    		if(!node)
    			return;
    		switch(v)
    		{
    			case "On Event": node.mode = LiteGraph.ON_EVENT; break;
    			case "On Trigger": node.mode = LiteGraph.ON_TRIGGER; break;
    			case "Never": node.mode = LiteGraph.NEVER; break;
    			case "Always":
    			default:
    				node.mode = LiteGraph.ALWAYS; break;
    		}
    	}
    
    	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;
    		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;
    		}
    		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.shape = v;
    		node.setDirtyCanvas(true);
    	}
    
    	return false;
    }
    
    LGraphCanvas.onMenuNodeRemove = function( value, options, e, menu, node )
    {
    	if(!node)
    		throw("no node passed");
    
    	if(node.removable === false)
    		return;
    
    	node.graph.remove(node);
    	node.setDirtyCanvas(true,true);
    }
    
    LGraphCanvas.onMenuNodeClone = function( value, options, e, menu, 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);
    	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;
    	if(this.getMenuOptions)
    		options = this.getMenuOptions();
    	else
    	{
    		options = [
    			{ content:"Add Node", has_submenu: true, callback: LGraphCanvas.onMenuAdd },
    			{ content:"Add Group", callback: LGraphCanvas.onGroupAdd }
    			//{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll }
    		];
    
    		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 },
    			{content:"Resize", callback: LGraphCanvas.onResizeNode },
    			{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);
    		if(extra)
    		{
    			extra.push(null);
    			options = extra.concat( options );
    		}
    	}
    
    	if( node.clonable !== false )
    			options.push({content:"Clone", callback: LGraphCanvas.onMenuNodeClone });
    	if( node.removable !== false )
    			options.push(null,{content:"Remove", 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 };
    
    	//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(slot && slot.output && slot.output.links && slot.output.links.length)
    			menu_info.push( { content: "Disconnect Links", slot: slot } );
    		menu_info.push( slot.locked ? "Cannot remove"  : { content: "Remove Slot", slot: slot } );
    		menu_info.push( slot.nameLocked ? "Cannot rename" : { 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;
    			if(info.input)
    				node.removeInput( info.slot );
    			else if(info.output)
    				node.removeOutput( info.slot );
    			return;
    		}
    		else if(v.content == "Disconnect Links")
    		{
    			var info = v.slot;
    			if(info.output)
    				node.disconnectOutput( info.slot );
    			else if(info.input)
    				node.disconnectInput( info.slot );
    			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 || "";
    			}
    			dialog.querySelector("button").addEventListener("click",function(e){
    				if(input.value)
    				{
    					if( slot_info )
    						slot_info.label = input.value;
    					that.setDirty(true);
    				}
    				dialog.close();
    			});
    		}
    
    		//if(v.callback)
    		//	return v.callback.call(that, node, options, e, menu, that, event );
    	}
    }
    
    
    
    
    
    
    //API *************************************************
    //like rect but rounded corners
    if(this.CanvasRenderingContext2D)
    CanvasRenderingContext2D.prototype.roundRect = function (x, y, width, height, radius, radius_low) {
      if ( radius === undefined ) {
        radius = 5;
      }
    
      if(radius_low === undefined)
    	 radius_low  = radius;
    
      this.moveTo(x + radius, y);
      this.lineTo(x + width - radius, y);
      this.quadraticCurveTo(x + width, y, x + width, y + radius);
    
      this.lineTo(x + width, y + height - radius_low);
      this.quadraticCurveTo(x + width, y + height, x + width - radius_low, y + height);
      this.lineTo(x + radius_low, y + height);
      this.quadraticCurveTo(x, y + height, x, y + height - radius_low);
      this.lineTo(x, y + radius);
      this.quadraticCurveTo(x, y, x + radius, y);
    }
    
    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 boundin 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;
    
    //boundings 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;
    		}
    	}
    
    	if(options.event && options.event.constructor !== MouseEvent && options.event.constructor !== CustomEvent)
    	{
    		console.error("Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it.");
    		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 caugh by this element
    
    	//this prevents the default context browser menu to open in case this menu was created when pressing right button
    	root.addEventListener("mouseup", function(e){
    		e.preventDefault(); return true;
    	}, true);
    	root.addEventListener("contextmenu", function(e) {
    		if(e.button != 2) //right button
    			return false;
    		e.preventDefault();
    		return false;
    	},true);
    
    	root.addEventListener("mousedown", function(e){
    		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 in values)
    	{
    		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
    	root.addEventListener("mouseleave", function(e) {
    		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);
    	});
    
    	root.addEventListener("mouseenter", function(e) {
    		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;
    	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(left > (body_rect.width - root_rect.width - 10))
    			left = (body_rect.width - root_rect.width - 10);
    		if(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(options.autoopen)
    		element.addEventListener("mouseenter", 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, "mouseleave", e );
    		}
    	}
    	if(this.current_submenu)
    		this.current_submenu.close(e, true);
    
    	if(this.root.closing_timer)
    		clearTimeout( this.root.closing_timer );
    }
    
    //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 in result)
    	{
    		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 enumerables
    		{
    			if(!origin.prototype.hasOwnProperty(i))
    				continue;
    
    			if(target.prototype.hasOwnProperty(i)) //avoid overwritting 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 to create nodes from wrapping functions
    LiteGraph.getParameterNames = function(func) {
        return (func + '')
          .replace(/[/][/].*$/mg,'') // 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 [""]
    }
    
    Math.clamp = function(v,a,b) { return (a > v ? a : (b < v ? b : v)); }
    
    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;
    
        
    ================================================ FILE: doc/files/index.html ================================================ Redirector Click here to redirect ================================================ FILE: doc/index.html ================================================

    API Docs for:
    Show:

    Browse to a module or class using the sidebar to view its API documentation.

    Keyboard Shortcuts

    • Press s to focus the API search box.

    • Use Up and Down to select classes, modules, and search results.

    • With the API search box or sidebar focused, use -Left or -Right to switch sidebar tabs.

    • With the API search box or sidebar focused, use Ctrl+Left and Ctrl+Right to switch sidebar tabs.

    ================================================ FILE: doc/modules/index.html ================================================ Redirector Click here to redirect ================================================ FILE: editor/editor_mobile.html ================================================ LiteGraph
    ================================================ FILE: editor/examples/audio.json ================================================ {"last_node_id":16,"last_link_id":16,"nodes":[{"id":9,"type":"widget/knob","pos":[440,81],"size":[80,100],"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[8,9]}],"properties":{"min":0,"max":4,"value":0.24000000000000002,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":10,"type":"basic/watch","pos":[537,81],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"value","type":0,"link":9,"label":"0.000"}],"outputs":[{"name":"value","type":0,"links":null,"label":""}],"properties":{"value":0.24000000000000002}},{"id":1,"type":"audio/destination","pos":[699,83],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"in","type":"audio","link":1}],"properties":{}},{"id":6,"type":"widget/knob","pos":[116,179],"size":[80,100],"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[5]}],"properties":{"min":0,"max":1,"value":0.5099999999999996,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":0,"type":"audio/source","pos":[272,192],"size":{"0":140,"1":86},"flags":{},"mode":0,"inputs":[{"name":"gain","type":"number","link":5},{"name":"Play","type":-1,"link":6},{"name":"Stop","type":-1,"link":7},{"name":"playbackRate","type":"number","link":8}],"outputs":[{"name":"out","type":"audio","links":[0]}],"properties":{"src":"demodata/audio.wav","gain":0.5,"loop":true,"autoplay":true,"playbackRate":0.24000000000000002},"boxcolor":"#AA4"},{"id":2,"type":"audio/biquadfilter","pos":[442,228],"size":{"0":140,"1":46},"flags":{},"mode":0,"inputs":[{"name":"in","type":"audio","link":0},{"name":"frequency","type":"number","link":4}],"outputs":[{"name":"out","type":"audio","links":[1,2]}],"properties":{"frequency":350,"detune":0,"Q":1,"type":"lowpass"}},{"id":3,"type":"audio/analyser","pos":[704,231],"size":{"0":140,"1":46},"flags":{},"mode":0,"inputs":[{"name":"in","type":"audio","link":2}],"outputs":[{"name":"freqs","type":"array","links":[3,10]},{"name":"samples","type":"array","links":null}],"properties":{"fftSize":2048,"minDecibels":-100,"maxDecibels":-10,"smoothingTimeConstant":0.5}},{"id":11,"type":"audio/signal","pos":[882,395],"size":{"0":140,"1":46},"flags":{},"mode":0,"inputs":[{"name":"freqs","type":"array","link":10},{"name":"band","type":"number","link":11}],"outputs":[{"name":"signal","type":"number","links":[12]}],"properties":{"band":440,"amplitude":1,"samplerate":44100}},{"id":4,"type":"audio/visualization","pos":[885,503],"size":{"0":140,"1":46},"flags":{},"mode":0,"inputs":[{"name":"freqs","type":"array","link":3},{"name":"mark","type":"number","link":13}],"properties":{"continuous":true,"mark":12000.000000000005,"samplerate":44100}},{"id":5,"type":"widget/knob","pos":[112,314],"size":[80,100],"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[4]}],"properties":{"min":0,"max":20000,"value":14800.00000000001,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":13,"type":"basic/watch","pos":[110,458],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"value","type":0,"link":12,"label":"0.000"}],"outputs":[{"name":"value","type":0,"links":[14],"label":""}],"title":"Max. Signal","properties":{"value":0.3843137254901945}},{"id":14,"type":"widget/progress","pos":[300,460],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"","type":"number","link":14}],"properties":{"min":0,"max":1,"value":0.3843137254901945,"wcolor":"#AAF"}},{"id":12,"type":"widget/knob","pos":[460,458],"size":[80,100],"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[11,13,15]}],"properties":{"min":0,"max":24000,"value":12000.000000000005,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":15,"type":"basic/watch","pos":[888,598],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"value","type":0,"link":15,"label":"0.000"}],"outputs":[{"name":"value","type":0,"links":null,"label":""}],"properties":{"value":12000.000000000005}},{"id":7,"type":"widget/button","pos":[113,83],"size":{"0":141,"1":60},"flags":{},"mode":0,"outputs":[{"name":"clicked","type":-1,"links":[6]}],"properties":{"text":"Play","font_size":30,"message":"","font":"40px Arial"}},{"id":8,"type":"widget/button","pos":[273,83],"size":{"0":143,"1":62},"flags":{},"mode":0,"outputs":[{"name":"clicked","type":-1,"links":[7]}],"properties":{"text":"Stop","font_size":30,"message":"","font":"40px Arial"}}],"links":[[0,0,0,2,0,null],[1,2,0,1,0,null],[2,2,0,3,0,null],[3,3,0,4,0,null],[4,5,0,2,1,null],[5,6,0,0,0,null],[6,7,0,0,1,null],[7,8,0,0,2,null],[8,9,0,0,3,null],[9,9,0,10,0,null],[10,3,0,11,0,null],[11,12,0,11,1,null],[12,11,0,13,0,null],[13,12,0,4,1,null],[14,13,0,14,0,null],[15,12,0,15,0,null]],"groups":[],"config":{}} ================================================ FILE: editor/examples/audio_delay.json ================================================ {"last_node_id":7,"last_link_id":7,"nodes":[{"id":6,"type":"widget/knob","pos":[199,296],"size":[64,84],"flags":{},"order":3,"mode":0,"outputs":[{"name":"","type":"number","links":[6]}],"properties":{"min":0,"max":2,"value":0.8799999999999999,"color":"#7AF","precision":2,"wcolor":"#7AF","size":50},"boxcolor":"rgba(112,112,112,1.0)"},{"id":0,"type":"audio/source","pos":[195,187],"size":{"0":137,"1":33},"flags":{},"order":0,"mode":0,"inputs":[{"name":"gain","type":"number","link":null}],"outputs":[{"name":"out","type":"audio","links":[0,1]}],"properties":{"src":"demodata/audio.wav","gain":0.5,"loop":true,"autoplay":true,"playbackRate":1},"boxcolor":"#AA4"},{"id":4,"type":"widget/knob","pos":[408,59],"size":[82,75],"flags":{},"order":1,"mode":0,"outputs":[{"name":"","type":"number","links":[4]}],"properties":{"min":0,"max":1,"value":0.24000000000000007,"color":"#7AF","precision":2,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":2,"type":"audio/delay","pos":[385,255],"size":{"0":143,"1":49},"flags":{},"order":4,"mode":0,"inputs":[{"name":"in","type":"audio","link":1},{"name":"time","type":"number","link":6}],"outputs":[{"name":"out","type":"audio","links":[2]}],"properties":{"delayTime":0.5,"time":1}},{"id":5,"type":"widget/knob","pos":[433,371],"size":[79,79],"flags":{},"order":2,"mode":0,"outputs":[{"name":"","type":"number","links":[5]}],"properties":{"min":0,"max":1,"value":0.5199999999999996,"color":"#7AF","precision":2,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":1,"type":"audio/mixer","pos":[657,180],"size":{"0":147,"1":91},"flags":{},"order":5,"mode":0,"inputs":[{"name":"in1","type":"audio","link":0},{"name":"in1 gain","type":"number","link":4},{"name":"in2","type":"audio","link":2},{"name":"in2 gain","type":"number","link":5}],"outputs":[{"name":"out","type":"audio","links":[3]}],"properties":{"gain1":0.5,"gain2":0.8}},{"id":3,"type":"audio/destination","pos":[911,180],"size":{"0":145,"1":30},"flags":{},"order":6,"mode":0,"inputs":[{"name":"in","type":"audio","link":3}],"properties":{}}],"links":[[0,0,0,1,0,null],[1,0,0,2,0,null],[2,2,0,1,2,null],[3,1,0,3,0,null],[4,4,0,1,1,null],[5,5,0,1,3,null],[6,6,0,2,1,null]],"groups":[],"config":{},"version":0.4} ================================================ FILE: editor/examples/audio_reverb.json ================================================ {"last_node_id":8,"last_link_id":9,"nodes":[{"id":4,"type":"widget/knob","pos":[408,59],"size":[81,93],"flags":{},"order":2,"mode":0,"outputs":[{"name":"","type":"number","links":[4]}],"title":"Main","properties":{"min":0,"max":1,"value":0.21000000000000002,"color":"#7AF","precision":2,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":5,"type":"widget/knob","pos":[398,350],"size":[84,100],"flags":{},"order":1,"mode":0,"outputs":[{"name":"","type":"number","links":[5]}],"title":"Reverb","properties":{"min":0,"max":1,"value":0.4099999999999999,"color":"#7AF","precision":2,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":7,"type":"audio/convolver","pos":[421,253],"size":{"0":140,"1":31},"flags":{},"order":3,"mode":0,"inputs":[{"name":"in","type":"audio","link":7}],"outputs":[{"name":"out","type":"audio","links":[8]}],"properties":{"impulse_src":"demodata/impulse.wav","normalize":true}},{"id":1,"type":"audio/mixer","pos":[655,183],"size":{"0":145,"1":94},"flags":{},"order":4,"mode":0,"inputs":[{"name":"in1","type":"audio","link":0},{"name":"in1 gain","type":"number","link":4},{"name":"in2","type":"audio","link":8},{"name":"in2 gain","type":"number","link":5}],"outputs":[{"name":"out","type":"audio","links":[3]}],"properties":{"gain1":0.5,"gain2":0.8}},{"id":3,"type":"audio/destination","pos":[911,180],"size":{"0":141,"1":34},"flags":{},"order":5,"mode":0,"inputs":[{"name":"in","type":"audio","link":3}],"properties":{}},{"id":0,"type":"audio/source","pos":[195,187],"size":{"0":143,"1":30},"flags":{},"order":0,"mode":0,"inputs":[{"name":"gain","type":"number","link":null}],"outputs":[{"name":"out","type":"audio","links":[0,7]}],"properties":{"src":"demodata/audio.wav","gain":0.5,"loop":true,"autoplay":true,"playbackRate":1},"boxcolor":"#AA4"}],"links":[[0,0,0,1,0,null],[1,0,0,2,0,null],[3,1,0,3,0,null],[4,4,0,1,1,null],[5,5,0,1,3,null],[6,6,0,2,1,null],[7,0,0,7,0,null],[8,7,0,1,2,null]],"groups":[],"config":{},"version":0.4} ================================================ FILE: editor/examples/benchmark.json ================================================ {"last_node_id":60,"last_link_id":50,"nodes":[{"id":9,"type":"features/slots","pos":[846,473],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":6}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":8,"type":"features/slots","pos":[671,475],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":10}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":18,"type":"features/slots","pos":[751.5619834710739,854.9586776859505],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":16}],"outputs":[{"name":"A","type":"number","links":[17,18]},{"name":"B","type":"number","links":[]}],"properties":{}},{"id":28,"type":"features/slots","pos":[1386.361032999997,389.62936599999995],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":23}],"outputs":[{"name":"A","type":"number","links":[24,25]},{"name":"B","type":"number","links":[]}],"properties":{}},{"id":35,"type":"features/shape","pos":[1555.6387189504107,741.7260602148749],"size":{"0":140,"1":39},"flags":{},"mode":0,"inputs":[{"name":"","type":"number","link":26}],"outputs":[{"name":"","type":"number","links":null}],"properties":{}},{"id":38,"type":"features/slots","pos":[1461.838718950411,987.9260602148759],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":28}],"outputs":[{"name":"A","type":"number","links":[29,30]},{"name":"B","type":"number","links":[]}],"properties":{}},{"id":39,"type":"features/slots","pos":[1376.8387189504106,1088.9260602148756],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":29}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":40,"type":"features/slots","pos":[1551.838718950411,1086.9260602148754],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":30}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":45,"type":"features/slots","pos":[588.5619834710739,1055.9586776859496],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":33}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":46,"type":"features/slots","pos":[721.5619834710739,1058.9586776859496],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":34}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":19,"type":"features/slots","pos":[666.5619834710739,955.9586776859505],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":17}],"outputs":[{"name":"A","type":"number","links":[33]},{"name":"B","type":"number","links":[34]}],"properties":{}},{"id":47,"type":"features/slots","pos":[846.5619834710739,1060.9586776859496],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":35}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":48,"type":"features/slots","pos":[976.5619834710739,1059.9586776859496],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":36}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":20,"type":"features/slots","pos":[841.5619834710739,953.9586776859505],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":18}],"outputs":[{"name":"A","type":"number","links":[35]},{"name":"B","type":"number","links":[36]}],"properties":{}},{"id":29,"type":"features/slots","pos":[1307,490],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":24}],"outputs":[{"name":"A","type":"number","links":[37]},{"name":"B","type":"number","links":[38]}],"properties":{}},{"id":30,"type":"features/slots","pos":[1476.3610329999974,488.62936599999995],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":25}],"outputs":[{"name":"A","type":"number","links":[39]},{"name":"B","type":"number","links":[40]}],"properties":{}},{"id":7,"type":"features/slots","pos":[756,374],"size":[100,40],"flags":{"horizontal":true,"collapsed":false},"mode":0,"inputs":[{"name":"C","type":"number","link":13}],"outputs":[{"name":"A","type":"number","links":[10]},{"name":"B","type":"number","links":[6]}],"properties":{}},{"id":34,"type":"features/widgets","pos":[1054,349],"size":{"0":189,"1":176},"flags":{"collapsed":false},"mode":0,"outputs":[{"name":"","type":"number","links":[23]}],"properties":{}},{"id":12,"type":"input/gamepad","pos":[607,204],"size":{"0":155,"1":69},"flags":{"collapsed":false},"mode":0,"outputs":[{"name":"left_x_axis","type":"number","links":null},{"name":"left_y_axis","type":"number","links":null},{"name":"button_pressed","type":-1,"links":[12]}],"properties":{"gamepad_index":0,"threshold":0.1}},{"id":4,"type":"math/operation","pos":[604,106],"size":[164,51],"flags":{"collapsed":false},"mode":0,"inputs":[{"name":"A","type":"number","link":2},{"name":"B","type":"number","link":9}],"outputs":[{"name":"=","type":"number","links":[1]}],"properties":{"A":2,"B":0.5,"OP":"+"},"shape":2},{"id":2,"type":"features/shape","pos":[867,112],"size":{"0":140,"1":39},"flags":{},"mode":0,"inputs":[{"name":"","type":"number","link":1}],"outputs":[{"name":"","type":"number","links":null}],"properties":{}},{"id":13,"type":"events/log","pos":[868,215],"size":{"0":143,"1":30},"flags":{},"mode":0,"inputs":[{"name":"event","type":-1,"link":12}],"properties":{}},{"id":14,"type":"features/widgets","pos":[432,357],"size":{"0":209,"1":178},"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[13]}],"properties":{}},{"id":24,"type":"features/widgets","pos":[429,841],"size":{"0":184.9173583984375,"1":176.34710693359375},"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[16]}],"properties":{}},{"id":44,"type":"features/widgets","pos":[1154,942],"size":{"0":191,"1":174},"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[28]}],"properties":{}},{"id":36,"type":"math/operation","pos":[1333,729],"size":[144,45],"flags":{},"mode":0,"inputs":[{"name":"A","type":"number","link":27},{"name":"B","type":"number","link":null}],"outputs":[{"name":"=","type":"number","links":[26]}],"properties":{"A":2,"B":1,"OP":"+"},"shape":2},{"id":42,"type":"input/gamepad","pos":[1316,821],"size":{"0":182.3612823486328,"1":69.27394104003906},"flags":{"collapsed":false},"mode":0,"outputs":[{"name":"left_x_axis","type":"number","links":null},{"name":"left_y_axis","type":"number","links":null},{"name":"button_pressed","type":-1,"links":[]}],"properties":{"gamepad_index":0,"threshold":0.1}},{"id":43,"type":"events/log","pos":[1562,845],"size":{"0":142.16128540039062,"1":31.07394027709961},"flags":{},"mode":0,"inputs":[{"name":"event","type":-1,"link":null}],"properties":{}},{"id":41,"type":"widget/knob","pos":[1155,824],"size":[54,74],"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[]}],"properties":{"min":0,"max":1,"value":0.5,"color":"#7AF","precision":2,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":37,"type":"math/operation","pos":[1138,739],"size":[148,44],"flags":{},"mode":0,"inputs":[{"name":"A","type":"number","link":null},{"name":"B","type":"number","link":null}],"outputs":[{"name":"=","type":"number","links":[27]}],"properties":{"A":1,"B":1,"OP":"+"},"color":"#233","bgcolor":"#355","shape":2},{"id":33,"type":"events/log","pos":[1493.3610329999974,224.629366],"size":{"0":147.6389617919922,"1":29.370634078979492},"flags":{},"mode":0,"inputs":[{"name":"event","type":-1,"link":32}],"properties":{}},{"id":26,"type":"math/operation","pos":[1268,128],"size":[144,50],"flags":{},"mode":0,"inputs":[{"name":"A","type":"number","link":22},{"name":"B","type":"number","link":31}],"outputs":[{"name":"=","type":"number","links":[21]}],"properties":{"A":2,"B":0.5,"OP":"+"},"shape":2},{"id":5,"type":"math/operation","pos":[437,114],"size":[140,48],"flags":{"collapsed":true},"mode":0,"inputs":[{"name":"A","type":"number","link":null},{"name":"B","type":"number","link":null}],"outputs":[{"name":"=","type":"number","links":[2]}],"properties":{"A":1,"B":1,"OP":"+"},"shape":2},{"id":27,"type":"math/operation","pos":[1061,129],"size":[144,44],"flags":{"collapsed":true},"mode":0,"inputs":[{"name":"A","type":"number","link":null},{"name":"B","type":"number","link":null}],"outputs":[{"name":"=","type":"number","links":[22]}],"properties":{"A":1,"B":1,"OP":"+"},"shape":2},{"id":49,"type":"features/slots","pos":[1210,589],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":37}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":50,"type":"features/slots","pos":[1342,591],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":38}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":51,"type":"features/slots","pos":[1471,590],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":39}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":52,"type":"features/slots","pos":[1597,588],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":40}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":25,"type":"features/shape","pos":[1500,116],"size":{"0":140,"1":39},"flags":{},"mode":0,"inputs":[{"name":"","type":"number","link":21}],"outputs":[{"name":"","type":"number","links":null}],"properties":{}},{"id":10,"type":"widget/knob","pos":[435,161],"size":[91,111],"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[9]}],"properties":{"min":0,"max":1,"value":0.5,"color":"#7AF","precision":2,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":31,"type":"widget/knob","pos":[1058,182],"size":[95,114],"flags":{"collapsed":false},"mode":0,"outputs":[{"name":"","type":"number","links":[31]}],"properties":{"min":0,"max":1,"value":0.5,"color":"#7AF","precision":2,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":32,"type":"input/gamepad","pos":[1232,231],"size":{"0":191.6389617919922,"1":73.37063598632812},"flags":{"collapsed":false},"mode":0,"outputs":[{"name":"left_x_axis","type":"number","links":[46]},{"name":"left_y_axis","type":"number","links":[47]},{"name":"button_pressed","type":-1,"links":[32]}],"properties":{"gamepad_index":0,"threshold":0.1}},{"id":57,"type":"graphics/plot","pos":[1722,335],"size":{"0":140,"1":86},"flags":{},"mode":0,"inputs":[{"name":"A","type":"Number","link":48},{"name":"B","type":"Number","link":null},{"name":"C","type":"Number","link":null},{"name":"D","type":"Number","link":null}],"properties":{"scale":2}},{"id":21,"type":"widget/knob","pos":[435,591],"size":[72,90],"flags":{"collapsed":false},"mode":0,"outputs":[{"name":"","type":"number","links":[43]}],"properties":{"min":0,"max":1,"value":0.7297408895318863,"color":"#7AF","precision":2,"wcolor":"#7AF","size":50},"boxcolor":"rgba(186,186,186,1.0)"},{"id":16,"type":"math/operation","pos":[583,611],"size":[140,47],"flags":{"collapsed":false},"mode":0,"inputs":[{"name":"A","type":"number","link":43},{"name":"B","type":"number","link":45}],"outputs":[{"name":"=","type":"number","links":[44]}],"properties":{"A":0.7297408895318863,"B":5000,"OP":"*"},"shape":2},{"id":54,"type":"events/timer","pos":[764,620],"size":{"0":147,"1":30},"flags":{"collapsed":false},"mode":0,"inputs":[{"name":"interval","type":"number","link":44}],"outputs":[{"name":"on_tick","type":-1,"links":[42]}],"properties":{"interval":1000,"event":"tick"},"boxcolor":"#222","shape":2},{"id":23,"type":"events/log","pos":[961,622],"size":{"0":137,"1":28},"flags":{},"mode":0,"inputs":[{"name":"event","type":-1,"link":42}],"properties":{}},{"id":56,"type":"graph/subgraph","pos":[1543,336],"size":{"0":140,"1":86},"flags":{},"mode":0,"inputs":[{"name":"enabled","type":"boolean","link":49},{"name":"AxisX","type":0,"link":46},{"name":"AxisY","type":0,"link":47}],"outputs":[{"name":"sum","type":0,"links":[48]}],"properties":{"enabled":true},"subgraph":{"last_node_id":6,"last_link_id":4,"nodes":[{"id":4,"type":"graph/output","pos":[1655,311],"size":[180,60],"flags":{},"mode":0,"inputs":[{"name":"","type":0,"link":3}],"properties":{"name":"sum","type":0}},{"id":2,"type":"graph/input","pos":[1227,233],"size":[180,60],"flags":{},"mode":0,"outputs":[{"name":"","type":"","links":[1]}],"properties":{"name":"AxisX","type":""}},{"id":3,"type":"graph/input","pos":[1234,341],"size":[180,60],"flags":{},"mode":0,"outputs":[{"name":"","type":"","links":[2]}],"properties":{"name":"AxisY","type":""}},{"id":6,"type":"math/operation","pos":[1496,268],"size":[100,60],"flags":{},"mode":0,"inputs":[{"name":"A","type":"number","link":1},{"name":"B","type":"number","link":2}],"outputs":[{"name":"=","type":"number","links":[3]}],"properties":{"A":1,"B":1,"OP":"+"}}],"links":[[1,2,0,6,0,"number"],[2,3,0,6,1,"number"],[3,6,0,4,0,0]],"groups":[],"config":{},"version":0.4}},{"id":58,"type":"widget/toggle","pos":[1686,131],"size":[160,44],"flags":{},"mode":0,"inputs":[{"name":"","type":"boolean","link":null},{"name":"e","type":-1,"link":null}],"outputs":[{"name":"v","type":"boolean","links":[49]},{"name":"e","type":-1,"links":null}],"properties":{"font":"","value":true}},{"id":60,"type":"widget/number","pos":[626,706],"size":[74,54],"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":null}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":55,"type":"basic/const","pos":[422,738],"size":{"0":139,"1":28},"flags":{"collapsed":false},"mode":0,"outputs":[{"name":"value","type":"number","links":[45],"label":"5000.000"}],"properties":{"value":5000}}],"links":[[1,4,0,2,0,"number"],[2,5,0,4,0,"number"],[6,7,1,9,0,"number"],[9,10,0,4,1,"number"],[10,7,0,8,0,"number"],[12,12,2,13,0,-1],[13,14,0,7,0,"number"],[16,24,0,18,0,"number"],[17,18,0,19,0,"number"],[18,18,0,20,0,"number"],[21,26,0,25,0,"number"],[22,27,0,26,0,"number"],[23,34,0,28,0,"number"],[24,28,0,29,0,"number"],[25,28,0,30,0,"number"],[26,36,0,35,0,"number"],[27,37,0,36,0,"number"],[28,44,0,38,0,"number"],[29,38,0,39,0,"number"],[30,38,0,40,0,"number"],[31,31,0,26,1,"number"],[32,32,2,33,0,-1],[33,19,0,45,0,"number"],[34,19,1,46,0,"number"],[35,20,0,47,0,"number"],[36,20,1,48,0,"number"],[37,29,0,49,0,"number"],[38,29,1,50,0,"number"],[39,30,0,51,0,"number"],[40,30,1,52,0,"number"],[42,54,0,23,0,-1],[43,21,0,16,0,"number"],[44,16,0,54,0,"number"],[45,55,0,16,1,"number"],[46,32,0,56,1,0],[47,32,1,56,2,0],[48,56,0,57,0,"Number"],[49,58,0,56,0,"boolean"]],"groups":[{"title":"Group","bounding":[417,292,564,255],"color":"#3f789e"},{"title":"Group","bounding":[1120,678,642,461],"color":"#A88"},{"title":"Group","bounding":[411,777,679,361],"color":"#8A8"}],"config":{},"version":0.4} ================================================ FILE: editor/examples/copypaste.json ================================================ {"last_node_id":62,"last_link_id":157,"nodes":[{"id":35,"type":"widget/number","pos":[354.0977802999988,-703.6940983999988],"size":[80,60],"flags":{},"order":0,"mode":0,"outputs":[{"name":"","type":"number","links":[32,52,72,94,115,138]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":36,"type":"widget/number","pos":[356.0977802999989,-596.6940983999991],"size":[80,60],"flags":{},"order":1,"mode":0,"outputs":[{"name":"","type":"number","links":[33,53,73,95,116,139]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":37,"type":"widget/number","pos":[359.0977802999989,-495.69409839999975],"size":[80,60],"flags":{},"order":2,"mode":0,"outputs":[{"name":"","type":"number","links":[34,54,74,96,117,140]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":38,"type":"widget/number","pos":[361.0977802999988,-385.69409839999867],"size":[80,60],"flags":{},"order":3,"mode":0,"outputs":[{"name":"","type":"number","links":[35,55,75,97,118,141]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":39,"type":"widget/number","pos":[363.0977802999988,-269.6940983999987],"size":[80,60],"flags":{},"order":4,"mode":0,"outputs":[{"name":"","type":"number","links":[36,56,76,98,119,142]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":40,"type":"widget/number","pos":[366.3589802999999,-157.40999840000026],"size":[80,60],"flags":{},"order":5,"mode":0,"outputs":[{"name":"","type":"number","links":[37,57,77,99,120,143]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":41,"type":"widget/number","pos":[367.3589802999999,-49.40999839999942],"size":[80,60],"flags":{},"order":6,"mode":0,"outputs":[{"name":"","type":"number","links":[38,58,78,100,121,144]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":42,"type":"widget/number","pos":[366.3589802999999,53.59000160000091],"size":[80,60],"flags":{},"order":7,"mode":0,"outputs":[{"name":"","type":"number","links":[39,59,79,101,122,145]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":43,"type":"widget/number","pos":[366.3589802999999,157.59000160000005],"size":[80,60],"flags":{},"order":8,"mode":0,"outputs":[{"name":"","type":"number","links":[40,60,80,102,123,146]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":45,"type":"widget/number","pos":[374,367.58986919999944],"size":[80,60],"flags":{},"order":9,"mode":0,"outputs":[{"name":"","type":"number","links":[42,62,82,104,125,148]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":46,"type":"widget/number","pos":[375,464.58986920000046],"size":[80,60],"flags":{},"order":10,"mode":0,"outputs":[{"name":"","type":"number","links":[43,63,83,105,126,149]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":47,"type":"widget/number","pos":[377,564.5898692000013],"size":[80,60],"flags":{},"order":11,"mode":0,"outputs":[{"name":"","type":"number","links":[44,64,84,106,127,150]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":48,"type":"widget/number","pos":[380,661.5898692000013],"size":[80,60],"flags":{},"order":12,"mode":0,"outputs":[{"name":"","type":"number","links":[45,65,85,107,128,151]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":44,"type":"widget/number","pos":[372,264],"size":[80,60],"flags":{},"order":13,"mode":0,"outputs":[{"name":"","type":"number","links":[41,61,81,103,124,147]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":49,"type":"widget/number","pos":[383,768],"size":[80,60],"flags":{},"order":14,"mode":0,"outputs":[{"name":"","type":"number","links":[46,66,86,108,129,152]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":50,"type":"widget/number","pos":[386,871],"size":[80,60],"flags":{},"order":15,"mode":0,"outputs":[{"name":"","type":"number","links":[47,67,87,109,130,153]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":51,"type":"widget/number","pos":[390,976],"size":[80,60],"flags":{},"order":16,"mode":0,"outputs":[{"name":"","type":"number","links":[48,68,88,110,131,154]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":52,"type":"widget/number","pos":[396,1079],"size":[80,60],"flags":{},"order":17,"mode":0,"outputs":[{"name":"","type":"number","links":[49,89,111]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":55,"type":"features/largeinput_editor","pos":[814.2834913867189,1257.9045020637213],"size":[200,410],"flags":{},"order":28,"mode":0,"inputs":[{"name":"in 1","type":"number","link":71,"slot_index":0},{"name":"in 2","type":"number","link":72,"slot_index":1},{"name":"in 3","type":"number","link":73,"slot_index":2},{"name":"in 4","type":"number","link":74,"slot_index":3},{"name":"in 5","type":"number","link":75,"slot_index":4},{"name":"in 6","type":"number","link":76,"slot_index":5},{"name":"in 7","type":"number","link":77,"slot_index":6},{"name":"in 8","type":"number","link":78,"slot_index":7},{"name":"in 9","type":"number","link":79,"slot_index":8},{"name":"in 10","type":"number","link":80,"slot_index":9},{"name":"in 11","type":"number","link":81,"slot_index":10},{"name":"in 12","type":"number","link":82,"slot_index":11},{"name":"in 13","type":"number","link":83,"slot_index":12},{"name":"in 14","type":"number","link":84,"slot_index":13},{"name":"in 15","type":"number","link":85,"slot_index":14},{"name":"in 16","type":"number","link":86,"slot_index":15},{"name":"in 17","type":"number","link":87,"slot_index":16},{"name":"in 18","type":"number","link":88,"slot_index":17},{"name":"in 19","type":"number","link":89,"slot_index":18},{"name":"in 20","type":"number","link":91,"slot_index":19}],"properties":{}},{"id":54,"type":"features/largeinput_editor","pos":[816.2834913867189,800.9045020637204],"size":[200,410],"flags":{},"order":27,"mode":0,"inputs":[{"name":"in 1","type":"number","link":51,"slot_index":0},{"name":"in 2","type":"number","link":52,"slot_index":1},{"name":"in 3","type":"number","link":53,"slot_index":2},{"name":"in 4","type":"number","link":54,"slot_index":3},{"name":"in 5","type":"number","link":55,"slot_index":4},{"name":"in 6","type":"number","link":56,"slot_index":5},{"name":"in 7","type":"number","link":57,"slot_index":6},{"name":"in 8","type":"number","link":58,"slot_index":7},{"name":"in 9","type":"number","link":59,"slot_index":8},{"name":"in 10","type":"number","link":60,"slot_index":9},{"name":"in 11","type":"number","link":61,"slot_index":10},{"name":"in 12","type":"number","link":62,"slot_index":11},{"name":"in 13","type":"number","link":63,"slot_index":12},{"name":"in 14","type":"number","link":64,"slot_index":13},{"name":"in 15","type":"number","link":65,"slot_index":14},{"name":"in 16","type":"number","link":66,"slot_index":15},{"name":"in 17","type":"number","link":67,"slot_index":16},{"name":"in 18","type":"number","link":68,"slot_index":17},{"name":"in 19","type":"number","link":92,"slot_index":18},{"name":"in 20","type":"number","link":70,"slot_index":19}],"properties":{}},{"id":33,"type":"features/largeinput_editor","pos":[818.2834913867189,334.90450206372003],"size":[200,410],"flags":{},"order":26,"mode":0,"inputs":[{"name":"in 1","type":"number","link":31,"slot_index":0},{"name":"in 2","type":"number","link":32,"slot_index":1},{"name":"in 3","type":"number","link":33,"slot_index":2},{"name":"in 4","type":"number","link":34,"slot_index":3},{"name":"in 5","type":"number","link":35,"slot_index":4},{"name":"in 6","type":"number","link":36,"slot_index":5},{"name":"in 7","type":"number","link":37,"slot_index":6},{"name":"in 8","type":"number","link":38,"slot_index":7},{"name":"in 9","type":"number","link":39,"slot_index":8},{"name":"in 10","type":"number","link":40,"slot_index":9},{"name":"in 11","type":"number","link":41,"slot_index":10},{"name":"in 12","type":"number","link":42,"slot_index":11},{"name":"in 13","type":"number","link":43,"slot_index":12},{"name":"in 14","type":"number","link":44,"slot_index":13},{"name":"in 15","type":"number","link":45,"slot_index":14},{"name":"in 16","type":"number","link":46,"slot_index":15},{"name":"in 17","type":"number","link":47,"slot_index":16},{"name":"in 18","type":"number","link":48,"slot_index":17},{"name":"in 19","type":"number","link":49,"slot_index":18},{"name":"in 20","type":"number","link":50,"slot_index":19}],"properties":{}},{"id":53,"type":"widget/number","pos":[399,1182],"size":[80,60],"flags":{},"order":18,"mode":0,"outputs":[{"name":"","type":"number","links":[50,70,112,135],"slot_index":0}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":57,"type":"widget/number","pos":[405,1443],"size":[80,60],"flags":{},"order":19,"mode":0,"outputs":[{"name":"","type":"number","links":[92,136,155],"slot_index":0}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":61,"type":"features/largeinput_editor","pos":[1070.283491386718,1265.9045020637213],"size":[200,410],"flags":{},"order":25,"mode":0,"inputs":[{"name":"in 1","type":"number","link":137,"slot_index":0},{"name":"in 2","type":"number","link":138,"slot_index":1},{"name":"in 3","type":"number","link":139,"slot_index":2},{"name":"in 4","type":"number","link":140,"slot_index":3},{"name":"in 5","type":"number","link":141,"slot_index":4},{"name":"in 6","type":"number","link":142,"slot_index":5},{"name":"in 7","type":"number","link":143,"slot_index":6},{"name":"in 8","type":"number","link":144,"slot_index":7},{"name":"in 9","type":"number","link":145,"slot_index":8},{"name":"in 10","type":"number","link":146,"slot_index":9},{"name":"in 11","type":"number","link":147,"slot_index":10},{"name":"in 12","type":"number","link":148,"slot_index":11},{"name":"in 13","type":"number","link":149,"slot_index":12},{"name":"in 14","type":"number","link":150,"slot_index":13},{"name":"in 15","type":"number","link":151,"slot_index":14},{"name":"in 16","type":"number","link":152,"slot_index":15},{"name":"in 17","type":"number","link":153,"slot_index":16},{"name":"in 18","type":"number","link":154,"slot_index":17},{"name":"in 19","type":"number","link":155,"slot_index":18},{"name":"in 20","type":"number","link":157,"slot_index":19}],"properties":{}},{"id":56,"type":"widget/number","pos":[446,1703],"size":[80,60],"flags":{},"order":20,"mode":0,"outputs":[{"name":"","type":"number","links":[91,157],"slot_index":0}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":60,"type":"features/largeinput_editor","pos":[1069.283491386718,804.9045020637204],"size":[200,410],"flags":{},"order":24,"mode":0,"inputs":[{"name":"in 1","type":"number","link":114,"slot_index":0},{"name":"in 2","type":"number","link":115,"slot_index":1},{"name":"in 3","type":"number","link":116,"slot_index":2},{"name":"in 4","type":"number","link":117,"slot_index":3},{"name":"in 5","type":"number","link":118,"slot_index":4},{"name":"in 6","type":"number","link":119,"slot_index":5},{"name":"in 7","type":"number","link":120,"slot_index":6},{"name":"in 8","type":"number","link":121,"slot_index":7},{"name":"in 9","type":"number","link":122,"slot_index":8},{"name":"in 10","type":"number","link":123,"slot_index":9},{"name":"in 11","type":"number","link":124,"slot_index":10},{"name":"in 12","type":"number","link":125,"slot_index":11},{"name":"in 13","type":"number","link":126,"slot_index":12},{"name":"in 14","type":"number","link":127,"slot_index":13},{"name":"in 15","type":"number","link":128,"slot_index":14},{"name":"in 16","type":"number","link":129,"slot_index":15},{"name":"in 17","type":"number","link":130,"slot_index":16},{"name":"in 18","type":"number","link":131,"slot_index":17},{"name":"in 19","type":"number","link":136,"slot_index":18},{"name":"in 20","type":"number","link":135,"slot_index":19}],"properties":{}},{"id":59,"type":"widget/number","pos":[942.2834913867189,139.9045020637209],"size":[80,60],"flags":{},"order":21,"mode":0,"outputs":[{"name":"","type":"number","links":[113,114,137]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":34,"type":"widget/number","pos":[354,-815],"size":[80,60],"flags":{},"order":22,"mode":0,"outputs":[{"name":"","type":"number","links":[31,51,71]}],"properties":{"min":-1000,"max":1000,"value":1,"step":1}},{"id":58,"type":"features/largeinput_editor","pos":[1066,342],"size":[200,410],"flags":{},"order":23,"mode":0,"inputs":[{"name":"in 1","type":"number","link":113,"slot_index":0},{"name":"in 2","type":"number","link":94,"slot_index":1},{"name":"in 3","type":"number","link":95,"slot_index":2},{"name":"in 4","type":"number","link":96,"slot_index":3},{"name":"in 5","type":"number","link":97,"slot_index":4},{"name":"in 6","type":"number","link":98,"slot_index":5},{"name":"in 7","type":"number","link":99,"slot_index":6},{"name":"in 8","type":"number","link":100,"slot_index":7},{"name":"in 9","type":"number","link":101,"slot_index":8},{"name":"in 10","type":"number","link":102,"slot_index":9},{"name":"in 11","type":"number","link":103,"slot_index":10},{"name":"in 12","type":"number","link":104,"slot_index":11},{"name":"in 13","type":"number","link":105,"slot_index":12},{"name":"in 14","type":"number","link":106,"slot_index":13},{"name":"in 15","type":"number","link":107,"slot_index":14},{"name":"in 16","type":"number","link":108,"slot_index":15},{"name":"in 17","type":"number","link":109,"slot_index":16},{"name":"in 18","type":"number","link":110,"slot_index":17},{"name":"in 19","type":"number","link":111,"slot_index":18},{"name":"in 20","type":"number","link":112,"slot_index":19}],"properties":{}}],"links":[[31,34,0,33,0,"number"],[32,35,0,33,1,"number"],[33,36,0,33,2,"number"],[34,37,0,33,3,"number"],[35,38,0,33,4,"number"],[36,39,0,33,5,"number"],[37,40,0,33,6,"number"],[38,41,0,33,7,"number"],[39,42,0,33,8,"number"],[40,43,0,33,9,"number"],[41,44,0,33,10,"number"],[42,45,0,33,11,"number"],[43,46,0,33,12,"number"],[44,47,0,33,13,"number"],[45,48,0,33,14,"number"],[46,49,0,33,15,"number"],[47,50,0,33,16,"number"],[48,51,0,33,17,"number"],[49,52,0,33,18,"number"],[50,53,0,33,19,"number"],[51,34,0,54,0,"number"],[52,35,0,54,1,"number"],[53,36,0,54,2,"number"],[54,37,0,54,3,"number"],[55,38,0,54,4,"number"],[56,39,0,54,5,"number"],[57,40,0,54,6,"number"],[58,41,0,54,7,"number"],[59,42,0,54,8,"number"],[60,43,0,54,9,"number"],[61,44,0,54,10,"number"],[62,45,0,54,11,"number"],[63,46,0,54,12,"number"],[64,47,0,54,13,"number"],[65,48,0,54,14,"number"],[66,49,0,54,15,"number"],[67,50,0,54,16,"number"],[68,51,0,54,17,"number"],[70,53,0,54,19,"number"],[71,34,0,55,0,"number"],[72,35,0,55,1,"number"],[73,36,0,55,2,"number"],[74,37,0,55,3,"number"],[75,38,0,55,4,"number"],[76,39,0,55,5,"number"],[77,40,0,55,6,"number"],[78,41,0,55,7,"number"],[79,42,0,55,8,"number"],[80,43,0,55,9,"number"],[81,44,0,55,10,"number"],[82,45,0,55,11,"number"],[83,46,0,55,12,"number"],[84,47,0,55,13,"number"],[85,48,0,55,14,"number"],[86,49,0,55,15,"number"],[87,50,0,55,16,"number"],[88,51,0,55,17,"number"],[89,52,0,55,18,"number"],[91,56,0,55,19,"number"],[92,57,0,54,18,"number"],[94,35,0,58,1,"number"],[95,36,0,58,2,"number"],[96,37,0,58,3,"number"],[97,38,0,58,4,"number"],[98,39,0,58,5,"number"],[99,40,0,58,6,"number"],[100,41,0,58,7,"number"],[101,42,0,58,8,"number"],[102,43,0,58,9,"number"],[103,44,0,58,10,"number"],[104,45,0,58,11,"number"],[105,46,0,58,12,"number"],[106,47,0,58,13,"number"],[107,48,0,58,14,"number"],[108,49,0,58,15,"number"],[109,50,0,58,16,"number"],[110,51,0,58,17,"number"],[111,52,0,58,18,"number"],[112,53,0,58,19,"number"],[113,59,0,58,0,"number"],[114,59,0,60,0,"number"],[115,35,0,60,1,"number"],[116,36,0,60,2,"number"],[117,37,0,60,3,"number"],[118,38,0,60,4,"number"],[119,39,0,60,5,"number"],[120,40,0,60,6,"number"],[121,41,0,60,7,"number"],[122,42,0,60,8,"number"],[123,43,0,60,9,"number"],[124,44,0,60,10,"number"],[125,45,0,60,11,"number"],[126,46,0,60,12,"number"],[127,47,0,60,13,"number"],[128,48,0,60,14,"number"],[129,49,0,60,15,"number"],[130,50,0,60,16,"number"],[131,51,0,60,17,"number"],[135,53,0,60,19,"number"],[136,57,0,60,18,"number"],[137,59,0,61,0,"number"],[138,35,0,61,1,"number"],[139,36,0,61,2,"number"],[140,37,0,61,3,"number"],[141,38,0,61,4,"number"],[142,39,0,61,5,"number"],[143,40,0,61,6,"number"],[144,41,0,61,7,"number"],[145,42,0,61,8,"number"],[146,43,0,61,9,"number"],[147,44,0,61,10,"number"],[148,45,0,61,11,"number"],[149,46,0,61,12,"number"],[150,47,0,61,13,"number"],[151,48,0,61,14,"number"],[152,49,0,61,15,"number"],[153,50,0,61,16,"number"],[154,51,0,61,17,"number"],[155,57,0,61,18,"number"],[157,56,0,61,19,"number"]],"groups":[{"title":"Use Ctrl+C/Ctrl+Shift+V to easily copy and paste new nodes maintaining connection to the outputs of unselected nodes","bounding":[1137,140,263,82],"color":"#3f789e"}],"config":{},"extra":{},"version":0.4} ================================================ FILE: editor/examples/features.json ================================================ {"last_node_id":14,"last_link_id":14,"nodes":[{"id":9,"type":"features/slots","pos":[847,479],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":6}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":7,"type":"features/slots","pos":[757,380],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":13}],"outputs":[{"name":"A","type":"number","links":[10]},{"name":"B","type":"number","links":[6]}],"properties":{}},{"id":8,"type":"features/slots","pos":[672,481],"size":[100,40],"flags":{"horizontal":true},"mode":0,"inputs":[{"name":"C","type":"number","link":10}],"outputs":[{"name":"A","type":"number","links":null},{"name":"B","type":"number","links":null}],"properties":{}},{"id":5,"type":"math/operation","pos":[413,101],"size":[140,34],"flags":{"collapsed":true},"mode":0,"inputs":[{"name":"A","type":"number","link":null},{"name":"B","type":"number","link":null}],"outputs":[{"name":"=","type":"number","links":[2]}],"properties":{"A":1,"B":1,"OP":"+"},"shape":2},{"id":12,"type":"input/gamepad","pos":[593,208],"size":{"0":175,"1":74},"flags":{},"mode":0,"outputs":[{"name":"left_x_axis","type":"number","links":null},{"name":"left_y_axis","type":"number","links":null},{"name":"button_pressed","type":-1,"links":[12]}],"properties":{"gamepad_index":0,"threshold":0.1}},{"id":13,"type":"events/log","pos":[862,246],"size":{"0":144,"1":32},"flags":{},"mode":0,"inputs":[{"name":"event","type":-1,"link":12}],"properties":{}},{"id":14,"type":"features/widgets","pos":[441,365],"size":{"0":180,"1":170},"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[13]}],"properties":{}},{"id":10,"type":"widget/knob","pos":[421,197],"size":[74,92],"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[9]}],"properties":{"min":0,"max":1,"value":0.5,"wcolor":"#7AF","size":50}},{"id":4,"type":"math/operation","pos":[596,116],"size":[148,48],"flags":{},"mode":0,"inputs":[{"name":"A","type":"number","link":2},{"name":"B","type":"number","link":9}],"outputs":[{"name":"=","type":"number","links":[1]}],"properties":{"A":1,"B":1,"OP":"+"},"shape":2},{"id":2,"type":"features/shape","pos":[850,97],"size":{"0":140,"1":39},"flags":{},"mode":0,"inputs":[{"name":"","type":"number","link":1}],"outputs":[{"name":"","type":"number","links":null}],"properties":{}}],"links":[[1,4,0,2,0,"number"],[2,5,0,4,0,"number"],[6,7,1,9,0,"number"],[9,10,0,4,1,"number"],[10,7,0,8,0,"number"],[12,12,2,13,0,-1],[13,14,0,7,0,"number"]],"groups":[{"title":"Group","bounding":[418,298,609,255],"color":"#3f789e"}],"config":{}} ================================================ FILE: editor/examples/midi_generation.json ================================================ {"last_node_id":47,"last_link_id":64,"nodes":[{"id":8,"type":"midi/generator","pos":[548,390],"size":{"0":140,"1":66},"flags":{},"mode":0,"inputs":[{"name":"generate","type":-1,"link":11},{"name":"scale","type":"string","link":47},{"name":"octave","type":"number","link":null}],"outputs":[{"name":"note","type":-1,"links":[10,19]}],"properties":{"notes":"A,B,C","octave":2,"duration":0.5,"mode":"sequence"}},{"id":20,"type":"midi/transpose","pos":[726,489],"size":{"0":140,"1":46},"flags":{},"mode":0,"inputs":[{"name":"in","type":-1,"link":19},{"name":"amount","type":"number","link":null}],"outputs":[{"name":"out","type":-1,"links":[21]}],"properties":{"amount":5}},{"id":32,"type":"midi/event","pos":[1465,656],"size":{"0":140,"1":46},"flags":{},"mode":0,"inputs":[{"name":"send","type":-1,"link":null},{"name":"assign","type":-1,"link":44}],"outputs":[{"name":"on_midi","type":-1,"links":null},{"name":"note","type":"number","links":[45]}],"properties":{"channel":0,"cmd":128,"value1":0,"value2":0}},{"id":19,"type":"midi/play","pos":[1132,611],"size":{"0":140,"1":66},"flags":{},"mode":0,"inputs":[{"name":"note","type":-1,"link":53},{"name":"volume","type":"number","link":36},{"name":"duration","type":"number","link":null}],"outputs":[{"name":"note","type":-1,"links":[35,44]}],"properties":{"volume":0.3599999999999999,"duration":4,"value":0}},{"id":21,"type":"midi/quantize","pos":[903,589],"size":{"0":159.60000610351562,"1":46},"flags":{},"mode":0,"inputs":[{"name":"note","type":-1,"link":21},{"name":"scale","type":"string","link":49}],"outputs":[{"name":"out","type":-1,"links":[53]}],"properties":{"scale":"A,A#,B,C,C#,D,D#,E,F,F#,G,G#","amount":"A,B,C,D,E,F,G"}},{"id":37,"type":"basic/watch","pos":[547,615],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"value","type":0,"link":52,"label":"A,B,C,B"}],"properties":{}},{"id":35,"type":"basic/string","pos":[79,456],"size":[210,58],"flags":{},"mode":0,"outputs":[{"name":"value","type":"string","links":[50],"label":"A,B,C"}],"title":"NOTE SCALE","properties":{"value":"A,B,C,B"}},{"id":7,"type":"midi/generator","pos":[549,289],"size":{"0":140,"1":66},"flags":{},"mode":0,"inputs":[{"name":"generate","type":-1,"link":5},{"name":"scale","type":"string","link":48},{"name":"octave","type":"number","link":null}],"outputs":[{"name":"note","type":-1,"links":[7,12]}],"properties":{"notes":"A,B,C","octave":2,"duration":0.5,"mode":"random"}},{"id":41,"type":"midi/generator","pos":[552,189],"size":{"0":140,"1":66},"flags":{},"mode":0,"inputs":[{"name":"generate","type":-1,"link":57},{"name":"scale","type":"string","link":55},{"name":"octave","type":"number","link":null}],"outputs":[{"name":"note","type":-1,"links":[62]}],"properties":{"notes":"A,B,C","octave":3,"duration":0.5,"mode":"sequence"}},{"id":12,"type":"events/timer","pos":[180,284],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"interval","type":"number","link":null}],"outputs":[{"name":"on_tick","type":-1,"links":[11]}],"properties":{"interval":1200,"event":"tick"},"boxcolor":"#222"},{"id":34,"type":"logic/selector","pos":[351,468],"size":{"0":140,"1":106},"flags":{},"mode":0,"inputs":[{"name":"sel","type":"number","link":58},{"name":"A","type":0,"link":46},{"name":"B","type":0,"link":50},{"name":"C","type":0,"link":59},{"name":"D","type":0,"link":null}],"outputs":[{"name":"out","links":[47,48,49,52,55]}],"properties":{}},{"id":47,"type":"midi/keys","pos":[1153,88],"size":[423,104],"flags":{},"mode":0,"inputs":[{"name":"note","type":-1,"link":62},{"name":"reset","type":-1,"link":null}],"outputs":[{"name":"note","type":-1,"links":[63]}],"properties":{"num_octaves":2,"start_octave":3}},{"id":15,"type":"math/floor","pos":[505,85],"size":[140,26],"flags":{"collapsed":true},"mode":0,"inputs":[{"name":"in","type":"number","link":14}],"outputs":[{"name":"out","type":"number","links":[15]}],"properties":{}},{"id":14,"type":"math/rand","pos":[344,83],"size":[140,26],"flags":{"collapsed":true},"mode":0,"outputs":[{"name":"value","type":"number","links":[14],"label":"1.191"}],"properties":{"min":-1,"max":2}},{"id":16,"type":"math/operation","pos":[645,85],"size":[100,50],"flags":{"collapsed":true},"mode":0,"inputs":[{"name":"A","type":"number","link":15},{"name":"B","type":"number","link":null}],"outputs":[{"name":"=","type":"number","links":[16]}],"properties":{"A":1,"B":12,"OP":"*"}},{"id":10,"type":"basic/string","pos":[77,360],"size":[208,48],"flags":{},"mode":0,"outputs":[{"name":"value","type":"string","links":[46],"label":"A,B,C"}],"title":"NOTE SCALE","properties":{"value":"A,B,C,D,E,F,G"}},{"id":43,"type":"basic/string","pos":[79,556],"size":[210,58],"flags":{},"mode":0,"outputs":[{"name":"value","type":"string","links":[59],"label":"A,B,C"}],"title":"NOTE SCALE","properties":{"value":"D,E,F,G,F,E"}},{"id":44,"type":"math/rand","pos":[143,664],"size":[140,26],"flags":{},"mode":0,"outputs":[{"name":"value","type":"number","links":[58],"label":"0.750"}],"properties":{"min":0,"max":1}},{"id":11,"type":"midi/play","pos":[1135,496],"size":{"0":140,"1":66},"flags":{},"mode":0,"inputs":[{"name":"note","type":-1,"link":10},{"name":"volume","type":"number","link":18},{"name":"duration","type":"number","link":null}],"outputs":[{"name":"note","type":-1,"links":[34,43]}],"properties":{"volume":0.3599999999999999,"duration":4,"value":0}},{"id":13,"type":"midi/transpose","pos":[893,258],"size":{"0":140,"1":46},"flags":{},"mode":0,"inputs":[{"name":"in","type":-1,"link":12},{"name":"amount","type":"number","link":16}],"outputs":[{"name":"out","type":-1,"links":[54]}],"properties":{"amount":12}},{"id":4,"type":"midi/play","pos":[1155,249],"size":{"0":140,"1":66},"flags":{},"mode":0,"inputs":[{"name":"note","type":-1,"link":54},{"name":"volume","type":"number","link":17},{"name":"duration","type":"number","link":null}],"outputs":[{"name":"note","type":-1,"links":[33,39]}],"properties":{"volume":0.21000000000000005,"duration":1,"value":0}},{"id":30,"type":"midi/event","pos":[1433,260],"size":{"0":140,"1":46},"flags":{},"mode":0,"inputs":[{"name":"send","type":-1,"link":null},{"name":"assign","type":-1,"link":39}],"outputs":[{"name":"on_midi","type":-1,"links":null},{"name":"note","type":"number","links":[38]}],"properties":{"channel":0,"cmd":128,"value1":57,"value2":0}},{"id":28,"type":"midi/output","pos":[1428,414],"size":{"0":140,"1":66},"flags":{},"mode":0,"inputs":[{"name":"send","type":-1,"link":33},{"name":"send","type":-1,"link":34},{"name":"send","type":-1,"link":35}],"properties":{"port":0}},{"id":31,"type":"midi/event","pos":[1469,563],"size":{"0":140,"1":46},"flags":{},"mode":0,"inputs":[{"name":"send","type":-1,"link":null},{"name":"assign","type":-1,"link":43}],"outputs":[{"name":"on_midi","type":-1,"links":null},{"name":"note","type":"number","links":[42]}],"properties":{"channel":0,"cmd":128,"value1":50,"value2":0}},{"id":29,"type":"graphics/plot","pos":[1675,328],"size":{"0":348,"1":139},"flags":{},"mode":0,"inputs":[{"name":"A","type":"Number","link":38},{"name":"B","type":"Number","link":42},{"name":"C","type":"Number","link":45},{"name":"D","type":"Number","link":null}],"properties":{"scale":100}},{"id":46,"type":"math/rand","pos":[1455,42],"size":[140,26],"flags":{"collapsed":true},"mode":0,"outputs":[{"name":"value","type":"number","links":[60],"label":"0.007"}],"properties":{"min":0,"max":0.2}},{"id":39,"type":"midi/play","pos":[1656,116],"size":{"0":140,"1":66},"flags":{},"mode":0,"inputs":[{"name":"note","type":-1,"link":63},{"name":"volume","type":"number","link":60},{"name":"duration","type":"number","link":null}],"outputs":[{"name":"note","type":-1,"links":[]}],"properties":{"volume":0.006812153971126511,"duration":1,"value":0}},{"id":3,"type":"events/timer","pos":[178,212],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"interval","type":"number","link":null}],"outputs":[{"name":"on_tick","type":-1,"links":[5,57]}],"properties":{"interval":300,"event":"tick"},"boxcolor":"#222"},{"id":18,"type":"widget/knob","pos":[819,62],"size":[82.78512396694214,93.87603305785123],"flags":{},"mode":0,"outputs":[{"name":"","type":"number","links":[18,36]}],"properties":{"min":0,"max":1,"value":0.4504132231404958,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":17,"type":"widget/knob","pos":[916,62],"size":[78.34710743801656,94.70247933884298],"flags":{"collapsed":false},"mode":0,"outputs":[{"name":"","type":"number","links":[17]}],"properties":{"min":0,"max":1,"value":0.21000000000000005,"wcolor":"#7AF","size":50},"boxcolor":"rgba(128,128,128,1.0)"},{"id":6,"type":"midi/show","pos":[898,357],"size":[266.5950413223138,61.685950413223],"flags":{},"mode":0,"inputs":[{"name":"on_midi","type":-1,"link":7}],"properties":{}}],"links":[[5,3,0,7,0,-1],[7,7,0,6,0,-1],[10,8,0,11,0,-1],[11,12,0,8,0,-1],[12,7,0,13,0,-1],[14,14,0,15,0,"number"],[15,15,0,16,0,"number"],[16,16,0,13,1,"number"],[17,17,0,4,1,"number"],[18,18,0,11,1,"number"],[19,8,0,20,0,-1],[21,20,0,21,0,-1],[33,4,0,28,0,-1],[34,11,0,28,1,-1],[35,19,0,28,2,-1],[36,18,0,19,1,"number"],[38,30,1,29,0,"Number"],[39,4,0,30,1,-1],[42,31,1,29,1,"Number"],[43,11,0,31,1,-1],[44,19,0,32,1,-1],[45,32,1,29,2,"Number"],[46,10,0,34,1,0],[47,34,0,8,1,"string"],[48,34,0,7,1,"string"],[49,34,0,21,1,"string"],[50,35,0,34,2,0],[52,34,0,37,0,0],[53,21,0,19,0,-1],[54,13,0,4,0,-1],[55,34,0,41,1,"string"],[57,3,0,41,0,-1],[58,44,0,34,0,"number"],[59,43,0,34,3,0],[60,46,0,39,1,"number"],[62,41,0,47,0,-1],[63,47,0,39,0,-1]],"groups":[],"config":{}} ================================================ FILE: editor/examples/subgraph.json ================================================ {"last_node_id":6,"last_link_id":5,"nodes":[{"id":3,"type":"basic/time","pos":[312,145],"size":{"0":140,"1":46},"flags":{},"mode":0,"outputs":[{"name":"in ms","type":"number","links":null},{"name":"in sec","type":"number","links":[1]}],"properties":{}},{"id":4,"type":"basic/watch","pos":[864,156],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"value","type":0,"link":2,"label":"5.000"}],"properties":{}},{"id":6,"type":"events/counter","pos":[864,229],"size":{"0":140,"1":66},"flags":{},"mode":0,"inputs":[{"name":"inc","type":-1,"link":4},{"name":"dec","type":-1,"link":null},{"name":"reset","type":-1,"link":null}],"outputs":[{"name":"change","type":-1,"links":null},{"name":"num","type":"number","links":null}],"properties":{}},{"id":2,"type":"graph/subgraph","pos":[573,168],"size":{"0":140,"1":86},"flags":{},"mode":0,"inputs":[{"name":"enabled","type":"boolean","link":null},{"name":"foo","type":"","link":1},{"name":"EV","type":-1,"link":3}],"outputs":[{"name":"faa","type":0,"links":[2]},{"name":"EV","type":-1,"links":[4]}],"properties":{"enabled":true},"subgraph":{"last_node_id":7,"last_link_id":6,"nodes":[{"id":3,"type":"graph/output","pos":[1119,139],"size":[180,60],"flags":{},"mode":0,"inputs":[{"name":"","type":0,"link":2}],"properties":{"name":"faa","type":0}},{"id":4,"type":"math/floor","pos":[872,194],"size":[112,28],"flags":{},"mode":0,"inputs":[{"name":"in","type":"number","link":1}],"outputs":[{"name":"out","type":"number","links":[2]}],"properties":{}},{"id":2,"type":"graph/input","pos":[440,149],"size":[180,60],"flags":{},"mode":0,"outputs":[{"name":"","type":"","links":[1]}],"properties":{"name":"foo","type":""}},{"id":5,"type":"graph/input","pos":[460,282],"size":[180,60],"flags":{},"mode":0,"outputs":[{"name":"","type":-1,"links":[4]}],"properties":{"name":"EV","type":-1}},{"id":6,"type":"graph/output","pos":[1054,293],"size":[180,60],"flags":{},"mode":0,"inputs":[{"name":"","type":-1,"link":5}],"properties":{"name":"EV","type":-1}},{"id":7,"type":"events/delay","pos":[742,300],"size":{"0":140,"1":26},"flags":{},"mode":0,"inputs":[{"name":"event","type":-1,"link":4}],"outputs":[{"name":"on_time","type":-1,"links":[5]}],"properties":{"time_in_ms":1000}}],"links":[[1,2,0,4,0,"number"],[2,4,0,3,0,0],[4,5,0,7,0,-1],[5,7,0,6,0,-1]],"groups":[],"config":{},"version":0.4}},{"id":5,"type":"events/timer","pos":[311,240],"size":{"0":140,"1":26},"flags":{},"mode":0,"outputs":[{"name":"on_tick","type":-1,"links":[3]}],"properties":{"interval":2000,"event":"tick"},"boxcolor":"#222"}],"links":[[1,3,1,2,1,0],[2,2,0,4,0,0],[3,5,0,2,2,-1],[4,2,1,6,0,-1]],"groups":[],"config":{},"version":0.4} ================================================ FILE: editor/index.html ================================================ LiteGraph
    ================================================ FILE: editor/js/code.js ================================================ var webgl_canvas = null; LiteGraph.node_images_path = "../nodes_data/"; var editor = new LiteGraph.Editor("main",{miniwindow:false}); window.graphcanvas = editor.graphcanvas; window.graph = editor.graph; updateEditorHiPPICanvas(); window.addEventListener("resize", function() { editor.graphcanvas.resize(); updateEditorHiPPICanvas(); } ); //window.addEventListener("keydown", editor.graphcanvas.processKey.bind(editor.graphcanvas) ); window.onbeforeunload = function(){ var data = JSON.stringify( graph.serialize() ); localStorage.setItem("litegraphg demo backup", data ); } function updateEditorHiPPICanvas() { const ratio = window.devicePixelRatio; if(ratio == 1) { return } const rect = editor.canvas.parentNode.getBoundingClientRect(); const { width, height } = rect; editor.canvas.width = width * ratio; editor.canvas.height = height * ratio; editor.canvas.style.width = width + "px"; editor.canvas.style.height = height + "px"; editor.canvas.getContext("2d").scale(ratio, ratio); return editor.canvas; } //enable scripting LiteGraph.allow_scripts = true; //test //editor.graphcanvas.viewport = [200,200,400,400]; //create scene selector var elem = document.createElement("span"); elem.id = "LGEditorTopBarSelector"; elem.className = "selector"; elem.innerHTML = ""; elem.innerHTML += "Demo | "; editor.tools.appendChild(elem); var select = elem.querySelector("select"); select.addEventListener("change", function(e){ var option = this.options[this.selectedIndex]; var url = option.dataset["url"]; if(url) graph.load( url ); else if(option.callback) option.callback(); else graph.clear(); }); elem.querySelector("#save").addEventListener("click",function(){ console.log("saved"); localStorage.setItem( "graphdemo_save", JSON.stringify( graph.serialize() ) ); }); elem.querySelector("#load").addEventListener("click",function(){ var data = localStorage.getItem( "graphdemo_save" ); if(data) graph.configure( JSON.parse( data ) ); console.log("loaded"); }); elem.querySelector("#download").addEventListener("click",function(){ var data = JSON.stringify( graph.serialize() ); var file = new Blob( [ data ] ); var url = URL.createObjectURL( file ); var element = document.createElement("a"); element.setAttribute('href', url); element.setAttribute('download', "graph.JSON" ); 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 }); elem.querySelector("#webgl").addEventListener("click", enableWebGL ); elem.querySelector("#multiview").addEventListener("click", function(){ editor.addMultiview() } ); function addDemo( name, url ) { var option = document.createElement("option"); if(url.constructor === String) option.dataset["url"] = url; else option.callback = url; option.innerHTML = name; select.appendChild( option ); } //some examples addDemo("Features", "examples/features.json"); addDemo("Benchmark", "examples/benchmark.json"); addDemo("Subgraph", "examples/subgraph.json"); addDemo("Audio", "examples/audio.json"); addDemo("Audio Delay", "examples/audio_delay.json"); addDemo("Audio Reverb", "examples/audio_reverb.json"); addDemo("MIDI Generation", "examples/midi_generation.json"); addDemo("Copy Paste", "examples/copypaste.json"); addDemo("autobackup", function(){ var data = localStorage.getItem("litegraphg demo backup"); if(!data) return; var graph_data = JSON.parse(data); graph.configure( graph_data ); }); //allows to use the WebGL nodes like textures function enableWebGL() { if( webgl_canvas ) { webgl_canvas.style.display = (webgl_canvas.style.display == "none" ? "block" : "none"); return; } var libs = [ "js/libs/gl-matrix-min.js", "js/libs/litegl.js", "../src/nodes/gltextures.js", "../src/nodes/glfx.js", "../src/nodes/glshaders.js", "../src/nodes/geometry.js" ]; function fetchJS() { if(libs.length == 0) return on_ready(); var script = null; script = document.createElement("script"); script.onload = fetchJS; script.src = libs.shift(); document.head.appendChild(script); } fetchJS(); function on_ready() { console.log(this.src); if(!window.GL) return; webgl_canvas = document.createElement("canvas"); webgl_canvas.width = 400; webgl_canvas.height = 300; webgl_canvas.style.position = "absolute"; webgl_canvas.style.top = "0px"; webgl_canvas.style.right = "0px"; webgl_canvas.style.border = "1px solid #AAA"; webgl_canvas.addEventListener("click", function(){ var rect = webgl_canvas.parentNode.getBoundingClientRect(); if( webgl_canvas.width != rect.width ) { webgl_canvas.width = rect.width; webgl_canvas.height = rect.height; } else { webgl_canvas.width = 400; webgl_canvas.height = 300; } }); var parent = document.querySelector(".editor-area"); parent.appendChild( webgl_canvas ); var gl = GL.create({ canvas: webgl_canvas }); if(!gl) return; editor.graph.onBeforeStep = ondraw; console.log("webgl ready"); function ondraw () { gl.clearColor(0,0,0,0); gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT ); gl.viewport(0,0,gl.canvas.width, gl.canvas.height ); } } } // Tests // CopyPasteWithConnectionToUnselectedOutputTest(); // demo(); ================================================ FILE: editor/js/defaults.js ================================================ LiteGraph.debug = false; LiteGraph.catch_exceptions = true; LiteGraph.throw_errors = true; LiteGraph.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 LiteGraph.searchbox_extras = {}; //used to add extra features to the search box LiteGraph.auto_sort_node_types = true; // [true!] If set to true; will automatically sort node types / categories in the context menus LiteGraph.node_box_coloured_when_on = true; // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action); visual feedback LiteGraph.node_box_coloured_by_mode = true; // [true!] nodebox based on node mode; visual feedback LiteGraph.dialog_close_on_mouse_leave = true; // [false on mobile] better true if not touch device; LiteGraph.dialog_close_on_mouse_leave_delay = 500; LiteGraph.shift_click_do_break_link_from = false; // [false!] prefer false if results too easy to break links LiteGraph.click_do_break_link_to = false; // [false!]prefer false; way too easy to break links LiteGraph.search_hide_on_mouse_leave = true; // [false on mobile] better true if not touch device; LiteGraph.search_filter_enabled = true; // [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] LiteGraph.search_show_all_on_open = true; // [true!] opens the results list when opening the search widget LiteGraph.auto_load_slot_types = true; // [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 LiteGraph.registered_slot_in_types = {}; // slot types for nodeclass LiteGraph.registered_slot_out_types = {}; // slot types for nodeclass LiteGraph.slot_types_in = []; // slot types IN LiteGraph.slot_types_out = []; // slot types OUT*/ LiteGraph.alt_drag_do_clone_nodes = true; // [true!] very handy; ALT click to clone and drag the new node LiteGraph.do_add_triggers_slots = true; // [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 LiteGraph.allow_multi_output_for_events = false; // [false!] being events; it is strongly reccomended to use them sequentially; one by one LiteGraph.middle_click_slot_add_default_node = true; //[true!] allows to create and connect a ndoe clicking with the third button (wheel) LiteGraph.release_link_on_empty_shows_menu = true; //[true!] dragging a link to empty space will open a menu, add from list, search or defaults LiteGraph.pointerevents_method = "mouse"; // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs = true; //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes ================================================ FILE: editor/js/defaults_mobile.js ================================================ LiteGraph.debug = false; LiteGraph.catch_exceptions = true; LiteGraph.throw_errors = true; LiteGraph.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 LiteGraph.searchbox_extras = {}; //used to add extra features to the search box LiteGraph.auto_sort_node_types = true; // [true!] If set to true; will automatically sort node types / categories in the context menus LiteGraph.node_box_coloured_when_on = true; // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action); visual feedback LiteGraph.node_box_coloured_by_mode = true; // [true!] nodebox based on node mode; visual feedback LiteGraph.dialog_close_on_mouse_leave = false; // [false on mobile] better true if not touch device; LiteGraph.dialog_close_on_mouse_leave_delay = 500; LiteGraph.shift_click_do_break_link_from = false; // [false!] prefer false if results too easy to break links LiteGraph.click_do_break_link_to = false; // [false!]prefer false; way too easy to break links LiteGraph.search_hide_on_mouse_leave = false; // [false on mobile] better true if not touch device; LiteGraph.search_filter_enabled = true; // [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] LiteGraph.search_show_all_on_open = true; // [true!] opens the results list when opening the search widget LiteGraph.auto_load_slot_types = true; // [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 LiteGraph.registered_slot_in_types = {}; // slot types for nodeclass LiteGraph.registered_slot_out_types = {}; // slot types for nodeclass LiteGraph.slot_types_in = []; // slot types IN LiteGraph.slot_types_out = []; // slot types OUT*/ LiteGraph.alt_drag_do_clone_nodes = true; // [true!] very handy; ALT click to clone and drag the new node LiteGraph.do_add_triggers_slots = true; // [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 LiteGraph.allow_multi_output_for_events = false; // [false!] being events; it is strongly reccomended to use them sequentially; one by one LiteGraph.middle_click_slot_add_default_node = true; //[true!] allows to create and connect a ndoe clicking with the third button (wheel) LiteGraph.release_link_on_empty_shows_menu = true; //[true!] dragging a link to empty space will open a menu, add from list, search or defaults LiteGraph.pointerevents_method = "pointer"; // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs = true; //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes ================================================ FILE: editor/js/demos.js ================================================ function demo() { multiConnection(); } function multiConnection() { var node_button = LiteGraph.createNode("widget/button"); node_button.pos = [100,400]; graph.add(node_button); var node_console = LiteGraph.createNode("basic/console"); node_console.pos = [400,400]; graph.add(node_console); node_button.connect(0, node_console ); var node_const_A = LiteGraph.createNode("basic/const"); node_const_A.pos = [200,200]; graph.add(node_const_A); node_const_A.setValue(4.5); var node_const_B = LiteGraph.createNode("basic/const"); node_const_B.pos = [200,300]; graph.add(node_const_B); node_const_B.setValue(10); var node_math = LiteGraph.createNode("math/operation"); node_math.pos = [400,200]; graph.add(node_math); var node_watch = LiteGraph.createNode("basic/watch"); node_watch.pos = [700,200]; graph.add(node_watch); var node_watch2 = LiteGraph.createNode("basic/watch"); node_watch2.pos = [700,300]; graph.add(node_watch2); node_const_A.connect(0,node_math,0 ); node_const_B.connect(0,node_math,1 ); node_math.connect(0,node_watch,0 ); node_math.connect(0,node_watch2,0 ); } function CopyPasteWithConnectionToUnselectedOutputTest() { // number var nodeConstA = LiteGraph.createNode("basic/const"); nodeConstA.pos = [200,200]; graph.add(nodeConstA); nodeConstA.setValue(4.5); // number var nodeConstB = LiteGraph.createNode("basic/const"); nodeConstB.pos = [200,300]; graph.add(nodeConstB); nodeConstB.setValue(10); // math var nodeMath = LiteGraph.createNode("math/operation"); nodeMath.pos = [400,200]; graph.add(nodeMath); // connection nodeConstA.connect(0,nodeMath,0 ); nodeConstB.connect(0,nodeMath,1 ); // copy with unselected nodes connected graphcanvas.selectNodes([nodeMath]); graphcanvas.copyToClipboard(); graphcanvas.pasteFromClipboard(true); var count = 1; var lastNode = null; for (const [key, element] of Object.entries(graphcanvas.selected_nodes)) { element.pos = [nodeMath.pos[0], nodeMath.pos[1] + 100 * count]; lastNode = element count++; } // copy with unselected nodes unconnected graphcanvas.pasteFromClipboard(false); var count = 1; for (const [key, element] of Object.entries(graphcanvas.selected_nodes)) { element.pos = [nodeMath.pos[0], lastNode.pos[1] + 100 * count]; count++; } } function sortTest() { var rand = LiteGraph.createNode("math/rand",null, {pos: [10,100] }); graph.add(rand); var nodes = []; for(var i = 4; i >= 1; i--) { var n = LiteGraph.createNode("basic/watch",null, {pos: [i * 120,100] }); graph.add(n); nodes[i-1] = n; } rand.connect(0, nodes[0], 0); for(var i = 0; i < nodes.length - 1; i++) nodes[i].connect(0,nodes[i+1], 0); } function benchmark() { var num_nodes = 200; var nodes = []; for(var i = 0; i < num_nodes; i++) { var n = LiteGraph.createNode("basic/watch",null, {pos: [(2000 * Math.random())|0, (2000 * Math.random())|0] }); graph.add(n); nodes.push(n); } for(var i = 0; i < nodes.length; i++) nodes[ (Math.random() * nodes.length)|0 ].connect(0, nodes[ (Math.random() * nodes.length)|0 ], 0 ); } //Show value inside the debug console function TestWidgetsNode() { this.addOutput("","number"); this.properties = {}; var that = this; this.slider = this.addWidget("slider","Slider", 0.5, function(v){}, { min: 0, max: 1} ); this.number = this.addWidget("number","Number", 0.5, function(v){}, { min: 0, max: 100} ); this.combo = this.addWidget("combo","Combo", "red", function(v){}, { values:["red","green","blue"]} ); this.text = this.addWidget("text","Text", "edit me", function(v){}, {} ); this.text2 = this.addWidget("text","Text", "multiline", function(v){}, { multiline:true } ); this.toggle = this.addWidget("toggle","Toggle", true, function(v){}, { on: "enabled", off:"disabled"} ); this.button = this.addWidget("button","Button", null, function(v){}, {} ); this.toggle2 = this.addWidget("toggle","Disabled", true, function(v){}, { on: "enabled", off:"disabled"} ); this.toggle2.disabled = true; this.size = this.computeSize(); this.serialize_widgets = true; } TestWidgetsNode.title = "Widgets"; LiteGraph.registerNodeType("features/widgets", TestWidgetsNode ); //Show value inside the debug console function TestSpecialNode() { this.addInput("","number"); this.addOutput("","number"); this.properties = {}; var that = this; this.size = this.computeSize(); this.enabled = false; this.visible = false; } TestSpecialNode.title = "Custom Shapes"; TestSpecialNode.title_mode = LiteGraph.TRANSPARENT_TITLE; TestSpecialNode.slot_start_y = 20; TestSpecialNode.prototype.onDrawBackground = function(ctx) { if(this.flags.collapsed) return; ctx.fillStyle = "#555"; ctx.fillRect(0,0,this.size[0],20); if(this.enabled) { ctx.fillStyle = "#AFB"; ctx.beginPath(); ctx.moveTo(this.size[0]-20,0); ctx.lineTo(this.size[0]-25,20); ctx.lineTo(this.size[0],20); ctx.lineTo(this.size[0],0); ctx.fill(); } if(this.visible) { ctx.fillStyle = "#ABF"; ctx.beginPath(); ctx.moveTo(this.size[0]-40,0); ctx.lineTo(this.size[0]-45,20); ctx.lineTo(this.size[0]-25,20); ctx.lineTo(this.size[0]-20,0); ctx.fill(); } ctx.strokeStyle = "#333"; ctx.beginPath(); ctx.moveTo(0,20); ctx.lineTo(this.size[0]+1,20); ctx.moveTo(this.size[0]-20,0); ctx.lineTo(this.size[0]-25,20); ctx.moveTo(this.size[0]-40,0); ctx.lineTo(this.size[0]-45,20); ctx.stroke(); if( this.mouseOver ) { ctx.fillStyle = "#AAA"; ctx.fillText( "Example of helper", 0, this.size[1] + 14 ); } } TestSpecialNode.prototype.onMouseDown = function(e, pos) { if(pos[1] > 20) return; if( pos[0] > this.size[0] - 20) this.enabled = !this.enabled; else if( pos[0] > this.size[0] - 40) this.visible = !this.visible; } TestSpecialNode.prototype.onBounding = function(rect) { if(!this.flags.collapsed && this.mouseOver ) rect[3] = this.size[1] + 20; } LiteGraph.registerNodeType("features/shape", TestSpecialNode ); //Show value inside the debug console function TestSlotsNode() { this.addInput("C","number"); this.addOutput("A","number"); this.addOutput("B","number"); this.horizontal = true; this.size = [100,40]; } TestSlotsNode.title = "Flat Slots"; LiteGraph.registerNodeType("features/slots", TestSlotsNode ); //Show value inside the debug console function TestPropertyEditorsNode() { this.properties = { name: "foo", age: 10, alive: true, children: ["John","Emily","Charles"], skills: { speed: 10, dexterity: 100 } } var that = this; this.addWidget("button","Log",null,function(){ console.log(that.properties); }); } TestPropertyEditorsNode.title = "Properties"; LiteGraph.registerNodeType("features/properties_editor", TestPropertyEditorsNode ); //Show value inside the debug console function LargeInputNode() { this.addInput("in 1","number"); this.addInput("in 2","number"); this.addInput("in 3","number"); this.addInput("in 4","number"); this.addInput("in 5","number"); this.addInput("in 6","number"); this.addInput("in 7","number"); this.addInput("in 8","number"); this.addInput("in 9","number"); this.addInput("in 10","number"); this.addInput("in 11","number"); this.addInput("in 12","number"); this.addInput("in 13","number"); this.addInput("in 14","number"); this.addInput("in 15","number"); this.addInput("in 16","number"); this.addInput("in 17","number"); this.addInput("in 18","number"); this.addInput("in 19","number"); this.addInput("in 20","number"); this.size = [200,410]; } LargeInputNode.title = "Large Input Node"; LiteGraph.registerNodeType("features/largeinput_editor", LargeInputNode); ================================================ FILE: editor/js/libs/audiosynth.js ================================================ var Synth, AudioSynth, AudioSynthInstrument; !function(){ var URL = window.URL || window.webkitURL; var Blob = window.Blob; if(!URL || !Blob) { throw new Error('This browser does not support AudioSynth'); } var _encapsulated = false; var AudioSynthInstance = null; var pack = function(c,arg){ return [new Uint8Array([arg, arg >> 8]), new Uint8Array([arg, arg >> 8, arg >> 16, arg >> 24])][c]; }; var setPrivateVar = function(n,v,w,e){Object.defineProperty(this,n,{value:v,writable:!!w,enumerable:!!e});}; var setPublicVar = function(n,v,w){setPrivateVar.call(this,n,v,w,true);}; AudioSynthInstrument = function AudioSynthInstrument(){this.__init__.apply(this,arguments);}; var setPriv = setPrivateVar.bind(AudioSynthInstrument.prototype); var setPub = setPublicVar.bind(AudioSynthInstrument.prototype); setPriv('__init__', function(a,b,c) { if(!_encapsulated) { throw new Error('AudioSynthInstrument can only be instantiated from the createInstrument method of the AudioSynth object.'); } setPrivateVar.call(this, '_parent', a); setPublicVar.call(this, 'name', b); setPrivateVar.call(this, '_soundID', c); }); setPub('play', function(note, octave, duration,volume) { return this._parent.play(this._soundID, note, octave, duration, volume); }); setPub('generate', function(note, octave, duration) { return this._parent.generate(this._soundID, note, octave, duration); }); AudioSynth = function AudioSynth(){if(AudioSynthInstance instanceof AudioSynth){return AudioSynthInstance;}else{ this.__init__(); return this; }}; setPriv = setPrivateVar.bind(AudioSynth.prototype); setPub = setPublicVar.bind(AudioSynth.prototype); setPriv('_debug',false,true); setPriv('_bitsPerSample',16); setPriv('_channels',1); setPriv('_sampleRate',44100,true); setPub('setSampleRate', function(v) { this._sampleRate = Math.max(Math.min(v|0,44100), 4000); this._clearCache(); return this._sampleRate; }); setPub('getSampleRate', function() { return this._sampleRate; }); setPriv('_volume',32768,true); setPub('setVolume', function(v) { v = parseFloat(v); if(isNaN(v)) { v = 0; } v = Math.round(v*32768); this._volume = Math.max(Math.min(v|0,32768), 0); this._clearCache(); return this._volume; }); setPub('getVolume', function() { return Math.round(this._volume/32768*10000)/10000; }); setPriv('_notes',{'C':261.63,'C#':277.18,'D':293.66,'D#':311.13,'E':329.63,'F':346.23,'F#':369.99,'G':392.00,'G#':415.30,'A':440.00,'A#':466.16,'B':493.88}); setPriv('_fileCache',[],true); setPriv('_temp',{},true); setPriv('_sounds',[],true); setPriv('_mod',[function(i,s,f,x){return Math.sin((2 * Math.PI)*(i/s)*f+x);}]); setPriv('_resizeCache', function() { var f = this._fileCache; var l = this._sounds.length; while(f.length> 8; } for (; i !== decayLen; i++) { val = volume * Math.pow((1-((i-(sampleRate*attack))/(sampleRate*(time-attack)))),dampen) * waveFunc.call(waveBind, i, sampleRate, frequency, volume); data[i << 1] = val; data[(i << 1) + 1] = val >> 8; } var out = [ 'RIFF', pack(1, 4 + (8 + 24/* chunk 1 length */) + (8 + 8/* chunk 2 length */)), // Length 'WAVE', // chunk 1 'fmt ', // Sub-chunk identifier pack(1, 16), // Chunk length pack(0, 1), // Audio format (1 is linear quantization) pack(0, channels), pack(1, sampleRate), pack(1, sampleRate * channels * bitsPerSample / 8), // Byte rate pack(0, channels * bitsPerSample / 8), pack(0, bitsPerSample), // chunk 2 'data', // Sub-chunk identifier pack(1, data.length * channels * bitsPerSample / 8), // Chunk length data ]; var blob = new Blob(out, {type: 'audio/wav'}); var dataURI = URL.createObjectURL(blob); this._fileCache[sound][octave-1][note][time] = dataURI; if(this._debug) { console.log((new Date).valueOf() - t, 'ms to generate'); } return dataURI; } }); setPub('play', function(sound, note, octave, duration, volume) { var src = this.generate( sound, note, octave, duration ); var audio = new Audio(src); if(volume != null) { if(volume <= 0) return true; audio.volume = volume > 1 ? 1 : volume; } audio.play(); return true; }); setPub('debug', function() { this._debug = true; }); setPub('createInstrument', function(sound) { var n = 0; var found = false; if(typeof(sound)=='string') { for(var i=0;i=(valueTable.length-1)?0:playVal+1] + valueTable[playVal]) * 0.5; if(playVal>=Math.floor(period)) { if(playVal=p_hundredth) { // Reset resetPlay = true; valueTable[playVal+1] = (valueTable[0] + valueTable[playVal+1]) * 0.5; vars.periodCount++; } } else { resetPlay = true; } } var _return = valueTable[playVal]; if(resetPlay) { vars.playVal = 0; } else { vars.playVal++; } return _return; } } }, { name: 'edm', attack: function() { return 0.002; }, dampen: function() { return 1; }, wave: function(i, sampleRate, frequency) { var base = this.modulate[0]; var mod = this.modulate.slice(1); return mod[0]( i, sampleRate, frequency, mod[9]( i, sampleRate, frequency, mod[2]( i, sampleRate, frequency, Math.pow(base(i, sampleRate, frequency, 0), 3) + Math.pow(base(i, sampleRate, frequency, 0.5), 5) + Math.pow(base(i, sampleRate, frequency, 1), 7) ) ) + mod[8]( i, sampleRate, frequency, base(i, sampleRate, frequency, 1.75) ) ); } }); ================================================ FILE: editor/js/libs/gl-matrix-min.js ================================================ /*! @fileoverview gl-matrix - High performance matrix and vector operations @author Brandon Jones @author Colin MacKenzie IV @version 2.7.0 Copyright (c) 2015-2018, Brandon Jones, Colin MacKenzie IV. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ !function(t,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var r=n();for(var a in r)("object"==typeof exports?exports:t)[a]=r[a]}}("undefined"!=typeof self?self:this,function(){return function(t){var n={};function r(a){if(n[a])return n[a].exports;var e=n[a]={i:a,l:!1,exports:{}};return t[a].call(e.exports,e,e.exports,r),e.l=!0,e.exports}return r.m=t,r.c=n,r.d=function(t,n,a){r.o(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:a})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,n){if(1&n&&(t=r(t)),8&n)return t;if(4&n&&"object"==typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(r.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:t}),2&n&&"string"!=typeof t)for(var e in t)r.d(a,e,function(n){return t[n]}.bind(null,e));return a},r.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(n,"a",n),n},r.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},r.p="",r(r.s=10)}([function(t,n,r){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.setMatrixArrayType=function(t){n.ARRAY_TYPE=t},n.toRadian=function(t){return t*e},n.equals=function(t,n){return Math.abs(t-n)<=a*Math.max(1,Math.abs(t),Math.abs(n))};var a=n.EPSILON=1e-6;n.ARRAY_TYPE="undefined"!=typeof Float32Array?Float32Array:Array,n.RANDOM=Math.random;var e=Math.PI/180},function(t,n,r){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.forEach=n.sqrLen=n.len=n.sqrDist=n.dist=n.div=n.mul=n.sub=void 0,n.create=e,n.clone=function(t){var n=new a.ARRAY_TYPE(4);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n},n.fromValues=function(t,n,r,e){var u=new a.ARRAY_TYPE(4);return u[0]=t,u[1]=n,u[2]=r,u[3]=e,u},n.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t},n.set=function(t,n,r,a,e){return t[0]=n,t[1]=r,t[2]=a,t[3]=e,t},n.add=function(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t},n.subtract=u,n.multiply=o,n.divide=i,n.ceil=function(t,n){return t[0]=Math.ceil(n[0]),t[1]=Math.ceil(n[1]),t[2]=Math.ceil(n[2]),t[3]=Math.ceil(n[3]),t},n.floor=function(t,n){return t[0]=Math.floor(n[0]),t[1]=Math.floor(n[1]),t[2]=Math.floor(n[2]),t[3]=Math.floor(n[3]),t},n.min=function(t,n,r){return t[0]=Math.min(n[0],r[0]),t[1]=Math.min(n[1],r[1]),t[2]=Math.min(n[2],r[2]),t[3]=Math.min(n[3],r[3]),t},n.max=function(t,n,r){return t[0]=Math.max(n[0],r[0]),t[1]=Math.max(n[1],r[1]),t[2]=Math.max(n[2],r[2]),t[3]=Math.max(n[3],r[3]),t},n.round=function(t,n){return t[0]=Math.round(n[0]),t[1]=Math.round(n[1]),t[2]=Math.round(n[2]),t[3]=Math.round(n[3]),t},n.scale=function(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t},n.scaleAndAdd=function(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t},n.distance=s,n.squaredDistance=c,n.length=f,n.squaredLength=M,n.negate=function(t,n){return t[0]=-n[0],t[1]=-n[1],t[2]=-n[2],t[3]=-n[3],t},n.inverse=function(t,n){return t[0]=1/n[0],t[1]=1/n[1],t[2]=1/n[2],t[3]=1/n[3],t},n.normalize=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r*r+a*a+e*e+u*u;o>0&&(o=1/Math.sqrt(o),t[0]=r*o,t[1]=a*o,t[2]=e*o,t[3]=u*o);return t},n.dot=function(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]+t[3]*n[3]},n.lerp=function(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t[2]=o+a*(r[2]-o),t[3]=i+a*(r[3]-i),t},n.random=function(t,n){var r,e,u,o,i,s;n=n||1;do{r=2*a.RANDOM()-1,e=2*a.RANDOM()-1,i=r*r+e*e}while(i>=1);do{u=2*a.RANDOM()-1,o=2*a.RANDOM()-1,s=u*u+o*o}while(s>=1);var c=Math.sqrt((1-i)/s);return t[0]=n*r,t[1]=n*e,t[2]=n*u*c,t[3]=n*o*c,t},n.transformMat4=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3];return t[0]=r[0]*a+r[4]*e+r[8]*u+r[12]*o,t[1]=r[1]*a+r[5]*e+r[9]*u+r[13]*o,t[2]=r[2]*a+r[6]*e+r[10]*u+r[14]*o,t[3]=r[3]*a+r[7]*e+r[11]*u+r[15]*o,t},n.transformQuat=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],s=r[2],c=r[3],f=c*a+i*u-s*e,M=c*e+s*a-o*u,h=c*u+o*e-i*a,l=-o*a-i*e-s*u;return t[0]=f*c+l*-o+M*-s-h*-i,t[1]=M*c+l*-i+h*-o-f*-s,t[2]=h*c+l*-s+f*-i-M*-o,t[3]=n[3],t},n.str=function(t){return"vec4("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"},n.exactEquals=function(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]},n.equals=function(t,n){var r=t[0],e=t[1],u=t[2],o=t[3],i=n[0],s=n[1],c=n[2],f=n[3];return Math.abs(r-i)<=a.EPSILON*Math.max(1,Math.abs(r),Math.abs(i))&&Math.abs(e-s)<=a.EPSILON*Math.max(1,Math.abs(e),Math.abs(s))&&Math.abs(u-c)<=a.EPSILON*Math.max(1,Math.abs(u),Math.abs(c))&&Math.abs(o-f)<=a.EPSILON*Math.max(1,Math.abs(o),Math.abs(f))};var a=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(r(0));function e(){var t=new a.ARRAY_TYPE(4);return a.ARRAY_TYPE!=Float32Array&&(t[0]=0,t[1]=0,t[2]=0,t[3]=0),t}function u(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t}function o(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t[2]=n[2]*r[2],t[3]=n[3]*r[3],t}function i(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t[2]=n[2]/r[2],t[3]=n[3]/r[3],t}function s(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2],u=n[3]-t[3];return Math.sqrt(r*r+a*a+e*e+u*u)}function c(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2],u=n[3]-t[3];return r*r+a*a+e*e+u*u}function f(t){var n=t[0],r=t[1],a=t[2],e=t[3];return Math.sqrt(n*n+r*r+a*a+e*e)}function M(t){var n=t[0],r=t[1],a=t[2],e=t[3];return n*n+r*r+a*a+e*e}n.sub=u,n.mul=o,n.div=i,n.dist=s,n.sqrDist=c,n.len=f,n.sqrLen=M,n.forEach=function(){var t=e();return function(n,r,a,e,u,o){var i=void 0,s=void 0;for(r||(r=4),a||(a=0),s=e?Math.min(e*r+a,n.length):n.length,i=a;i1?0:e<-1?Math.PI:Math.acos(e)},n.str=function(t){return"vec3("+t[0]+", "+t[1]+", "+t[2]+")"},n.exactEquals=function(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]},n.equals=function(t,n){var r=t[0],e=t[1],u=t[2],o=n[0],i=n[1],s=n[2];return Math.abs(r-o)<=a.EPSILON*Math.max(1,Math.abs(r),Math.abs(o))&&Math.abs(e-i)<=a.EPSILON*Math.max(1,Math.abs(e),Math.abs(i))&&Math.abs(u-s)<=a.EPSILON*Math.max(1,Math.abs(u),Math.abs(s))};var a=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(r(0));function e(){var t=new a.ARRAY_TYPE(3);return a.ARRAY_TYPE!=Float32Array&&(t[0]=0,t[1]=0,t[2]=0),t}function u(t){var n=t[0],r=t[1],a=t[2];return Math.sqrt(n*n+r*r+a*a)}function o(t,n,r){var e=new a.ARRAY_TYPE(3);return e[0]=t,e[1]=n,e[2]=r,e}function i(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t}function s(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t[2]=n[2]*r[2],t}function c(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t[2]=n[2]/r[2],t}function f(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2];return Math.sqrt(r*r+a*a+e*e)}function M(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2];return r*r+a*a+e*e}function h(t){var n=t[0],r=t[1],a=t[2];return n*n+r*r+a*a}function l(t,n){var r=n[0],a=n[1],e=n[2],u=r*r+a*a+e*e;return u>0&&(u=1/Math.sqrt(u),t[0]=n[0]*u,t[1]=n[1]*u,t[2]=n[2]*u),t}function v(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}n.sub=i,n.mul=s,n.div=c,n.dist=f,n.sqrDist=M,n.len=u,n.sqrLen=h,n.forEach=function(){var t=e();return function(n,r,a,e,u,o){var i=void 0,s=void 0;for(r||(r=3),a||(a=0),s=e?Math.min(e*r+a,n.length):n.length,i=a;ia.EPSILON?(t[0]=n[0]/e,t[1]=n[1]/e,t[2]=n[2]/e):(t[0]=1,t[1]=0,t[2]=0);return r},n.multiply=f,n.rotateX=function(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s+o*i,t[1]=e*s+u*i,t[2]=u*s-e*i,t[3]=o*s-a*i,t},n.rotateY=function(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s-u*i,t[1]=e*s+o*i,t[2]=u*s+a*i,t[3]=o*s-e*i,t},n.rotateZ=function(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s+e*i,t[1]=e*s-a*i,t[2]=u*s+o*i,t[3]=o*s-u*i,t},n.calculateW=function(t,n){var r=n[0],a=n[1],e=n[2];return t[0]=r,t[1]=a,t[2]=e,t[3]=Math.sqrt(Math.abs(1-r*r-a*a-e*e)),t},n.slerp=M,n.random=function(t){var n=a.RANDOM(),r=a.RANDOM(),e=a.RANDOM(),u=Math.sqrt(1-n),o=Math.sqrt(n);return t[0]=u*Math.sin(2*Math.PI*r),t[1]=u*Math.cos(2*Math.PI*r),t[2]=o*Math.sin(2*Math.PI*e),t[3]=o*Math.cos(2*Math.PI*e),t},n.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r*r+a*a+e*e+u*u,i=o?1/o:0;return t[0]=-r*i,t[1]=-a*i,t[2]=-e*i,t[3]=u*i,t},n.conjugate=function(t,n){return t[0]=-n[0],t[1]=-n[1],t[2]=-n[2],t[3]=n[3],t},n.fromMat3=h,n.fromEuler=function(t,n,r,a){var e=.5*Math.PI/180;n*=e,r*=e,a*=e;var u=Math.sin(n),o=Math.cos(n),i=Math.sin(r),s=Math.cos(r),c=Math.sin(a),f=Math.cos(a);return t[0]=u*s*f-o*i*c,t[1]=o*i*f+u*s*c,t[2]=o*s*c-u*i*f,t[3]=o*s*f+u*i*c,t},n.str=function(t){return"quat("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"};var a=i(r(0)),e=i(r(5)),u=i(r(2)),o=i(r(1));function i(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}function s(){var t=new a.ARRAY_TYPE(4);return a.ARRAY_TYPE!=Float32Array&&(t[0]=0,t[1]=0,t[2]=0),t[3]=1,t}function c(t,n,r){r*=.5;var a=Math.sin(r);return t[0]=a*n[0],t[1]=a*n[1],t[2]=a*n[2],t[3]=Math.cos(r),t}function f(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=r[0],s=r[1],c=r[2],f=r[3];return t[0]=a*f+o*i+e*c-u*s,t[1]=e*f+o*s+u*i-a*c,t[2]=u*f+o*c+a*s-e*i,t[3]=o*f-a*i-e*s-u*c,t}function M(t,n,r,e){var u=n[0],o=n[1],i=n[2],s=n[3],c=r[0],f=r[1],M=r[2],h=r[3],l=void 0,v=void 0,d=void 0,b=void 0,m=void 0;return(v=u*c+o*f+i*M+s*h)<0&&(v=-v,c=-c,f=-f,M=-M,h=-h),1-v>a.EPSILON?(l=Math.acos(v),d=Math.sin(l),b=Math.sin((1-e)*l)/d,m=Math.sin(e*l)/d):(b=1-e,m=e),t[0]=b*u+m*c,t[1]=b*o+m*f,t[2]=b*i+m*M,t[3]=b*s+m*h,t}function h(t,n){var r=n[0]+n[4]+n[8],a=void 0;if(r>0)a=Math.sqrt(r+1),t[3]=.5*a,a=.5/a,t[0]=(n[5]-n[7])*a,t[1]=(n[6]-n[2])*a,t[2]=(n[1]-n[3])*a;else{var e=0;n[4]>n[0]&&(e=1),n[8]>n[3*e+e]&&(e=2);var u=(e+1)%3,o=(e+2)%3;a=Math.sqrt(n[3*e+e]-n[3*u+u]-n[3*o+o]+1),t[e]=.5*a,a=.5/a,t[3]=(n[3*u+o]-n[3*o+u])*a,t[u]=(n[3*u+e]+n[3*e+u])*a,t[o]=(n[3*o+e]+n[3*e+o])*a}return t}n.clone=o.clone,n.fromValues=o.fromValues,n.copy=o.copy,n.set=o.set,n.add=o.add,n.mul=f,n.scale=o.scale,n.dot=o.dot,n.lerp=o.lerp;var l=n.length=o.length,v=(n.len=l,n.squaredLength=o.squaredLength),d=(n.sqrLen=v,n.normalize=o.normalize);n.exactEquals=o.exactEquals,n.equals=o.equals,n.rotationTo=function(){var t=u.create(),n=u.fromValues(1,0,0),r=u.fromValues(0,1,0);return function(a,e,o){var i=u.dot(e,o);return i<-.999999?(u.cross(t,n,e),u.len(t)<1e-6&&u.cross(t,r,e),u.normalize(t,t),c(a,t,Math.PI),a):i>.999999?(a[0]=0,a[1]=0,a[2]=0,a[3]=1,a):(u.cross(t,e,o),a[0]=t[0],a[1]=t[1],a[2]=t[2],a[3]=1+i,d(a,a))}}(),n.sqlerp=function(){var t=s(),n=s();return function(r,a,e,u,o,i){return M(t,a,o,i),M(n,e,u,i),M(r,t,n,2*i*(1-i)),r}}(),n.setAxes=function(){var t=e.create();return function(n,r,a,e){return t[0]=a[0],t[3]=a[1],t[6]=a[2],t[1]=e[0],t[4]=e[1],t[7]=e[2],t[2]=-r[0],t[5]=-r[1],t[8]=-r[2],d(n,h(n,t))}}()},function(t,n,r){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.sub=n.mul=void 0,n.create=function(){var t=new a.ARRAY_TYPE(16);a.ARRAY_TYPE!=Float32Array&&(t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[11]=0,t[12]=0,t[13]=0,t[14]=0);return t[0]=1,t[5]=1,t[10]=1,t[15]=1,t},n.clone=function(t){var n=new a.ARRAY_TYPE(16);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n[4]=t[4],n[5]=t[5],n[6]=t[6],n[7]=t[7],n[8]=t[8],n[9]=t[9],n[10]=t[10],n[11]=t[11],n[12]=t[12],n[13]=t[13],n[14]=t[14],n[15]=t[15],n},n.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t[9]=n[9],t[10]=n[10],t[11]=n[11],t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15],t},n.fromValues=function(t,n,r,e,u,o,i,s,c,f,M,h,l,v,d,b){var m=new a.ARRAY_TYPE(16);return m[0]=t,m[1]=n,m[2]=r,m[3]=e,m[4]=u,m[5]=o,m[6]=i,m[7]=s,m[8]=c,m[9]=f,m[10]=M,m[11]=h,m[12]=l,m[13]=v,m[14]=d,m[15]=b,m},n.set=function(t,n,r,a,e,u,o,i,s,c,f,M,h,l,v,d,b){return t[0]=n,t[1]=r,t[2]=a,t[3]=e,t[4]=u,t[5]=o,t[6]=i,t[7]=s,t[8]=c,t[9]=f,t[10]=M,t[11]=h,t[12]=l,t[13]=v,t[14]=d,t[15]=b,t},n.identity=e,n.transpose=function(t,n){if(t===n){var r=n[1],a=n[2],e=n[3],u=n[6],o=n[7],i=n[11];t[1]=n[4],t[2]=n[8],t[3]=n[12],t[4]=r,t[6]=n[9],t[7]=n[13],t[8]=a,t[9]=u,t[11]=n[14],t[12]=e,t[13]=o,t[14]=i}else t[0]=n[0],t[1]=n[4],t[2]=n[8],t[3]=n[12],t[4]=n[1],t[5]=n[5],t[6]=n[9],t[7]=n[13],t[8]=n[2],t[9]=n[6],t[10]=n[10],t[11]=n[14],t[12]=n[3],t[13]=n[7],t[14]=n[11],t[15]=n[15];return t},n.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8],M=n[9],h=n[10],l=n[11],v=n[12],d=n[13],b=n[14],m=n[15],p=r*i-a*o,P=r*s-e*o,A=r*c-u*o,E=a*s-e*i,O=a*c-u*i,R=e*c-u*s,y=f*d-M*v,q=f*b-h*v,x=f*m-l*v,_=M*b-h*d,Y=M*m-l*d,L=h*m-l*b,S=p*L-P*Y+A*_+E*x-O*q+R*y;if(!S)return null;return S=1/S,t[0]=(i*L-s*Y+c*_)*S,t[1]=(e*Y-a*L-u*_)*S,t[2]=(d*R-b*O+m*E)*S,t[3]=(h*O-M*R-l*E)*S,t[4]=(s*x-o*L-c*q)*S,t[5]=(r*L-e*x+u*q)*S,t[6]=(b*A-v*R-m*P)*S,t[7]=(f*R-h*A+l*P)*S,t[8]=(o*Y-i*x+c*y)*S,t[9]=(a*x-r*Y-u*y)*S,t[10]=(v*O-d*A+m*p)*S,t[11]=(M*A-f*O-l*p)*S,t[12]=(i*q-o*_-s*y)*S,t[13]=(r*_-a*q+e*y)*S,t[14]=(d*P-v*E-b*p)*S,t[15]=(f*E-M*P+h*p)*S,t},n.adjoint=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8],M=n[9],h=n[10],l=n[11],v=n[12],d=n[13],b=n[14],m=n[15];return t[0]=i*(h*m-l*b)-M*(s*m-c*b)+d*(s*l-c*h),t[1]=-(a*(h*m-l*b)-M*(e*m-u*b)+d*(e*l-u*h)),t[2]=a*(s*m-c*b)-i*(e*m-u*b)+d*(e*c-u*s),t[3]=-(a*(s*l-c*h)-i*(e*l-u*h)+M*(e*c-u*s)),t[4]=-(o*(h*m-l*b)-f*(s*m-c*b)+v*(s*l-c*h)),t[5]=r*(h*m-l*b)-f*(e*m-u*b)+v*(e*l-u*h),t[6]=-(r*(s*m-c*b)-o*(e*m-u*b)+v*(e*c-u*s)),t[7]=r*(s*l-c*h)-o*(e*l-u*h)+f*(e*c-u*s),t[8]=o*(M*m-l*d)-f*(i*m-c*d)+v*(i*l-c*M),t[9]=-(r*(M*m-l*d)-f*(a*m-u*d)+v*(a*l-u*M)),t[10]=r*(i*m-c*d)-o*(a*m-u*d)+v*(a*c-u*i),t[11]=-(r*(i*l-c*M)-o*(a*l-u*M)+f*(a*c-u*i)),t[12]=-(o*(M*b-h*d)-f*(i*b-s*d)+v*(i*h-s*M)),t[13]=r*(M*b-h*d)-f*(a*b-e*d)+v*(a*h-e*M),t[14]=-(r*(i*b-s*d)-o*(a*b-e*d)+v*(a*s-e*i)),t[15]=r*(i*h-s*M)-o*(a*h-e*M)+f*(a*s-e*i),t},n.determinant=function(t){var n=t[0],r=t[1],a=t[2],e=t[3],u=t[4],o=t[5],i=t[6],s=t[7],c=t[8],f=t[9],M=t[10],h=t[11],l=t[12],v=t[13],d=t[14],b=t[15];return(n*o-r*u)*(M*b-h*d)-(n*i-a*u)*(f*b-h*v)+(n*s-e*u)*(f*d-M*v)+(r*i-a*o)*(c*b-h*l)-(r*s-e*o)*(c*d-M*l)+(a*s-e*i)*(c*v-f*l)},n.multiply=u,n.translate=function(t,n,r){var a=r[0],e=r[1],u=r[2],o=void 0,i=void 0,s=void 0,c=void 0,f=void 0,M=void 0,h=void 0,l=void 0,v=void 0,d=void 0,b=void 0,m=void 0;n===t?(t[12]=n[0]*a+n[4]*e+n[8]*u+n[12],t[13]=n[1]*a+n[5]*e+n[9]*u+n[13],t[14]=n[2]*a+n[6]*e+n[10]*u+n[14],t[15]=n[3]*a+n[7]*e+n[11]*u+n[15]):(o=n[0],i=n[1],s=n[2],c=n[3],f=n[4],M=n[5],h=n[6],l=n[7],v=n[8],d=n[9],b=n[10],m=n[11],t[0]=o,t[1]=i,t[2]=s,t[3]=c,t[4]=f,t[5]=M,t[6]=h,t[7]=l,t[8]=v,t[9]=d,t[10]=b,t[11]=m,t[12]=o*a+f*e+v*u+n[12],t[13]=i*a+M*e+d*u+n[13],t[14]=s*a+h*e+b*u+n[14],t[15]=c*a+l*e+m*u+n[15]);return t},n.scale=function(t,n,r){var a=r[0],e=r[1],u=r[2];return t[0]=n[0]*a,t[1]=n[1]*a,t[2]=n[2]*a,t[3]=n[3]*a,t[4]=n[4]*e,t[5]=n[5]*e,t[6]=n[6]*e,t[7]=n[7]*e,t[8]=n[8]*u,t[9]=n[9]*u,t[10]=n[10]*u,t[11]=n[11]*u,t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15],t},n.rotate=function(t,n,r,e){var u=e[0],o=e[1],i=e[2],s=Math.sqrt(u*u+o*o+i*i),c=void 0,f=void 0,M=void 0,h=void 0,l=void 0,v=void 0,d=void 0,b=void 0,m=void 0,p=void 0,P=void 0,A=void 0,E=void 0,O=void 0,R=void 0,y=void 0,q=void 0,x=void 0,_=void 0,Y=void 0,L=void 0,S=void 0,w=void 0,I=void 0;if(s0?(r[0]=2*(c*s+h*e+f*i-M*u)/l,r[1]=2*(f*s+h*u+M*e-c*i)/l,r[2]=2*(M*s+h*i+c*u-f*e)/l):(r[0]=2*(c*s+h*e+f*i-M*u),r[1]=2*(f*s+h*u+M*e-c*i),r[2]=2*(M*s+h*i+c*u-f*e));return o(t,n,r),t},n.getTranslation=function(t,n){return t[0]=n[12],t[1]=n[13],t[2]=n[14],t},n.getScaling=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[4],o=n[5],i=n[6],s=n[8],c=n[9],f=n[10];return t[0]=Math.sqrt(r*r+a*a+e*e),t[1]=Math.sqrt(u*u+o*o+i*i),t[2]=Math.sqrt(s*s+c*c+f*f),t},n.getRotation=function(t,n){var r=n[0]+n[5]+n[10],a=0;r>0?(a=2*Math.sqrt(r+1),t[3]=.25*a,t[0]=(n[6]-n[9])/a,t[1]=(n[8]-n[2])/a,t[2]=(n[1]-n[4])/a):n[0]>n[5]&&n[0]>n[10]?(a=2*Math.sqrt(1+n[0]-n[5]-n[10]),t[3]=(n[6]-n[9])/a,t[0]=.25*a,t[1]=(n[1]+n[4])/a,t[2]=(n[8]+n[2])/a):n[5]>n[10]?(a=2*Math.sqrt(1+n[5]-n[0]-n[10]),t[3]=(n[8]-n[2])/a,t[0]=(n[1]+n[4])/a,t[1]=.25*a,t[2]=(n[6]+n[9])/a):(a=2*Math.sqrt(1+n[10]-n[0]-n[5]),t[3]=(n[1]-n[4])/a,t[0]=(n[8]+n[2])/a,t[1]=(n[6]+n[9])/a,t[2]=.25*a);return t},n.fromRotationTranslationScale=function(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3],s=e+e,c=u+u,f=o+o,M=e*s,h=e*c,l=e*f,v=u*c,d=u*f,b=o*f,m=i*s,p=i*c,P=i*f,A=a[0],E=a[1],O=a[2];return t[0]=(1-(v+b))*A,t[1]=(h+P)*A,t[2]=(l-p)*A,t[3]=0,t[4]=(h-P)*E,t[5]=(1-(M+b))*E,t[6]=(d+m)*E,t[7]=0,t[8]=(l+p)*O,t[9]=(d-m)*O,t[10]=(1-(M+v))*O,t[11]=0,t[12]=r[0],t[13]=r[1],t[14]=r[2],t[15]=1,t},n.fromRotationTranslationScaleOrigin=function(t,n,r,a,e){var u=n[0],o=n[1],i=n[2],s=n[3],c=u+u,f=o+o,M=i+i,h=u*c,l=u*f,v=u*M,d=o*f,b=o*M,m=i*M,p=s*c,P=s*f,A=s*M,E=a[0],O=a[1],R=a[2],y=e[0],q=e[1],x=e[2],_=(1-(d+m))*E,Y=(l+A)*E,L=(v-P)*E,S=(l-A)*O,w=(1-(h+m))*O,I=(b+p)*O,N=(v+P)*R,g=(b-p)*R,T=(1-(h+d))*R;return t[0]=_,t[1]=Y,t[2]=L,t[3]=0,t[4]=S,t[5]=w,t[6]=I,t[7]=0,t[8]=N,t[9]=g,t[10]=T,t[11]=0,t[12]=r[0]+y-(_*y+S*q+N*x),t[13]=r[1]+q-(Y*y+w*q+g*x),t[14]=r[2]+x-(L*y+I*q+T*x),t[15]=1,t},n.fromQuat=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r+r,i=a+a,s=e+e,c=r*o,f=a*o,M=a*i,h=e*o,l=e*i,v=e*s,d=u*o,b=u*i,m=u*s;return t[0]=1-M-v,t[1]=f+m,t[2]=h-b,t[3]=0,t[4]=f-m,t[5]=1-c-v,t[6]=l+d,t[7]=0,t[8]=h+b,t[9]=l-d,t[10]=1-c-M,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},n.frustum=function(t,n,r,a,e,u,o){var i=1/(r-n),s=1/(e-a),c=1/(u-o);return t[0]=2*u*i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=2*u*s,t[6]=0,t[7]=0,t[8]=(r+n)*i,t[9]=(e+a)*s,t[10]=(o+u)*c,t[11]=-1,t[12]=0,t[13]=0,t[14]=o*u*2*c,t[15]=0,t},n.perspective=function(t,n,r,a,e){var u=1/Math.tan(n/2),o=void 0;t[0]=u/r,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=u,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[11]=-1,t[12]=0,t[13]=0,t[15]=0,null!=e&&e!==1/0?(o=1/(a-e),t[10]=(e+a)*o,t[14]=2*e*a*o):(t[10]=-1,t[14]=-2*a);return t},n.perspectiveFromFieldOfView=function(t,n,r,a){var e=Math.tan(n.upDegrees*Math.PI/180),u=Math.tan(n.downDegrees*Math.PI/180),o=Math.tan(n.leftDegrees*Math.PI/180),i=Math.tan(n.rightDegrees*Math.PI/180),s=2/(o+i),c=2/(e+u);return t[0]=s,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=c,t[6]=0,t[7]=0,t[8]=-(o-i)*s*.5,t[9]=(e-u)*c*.5,t[10]=a/(r-a),t[11]=-1,t[12]=0,t[13]=0,t[14]=a*r/(r-a),t[15]=0,t},n.ortho=function(t,n,r,a,e,u,o){var i=1/(n-r),s=1/(a-e),c=1/(u-o);return t[0]=-2*i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=-2*s,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=2*c,t[11]=0,t[12]=(n+r)*i,t[13]=(e+a)*s,t[14]=(o+u)*c,t[15]=1,t},n.lookAt=function(t,n,r,u){var o=void 0,i=void 0,s=void 0,c=void 0,f=void 0,M=void 0,h=void 0,l=void 0,v=void 0,d=void 0,b=n[0],m=n[1],p=n[2],P=u[0],A=u[1],E=u[2],O=r[0],R=r[1],y=r[2];if(Math.abs(b-O)0&&(l=1/Math.sqrt(l),f*=l,M*=l,h*=l);var v=s*h-c*M,d=c*f-i*h,b=i*M-s*f;(l=v*v+d*d+b*b)>0&&(l=1/Math.sqrt(l),v*=l,d*=l,b*=l);return t[0]=v,t[1]=d,t[2]=b,t[3]=0,t[4]=M*b-h*d,t[5]=h*v-f*b,t[6]=f*d-M*v,t[7]=0,t[8]=f,t[9]=M,t[10]=h,t[11]=0,t[12]=e,t[13]=u,t[14]=o,t[15]=1,t},n.str=function(t){return"mat4("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+", "+t[6]+", "+t[7]+", "+t[8]+", "+t[9]+", "+t[10]+", "+t[11]+", "+t[12]+", "+t[13]+", "+t[14]+", "+t[15]+")"},n.frob=function(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+Math.pow(t[6],2)+Math.pow(t[7],2)+Math.pow(t[8],2)+Math.pow(t[9],2)+Math.pow(t[10],2)+Math.pow(t[11],2)+Math.pow(t[12],2)+Math.pow(t[13],2)+Math.pow(t[14],2)+Math.pow(t[15],2))},n.add=function(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t[4]=n[4]+r[4],t[5]=n[5]+r[5],t[6]=n[6]+r[6],t[7]=n[7]+r[7],t[8]=n[8]+r[8],t[9]=n[9]+r[9],t[10]=n[10]+r[10],t[11]=n[11]+r[11],t[12]=n[12]+r[12],t[13]=n[13]+r[13],t[14]=n[14]+r[14],t[15]=n[15]+r[15],t},n.subtract=i,n.multiplyScalar=function(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t[4]=n[4]*r,t[5]=n[5]*r,t[6]=n[6]*r,t[7]=n[7]*r,t[8]=n[8]*r,t[9]=n[9]*r,t[10]=n[10]*r,t[11]=n[11]*r,t[12]=n[12]*r,t[13]=n[13]*r,t[14]=n[14]*r,t[15]=n[15]*r,t},n.multiplyScalarAndAdd=function(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t[4]=n[4]+r[4]*a,t[5]=n[5]+r[5]*a,t[6]=n[6]+r[6]*a,t[7]=n[7]+r[7]*a,t[8]=n[8]+r[8]*a,t[9]=n[9]+r[9]*a,t[10]=n[10]+r[10]*a,t[11]=n[11]+r[11]*a,t[12]=n[12]+r[12]*a,t[13]=n[13]+r[13]*a,t[14]=n[14]+r[14]*a,t[15]=n[15]+r[15]*a,t},n.exactEquals=function(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]&&t[4]===n[4]&&t[5]===n[5]&&t[6]===n[6]&&t[7]===n[7]&&t[8]===n[8]&&t[9]===n[9]&&t[10]===n[10]&&t[11]===n[11]&&t[12]===n[12]&&t[13]===n[13]&&t[14]===n[14]&&t[15]===n[15]},n.equals=function(t,n){var r=t[0],e=t[1],u=t[2],o=t[3],i=t[4],s=t[5],c=t[6],f=t[7],M=t[8],h=t[9],l=t[10],v=t[11],d=t[12],b=t[13],m=t[14],p=t[15],P=n[0],A=n[1],E=n[2],O=n[3],R=n[4],y=n[5],q=n[6],x=n[7],_=n[8],Y=n[9],L=n[10],S=n[11],w=n[12],I=n[13],N=n[14],g=n[15];return Math.abs(r-P)<=a.EPSILON*Math.max(1,Math.abs(r),Math.abs(P))&&Math.abs(e-A)<=a.EPSILON*Math.max(1,Math.abs(e),Math.abs(A))&&Math.abs(u-E)<=a.EPSILON*Math.max(1,Math.abs(u),Math.abs(E))&&Math.abs(o-O)<=a.EPSILON*Math.max(1,Math.abs(o),Math.abs(O))&&Math.abs(i-R)<=a.EPSILON*Math.max(1,Math.abs(i),Math.abs(R))&&Math.abs(s-y)<=a.EPSILON*Math.max(1,Math.abs(s),Math.abs(y))&&Math.abs(c-q)<=a.EPSILON*Math.max(1,Math.abs(c),Math.abs(q))&&Math.abs(f-x)<=a.EPSILON*Math.max(1,Math.abs(f),Math.abs(x))&&Math.abs(M-_)<=a.EPSILON*Math.max(1,Math.abs(M),Math.abs(_))&&Math.abs(h-Y)<=a.EPSILON*Math.max(1,Math.abs(h),Math.abs(Y))&&Math.abs(l-L)<=a.EPSILON*Math.max(1,Math.abs(l),Math.abs(L))&&Math.abs(v-S)<=a.EPSILON*Math.max(1,Math.abs(v),Math.abs(S))&&Math.abs(d-w)<=a.EPSILON*Math.max(1,Math.abs(d),Math.abs(w))&&Math.abs(b-I)<=a.EPSILON*Math.max(1,Math.abs(b),Math.abs(I))&&Math.abs(m-N)<=a.EPSILON*Math.max(1,Math.abs(m),Math.abs(N))&&Math.abs(p-g)<=a.EPSILON*Math.max(1,Math.abs(p),Math.abs(g))};var a=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(r(0));function e(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=1,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=1,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t}function u(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=n[9],l=n[10],v=n[11],d=n[12],b=n[13],m=n[14],p=n[15],P=r[0],A=r[1],E=r[2],O=r[3];return t[0]=P*a+A*i+E*M+O*d,t[1]=P*e+A*s+E*h+O*b,t[2]=P*u+A*c+E*l+O*m,t[3]=P*o+A*f+E*v+O*p,P=r[4],A=r[5],E=r[6],O=r[7],t[4]=P*a+A*i+E*M+O*d,t[5]=P*e+A*s+E*h+O*b,t[6]=P*u+A*c+E*l+O*m,t[7]=P*o+A*f+E*v+O*p,P=r[8],A=r[9],E=r[10],O=r[11],t[8]=P*a+A*i+E*M+O*d,t[9]=P*e+A*s+E*h+O*b,t[10]=P*u+A*c+E*l+O*m,t[11]=P*o+A*f+E*v+O*p,P=r[12],A=r[13],E=r[14],O=r[15],t[12]=P*a+A*i+E*M+O*d,t[13]=P*e+A*s+E*h+O*b,t[14]=P*u+A*c+E*l+O*m,t[15]=P*o+A*f+E*v+O*p,t}function o(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=a+a,s=e+e,c=u+u,f=a*i,M=a*s,h=a*c,l=e*s,v=e*c,d=u*c,b=o*i,m=o*s,p=o*c;return t[0]=1-(l+d),t[1]=M+p,t[2]=h-m,t[3]=0,t[4]=M-p,t[5]=1-(f+d),t[6]=v+b,t[7]=0,t[8]=h+m,t[9]=v-b,t[10]=1-(f+l),t[11]=0,t[12]=r[0],t[13]=r[1],t[14]=r[2],t[15]=1,t}function i(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t[4]=n[4]-r[4],t[5]=n[5]-r[5],t[6]=n[6]-r[6],t[7]=n[7]-r[7],t[8]=n[8]-r[8],t[9]=n[9]-r[9],t[10]=n[10]-r[10],t[11]=n[11]-r[11],t[12]=n[12]-r[12],t[13]=n[13]-r[13],t[14]=n[14]-r[14],t[15]=n[15]-r[15],t}n.mul=u,n.sub=i},function(t,n,r){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.sub=n.mul=void 0,n.create=function(){var t=new a.ARRAY_TYPE(9);a.ARRAY_TYPE!=Float32Array&&(t[1]=0,t[2]=0,t[3]=0,t[5]=0,t[6]=0,t[7]=0);return t[0]=1,t[4]=1,t[8]=1,t},n.fromMat4=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[4],t[4]=n[5],t[5]=n[6],t[6]=n[8],t[7]=n[9],t[8]=n[10],t},n.clone=function(t){var n=new a.ARRAY_TYPE(9);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n[4]=t[4],n[5]=t[5],n[6]=t[6],n[7]=t[7],n[8]=t[8],n},n.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t},n.fromValues=function(t,n,r,e,u,o,i,s,c){var f=new a.ARRAY_TYPE(9);return f[0]=t,f[1]=n,f[2]=r,f[3]=e,f[4]=u,f[5]=o,f[6]=i,f[7]=s,f[8]=c,f},n.set=function(t,n,r,a,e,u,o,i,s,c){return t[0]=n,t[1]=r,t[2]=a,t[3]=e,t[4]=u,t[5]=o,t[6]=i,t[7]=s,t[8]=c,t},n.identity=function(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t},n.transpose=function(t,n){if(t===n){var r=n[1],a=n[2],e=n[5];t[1]=n[3],t[2]=n[6],t[3]=r,t[5]=n[7],t[6]=a,t[7]=e}else t[0]=n[0],t[1]=n[3],t[2]=n[6],t[3]=n[1],t[4]=n[4],t[5]=n[7],t[6]=n[2],t[7]=n[5],t[8]=n[8];return t},n.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8],M=f*o-i*c,h=-f*u+i*s,l=c*u-o*s,v=r*M+a*h+e*l;if(!v)return null;return v=1/v,t[0]=M*v,t[1]=(-f*a+e*c)*v,t[2]=(i*a-e*o)*v,t[3]=h*v,t[4]=(f*r-e*s)*v,t[5]=(-i*r+e*u)*v,t[6]=l*v,t[7]=(-c*r+a*s)*v,t[8]=(o*r-a*u)*v,t},n.adjoint=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8];return t[0]=o*f-i*c,t[1]=e*c-a*f,t[2]=a*i-e*o,t[3]=i*s-u*f,t[4]=r*f-e*s,t[5]=e*u-r*i,t[6]=u*c-o*s,t[7]=a*s-r*c,t[8]=r*o-a*u,t},n.determinant=function(t){var n=t[0],r=t[1],a=t[2],e=t[3],u=t[4],o=t[5],i=t[6],s=t[7],c=t[8];return n*(c*u-o*s)+r*(-c*e+o*i)+a*(s*e-u*i)},n.multiply=e,n.translate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=r[0],l=r[1];return t[0]=a,t[1]=e,t[2]=u,t[3]=o,t[4]=i,t[5]=s,t[6]=h*a+l*o+c,t[7]=h*e+l*i+f,t[8]=h*u+l*s+M,t},n.rotate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=Math.sin(r),l=Math.cos(r);return t[0]=l*a+h*o,t[1]=l*e+h*i,t[2]=l*u+h*s,t[3]=l*o-h*a,t[4]=l*i-h*e,t[5]=l*s-h*u,t[6]=c,t[7]=f,t[8]=M,t},n.scale=function(t,n,r){var a=r[0],e=r[1];return t[0]=a*n[0],t[1]=a*n[1],t[2]=a*n[2],t[3]=e*n[3],t[4]=e*n[4],t[5]=e*n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t},n.fromTranslation=function(t,n){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=n[0],t[7]=n[1],t[8]=1,t},n.fromRotation=function(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=r,t[2]=0,t[3]=-r,t[4]=a,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t},n.fromScaling=function(t,n){return t[0]=n[0],t[1]=0,t[2]=0,t[3]=0,t[4]=n[1],t[5]=0,t[6]=0,t[7]=0,t[8]=1,t},n.fromMat2d=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=0,t[3]=n[2],t[4]=n[3],t[5]=0,t[6]=n[4],t[7]=n[5],t[8]=1,t},n.fromQuat=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r+r,i=a+a,s=e+e,c=r*o,f=a*o,M=a*i,h=e*o,l=e*i,v=e*s,d=u*o,b=u*i,m=u*s;return t[0]=1-M-v,t[3]=f-m,t[6]=h+b,t[1]=f+m,t[4]=1-c-v,t[7]=l-d,t[2]=h-b,t[5]=l+d,t[8]=1-c-M,t},n.normalFromMat4=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8],M=n[9],h=n[10],l=n[11],v=n[12],d=n[13],b=n[14],m=n[15],p=r*i-a*o,P=r*s-e*o,A=r*c-u*o,E=a*s-e*i,O=a*c-u*i,R=e*c-u*s,y=f*d-M*v,q=f*b-h*v,x=f*m-l*v,_=M*b-h*d,Y=M*m-l*d,L=h*m-l*b,S=p*L-P*Y+A*_+E*x-O*q+R*y;if(!S)return null;return S=1/S,t[0]=(i*L-s*Y+c*_)*S,t[1]=(s*x-o*L-c*q)*S,t[2]=(o*Y-i*x+c*y)*S,t[3]=(e*Y-a*L-u*_)*S,t[4]=(r*L-e*x+u*q)*S,t[5]=(a*x-r*Y-u*y)*S,t[6]=(d*R-b*O+m*E)*S,t[7]=(b*A-v*R-m*P)*S,t[8]=(v*O-d*A+m*p)*S,t},n.projection=function(t,n,r){return t[0]=2/n,t[1]=0,t[2]=0,t[3]=0,t[4]=-2/r,t[5]=0,t[6]=-1,t[7]=1,t[8]=1,t},n.str=function(t){return"mat3("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+", "+t[6]+", "+t[7]+", "+t[8]+")"},n.frob=function(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+Math.pow(t[6],2)+Math.pow(t[7],2)+Math.pow(t[8],2))},n.add=function(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t[4]=n[4]+r[4],t[5]=n[5]+r[5],t[6]=n[6]+r[6],t[7]=n[7]+r[7],t[8]=n[8]+r[8],t},n.subtract=u,n.multiplyScalar=function(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t[4]=n[4]*r,t[5]=n[5]*r,t[6]=n[6]*r,t[7]=n[7]*r,t[8]=n[8]*r,t},n.multiplyScalarAndAdd=function(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t[4]=n[4]+r[4]*a,t[5]=n[5]+r[5]*a,t[6]=n[6]+r[6]*a,t[7]=n[7]+r[7]*a,t[8]=n[8]+r[8]*a,t},n.exactEquals=function(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]&&t[4]===n[4]&&t[5]===n[5]&&t[6]===n[6]&&t[7]===n[7]&&t[8]===n[8]},n.equals=function(t,n){var r=t[0],e=t[1],u=t[2],o=t[3],i=t[4],s=t[5],c=t[6],f=t[7],M=t[8],h=n[0],l=n[1],v=n[2],d=n[3],b=n[4],m=n[5],p=n[6],P=n[7],A=n[8];return Math.abs(r-h)<=a.EPSILON*Math.max(1,Math.abs(r),Math.abs(h))&&Math.abs(e-l)<=a.EPSILON*Math.max(1,Math.abs(e),Math.abs(l))&&Math.abs(u-v)<=a.EPSILON*Math.max(1,Math.abs(u),Math.abs(v))&&Math.abs(o-d)<=a.EPSILON*Math.max(1,Math.abs(o),Math.abs(d))&&Math.abs(i-b)<=a.EPSILON*Math.max(1,Math.abs(i),Math.abs(b))&&Math.abs(s-m)<=a.EPSILON*Math.max(1,Math.abs(s),Math.abs(m))&&Math.abs(c-p)<=a.EPSILON*Math.max(1,Math.abs(c),Math.abs(p))&&Math.abs(f-P)<=a.EPSILON*Math.max(1,Math.abs(f),Math.abs(P))&&Math.abs(M-A)<=a.EPSILON*Math.max(1,Math.abs(M),Math.abs(A))};var a=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(r(0));function e(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=r[0],l=r[1],v=r[2],d=r[3],b=r[4],m=r[5],p=r[6],P=r[7],A=r[8];return t[0]=h*a+l*o+v*c,t[1]=h*e+l*i+v*f,t[2]=h*u+l*s+v*M,t[3]=d*a+b*o+m*c,t[4]=d*e+b*i+m*f,t[5]=d*u+b*s+m*M,t[6]=p*a+P*o+A*c,t[7]=p*e+P*i+A*f,t[8]=p*u+P*s+A*M,t}function u(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t[4]=n[4]-r[4],t[5]=n[5]-r[5],t[6]=n[6]-r[6],t[7]=n[7]-r[7],t[8]=n[8]-r[8],t}n.mul=e,n.sub=u},function(t,n,r){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.forEach=n.sqrLen=n.sqrDist=n.dist=n.div=n.mul=n.sub=n.len=void 0,n.create=e,n.clone=function(t){var n=new a.ARRAY_TYPE(2);return n[0]=t[0],n[1]=t[1],n},n.fromValues=function(t,n){var r=new a.ARRAY_TYPE(2);return r[0]=t,r[1]=n,r},n.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t},n.set=function(t,n,r){return t[0]=n,t[1]=r,t},n.add=function(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t},n.subtract=u,n.multiply=o,n.divide=i,n.ceil=function(t,n){return t[0]=Math.ceil(n[0]),t[1]=Math.ceil(n[1]),t},n.floor=function(t,n){return t[0]=Math.floor(n[0]),t[1]=Math.floor(n[1]),t},n.min=function(t,n,r){return t[0]=Math.min(n[0],r[0]),t[1]=Math.min(n[1],r[1]),t},n.max=function(t,n,r){return t[0]=Math.max(n[0],r[0]),t[1]=Math.max(n[1],r[1]),t},n.round=function(t,n){return t[0]=Math.round(n[0]),t[1]=Math.round(n[1]),t},n.scale=function(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t},n.scaleAndAdd=function(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t},n.distance=s,n.squaredDistance=c,n.length=f,n.squaredLength=M,n.negate=function(t,n){return t[0]=-n[0],t[1]=-n[1],t},n.inverse=function(t,n){return t[0]=1/n[0],t[1]=1/n[1],t},n.normalize=function(t,n){var r=n[0],a=n[1],e=r*r+a*a;e>0&&(e=1/Math.sqrt(e),t[0]=n[0]*e,t[1]=n[1]*e);return t},n.dot=function(t,n){return t[0]*n[0]+t[1]*n[1]},n.cross=function(t,n,r){var a=n[0]*r[1]-n[1]*r[0];return t[0]=t[1]=0,t[2]=a,t},n.lerp=function(t,n,r,a){var e=n[0],u=n[1];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t},n.random=function(t,n){n=n||1;var r=2*a.RANDOM()*Math.PI;return t[0]=Math.cos(r)*n,t[1]=Math.sin(r)*n,t},n.transformMat2=function(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[2]*e,t[1]=r[1]*a+r[3]*e,t},n.transformMat2d=function(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[2]*e+r[4],t[1]=r[1]*a+r[3]*e+r[5],t},n.transformMat3=function(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[3]*e+r[6],t[1]=r[1]*a+r[4]*e+r[7],t},n.transformMat4=function(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[4]*e+r[12],t[1]=r[1]*a+r[5]*e+r[13],t},n.rotate=function(t,n,r,a){var e=n[0]-r[0],u=n[1]-r[1],o=Math.sin(a),i=Math.cos(a);return t[0]=e*i-u*o+r[0],t[1]=e*o+u*i+r[1],t},n.angle=function(t,n){var r=t[0],a=t[1],e=n[0],u=n[1],o=r*r+a*a;o>0&&(o=1/Math.sqrt(o));var i=e*e+u*u;i>0&&(i=1/Math.sqrt(i));var s=(r*e+a*u)*o*i;return s>1?0:s<-1?Math.PI:Math.acos(s)},n.str=function(t){return"vec2("+t[0]+", "+t[1]+")"},n.exactEquals=function(t,n){return t[0]===n[0]&&t[1]===n[1]},n.equals=function(t,n){var r=t[0],e=t[1],u=n[0],o=n[1];return Math.abs(r-u)<=a.EPSILON*Math.max(1,Math.abs(r),Math.abs(u))&&Math.abs(e-o)<=a.EPSILON*Math.max(1,Math.abs(e),Math.abs(o))};var a=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(r(0));function e(){var t=new a.ARRAY_TYPE(2);return a.ARRAY_TYPE!=Float32Array&&(t[0]=0,t[1]=0),t}function u(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t}function o(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t}function i(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t}function s(t,n){var r=n[0]-t[0],a=n[1]-t[1];return Math.sqrt(r*r+a*a)}function c(t,n){var r=n[0]-t[0],a=n[1]-t[1];return r*r+a*a}function f(t){var n=t[0],r=t[1];return Math.sqrt(n*n+r*r)}function M(t){var n=t[0],r=t[1];return n*n+r*r}n.len=f,n.sub=u,n.mul=o,n.div=i,n.dist=s,n.sqrDist=c,n.sqrLen=M,n.forEach=function(){var t=e();return function(n,r,a,e,u,o){var i=void 0,s=void 0;for(r||(r=2),a||(a=0),s=e?Math.min(e*r+a,n.length):n.length,i=a;i0){r=Math.sqrt(r);var a=n[0]/r,e=n[1]/r,u=n[2]/r,o=n[3]/r,i=n[4],s=n[5],c=n[6],f=n[7],M=a*i+e*s+u*c+o*f;t[0]=a,t[1]=e,t[2]=u,t[3]=o,t[4]=(i-a*M)/r,t[5]=(s-e*M)/r,t[6]=(c-u*M)/r,t[7]=(f-o*M)/r}return t},n.str=function(t){return"quat2("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+", "+t[6]+", "+t[7]+")"},n.exactEquals=function(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]&&t[4]===n[4]&&t[5]===n[5]&&t[6]===n[6]&&t[7]===n[7]},n.equals=function(t,n){var r=t[0],e=t[1],u=t[2],o=t[3],i=t[4],s=t[5],c=t[6],f=t[7],M=n[0],h=n[1],l=n[2],v=n[3],d=n[4],b=n[5],m=n[6],p=n[7];return Math.abs(r-M)<=a.EPSILON*Math.max(1,Math.abs(r),Math.abs(M))&&Math.abs(e-h)<=a.EPSILON*Math.max(1,Math.abs(e),Math.abs(h))&&Math.abs(u-l)<=a.EPSILON*Math.max(1,Math.abs(u),Math.abs(l))&&Math.abs(o-v)<=a.EPSILON*Math.max(1,Math.abs(o),Math.abs(v))&&Math.abs(i-d)<=a.EPSILON*Math.max(1,Math.abs(i),Math.abs(d))&&Math.abs(s-b)<=a.EPSILON*Math.max(1,Math.abs(s),Math.abs(b))&&Math.abs(c-m)<=a.EPSILON*Math.max(1,Math.abs(c),Math.abs(m))&&Math.abs(f-p)<=a.EPSILON*Math.max(1,Math.abs(f),Math.abs(p))};var a=o(r(0)),e=o(r(3)),u=o(r(4));function o(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}function i(t,n,r){var a=.5*r[0],e=.5*r[1],u=.5*r[2],o=n[0],i=n[1],s=n[2],c=n[3];return t[0]=o,t[1]=i,t[2]=s,t[3]=c,t[4]=a*c+e*s-u*i,t[5]=e*c+u*o-a*s,t[6]=u*c+a*i-e*o,t[7]=-a*o-e*i-u*s,t}function s(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t}n.getReal=e.copy;n.setReal=e.copy;function c(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=r[4],s=r[5],c=r[6],f=r[7],M=n[4],h=n[5],l=n[6],v=n[7],d=r[0],b=r[1],m=r[2],p=r[3];return t[0]=a*p+o*d+e*m-u*b,t[1]=e*p+o*b+u*d-a*m,t[2]=u*p+o*m+a*b-e*d,t[3]=o*p-a*d-e*b-u*m,t[4]=a*f+o*i+e*c-u*s+M*p+v*d+h*m-l*b,t[5]=e*f+o*s+u*i-a*c+h*p+v*b+l*d-M*m,t[6]=u*f+o*c+a*s-e*i+l*p+v*m+M*b-h*d,t[7]=o*f-a*i-e*s-u*c+v*p-M*d-h*b-l*m,t}n.mul=c;var f=n.dot=e.dot;var M=n.length=e.length,h=(n.len=M,n.squaredLength=e.squaredLength);n.sqrLen=h},function(t,n,r){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.sub=n.mul=void 0,n.create=function(){var t=new a.ARRAY_TYPE(6);a.ARRAY_TYPE!=Float32Array&&(t[1]=0,t[2]=0,t[4]=0,t[5]=0);return t[0]=1,t[3]=1,t},n.clone=function(t){var n=new a.ARRAY_TYPE(6);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n[4]=t[4],n[5]=t[5],n},n.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t},n.identity=function(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=0,t[5]=0,t},n.fromValues=function(t,n,r,e,u,o){var i=new a.ARRAY_TYPE(6);return i[0]=t,i[1]=n,i[2]=r,i[3]=e,i[4]=u,i[5]=o,i},n.set=function(t,n,r,a,e,u,o){return t[0]=n,t[1]=r,t[2]=a,t[3]=e,t[4]=u,t[5]=o,t},n.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=r*u-a*e;if(!s)return null;return s=1/s,t[0]=u*s,t[1]=-a*s,t[2]=-e*s,t[3]=r*s,t[4]=(e*i-u*o)*s,t[5]=(a*o-r*i)*s,t},n.determinant=function(t){return t[0]*t[3]-t[1]*t[2]},n.multiply=e,n.rotate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=Math.sin(r),f=Math.cos(r);return t[0]=a*f+u*c,t[1]=e*f+o*c,t[2]=a*-c+u*f,t[3]=e*-c+o*f,t[4]=i,t[5]=s,t},n.scale=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=r[0],f=r[1];return t[0]=a*c,t[1]=e*c,t[2]=u*f,t[3]=o*f,t[4]=i,t[5]=s,t},n.translate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=r[0],f=r[1];return t[0]=a,t[1]=e,t[2]=u,t[3]=o,t[4]=a*c+u*f+i,t[5]=e*c+o*f+s,t},n.fromRotation=function(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=r,t[2]=-r,t[3]=a,t[4]=0,t[5]=0,t},n.fromScaling=function(t,n){return t[0]=n[0],t[1]=0,t[2]=0,t[3]=n[1],t[4]=0,t[5]=0,t},n.fromTranslation=function(t,n){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=n[0],t[5]=n[1],t},n.str=function(t){return"mat2d("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+")"},n.frob=function(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+1)},n.add=function(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t[4]=n[4]+r[4],t[5]=n[5]+r[5],t},n.subtract=u,n.multiplyScalar=function(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t[4]=n[4]*r,t[5]=n[5]*r,t},n.multiplyScalarAndAdd=function(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t[4]=n[4]+r[4]*a,t[5]=n[5]+r[5]*a,t},n.exactEquals=function(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]&&t[4]===n[4]&&t[5]===n[5]},n.equals=function(t,n){var r=t[0],e=t[1],u=t[2],o=t[3],i=t[4],s=t[5],c=n[0],f=n[1],M=n[2],h=n[3],l=n[4],v=n[5];return Math.abs(r-c)<=a.EPSILON*Math.max(1,Math.abs(r),Math.abs(c))&&Math.abs(e-f)<=a.EPSILON*Math.max(1,Math.abs(e),Math.abs(f))&&Math.abs(u-M)<=a.EPSILON*Math.max(1,Math.abs(u),Math.abs(M))&&Math.abs(o-h)<=a.EPSILON*Math.max(1,Math.abs(o),Math.abs(h))&&Math.abs(i-l)<=a.EPSILON*Math.max(1,Math.abs(i),Math.abs(l))&&Math.abs(s-v)<=a.EPSILON*Math.max(1,Math.abs(s),Math.abs(v))};var a=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(r(0));function e(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=r[0],f=r[1],M=r[2],h=r[3],l=r[4],v=r[5];return t[0]=a*c+u*f,t[1]=e*c+o*f,t[2]=a*M+u*h,t[3]=e*M+o*h,t[4]=a*l+u*v+i,t[5]=e*l+o*v+s,t}function u(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t[4]=n[4]-r[4],t[5]=n[5]-r[5],t}n.mul=e,n.sub=u},function(t,n,r){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.sub=n.mul=void 0,n.create=function(){var t=new a.ARRAY_TYPE(4);a.ARRAY_TYPE!=Float32Array&&(t[1]=0,t[2]=0);return t[0]=1,t[3]=1,t},n.clone=function(t){var n=new a.ARRAY_TYPE(4);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n},n.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t},n.identity=function(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t},n.fromValues=function(t,n,r,e){var u=new a.ARRAY_TYPE(4);return u[0]=t,u[1]=n,u[2]=r,u[3]=e,u},n.set=function(t,n,r,a,e){return t[0]=n,t[1]=r,t[2]=a,t[3]=e,t},n.transpose=function(t,n){if(t===n){var r=n[1];t[1]=n[2],t[2]=r}else t[0]=n[0],t[1]=n[2],t[2]=n[1],t[3]=n[3];return t},n.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r*u-e*a;if(!o)return null;return o=1/o,t[0]=u*o,t[1]=-a*o,t[2]=-e*o,t[3]=r*o,t},n.adjoint=function(t,n){var r=n[0];return t[0]=n[3],t[1]=-n[1],t[2]=-n[2],t[3]=r,t},n.determinant=function(t){return t[0]*t[3]-t[2]*t[1]},n.multiply=e,n.rotate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s+u*i,t[1]=e*s+o*i,t[2]=a*-i+u*s,t[3]=e*-i+o*s,t},n.scale=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=r[0],s=r[1];return t[0]=a*i,t[1]=e*i,t[2]=u*s,t[3]=o*s,t},n.fromRotation=function(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=r,t[2]=-r,t[3]=a,t},n.fromScaling=function(t,n){return t[0]=n[0],t[1]=0,t[2]=0,t[3]=n[1],t},n.str=function(t){return"mat2("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"},n.frob=function(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2))},n.LDU=function(t,n,r,a){return t[2]=a[2]/a[0],r[0]=a[0],r[1]=a[1],r[3]=a[3]-t[2]*r[1],[t,n,r]},n.add=function(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t},n.subtract=u,n.exactEquals=function(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]},n.equals=function(t,n){var r=t[0],e=t[1],u=t[2],o=t[3],i=n[0],s=n[1],c=n[2],f=n[3];return Math.abs(r-i)<=a.EPSILON*Math.max(1,Math.abs(r),Math.abs(i))&&Math.abs(e-s)<=a.EPSILON*Math.max(1,Math.abs(e),Math.abs(s))&&Math.abs(u-c)<=a.EPSILON*Math.max(1,Math.abs(u),Math.abs(c))&&Math.abs(o-f)<=a.EPSILON*Math.max(1,Math.abs(o),Math.abs(f))},n.multiplyScalar=function(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t},n.multiplyScalarAndAdd=function(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t};var a=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(r(0));function e(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=r[0],s=r[1],c=r[2],f=r[3];return t[0]=a*i+u*s,t[1]=e*i+o*s,t[2]=a*c+u*f,t[3]=e*c+o*f,t}function u(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t}n.mul=e,n.sub=u},function(t,n,r){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.vec4=n.vec3=n.vec2=n.quat2=n.quat=n.mat4=n.mat3=n.mat2d=n.mat2=n.glMatrix=void 0;var a=l(r(0)),e=l(r(9)),u=l(r(8)),o=l(r(5)),i=l(r(4)),s=l(r(3)),c=l(r(7)),f=l(r(6)),M=l(r(2)),h=l(r(1));function l(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}n.glMatrix=a,n.mat2=e,n.mat2d=u,n.mat3=o,n.mat4=i,n.quat=s,n.quat2=c,n.vec2=f,n.vec3=M,n.vec4=h}])}); ================================================ FILE: editor/js/libs/litegl.js ================================================ //packer version //litegl.js by Javi Agenjo 2014 @tamat (tamats.com) //forked from lightgl.js by Evan Wallace (madebyevan.com) "use strict"; (function(global){ var GL = global.GL = {}; if(typeof(glMatrix) == "undefined") throw("litegl.js requires gl-matrix to work. It must be included before litegl."); else { if(!global.vec2) throw("litegl.js does not support gl-matrix 3.0, download 2.8 https://github.com/toji/gl-matrix/releases/tag/v2.8.1"); } //polyfill global.requestAnimationFrame = global.requestAnimationFrame || global.mozRequestAnimationFrame || global.webkitRequestAnimationFrame || function(callback) { setTimeout(callback, 1000 / 60); }; GL.blockable_keys = {"Up":true,"Down":true,"Left":true,"Right":true}; GL.reverse = null; //some consts //https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button GL.LEFT_MOUSE_BUTTON = 0; GL.MIDDLE_MOUSE_BUTTON = 1; GL.RIGHT_MOUSE_BUTTON = 2; GL.LEFT_MOUSE_BUTTON_MASK = 1; GL.RIGHT_MOUSE_BUTTON_MASK = 2; GL.MIDDLE_MOUSE_BUTTON_MASK = 4; GL.last_context_id = 0; //Define WEBGL ENUMS as statics (more to come in WebGL 2) //sometimes we need some gl enums before having the gl context, solution: define them globally because the specs says they are constant) GL.COLOR_BUFFER_BIT = 16384; GL.DEPTH_BUFFER_BIT = 256; GL.STENCIL_BUFFER_BIT = 1024; GL.TEXTURE_2D = 3553; GL.TEXTURE_CUBE_MAP = 34067; GL.TEXTURE_3D = 32879; GL.TEXTURE_MAG_FILTER = 10240; GL.TEXTURE_MIN_FILTER = 10241; GL.TEXTURE_WRAP_S = 10242; GL.TEXTURE_WRAP_T = 10243; GL.BYTE = 5120; GL.UNSIGNED_BYTE = 5121; GL.SHORT = 5122; GL.UNSIGNED_SHORT = 5123; GL.INT = 5124; GL.UNSIGNED_INT = 5125; GL.FLOAT = 5126; GL.HALF_FLOAT_OES = 36193; //webgl 1.0 only //webgl2 formats GL.HALF_FLOAT = 5131; GL.DEPTH_COMPONENT16 = 33189; GL.DEPTH_COMPONENT24 = 33190; GL.DEPTH_COMPONENT32F = 36012; GL.FLOAT_VEC2 = 35664; GL.FLOAT_VEC3 = 35665; GL.FLOAT_VEC4 = 35666; GL.INT_VEC2 = 35667; GL.INT_VEC3 = 35668; GL.INT_VEC4 = 35669; GL.BOOL = 35670; GL.BOOL_VEC2 = 35671; GL.BOOL_VEC3 = 35672; GL.BOOL_VEC4 = 35673; GL.FLOAT_MAT2 = 35674; GL.FLOAT_MAT3 = 35675; GL.FLOAT_MAT4 = 35676; //used to know the amount of data to reserve per uniform GL.TYPE_LENGTH = {}; GL.TYPE_LENGTH[ GL.FLOAT ] = GL.TYPE_LENGTH[ GL.INT ] = GL.TYPE_LENGTH[ GL.BYTE ] = GL.TYPE_LENGTH[ GL.BOOL ] = 1; GL.TYPE_LENGTH[ GL.FLOAT_VEC2 ] = GL.TYPE_LENGTH[ GL.INT_VEC2 ] = GL.TYPE_LENGTH[ GL.BOOL_VEC2 ] = 2; GL.TYPE_LENGTH[ GL.FLOAT_VEC3 ] = GL.TYPE_LENGTH[ GL.INT_VEC3 ] = GL.TYPE_LENGTH[ GL.BOOL_VEC3 ] = 3; GL.TYPE_LENGTH[ GL.FLOAT_VEC4 ] = GL.TYPE_LENGTH[ GL.INT_VEC4 ] = GL.TYPE_LENGTH[ GL.BOOL_VEC4 ] = 4; GL.TYPE_LENGTH[ GL.FLOAT_MAT3 ] = 9; GL.TYPE_LENGTH[ GL.FLOAT_MAT4 ] = 16; GL.SAMPLER_2D = 35678; GL.SAMPLER_3D = 35679; GL.SAMPLER_CUBE = 35680; GL.DEPTH_COMPONENT = 6402; GL.ALPHA = 6406; GL.RGB = 6407; GL.RGBA = 6408; GL.LUMINANCE = 6409; GL.LUMINANCE_ALPHA = 6410; GL.DEPTH_STENCIL = 34041; GL.UNSIGNED_INT_24_8_WEBGL = 34042; //webgl2 formats GL.R8 = 33321; GL.R16F = 33325; GL.R32F = 33326; GL.R8UI = 33330; GL.RG8 = 33323; GL.RG16F = 33327; GL.RG32F = 33328; GL.RGB8 = 32849; GL.SRGB8 = 35905; GL.RGB565 = 36194; GL.R11F_G11F_B10F = 35898; GL.RGB9_E5 = 35901; GL.RGB16F = 34843; GL.RGB32F = 34837; GL.RGB8UI = 36221; GL.RGBA8 = 32856; GL.RGB5_A1 = 32855; GL.RGBA16F = 34842; GL.RGBA32F = 34836; GL.RGBA8UI = 36220; GL.RGBA16I = 36232; GL.RGBA16UI = 36214; GL.RGBA32I = 36226; GL.RGBA32UI = 36208; GL.NEAREST = 9728; GL.LINEAR = 9729; GL.NEAREST_MIPMAP_NEAREST = 9984; GL.LINEAR_MIPMAP_NEAREST = 9985; GL.NEAREST_MIPMAP_LINEAR = 9986; GL.LINEAR_MIPMAP_LINEAR = 9987; GL.REPEAT = 10497; GL.CLAMP_TO_EDGE = 33071; GL.MIRRORED_REPEAT = 33648; GL.ZERO = 0; GL.ONE = 1; GL.SRC_COLOR = 768; GL.ONE_MINUS_SRC_COLOR = 769; GL.SRC_ALPHA = 770; GL.ONE_MINUS_SRC_ALPHA = 771; GL.DST_ALPHA = 772; GL.ONE_MINUS_DST_ALPHA = 773; GL.DST_COLOR = 774; GL.ONE_MINUS_DST_COLOR = 775; GL.SRC_ALPHA_SATURATE = 776; GL.CONSTANT_COLOR = 32769; GL.ONE_MINUS_CONSTANT_COLOR = 32770; GL.CONSTANT_ALPHA = 32771; GL.ONE_MINUS_CONSTANT_ALPHA = 32772; GL.VERTEX_SHADER = 35633; GL.FRAGMENT_SHADER = 35632; GL.FRONT = 1028; GL.BACK = 1029; GL.FRONT_AND_BACK = 1032; GL.NEVER = 512; GL.LESS = 513; GL.EQUAL = 514; GL.LEQUAL = 515; GL.GREATER = 516; GL.NOTEQUAL = 517; GL.GEQUAL = 518; GL.ALWAYS = 519; GL.KEEP = 7680; GL.REPLACE = 7681; GL.INCR = 7682; GL.DECR = 7683; GL.INCR_WRAP = 34055; GL.DECR_WRAP = 34056; GL.INVERT = 5386; GL.STREAM_DRAW = 35040; GL.STATIC_DRAW = 35044; GL.DYNAMIC_DRAW = 35048; GL.ARRAY_BUFFER = 34962; GL.ELEMENT_ARRAY_BUFFER = 34963; GL.POINTS = 0; GL.LINES = 1; GL.LINE_LOOP = 2; GL.LINE_STRIP = 3; GL.TRIANGLES = 4; GL.TRIANGLE_STRIP = 5; GL.TRIANGLE_FAN = 6; GL.CW = 2304; GL.CCW = 2305; GL.CULL_FACE = 2884; GL.DEPTH_TEST = 2929; GL.BLEND = 3042; GL.temp_vec3 = vec3.create(); GL.temp2_vec3 = vec3.create(); GL.temp_vec4 = vec4.create(); GL.temp_quat = quat.create(); GL.temp_mat3 = mat3.create(); GL.temp_mat4 = mat4.create(); global.DEG2RAD = 0.0174532925; global.RAD2DEG = 57.295779578552306; global.EPSILON = 0.000001; /** * Tells if one number is power of two (used for textures) * @method isPowerOfTwo * @param {v} number * @return {boolean} */ global.isPowerOfTwo = GL.isPowerOfTwo = function isPowerOfTwo(v) { return ((Math.log(v) / Math.log(2)) % 1) == 0; } /** * Tells if one number is power of two (used for textures) * @method isPowerOfTwo * @param {v} number * @return {boolean} */ global.nearestPowerOfTwo = GL.nearestPowerOfTwo = function nearestPowerOfTwo(v) { return Math.pow(2, Math.round( Math.log( v ) / Math.log(2) ) ) } /** * converts from polar to cartesian * @method polarToCartesian * @param {vec3} out * @param {number} azimuth orientation from 0 to 2PI * @param {number} inclianation from -PI to PI * @param {number} radius * @return {vec3} returns out */ global.polarToCartesian = function( out, azimuth, inclination, radius ) { out = out || vec3.create(); out[0] = radius * Math.sin(inclination) * Math.cos(azimuth); out[1] = radius * Math.cos(inclination); out[2] = radius * Math.sin(inclination) * Math.sin(azimuth); return out; } /** * converts from cartesian to polar * @method cartesianToPolar * @param {vec3} out * @param {number} x * @param {number} y * @param {number} z * @return {vec3} returns [azimuth,inclination,radius] */ global.cartesianToPolar = function( out, x,y,z ) { out = out || vec3.create(); out[2] = Math.sqrt(x*x+y*y+z*z); out[0] = Math.atan2(x,z); out[1] = Math.acos(z/out[2]); return out; } //Global Scope //better array conversion to string for serializing var typed_arrays = [ Uint8Array, Int8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array ]; function typedToArray(){ return Array.prototype.slice.call(this); } typed_arrays.forEach( function(v) { if(!v.prototype.toJSON) Object.defineProperty( v.prototype, "toJSON", { value: typedToArray, enumerable: false }); }); /** * Get current time in milliseconds * @method getTime * @return {number} */ if(typeof(performance) != "undefined") global.getTime = performance.now.bind(performance); else global.getTime = Date.now.bind( Date ); GL.getTime = global.getTime; global.isFunction = function isFunction(obj) { return !!(obj && obj.constructor && obj.call && obj.apply); } global.isArray = function isArray(obj) { return (obj && obj.constructor === Array ); //var str = Object.prototype.toString.call(obj); //return str == '[object Array]' || str == '[object Float32Array]'; } global.isNumber = function isNumber(obj) { return (obj != null && obj.constructor === Number ); } global.getClassName = function getClassName(obj) { if (!obj) return; //from function info, but not standard if(obj.name) return obj.name; //from sourcecode if(obj.toString) { var arr = obj.toString().match( /function\s*(\w+)/); if (arr && arr.length == 2) { return arr[1]; } } } /** * clone one object recursively, only allows objects containing number,strings,typed-arrays or other objects * @method cloneObject * @param {Object} object * @param {Object} target if omited an empty object is created * @return {Object} */ global.cloneObject = GL.cloneObject = function(o, t) { if(o.constructor !== Object) throw("cloneObject only can clone pure javascript objects, not classes"); t = t || {}; for(var i in o) { var v = o[i]; if(v === null) { t[i] = null; continue; } switch(v.constructor) { case Int8Array: case Uint8Array: case Int16Array: case Uint16Array: case Int32Array: case Uint32Array: case Float32Array: case Float64Array: t[i] = new v.constructor(v); break; case Boolean: case Number: case String: t[i] = v; break; case Array: t[i] = v.concat(); //content is not cloned break; case Object: t[i] = GL.cloneObject(v); break; } } return t; } /* SLOW because accepts booleans function isNumber(obj) { var str = Object.prototype.toString.call(obj); return str == '[object Number]' || str == '[object Boolean]'; } */ //given a regular expression, a text and a callback, it calls the function every time it finds it global.regexMap = function regexMap(regex, text, callback) { var result; while ((result = regex.exec(text)) != null) { callback(result); } } global.createCanvas = GL.createCanvas = function createCanvas(width, height) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } global.cloneCanvas = GL.cloneCanvas = function cloneCanvas(c) { var canvas = document.createElement('canvas'); canvas.width = c.width; canvas.height = c.height; var ctx = canvas.getContext("2d"); ctx.drawImage(c,0,0); return canvas; } if(typeof(Image) != "undefined") //not existing inside workers { Image.prototype.getPixels = function() { var canvas = document.createElement('canvas'); canvas.width = this.width; canvas.height = this.height; var ctx = canvas.getContext("2d"); ctx.drawImage(this,0,0); return ctx.getImageData(0, 0, this.width, this.height).data; } } //you must pass an object with characters to replace and replace with what {"a":"A","c":"C"} if(!String.prototype.hasOwnProperty("replaceAll")) Object.defineProperty(String.prototype, "replaceAll", { value: function(words){ var str = this; for(var i in words) str = str.split(i).join(words[i]); return str; }, enumerable: false }); /* String.prototype.replaceAll = function(words){ var str = this; for(var i in words) str = str.split(i).join(words[i]); return str; }; */ //used for hashing keys if(!String.prototype.hasOwnProperty("hashCode")) Object.defineProperty(String.prototype, "hashCode", { value: function(){ var hash = 0, i, c, l; if (this.length == 0) return hash; for (i = 0, l = this.length; i < l; ++i) { c = this.charCodeAt(i); hash = ((hash<<5)-hash)+c; hash |= 0; // Convert to 32bit integer } return hash; }, enumerable: false }); //avoid errors when Typed array is expected and regular array is found //Array.prototype.subarray = Array.prototype.slice; //if(!Array.prototype.hasOwnProperty("subarray")) // Object.defineProperty(Array.prototype, "subarray", { value: Array.prototype.slice, enumerable: false }); if(!Array.prototype.hasOwnProperty("clone")) Object.defineProperty(Array.prototype, "clone", { value: Array.prototype.concat, enumerable: false }); if(!Float32Array.prototype.hasOwnProperty("clone")) Object.defineProperty(Float32Array.prototype, "clone", { value: function() { return new Float32Array(this); }, enumerable: false }); // remove all properties on obj, effectively reverting it to a new object (to reduce garbage) global.wipeObject = function wipeObject(obj) { for (var p in obj) { if (obj.hasOwnProperty(p)) delete obj[p]; } }; //copy methods from origin to target global.extendClass = GL.extendClass = function extendClass( 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 { var prop_names = Object.getOwnPropertyNames( origin.prototype ); for(var i = 0; i < prop_names.length; ++i) //only enumerables { var name = prop_names[i]; //if(!origin.prototype.hasOwnProperty(name)) // continue; if(target.prototype.hasOwnProperty(name)) //avoid overwritting existing ones continue; //copy getters if(origin.prototype.__lookupGetter__(name)) target.prototype.__defineGetter__(name, origin.prototype.__lookupGetter__(name)); else target.prototype[name] = origin.prototype[name]; //and setters if(origin.prototype.__lookupSetter__(name)) target.prototype.__defineSetter__(name, origin.prototype.__lookupSetter__(name)); } } if(!target.hasOwnProperty("superclass")) Object.defineProperty(target, "superclass", { get: function() { return origin }, enumerable: false }); } //simple http request global.HttpRequest = GL.request = function HttpRequest( url, params, callback, error, options ) { var async = true; if(options && options.async !== undefined) async = options.async; if(params) { var params_str = null; var params_arr = []; for(var i in params) params_arr.push(i + "=" + params[i]); params_str = params_arr.join("&"); url = url + "?" + params_str; } var xhr = new XMLHttpRequest(); xhr.open('GET', url, async); xhr.onload = function(e) { var response = this.response; var type = this.getResponseHeader("Content-Type"); if(this.status != 200) { LEvent.trigger(xhr,"fail",this.status); if(error) error(this.status); return; } LEvent.trigger(xhr,"done",this.response); if(callback) callback(this.response); return; } xhr.onerror = function(err) { LEvent.trigger(xhr,"fail",err); } if(options) { for(var i in options) xhr[i] = options[i]; if(options.binary) xhr.responseType = "arraybuffer"; } xhr.send(); return xhr; } //cheap simple promises if( global.XMLHttpRequest ) { if( !XMLHttpRequest.prototype.hasOwnProperty("done") ) Object.defineProperty( XMLHttpRequest.prototype, "done", { enumerable: false, value: function(callback) { LEvent.bind(this,"done", function(e,err) { callback(err); } ); return this; }}); if( !XMLHttpRequest.prototype.hasOwnProperty("fail") ) Object.defineProperty( XMLHttpRequest.prototype, "fail", { enumerable: false, value: function(callback) { LEvent.bind(this,"fail", function(e,err) { callback(err); } ); return this; }}); } global.getFileExtension = function getFileExtension(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(); } //allows to pack several (text)files inside one single file (useful for shaders) //every file must start with \filename.ext or /filename.ext global.loadFileAtlas = GL.loadFileAtlas = function loadFileAtlas(url, callback, sync) { var deferred_callback = null; HttpRequest(url, null, function(data) { var files = GL.processFileAtlas(data); if(callback) callback(files); if(deferred_callback) deferred_callback(files); }, alert, sync); return { done: function(callback) { deferred_callback = callback; } }; } //This parses a text file that contains several text files (they are separated by "\filename"), and returns an object with every file separatly global.processFileAtlas = GL.processFileAtlas = function(data, skip_trim) { var lines = data.split("\n"); var files = {}; var current_file_lines = []; var current_file_name = ""; for(var i = 0, l = lines.length; i < l; i++) { var line = skip_trim ? lines[i] : lines[i].trim(); if(!line.length) continue; if( line[0] != "\\") { current_file_lines.push(line); continue; } if( current_file_lines.length ) files[ current_file_name ] = current_file_lines.join("\n"); current_file_lines.length = 0; current_file_name = line.substr(1); } if( current_file_lines.length ) files[ current_file_name ] = current_file_lines.join("\n"); return files; } /* global.halfFloatToFloat = function( h ) { function convertMantissa(i) { if (i == 0) return 0 else if (i < 1024) { var m = i << 13; var e = 0; while (!(m & 0x00800000)) { e -= 0x00800000 m = m << 1 } m &= ~0x00800000 e += 0x38800000 return m | e; } return 0x38000000 + ((i - 1024) << 13); } function convertExponent(i) { if (i == 0) return 0; else if (i >= 1 && i <= 31) return i << 23; else if (i == 31) return 0x47800000; else if (i == 32) return 0x80000000; else if (i >= 33 && i <= 63) return 0x80000000 + ((i - 32) << 23); return 0xC7800000; } function convertOffset(i) { if (i == 0 || i == 32) return 0 return 1024; } var v = convertMantissa( convertOffset( h >> 10) + (h & 0x3ff) ) + convertExponent(h >> 10); var a = new Uint32Array([v]); return (new Float32Array(a.buffer))[0]; } */ global.typedArrayToArray = function(array) { var r = []; r.length = array.length; for(var i = 0; i < array.length; i++) r[i] = array[i]; return r; } global.RGBToHex = function(r, g, b) { r = Math.min(255, r*255)|0; g = Math.min(255, g*255)|0; b = Math.min(255, b*255)|0; return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } global.HUEToRGB = function ( p, q, t ){ if(t < 0) t += 1; if(t > 1) t -= 1; if(t < 1/6) return p + (q - p) * 6 * t; if(t < 1/2) return q; if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } global.HSLToRGB = function( h, s, l, out ){ var r, g, b; out = out || vec3.create(); if(s == 0){ r = g = b = l; // achromatic }else{ var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = HUEToRGB(p, q, h + 1/3); g = HUEToRGB(p, q, h); b = HUEToRGB(p, q, h - 1/3); } out[0] = r; out[1] = g; out[2] = b; return out; } global.hexColorToRGBA = (function() { //to change the color: from http://www.w3schools.com/cssref/css_colorsfull.asp var string_colors = { white: [1,1,1], black: [0,0,0], gray: [0.501960813999176, 0.501960813999176, 0.501960813999176], red: [1,0,0], orange: [1, 0.6470588445663452, 0], pink: [1, 0.7529411911964417, 0.7960784435272217], green: [0, 0.501960813999176, 0], lime: [0,1,0], blue: [0,0,1], violet: [0.9333333373069763, 0.5098039507865906, 0.9333333373069763], magenta: [1,0,1], cyan: [0,1,1], yellow: [1,1,0], brown: [0.6470588445663452, 0.16470588743686676, 0.16470588743686676], silver: [0.7529411911964417, 0.7529411911964417, 0.7529411911964417], gold: [1, 0.843137264251709, 0], transparent: [0,0,0,0] }; return function( hex, color, alpha ) { alpha = (alpha === undefined ? 1 : alpha); color = color || new Float32Array(4); color[3] = alpha; if(typeof(hex) != "string") return color; //for those hardcoded colors var col = string_colors[hex]; if( col !== undefined ) { color.set( col ); if(color.length == 3) color[3] = alpha; else color[3] *= alpha; return color; } //rgba colors var pos = hex.indexOf("rgba("); if(pos != -1) { var str = hex.substr(5,hex.length-2); str = str.split(","); color[0] = parseInt( str[0] ) / 255; color[1] = parseInt( str[1] ) / 255; color[2] = parseInt( str[2] ) / 255; color[3] = parseFloat( str[3] ) * alpha; return color; } var pos = hex.indexOf("hsla("); if(pos != -1) { var str = hex.substr(5,hex.length-2); str = str.split(","); HSLToRGB( parseInt( str[0] ) / 360, parseInt( str[1] ) / 100, parseInt( str[2] ) / 100, color ); color[3] = parseFloat( str[3] ) * alpha; return color; } color[3] = alpha; //rgb colors var pos = hex.indexOf("rgb("); if(pos != -1) { var str = hex.substr(4,hex.length-2); str = str.split(","); color[0] = parseInt( str[0] ) / 255; color[1] = parseInt( str[1] ) / 255; color[2] = parseInt( str[2] ) / 255; return color; } var pos = hex.indexOf("hsl("); if(pos != -1) { var str = hex.substr(4,hex.length-2); str = str.split(","); HSLToRGB( parseInt( str[0] ) / 360, parseInt( str[1] ) / 100, parseInt( str[2] ) / 100, color ); return color; } //the rest // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace( shorthandRegex, function(m, r, g, b) { return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if(!result) return color; color[0] = parseInt(result[1], 16) / 255; color[1] = parseInt(result[2], 16) / 255; color[2] = parseInt(result[3], 16) / 255; return color; } })(); /** * @fileoverview dds - Utilities for loading DDS texture files * @author Brandon Jones * @version 0.1 */ /* * Copyright (c) 2012 Brandon Jones * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source * distribution. */ var DDS = (function () { "use strict"; // All values and structures referenced from: // http://msdn.microsoft.com/en-us/library/bb943991.aspx/ var DDS_MAGIC = 0x20534444; var DDSD_CAPS = 0x1, DDSD_HEIGHT = 0x2, DDSD_WIDTH = 0x4, DDSD_PITCH = 0x8, DDSD_PIXELFORMAT = 0x1000, DDSD_MIPMAPCOUNT = 0x20000, DDSD_LINEARSIZE = 0x80000, DDSD_DEPTH = 0x800000; var DDSCAPS_COMPLEX = 0x8, DDSCAPS_MIPMAP = 0x400000, DDSCAPS_TEXTURE = 0x1000; var DDSCAPS2_CUBEMAP = 0x200, DDSCAPS2_CUBEMAP_POSITIVEX = 0x400, DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800, DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000, DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000, DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000, DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000, DDSCAPS2_VOLUME = 0x200000; var DDPF_ALPHAPIXELS = 0x1, DDPF_ALPHA = 0x2, DDPF_FOURCC = 0x4, DDPF_RGB = 0x40, DDPF_YUV = 0x200, DDPF_LUMINANCE = 0x20000; function fourCCToInt32(value) { return value.charCodeAt(0) + (value.charCodeAt(1) << 8) + (value.charCodeAt(2) << 16) + (value.charCodeAt(3) << 24); } function int32ToFourCC(value) { return String.fromCharCode( value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff ); } var FOURCC_DXT1 = fourCCToInt32("DXT1"); var FOURCC_DXT3 = fourCCToInt32("DXT3"); var FOURCC_DXT5 = fourCCToInt32("DXT5"); var headerLengthInt = 31; // The header length in 32 bit ints // Offsets into the header array var off_magic = 0; var off_size = 1; var off_flags = 2; var off_height = 3; var off_width = 4; var off_mipmapCount = 7; var off_pfFlags = 20; var off_pfFourCC = 21; var off_caps = 27; // Little reminder for myself where the above values come from /*DDS_PIXELFORMAT { int32 dwSize; // offset: 19 int32 dwFlags; char[4] dwFourCC; int32 dwRGBBitCount; int32 dwRBitMask; int32 dwGBitMask; int32 dwBBitMask; int32 dwABitMask; // offset: 26 }; DDS_HEADER { int32 dwSize; // 1 int32 dwFlags; int32 dwHeight; int32 dwWidth; int32 dwPitchOrLinearSize; int32 dwDepth; int32 dwMipMapCount; // offset: 7 int32[11] dwReserved1; DDS_PIXELFORMAT ddspf; // offset 19 int32 dwCaps; // offset: 27 int32 dwCaps2; int32 dwCaps3; int32 dwCaps4; int32 dwReserved2; // offset 31 };*/ /** * Transcodes DXT into RGB565. * Optimizations: * 1. Use integer math to compute c2 and c3 instead of floating point * math. Specifically: * c2 = 5/8 * c0 + 3/8 * c1 * c3 = 3/8 * c0 + 5/8 * c1 * This is about a 40% performance improvement. It also appears to * match what hardware DXT decoders do, as the colors produced * by this integer math match what hardware produces, while the * floating point in dxtToRgb565Unoptimized() produce slightly * different colors (for one GPU this was tested on). * 2. Unroll the inner loop. Another ~10% improvement. * 3. Compute r0, g0, b0, r1, g1, b1 only once instead of twice. * Another 10% improvement. * 4. Use a Uint16Array instead of a Uint8Array. Another 10% improvement. * @author Evan Parker * @param {Uint16Array} src The src DXT bits as a Uint16Array. * @param {number} srcByteOffset * @param {number} width * @param {number} height * @return {Uint16Array} dst */ function dxtToRgb565(src, src16Offset, width, height) { var c = new Uint16Array(4); var dst = new Uint16Array(width * height); var nWords = (width * height) / 4; var m = 0; var dstI = 0; var i = 0; var r0 = 0, g0 = 0, b0 = 0, r1 = 0, g1 = 0, b1 = 0; var blockWidth = width / 4; var blockHeight = height / 4; for (var blockY = 0; blockY < blockHeight; blockY++) { for (var blockX = 0; blockX < blockWidth; blockX++) { i = src16Offset + 4 * (blockY * blockWidth + blockX); c[0] = src[i]; c[1] = src[i + 1]; r0 = c[0] & 0x1f; g0 = c[0] & 0x7e0; b0 = c[0] & 0xf800; r1 = c[1] & 0x1f; g1 = c[1] & 0x7e0; b1 = c[1] & 0xf800; // Interpolate between c0 and c1 to get c2 and c3. // Note that we approximate 1/3 as 3/8 and 2/3 as 5/8 for // speed. This also appears to be what the hardware DXT // decoder in many GPUs does :) c[2] = ((5 * r0 + 3 * r1) >> 3) | (((5 * g0 + 3 * g1) >> 3) & 0x7e0) | (((5 * b0 + 3 * b1) >> 3) & 0xf800); c[3] = ((5 * r1 + 3 * r0) >> 3) | (((5 * g1 + 3 * g0) >> 3) & 0x7e0) | (((5 * b1 + 3 * b0) >> 3) & 0xf800); m = src[i + 2]; dstI = (blockY * 4) * width + blockX * 4; dst[dstI] = c[m & 0x3]; dst[dstI + 1] = c[(m >> 2) & 0x3]; dst[dstI + 2] = c[(m >> 4) & 0x3]; dst[dstI + 3] = c[(m >> 6) & 0x3]; dstI += width; dst[dstI] = c[(m >> 8) & 0x3]; dst[dstI + 1] = c[(m >> 10) & 0x3]; dst[dstI + 2] = c[(m >> 12) & 0x3]; dst[dstI + 3] = c[(m >> 14)]; m = src[i + 3]; dstI += width; dst[dstI] = c[m & 0x3]; dst[dstI + 1] = c[(m >> 2) & 0x3]; dst[dstI + 2] = c[(m >> 4) & 0x3]; dst[dstI + 3] = c[(m >> 6) & 0x3]; dstI += width; dst[dstI] = c[(m >> 8) & 0x3]; dst[dstI + 1] = c[(m >> 10) & 0x3]; dst[dstI + 2] = c[(m >> 12) & 0x3]; dst[dstI + 3] = c[(m >> 14)]; } } return dst; } function BGRtoRGB( byteArray ) { for(var j = 0, l = byteArray.length, tmp = 0; j < l; j+=4) //BGR fix { tmp = byteArray[j]; byteArray[j] = byteArray[j+2]; byteArray[j+2] = tmp; } } function flipDXT( width, blockBytes, byteArray ) { //TODO //var row = Uint8Array(width); } /** * Parses a DDS file from the given arrayBuffer and uploads it into the currently bound texture * * @param {WebGLRenderingContext} gl WebGL rendering context * @param {WebGLCompressedTextureS3TC} ext WEBGL_compressed_texture_s3tc extension object * @param {TypedArray} arrayBuffer Array Buffer containing the DDS files data * @param {boolean} [loadMipmaps] If false only the top mipmap level will be loaded, otherwise all available mipmaps will be uploaded * * @returns {number} Number of mipmaps uploaded, 0 if there was an error */ function uploadDDSLevels(gl, ext, arrayBuffer, loadMipmaps) { var header = new Int32Array(arrayBuffer, 0, headerLengthInt), fourCC, blockBytes, internalFormat, width, height, dataLength, dataOffset, is_cubemap, rgb565Data, byteArray, mipmapCount, i, face; if(header[off_magic] != DDS_MAGIC) { console.error("Invalid magic number in DDS header"); return 0; } if(!header[off_pfFlags] & DDPF_FOURCC) { console.error("Unsupported format, must contain a FourCC code"); return 0; } fourCC = header[off_pfFourCC]; switch(fourCC) { case FOURCC_DXT1: blockBytes = 8; internalFormat = ext ? ext.COMPRESSED_RGB_S3TC_DXT1_EXT : null; break; /* case FOURCC_DXT1: blockBytes = 8; internalFormat = ext ? ext.COMPRESSED_RGBA_S3TC_DXT1_EXT : null; break; */ case FOURCC_DXT3: blockBytes = 16; internalFormat = ext ? ext.COMPRESSED_RGBA_S3TC_DXT3_EXT : null; break; case FOURCC_DXT5: blockBytes = 16; internalFormat = ext ? ext.COMPRESSED_RGBA_S3TC_DXT5_EXT : null; break; default: blockBytes = 4; fourCC = null; internalFormat = gl.RGBA; //console.error("Unsupported FourCC code:", int32ToFourCC(fourCC), fourCC); //return null; } mipmapCount = 1; if(header[off_flags] & DDSD_MIPMAPCOUNT && loadMipmaps !== false) { mipmapCount = Math.max(1, header[off_mipmapCount]); } width = header[off_width]; height = header[off_height]; dataOffset = header[off_size] + 4; is_cubemap = !!(header[off_caps+1] & DDSCAPS2_CUBEMAP); if(is_cubemap) { //console.error("Cubemaps not supported in DDS"); //return null; for(face = 0; face < 6; ++face) { width = header[off_width]; height = header[off_height]; for(var i = 0; i < mipmapCount; ++i) { if(fourCC) { dataLength = Math.max( 4, width )/4 * Math.max( 4, height )/4 * blockBytes; byteArray = new Uint8Array(arrayBuffer, dataOffset, dataLength); flipDXT( width, blockBytes, byteArray ); gl.compressedTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + face, i, internalFormat, width, height, 0, byteArray); } else { gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false ); dataLength = width * height * blockBytes; byteArray = new Uint8Array(arrayBuffer, dataOffset, dataLength); BGRtoRGB(byteArray); gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + face, i, internalFormat, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, byteArray); } dataOffset += dataLength; width *= 0.5; height *= 0.5; } } } else //2d texture { if(ext) { gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true ); for(var i = 0; i < mipmapCount; ++i) { if(fourCC) { dataLength = Math.max( 4, width )/4 * Math.max( 4, height )/4 * blockBytes; byteArray = new Uint8Array(arrayBuffer, dataOffset, dataLength); gl.compressedTexImage2D(gl.TEXTURE_2D, i, internalFormat, width, height, 0, byteArray); } else { dataLength = width * height * blockBytes; byteArray = new Uint8Array(arrayBuffer, dataOffset, dataLength); BGRtoRGB(byteArray); gl.texImage2D(gl.TEXTURE_2D, i, internalFormat, width, height, 0, internalFormat, gl.UNSIGNED_BYTE, byteArray); } dataOffset += dataLength; width *= 0.5; height *= 0.5; } } else { if(fourCC == FOURCC_DXT1) { dataLength = Math.max( 4, width )/4 * Math.max( 4, height )/4 * blockBytes; byteArray = new Uint16Array(arrayBuffer); //Decompress rgb565Data = dxtToRgb565(byteArray, dataOffset / 2, width, height); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.RGB, gl.UNSIGNED_SHORT_5_6_5, rgb565Data); if(loadMipmaps) { gl.generateMipmap(gl.TEXTURE_2D); } } else { console.error("No manual decoder for", int32ToFourCC(fourCC), "and no native support"); return 0; } } } return mipmapCount; } /** * Parses a DDS file from the given arrayBuffer and uploads it into the currently bound texture * * @param {WebGLRenderingContext} gl WebGL rendering context * @param {WebGLCompressedTextureS3TC} ext WEBGL_compressed_texture_s3tc extension object * @param {TypedArray} arrayBuffer Array Buffer containing the DDS files data * @param {boolean} [loadMipmaps] If false only the top mipmap level will be loaded, otherwise all available mipmaps will be uploaded * * @returns {number} Number of mipmaps uploaded, 0 if there was an error */ function getDDSLevels( arrayBuffer, compressed_not_supported ) { var header = new Int32Array(arrayBuffer, 0, headerLengthInt), fourCC, blockBytes, internalFormat, width, height, dataLength, dataOffset, is_cubemap, rgb565Data, byteArray, mipmapCount, i, face; if(header[off_magic] != DDS_MAGIC) { console.error("Invalid magic number in DDS header"); return 0; } if(!header[off_pfFlags] & DDPF_FOURCC) { console.error("Unsupported format, must contain a FourCC code"); return 0; } fourCC = header[off_pfFourCC]; switch(fourCC) { case FOURCC_DXT1: blockBytes = 8; internalFormat = "COMPRESSED_RGB_S3TC_DXT1_EXT"; break; case FOURCC_DXT3: blockBytes = 16; internalFormat = "COMPRESSED_RGBA_S3TC_DXT3_EXT"; break; case FOURCC_DXT5: blockBytes = 16; internalFormat = "COMPRESSED_RGBA_S3TC_DXT5_EXT"; break; default: blockBytes = 4; internalFormat = "RGBA"; //console.error("Unsupported FourCC code:", int32ToFourCC(fourCC), fourCC); //return null; } mipmapCount = 1; if(header[off_flags] & DDSD_MIPMAPCOUNT && loadMipmaps !== false) { mipmapCount = Math.max(1, header[off_mipmapCount]); } width = header[off_width]; height = header[off_height]; dataOffset = header[off_size] + 4; is_cubemap = !!(header[off_caps+1] & DDSCAPS2_CUBEMAP); var buffers = []; if(is_cubemap) { for(var face = 0; face < 6; ++face) { width = header[off_width]; height = header[off_height]; for(var i = 0; i < mipmapCount; ++i) { if(fourCC) { dataLength = Math.max( 4, width )/4 * Math.max( 4, height )/4 * blockBytes; byteArray = new Uint8Array(arrayBuffer, dataOffset, dataLength); buffers.push({ tex: "TEXTURE_CUBE_MAP", face: face, mipmap: i, internalFormat: internalFormat, width: width, height: height, offset: 0, dataOffset: dataOffset, dataLength: dataLength }); } else { dataLength = width * height * blockBytes; byteArray = new Uint8Array(arrayBuffer, dataOffset, dataLength); BGRtoRGB(byteArray); buffers.push({ tex: "TEXTURE_CUBE_MAP", face: face, mipmap: i, internalFormat: internalFormat, width: width, height: height, offset: 0, type: "UNSIGNED_BYTE", dataOffset: dataOffset, dataLength: dataLength }); } dataOffset += dataLength; width *= 0.5; height *= 0.5; } } } else //2d texture { if(!compressed_not_supported) { for(var i = 0; i < mipmapCount; ++i) { dataLength = Math.max( 4, width )/4 * Math.max( 4, height )/4 * blockBytes; byteArray = new Uint8Array(arrayBuffer, dataOffset, dataLength); //gl.compressedTexImage2D(gl.TEXTURE_2D, i, internalFormat, width, height, 0, byteArray); buffers.push({ tex: "TEXTURE_2D", mipmap: i, internalFormat: internalFormat, width: width, height: height, offset: 0, type: "UNSIGNED_BYTE", dataOffset: dataOffset, dataLength: dataLength }); dataOffset += dataLength; width *= 0.5; height *= 0.5; } } else { if(fourCC == FOURCC_DXT1) { dataLength = Math.max( 4, width )/4 * Math.max( 4, height )/4 * blockBytes; byteArray = new Uint16Array(arrayBuffer); rgb565Data = dxtToRgb565(byteArray, dataOffset / 2, width, height); //gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.RGB, gl.UNSIGNED_SHORT_5_6_5, rgb565Data); buffers.push({ tex: "TEXTURE_2D", mipmap: 0, internalFormat: "RGB", width: width, height: height, offset: 0, format:"RGB", type: "UNSIGNED_SHORT_5_6_5", data: rgb565Data }); } else { console.error("No manual decoder for", int32ToFourCC(fourCC), "and no native support"); return 0; } } } return buffers; } /** * Creates a texture from the DDS file at the given URL. Simple shortcut for the most common use case * * @param {WebGLRenderingContext} gl WebGL rendering context * @param {WebGLCompressedTextureS3TC} ext WEBGL_compressed_texture_s3tc extension object * @param {string} src URL to DDS file to be loaded * @param {function} [callback] callback to be fired when the texture has finished loading * * @returns {WebGLTexture} New texture that will receive the DDS image data */ function loadDDSTextureEx(gl, ext, src, texture, loadMipmaps, callback) { var xhr = new XMLHttpRequest(); xhr.open('GET', src, true); xhr.responseType = "arraybuffer"; xhr.onload = function() { if(this.status == 200) { var header = new Int32Array(this.response, 0, headerLengthInt) var is_cubemap = !!(header[off_caps+1] & DDSCAPS2_CUBEMAP); var tex_type = is_cubemap ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D; gl.bindTexture(tex_type, texture); var mipmaps = uploadDDSLevels(gl, ext, this.response, loadMipmaps); gl.texParameteri(tex_type, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(tex_type, gl.TEXTURE_MIN_FILTER, mipmaps > 1 ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR); gl.bindTexture(tex_type, null); texture.texture_type = tex_type; texture.width = header[off_width]; texture.height = header[off_height]; } if(callback) { callback(texture); } }; xhr.send(null); return texture; } /** * Creates a texture from the DDS file at the given ArrayBuffer. * * @param {WebGLRenderingContext} gl WebGL rendering context * @param {WebGLCompressedTextureS3TC} ext WEBGL_compressed_texture_s3tc extension object * @param {ArrayBuffer} data containing the DDS file * @param {Texture} texture from GL.Texture * @returns {WebGLTexture} New texture that will receive the DDS image data */ function loadDDSTextureFromMemoryEx(gl, ext, data, texture, loadMipmaps) { var header = new Int32Array(data, 0, headerLengthInt) var is_cubemap = !!(header[off_caps+1] & DDSCAPS2_CUBEMAP); var tex_type = is_cubemap ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D; var handler = texture.handler || texture; gl.bindTexture(tex_type, texture.handler); //upload data var mipmaps = uploadDDSLevels(gl, ext, data, loadMipmaps); gl.texParameteri(tex_type, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(tex_type, gl.TEXTURE_MIN_FILTER, mipmaps > 1 ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR); if(is_cubemap) { gl.texParameteri(tex_type, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE ); gl.texParameteri(tex_type, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE ); } gl.bindTexture(tex_type, null); //unbind if(texture.handler) { texture.texture_type = tex_type; texture.width = header[off_width]; texture.height = header[off_height]; } return texture; } /** * Extracts the texture info from a DDS file at the given ArrayBuffer. * * @param {ArrayBuffer} data containing the DDS file * * @returns {Object} contains mipmaps and properties */ function getDDSTextureFromMemoryEx(data) { var header = new Int32Array(data, 0, headerLengthInt) var is_cubemap = !!(header[off_caps+1] & DDSCAPS2_CUBEMAP); var tex_type = is_cubemap ? "TEXTURE_CUBE_MAP" : "TEXTURE_2D"; var buffers = getDDSLevels(data); var texture = { type: tex_type, buffers: buffers, data: data, width: header[off_width], height: header[off_height] }; return texture; } /** * Creates a texture from the DDS file at the given URL. Simple shortcut for the most common use case * * @param {WebGLRenderingContext} gl WebGL rendering context * @param {WebGLCompressedTextureS3TC} ext WEBGL_compressed_texture_s3tc extension object * @param {string} src URL to DDS file to be loaded * @param {function} [callback] callback to be fired when the texture has finished loading * * @returns {WebGLTexture} New texture that will receive the DDS image data */ function loadDDSTexture(gl, ext, src, callback) { var texture = gl.createTexture(); var ext = gl.getExtension("WEBGL_compressed_texture_s3tc"); loadDDSTextureEx(gl, ext, src, texture, true, callback); return texture; } return { dxtToRgb565: dxtToRgb565, uploadDDSLevels: uploadDDSLevels, loadDDSTextureEx: loadDDSTextureEx, loadDDSTexture: loadDDSTexture, loadDDSTextureFromMemoryEx: loadDDSTextureFromMemoryEx, getDDSTextureFromMemoryEx: getDDSTextureFromMemoryEx }; })(); if(typeof(global) != "undefined") global.DDS = DDS; /* this file adds some extra functions to gl-matrix library */ if(typeof(glMatrix) == "undefined") throw("You must include glMatrix on your project"); Math.clamp = function(v,a,b) { return (a > v ? a : (b < v ? b : v)); } var V3 = vec3.create; var M4 = vec3.create; vec3.ZERO = vec3.fromValues(0,0,0); vec3.FRONT = vec3.fromValues(0,0,-1); vec3.UP = vec3.fromValues(0,1,0); vec3.RIGHT = vec3.fromValues(1,0,0); vec2.rotate = function(out,vec,angle_in_rad) { var x = vec[0], y = vec[1]; var cos = Math.cos(angle_in_rad); var sin = Math.sin(angle_in_rad); out[0] = x * cos - y * sin; out[1] = x * sin + y * cos; return out; } vec3.zero = function(a) { a[0] = a[1] = 0.0; return a; } //for signed angles vec2.perpdot = function(a,b) { return a[1] * b[0] + -a[0] * b[1]; } vec2.computeSignedAngle = function( a, b ) { return Math.atan2( vec2.perpdot(a,b), vec2.dot(a,b) ); } vec2.random = function( vec, scale ) { scale = scale || 1.0; vec[0] = Math.random() * scale; vec[1] = Math.random() * scale; return vec; } vec3.zero = function(a) { a[0] = a[1] = a[2] = 0.0; return a; } vec3.minValue = function(a) { if(a[0] < a[1] && a[0] < a[2]) return a[0]; if(a[1] < a[2]) return a[1]; return a[2]; } vec3.maxValue = function(a) { if(a[0] > a[1] && a[0] > a[2]) return a[0]; if(a[1] > a[2]) return a[1]; return a[2]; } vec3.minValue = function(a) { if(a[0] < a[1] && a[0] < a[2]) return a[0]; if(a[1] < a[2]) return a[1]; return a[2]; } vec3.addValue = function(out,a,v) { out[0] = a[0] + v; out[1] = a[1] + v; out[2] = a[2] + v; } vec3.subValue = function(out,a,v) { out[0] = a[0] - v; out[1] = a[1] - v; out[2] = a[2] - v; } vec3.toArray = function(vec) { return [vec[0],vec[1],vec[2]]; } vec3.rotateX = function(out,vec,angle_in_rad) { var y = vec[1], z = vec[2]; var cos = Math.cos(angle_in_rad); var sin = Math.sin(angle_in_rad); out[0] = vec[0]; out[1] = y * cos - z * sin; out[2] = y * sin + z * cos; return out; } vec3.rotateY = function(out,vec,angle_in_rad) { var x = vec[0], z = vec[2]; var cos = Math.cos(angle_in_rad); var sin = Math.sin(angle_in_rad); out[0] = x * cos - z * sin; out[1] = vec[1]; out[2] = x * sin + z * cos; return out; } vec3.rotateZ = function(out,vec,angle_in_rad) { var x = vec[0], y = vec[1]; var cos = Math.cos(angle_in_rad); var sin = Math.sin(angle_in_rad); out[0] = x * cos - y * sin; out[1] = x * sin + y * cos; out[2] = vec[2]; return out; } vec3.angle = function( a, b ) { return Math.acos( vec3.dot(a,b) ); } vec3.signedAngle = function(from, to, axis) { var unsignedAngle = vec3.angle( from, to ); var cross_x = from[1] * to[2] - from[2] * to[1]; var cross_y = from[2] * to[0] - from[0] * to[2]; var cross_z = from[0] * to[1] - from[1] * to[0]; var sign = Math.sign(axis[0] * cross_x + axis[1] * cross_y + axis[2] * cross_z); return unsignedAngle * sign; } vec3.random = function(vec, scale) { scale = scale || 1.0; vec[0] = Math.random() * scale; vec[1] = Math.random() * scale; vec[2] = Math.random() * scale; return vec; } //converts a polar coordinate (radius, lat, long) to (x,y,z) vec3.polarToCartesian = function(out, v) { var r = v[0]; var lat = v[1]; var lon = v[2]; out[0] = r * Math.cos(lat) * Math.sin(lon); out[1] = r * Math.sin(lat); out[2] = r * Math.cos(lat) * Math.cos(lon); return out; } vec3.reflect = function(out, v, n) { var x = v[0]; var y = v[1]; var z = v[2]; vec3.scale( out, n, -2 * vec3.dot(v,n) ); out[0] += x; out[1] += y; out[2] += z; return out; } /* VEC4 */ vec4.random = function(vec, scale) { scale = scale || 1.0; vec[0] = Math.random() * scale; vec[1] = Math.random() * scale; vec[2] = Math.random() * scale; vec[3] = Math.random() * scale; return vec; } vec4.toArray = function(vec) { return [vec[0],vec[1],vec[2],vec[3]]; } /** MATRIX ********************/ mat3.IDENTITY = mat3.create(); mat4.IDENTITY = mat4.create(); mat4.toArray = function(mat) { return [mat[0],mat[1],mat[2],mat[3],mat[4],mat[5],mat[6],mat[7],mat[8],mat[9],mat[10],mat[11],mat[12],mat[13],mat[14],mat[15]]; } mat4.setUpAndOrthonormalize = function(out, m, up) { if(m != out) mat4.copy(out,m); var right = out.subarray(0,3); vec3.normalize(out.subarray(4,7),up); var front = out.subarray(8,11); vec3.cross( right, up, front ); vec3.normalize( right, right ); vec3.cross( front, right, up ); vec3.normalize( front, front ); } mat4.multiplyVec3 = function(out, m, a) { var x = a[0], y = a[1], z = a[2]; out[0] = m[0] * x + m[4] * y + m[8] * z + m[12]; out[1] = m[1] * x + m[5] * y + m[9] * z + m[13]; out[2] = m[2] * x + m[6] * y + m[10] * z + m[14]; return out; }; //from https://github.com/hughsk/from-3d-to-2d/blob/master/index.js //m should be a projection matrix (or a VP or MVP) //projects vector from 3D to 2D and returns the value in normalized screen space mat4.projectVec3 = function(out, m, a) { var ix = a[0]; var iy = a[1]; var iz = a[2]; var ox = m[0] * ix + m[4] * iy + m[8] * iz + m[12]; var oy = m[1] * ix + m[5] * iy + m[9] * iz + m[13]; var oz = m[2] * ix + m[6] * iy + m[10] * iz + m[14]; var ow = m[3] * ix + m[7] * iy + m[11] * iz + m[15]; out[0] = (ox / ow + 1) / 2; out[1] = (oy / ow + 1) / 2; out[2] = (oz / ow + 1) / 2; return out; }; //from https://github.com/hughsk/from-3d-to-2d/blob/master/index.js vec3.project = function(out, vec, mvp, viewport) { viewport = viewport || gl.viewport_data; var m = mvp; var ix = vec[0]; var iy = vec[1]; var iz = vec[2]; var ox = m[0] * ix + m[4] * iy + m[8] * iz + m[12]; var oy = m[1] * ix + m[5] * iy + m[9] * iz + m[13]; var oz = m[2] * ix + m[6] * iy + m[10] * iz + m[14]; var ow = m[3] * ix + m[7] * iy + m[11] * iz + m[15]; var projx = (ox / ow + 1) / 2; var projy = 1 - (oy / ow + 1) / 2; var projz = (oz / ow + 1) / 2; out[0] = projx * viewport[2] + viewport[0]; out[1] = projy * viewport[3] + viewport[1]; out[2] = projz; //ow return out; }; var unprojectMat = mat4.create(); var unprojectVec = vec4.create(); vec3.unproject = function (out, vec, viewprojection, viewport) { var m = unprojectMat; var v = unprojectVec; v[0] = (vec[0] - viewport[0]) * 2.0 / viewport[2] - 1.0; v[1] = (vec[1] - viewport[1]) * 2.0 / viewport[3] - 1.0; v[2] = 2.0 * vec[2] - 1.0; v[3] = 1.0; if(!mat4.invert(m,viewprojection)) return null; vec4.transformMat4(v, v, m); if(v[3] === 0.0) return null; out[0] = v[0] / v[3]; out[1] = v[1] / v[3]; out[2] = v[2] / v[3]; return out; }; //without translation mat4.rotateVec3 = function(out, m, a) { var x = a[0], y = a[1], z = a[2]; out[0] = m[0] * x + m[4] * y + m[8] * z; out[1] = m[1] * x + m[5] * y + m[9] * z; out[2] = m[2] * x + m[6] * y + m[10] * z; return out; }; mat4.fromTranslationFrontTop = function (out, pos, front, top) { vec3.cross(out.subarray(0,3), front, top); out.set(top,4); out.set(front,8); out.set(pos,12); return out; } mat4.translationMatrix = function (v) { var out = mat4.create(); out[12] = v[0]; out[13] = v[1]; out[14] = v[2]; return out; } mat4.setTranslation = function (out, v) { out[12] = v[0]; out[13] = v[1]; out[14] = v[2]; return out; } mat4.getTranslation = function (out, matrix) { out[0] = matrix[12]; out[1] = matrix[13]; out[2] = matrix[14]; return out; } //returns the matrix without rotation mat4.toRotationMat4 = function (out, mat) { mat4.copy(out,mat); out[12] = out[13] = out[14] = 0.0; return out; }; mat4.swapRows = function(out, mat, row, row2) { if(out != mat) { mat4.copy(out, mat); out[4*row] = mat[4*row2]; out[4*row+1] = mat[4*row2+1]; out[4*row+2] = mat[4*row2+2]; out[4*row+3] = mat[4*row2+3]; out[4*row2] = mat[4*row]; out[4*row2+1] = mat[4*row+1]; out[4*row2+2] = mat[4*row+2]; out[4*row2+3] = mat[4*row+3]; return out; } var temp = new Float32Array(matrix.subarray(row*4,row*5)); matrix.set( matrix.subarray(row2*4,row2*5), row*4 ); matrix.set( temp, row2*4 ); return out; } //used in skinning mat4.scaleAndAdd = function(out, mat, mat2, v) { out[0] = mat[0] + mat2[0] * v; out[1] = mat[1] + mat2[1] * v; out[2] = mat[2] + mat2[2] * v; out[3] = mat[3] + mat2[3] * v; out[4] = mat[4] + mat2[4] * v; out[5] = mat[5] + mat2[5] * v; out[6] = mat[6] + mat2[6] * v; out[7] = mat[7] + mat2[7] * v; out[8] = mat[8] + mat2[8] * v; out[9] = mat[9] + mat2[9] * v; out[10] = mat[10] + mat2[10] * v; out[11] = mat[11] + mat2[11] * v; out[12] = mat[12] + mat2[12] * v; out[13] = mat[13] + mat2[13] * v; out[14] = mat[14] + mat2[14] * v; out[15] = mat[15] + mat2[15] * v; return out; } quat.fromAxisAngle = function(axis, rad) { var out = quat.create(); rad = rad * 0.5; var s = Math.sin(rad); out[0] = s * axis[0]; out[1] = s * axis[1]; out[2] = s * axis[2]; out[3] = Math.cos(rad); return out; } //from https://answers.unity.com/questions/467614/what-is-the-source-code-of-quaternionlookrotation.html quat.lookRotation = (function(){ var vector = vec3.create(); var vector2 = vec3.create(); var vector3 = vec3.create(); return function( q, front, up ) { vec3.normalize(vector,front); vec3.cross( vector2, up, vector ); vec3.normalize(vector2,vector2); vec3.cross( vector3, vector, vector2 ); var m00 = vector2[0]; var m01 = vector2[1]; var m02 = vector2[2]; var m10 = vector3[0]; var m11 = vector3[1]; var m12 = vector3[2]; var m20 = vector[0]; var m21 = vector[1]; var m22 = vector[2]; var num8 = (m00 + m11) + m22; if (num8 > 0) { var num = Math.sqrt(num8 + 1); q[3] = num * 0.5; num = 0.5 / num; q[0] = (m12 - m21) * num; q[1] = (m20 - m02) * num; q[2] = (m01 - m10) * num; return q; } if ((m00 >= m11) && (m00 >= m22)) { var num7 = Math.sqrt(((1 + m00) - m11) - m22); var num4 = 0.5 / num7; q[0] = 0.5 * num7; q[1] = (m01 + m10) * num4; q[2] = (m02 + m20) * num4; q[3] = (m12 - m21) * num4; return q; } if (m11 > m22) { var num6 = Math.sqrt(((1 + m11) - m00) - m22); var num3 = 0.5 / num6; q[0] = (m10+ m01) * num3; q[1] = 0.5 * num6; q[2] = (m21 + m12) * num3; q[3] = (m20 - m02) * num3; return q; } var num5 = Math.sqrt(((1 + m22) - m00) - m11); var num2 = 0.5 / num5; q[0] = (m20 + m02) * num2; q[1] = (m21 + m12) * num2; q[2] = 0.5 * num5; q[3] = (m01 - m10) * num2; return q; }; })(); /* quat.toEuler = function(out, quat) { var q = quat; var heading, attitude, bank; if( (q[0]*q[1] + q[2]*q[3]) == 0.5 ) { heading = 2 * Math.atan2(q[0],q[3]); bank = 0; attitude = 0; //? } else if( (q[0]*q[1] + q[2]*q[3]) == 0.5 ) { heading = -2 * Math.atan2(q[0],q[3]); bank = 0; attitude = 0; //? } else { heading = Math.atan2( 2*(q[1]*q[3] - q[0]*q[2]) , 1 - 2 * (q[1]*q[1] - q[2]*q[2]) ); attitude = Math.asin( 2*(q[0]*q[1] - q[2]*q[3]) ); bank = Math.atan2( 2*(q[0]*q[3] - q[1]*q[2]), 1 - 2*(q[0]*q[0] - q[2]*q[2]) ); } if(!out) out = vec3.create(); vec3.set(out, heading, attitude, bank); return out; } */ /* //FROM https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles //doesnt work well quat.toEuler = function(out, q) { var yaw = Math.atan2(2*q[0]*q[3] + 2*q[1]*q[2], 1 - 2*q[2]*q[2] - 2*q[3]*q[3]); var pitch = Math.asin(2*q[0]*q[2] - 2*q[3]*q[1]); var roll = Math.atan2(2*q[0]*q[1] + 2*q[2]*q[3], 1 - 2*q[1]*q[1] - 2*q[2]*q[2]); if(!out) out = vec3.create(); vec3.set(out, yaw, pitch, roll); return out; } quat.fromEuler = function(out, vec) { var yaw = vec[0]; var pitch = vec[1]; var roll = vec[2]; var C1 = Math.cos(yaw*0.5); var C2 = Math.cos(pitch*0.5); var C3 = Math.cos(roll*0.5); var S1 = Math.sin(yaw*0.5); var S2 = Math.sin(pitch*0.5); var S3 = Math.sin(roll*0.5); var x = C1*C2*C3 + S1*S2*S3; var y = S1*C2*C3 - C1*S2*S3; var z = C1*S2*C3 + S1*C2*S3; var w = C1*C2*S3 - S1*S2*C3; quat.set(out, x,y,z,w ); quat.normalize(out,out); //necessary? return out; } */ quat.toEuler = function(out, q) { var heading = Math.atan2(2*q[1]*q[3] - 2*q[0]*q[2], 1 - 2*q[1]*q[1] - 2*q[2]*q[2]); var attitude = Math.asin(2*q[0]*q[1] + 2*q[2]*q[3]); var bank = Math.atan2(2*q[0]*q[3] - 2*q[1]*q[2], 1 - 2*q[0]*q[0] - 2*q[2]*q[2]); if(!out) out = vec3.create(); vec3.set(out, heading, attitude, bank); return out; } quat.fromEuler = function(out, vec) { var heading = vec[0]; var attitude = vec[1]; var bank = vec[2]; var C1 = Math.cos(heading); //yaw var C2 = Math.cos(attitude); //pitch var C3 = Math.cos(bank); //roll var S1 = Math.sin(heading); var S2 = Math.sin(attitude); var S3 = Math.sin(bank); var w = Math.sqrt(1.0 + C1 * C2 + C1*C3 - S1 * S2 * S3 + C2*C3) * 0.5; if(w == 0.0) { w = 0.000001; //quat.set(out, 0,0,0,1 ); //return out; } var x = (C2 * S3 + C1 * S3 + S1 * S2 * C3) / (4.0 * w); var y = (S1 * C2 + S1 * C3 + C1 * S2 * S3) / (4.0 * w); var z = (-S1 * S3 + C1 * S2 * C3 + S2) /(4.0 * w); quat.set(out, x,y,z,w ); quat.normalize(out,out); return out; }; //not tested quat.fromMat4 = function(out,m) { var trace = m[0] + m[5] + m[10]; if ( trace > 0.0 ) { var s = Math.sqrt( trace + 1.0 ); out[3] = s * 0.5;//w var recip = 0.5 / s; out[0] = ( m[9] - m[6] ) * recip; //2,1 1,2 out[1] = ( m[2] - m[8] ) * recip; //0,2 2,0 out[2] = ( m[4] - m[1] ) * recip; //1,0 0,1 } else { var i = 0; if( m[5] > m[0] ) i = 1; if( m[10] > m[i*4+i] ) i = 2; var j = ( i + 1 ) % 3; var k = ( j + 1 ) % 3; var s = Math.sqrt( m[i*4+i] - m[j*4+j] - m[k*4+k] + 1.0 ); out[i] = 0.5 * s; var recip = 0.5 / s; out[3] = ( m[k*4+j] - m[j*4+k] ) * recip;//w out[j] = ( m[j*4+i] + m[i*4+j] ) * recip; out[k] = ( m[k*4+i] + m[i*4+k] ) * recip; } quat.normalize(out,out); } //col according to common matrix notation, here are stored as rows vec3.getMat3Column = function(out, m, index ) { out[0] = m[index*3]; out[1] = m[index*3 + 1]; out[2] = m[index*3 + 2]; return out; } mat3.setColumn = function(out, v, index ) { out[index*3] = v[0]; out[index*3+1] = v[1]; out[index*3+2] = v[2]; return out; } //http://matthias-mueller-fischer.ch/publications/stablePolarDecomp.pdf //reusing the previous quaternion as an indicator to keep perpendicularity quat.fromMat3AndQuat = (function(){ var temp_mat3 = mat3.create(); var temp_quat = quat.create(); var Rcol0 = vec3.create(); var Rcol1 = vec3.create(); var Rcol2 = vec3.create(); var Acol0 = vec3.create(); var Acol1 = vec3.create(); var Acol2 = vec3.create(); var RAcross0 = vec3.create(); var RAcross1 = vec3.create(); var RAcross2 = vec3.create(); var omega = vec3.create(); var axis = mat3.create(); return function( q, A, max_iter ) { max_iter = max_iter || 25; for (var iter = 0; iter < max_iter; ++iter) { var R = mat3.fromQuat( temp_mat3, q ); vec3.getMat3Column(Rcol0,R,0); vec3.getMat3Column(Rcol1,R,1); vec3.getMat3Column(Rcol2,R,2); vec3.getMat3Column(Acol0,A,0); vec3.getMat3Column(Acol1,A,1); vec3.getMat3Column(Acol2,A,2); vec3.cross( RAcross0, Rcol0, Acol0 ); vec3.cross( RAcross1, Rcol1, Acol1 ); vec3.cross( RAcross2, Rcol2, Acol2 ); vec3.add( omega, RAcross0, RAcross1 ); vec3.add( omega, omega, RAcross2 ); var d = 1.0 / Math.abs( vec3.dot(Rcol0,Acol0) + vec3.dot(Rcol1,Acol1) + vec3.dot(Rcol2,Acol2) ) + 1.0e-9; vec3.scale( omega, omega, d ); var w = vec3.length(omega); if (w < 1.0e-9) break; vec3.scale(omega,omega,1/w); //normalize quat.setAxisAngle( temp_quat, omega, w ); quat.mul( q, temp_quat, q ); quat.normalize(q,q); } return q; }; })(); //http://number-none.com/product/IK%20with%20Quaternion%20Joint%20Limits/ quat.rotateToFrom = (function(){ var tmp = vec3.create(); return function(out, v1, v2) { out = out || quat.create(); var axis = vec3.cross(tmp, v1, v2); var dot = vec3.dot(v1, v2); if( dot < -1 + 0.01){ out[0] = 0; out[1] = 1; out[2] = 0; out[3] = 0; return out; } out[0] = axis[0] * 0.5; out[1] = axis[1] * 0.5; out[2] = axis[2] * 0.5; out[3] = (1 + dot) * 0.5; quat.normalize(out, out); return out; } })(); quat.lookAt = (function(){ var axis = vec3.create(); return function( out, forwardVector, up ) { var dot = vec3.dot( vec3.FRONT, forwardVector ); if ( Math.abs( dot - (-1.0)) < 0.000001 ) { out.set( vec3.UP ); out[3] = Math.PI; return out; } if ( Math.abs(dot - 1.0) < 0.000001 ) { return quat.identity( out ); } var rotAngle = Math.acos( dot ); vec3.cross( axis, vec3.FRONT, forwardVector ); vec3.normalize( axis, axis ); quat.setAxisAngle( out, axis, rotAngle ); return out; } })(); /** * @namespace GL */ /** * Indexer used to reuse vertices among a mesh * @class Indexer * @constructor */ GL.Indexer = function Indexer() { this.unique = []; this.indices = []; this.map = {}; } GL.Indexer.prototype = { add: function(obj) { var key = JSON.stringify(obj); if (!(key in this.map)) { this.map[key] = this.unique.length; this.unique.push(obj); } return this.map[key]; } }; /** * A data buffer to be stored in the GPU * @class Buffer * @constructor * @param {Number} target gl.ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER * @param {ArrayBufferView} data the data in typed-array format * @param {number} spacing number of numbers per component (3 per vertex, 2 per uvs...), default 3 * @param {enum} stream_type default gl.STATIC_DRAW (other: gl.DYNAMIC_DRAW, gl.STREAM_DRAW */ GL.Buffer = function Buffer( target, data, spacing, stream_type, gl ) { if(GL.debug) console.log("GL.Buffer created"); if(gl !== null) gl = gl || global.gl; this.gl = gl; this.buffer = null; //webgl buffer this.target = target; //GL.ARRAY_BUFFER, GL.ELEMENT_ARRAY_BUFFER this.attribute = null; //name of the attribute in the shader ("a_vertex","a_normal","a_coord",...) //optional this.data = data; this.spacing = spacing || 3; if(this.data && this.gl) this.upload(stream_type); } /** * binds the buffer to a attrib location * @method bind * @param {number} location the location of the shader (from shader.attributes[ name ]) */ GL.Buffer.prototype.bind = function( location, gl ) { gl = gl || this.gl; gl.bindBuffer( gl.ARRAY_BUFFER, this.buffer ); gl.enableVertexAttribArray( location ); gl.vertexAttribPointer( location, this.spacing, this.buffer.gl_type, false, 0, 0); } /** * unbinds the buffer from an attrib location * @method unbind * @param {number} location the location of the shader */ GL.Buffer.prototype.unbind = function( location, gl ) { gl = gl || this.gl; gl.disableVertexAttribArray( location ); } /** * Applies an action to every vertex in this buffer * @method forEach * @param {function} callback to be called for every vertex (or whatever is contained in the buffer) */ GL.Buffer.prototype.forEach = function(callback) { var d = this.data; for (var i = 0, s = this.spacing, l = d.length; i < l; i += s) { callback(d.subarray(i,i+s),i); } return this; //to concatenate } /** * Applies a mat4 transform to every triplets in the buffer (assuming they are points) * No upload is performed (to ensure efficiency in case there are several operations performed) * @method applyTransform * @param {mat4} mat */ GL.Buffer.prototype.applyTransform = function(mat) { var d = this.data; for (var i = 0, s = this.spacing, l = d.length; i < l; i += s) { var v = d.subarray(i,i+s); vec3.transformMat4(v,v,mat); } return this; //to concatenate } /** * Uploads the buffer data (stored in this.data) to the GPU * @method upload * @param {number} stream_type default gl.STATIC_DRAW (other: gl.DYNAMIC_DRAW, gl.STREAM_DRAW */ GL.Buffer.prototype.upload = function( stream_type ) { //default gl.STATIC_DRAW (other: gl.DYNAMIC_DRAW, gl.STREAM_DRAW ) var spacing = this.spacing || 3; //default spacing var gl = this.gl; if(!gl) return; if(!this.data) throw("No data supplied"); var data = this.data; if(!data.buffer) throw("Buffers must be typed arrays"); //I store some stuff inside the WebGL buffer instance, it is supported this.buffer = this.buffer || gl.createBuffer(); if(!this.buffer) return; //if the context is lost... this.buffer.length = data.length; this.buffer.spacing = spacing; //store the data format switch( data.constructor ) { case Int8Array: this.buffer.gl_type = gl.BYTE; break; case Uint8ClampedArray: case Uint8Array: this.buffer.gl_type = gl.UNSIGNED_BYTE; break; case Int16Array: this.buffer.gl_type = gl.SHORT; break; case Uint16Array: this.buffer.gl_type = gl.UNSIGNED_SHORT; break; case Int32Array: this.buffer.gl_type = gl.INT; break; case Uint32Array: this.buffer.gl_type = gl.UNSIGNED_INT; break; case Float32Array: this.buffer.gl_type = gl.FLOAT; break; default: throw("unsupported buffer type"); } if(this.target == gl.ARRAY_BUFFER && ( this.buffer.gl_type == gl.INT || this.buffer.gl_type == gl.UNSIGNED_INT )) { console.warn("WebGL does not support UINT32 or INT32 as vertex buffer types, converting to FLOAT"); this.buffer.gl_type = gl.FLOAT; data = new Float32Array(data); } gl.bindBuffer(this.target, this.buffer); gl.bufferData(this.target, data , stream_type || this.stream_type || gl.STATIC_DRAW); }; //legacy GL.Buffer.prototype.compile = GL.Buffer.prototype.upload; /** * Assign data to buffer and uploads it (it allows range) * @method setData * @param {ArrayBufferView} data in Float32Array format usually * @param {number} offset offset in bytes */ GL.Buffer.prototype.setData = function( data, offset ) { if(!data.buffer) throw("Data must be typed array"); offset = offset || 0; if(!this.data) { this.data = data; this.upload(); return; } else if( this.data.length < data.length ) throw("buffer is not big enough, you cannot set data to a smaller buffer"); if(this.data != data) { if(this.data.length == data.length) { this.data.set( data ); this.upload(); return; } //upload just part of it var new_data_view = new Uint8Array( data.buffer, data.buffer.byteOffset, data.buffer.byteLength ); var data_view = new Uint8Array( this.data.buffer ); data_view.set( new_data_view, offset ); this.uploadRange( offset, new_data_view.length ); } }; /** * Uploads part of the buffer data (stored in this.data) to the GPU * @method uploadRange * @param {number} start offset in bytes * @param {number} size sizes in bytes */ GL.Buffer.prototype.uploadRange = function(start, size) { if(!this.data) throw("No data stored in this buffer"); var data = this.data; if(!data.buffer) throw("Buffers must be typed arrays"); //cut fragment to upload (no way to avoid GC here, no function to specify the size in WebGL 1.0, but there is one in WebGL 2.0) var view = new Uint8Array( this.data.buffer, start, size ); var gl = this.gl; gl.bindBuffer(this.target, this.buffer); gl.bufferSubData(this.target, start, view ); }; /** * Clones one buffer (it allows to share the same data between both buffers) * @method clone * @param {boolean} share if you want that both buffers share the same data (default false) * return {GL.Buffer} buffer cloned */ GL.Buffer.prototype.clone = function(share) { var buffer = new GL.Buffer(); if(share) { for(var i in this) buffer[i] = this[i]; } else { if(this.target) buffer.target = this.target; if(this.gl) buffer.gl = this.gl; if(this.spacing) buffer.spacing = this.spacing; if(this.data) //clone data { buffer.data = new global[ this.data.constructor ]( this.data ); buffer.upload(); } } return buffer; } GL.Buffer.prototype.toJSON = function() { if(!this.data) { console.error("cannot serialize a mesh without data"); return null; } return { data_type: getClassName(this.data), data: this.data.toJSON(), target: this.target, attribute: this.attribute, spacing: this.spacing }; } GL.Buffer.prototype.fromJSON = function(o) { var data_type = global[ o.data_type ] || Float32Array; this.data = new data_type( o.data ); //cloned this.target = o.target; this.spacing = o.spacing || 3; this.attribute = o.attribute; this.upload( GL.STATIC_DRAW ); } /** * Deletes the content from the GPU and destroys the handler * @method delete */ GL.Buffer.prototype.delete = function() { var gl = this.gl; gl.deleteBuffer( this.buffer ); this.buffer = null; } /** * Base class for meshes, it wraps several buffers and some global info like the bounding box * @class Mesh * @param {Object} vertexBuffers object with all the vertex streams * @param {Object} indexBuffers object with all the indices streams * @param {Object} options * @param {WebGLContext} gl [Optional] gl context where to create the mesh * @constructor */ global.Mesh = GL.Mesh = function Mesh( vertexbuffers, indexbuffers, options, gl ) { if(GL.debug) console.log("GL.Mesh created"); if( gl !== null ) { gl = gl || global.gl; this.gl = gl; } //used to avoid problems with resources moving between different webgl context this._context_id = gl.context_id; this.vertexBuffers = {}; this.indexBuffers = {}; //here you can store extra info, like groups, which is an array of { name, start, length, material } this.info = { groups: [] }; this._bounding = BBox.create(); //here you can store a AABB in BBox format if(vertexbuffers || indexbuffers) this.addBuffers( vertexbuffers, indexbuffers, options ? options.stream_type : null ); if(options) for(var i in options) this[i] = options[i]; }; Mesh.common_buffers = { "vertices": { spacing:3, attribute: "a_vertex"}, "vertices2D": { spacing:2, attribute: "a_vertex2D"}, "normals": { spacing:3, attribute: "a_normal"}, "coords": { spacing:2, attribute: "a_coord"}, "coords1": { spacing:2, attribute: "a_coord1"}, "coords2": { spacing:2, attribute: "a_coord2"}, "colors": { spacing:4, attribute: "a_color"}, "tangents": { spacing:3, attribute: "a_tangent"}, "bone_indices": { spacing:4, attribute: "a_bone_indices", type: Uint8Array }, "weights": { spacing:4, attribute: "a_weights"}, "extra": { spacing:1, attribute: "a_extra"}, "extra2": { spacing:2, attribute: "a_extra2"}, "extra3": { spacing:3, attribute: "a_extra3"}, "extra4": { spacing:4, attribute: "a_extra4"} }; Mesh.default_datatype = Float32Array; Object.defineProperty( Mesh.prototype, "bounding", { set: function(v) { if(!v) return; if(v.length < 13) throw("Bounding must use the BBox bounding format of 13 floats: center, halfsize, min, max, radius"); this._bounding.set(v); }, get: function() { return this._bounding; } }); /** * Adds buffer to mesh * @method addBuffer * @param {string} name * @param {Buffer} buffer */ Mesh.prototype.addBuffer = function(name, buffer) { if(buffer.target == gl.ARRAY_BUFFER) this.vertexBuffers[name] = buffer; else this.indexBuffers[name] = buffer; if(!buffer.attribute) { var info = GL.Mesh.common_buffers[name]; if(info) buffer.attribute = info.attribute; } } /** * Adds vertex and indices buffers to a mesh * @method addBuffers * @param {Object} vertexBuffers object with all the vertex streams * @param {Object} indexBuffers object with all the indices streams * @param {enum} stream_type default gl.STATIC_DRAW (other: gl.DYNAMIC_DRAW, gl.STREAM_DRAW ) */ Mesh.prototype.addBuffers = function( vertexbuffers, indexbuffers, stream_type ) { var num_vertices = 0; if(this.vertexBuffers["vertices"]) num_vertices = this.vertexBuffers["vertices"].data.length / 3; for(var i in vertexbuffers) { var data = vertexbuffers[i]; if(!data) continue; if( data.constructor == GL.Buffer ) { data = data.data; } else if( typeof(data[0]) != "number") //linearize: (transform Arrays in typed arrays) { var newdata = []; for (var j = 0, chunk = 10000; j < data.length; j += chunk) { newdata = Array.prototype.concat.apply(newdata, data.slice(j, j + chunk)); } data = newdata; } var stream_info = GL.Mesh.common_buffers[i]; //cast to typed float32 if no type is specified if(data.constructor === Array) { var datatype = GL.Mesh.default_datatype; if(stream_info && stream_info.type) datatype = stream_info.type; data = new datatype( data ); } //compute spacing if(i == "vertices") num_vertices = data.length / 3; var spacing = data.length / num_vertices; if(stream_info && stream_info.spacing) spacing = stream_info.spacing; //add and upload var attribute = "a_" + i; if(stream_info && stream_info.attribute) attribute = stream_info.attribute; if( this.vertexBuffers[i] ) this.updateVertexBuffer( i, attribute, spacing, data, stream_type ); else this.createVertexBuffer( i, attribute, spacing, data, stream_type ); } if(indexbuffers) for(var i in indexbuffers) { var data = indexbuffers[i]; if(!data) continue; if( data.constructor == GL.Buffer ) { data = data.data; } if( typeof(data[0]) != "number") //linearize { newdata = []; for (var i = 0, chunk = 10000; i < data.length; i += chunk) { newdata = Array.prototype.concat.apply(newdata, data.slice(i, i + chunk)); } data = newdata; } //cast to typed if(data.constructor === Array) { var datatype = Uint16Array; if(num_vertices > 256*256) datatype = Uint32Array; data = new datatype( data ); } this.createIndexBuffer( i, data ); } } /** * Creates a new empty buffer and attachs it to this mesh * @method createVertexBuffer * @param {String} name "vertices","normals"... * @param {String} attribute name of the stream in the shader "a_vertex","a_normal",... [optional, if omitted is used the common_buffers] * @param {number} spacing components per vertex [optional, if ommited is used the common_buffers, if not found then uses 3 ] * @param {ArrayBufferView} buffer_data the data in typed array format [optional, if ommited it created an empty array of getNumVertices() * spacing] * @param {enum} stream_type [optional, default = gl.STATIC_DRAW (other: gl.DYNAMIC_DRAW, gl.STREAM_DRAW ) ] */ Mesh.prototype.createVertexBuffer = function( name, attribute, buffer_spacing, buffer_data, stream_type ) { var common = GL.Mesh.common_buffers[name]; //generic info about a buffer with the same name if (!attribute && common) attribute = common.attribute; if (!attribute) throw("Buffer added to mesh without attribute name"); if (!buffer_spacing && common) { if(common && common.spacing) buffer_spacing = common.spacing; else buffer_spacing = 3; } if(!buffer_data) { var num = this.getNumVertices(); if(!num) throw("Cannot create an empty buffer in a mesh without vertices (vertices are needed to know the size)"); buffer_data = new (GL.Mesh.default_datatype)(num * buffer_spacing); } if(!buffer_data.buffer) throw("Buffer data MUST be typed array"); //used to ensure the buffers are held in the same gl context as the mesh var buffer = this.vertexBuffers[name] = new GL.Buffer( gl.ARRAY_BUFFER, buffer_data, buffer_spacing, stream_type, this.gl ); buffer.name = name; buffer.attribute = attribute; return buffer; } /** * Updates a vertex buffer * @method updateVertexBuffer * @param {String} name the name of the buffer * @param {String} attribute the name of the attribute in the shader * @param {number} spacing number of numbers per component (3 per vertex, 2 per uvs...), default 3 * @param {*} data the array with all the data * @param {enum} stream_type default gl.STATIC_DRAW (other: gl.DYNAMIC_DRAW, gl.STREAM_DRAW */ Mesh.prototype.updateVertexBuffer = function( name, attribute, buffer_spacing, buffer_data, stream_type ) { var buffer = this.vertexBuffers[name]; if(!buffer) { console.log("buffer not found: ",name); return; } if(!buffer_data.length) return; buffer.attribute = attribute; buffer.spacing = buffer_spacing; buffer.data = buffer_data; buffer.upload( stream_type ); } /** * Removes a vertex buffer from the mesh * @method removeVertexBuffer * @param {String} name "vertices","normals"... * @param {Boolean} free if you want to remove the data from the GPU */ Mesh.prototype.removeVertexBuffer = function(name, free) { var buffer = this.vertexBuffers[name]; if(!buffer) return; if(free) buffer.delete(); delete this.vertexBuffers[name]; } /** * Returns a vertex buffer * @method getVertexBuffer * @param {String} name of vertex buffer * @return {Buffer} the buffer */ Mesh.prototype.getVertexBuffer = function(name) { return this.vertexBuffers[name]; } /** * Creates a new empty index buffer and attachs it to this mesh * @method createIndexBuffer * @param {String} name * @param {Typed array} data * @param {enum} stream_type gl.STATIC_DRAW, gl.DYNAMIC_DRAW, gl.STREAM_DRAW */ Mesh.prototype.createIndexBuffer = function(name, buffer_data, stream_type) { //(target, data, spacing, stream_type, gl) //cast to typed if(buffer_data.constructor === Array) { var datatype = Uint16Array; var vertices = this.vertexBuffers["vertices"]; if(vertices) { var num_vertices = vertices.data.length / 3; if(num_vertices > 256*256) datatype = Uint32Array; buffer_data = new datatype( buffer_data ); } } var buffer = this.indexBuffers[name] = new GL.Buffer(gl.ELEMENT_ARRAY_BUFFER, buffer_data, 0, stream_type, this.gl ); return buffer; } /** * Returns a vertex buffer * @method getBuffer * @param {String} name of vertex buffer * @return {Buffer} the buffer */ Mesh.prototype.getBuffer = function(name) { return this.vertexBuffers[name]; } /** * Returns a index buffer * @method getIndexBuffer * @param {String} name of index buffer * @return {Buffer} the buffer */ Mesh.prototype.getIndexBuffer = function(name) { return this.indexBuffers[name]; } /** * Removes an index buffer from the mesh * @method removeIndexBuffer * @param {String} name "vertices","normals"... * @param {Boolean} free if you want to remove the data from the GPU */ Mesh.prototype.removeIndexBuffer = function(name, free) { var buffer = this.indexBuffers[name]; if(!buffer) return; if(free) buffer.delete(); delete this.indexBuffers[name]; } /** * Uploads data inside buffers to VRAM. * @method upload * @param {number} buffer_type gl.STATIC_DRAW, gl.DYNAMIC_DRAW, gl.STREAM_DRAW */ Mesh.prototype.upload = function(buffer_type) { for (var attribute in this.vertexBuffers) { var buffer = this.vertexBuffers[attribute]; //buffer.data = this[buffer.name]; buffer.upload(buffer_type); } for (var name in this.indexBuffers) { var buffer = this.indexBuffers[name]; //buffer.data = this[name]; buffer.upload(); } } //LEGACY, plz remove Mesh.prototype.compile = Mesh.prototype.upload; Mesh.prototype.deleteBuffers = function() { for(var i in this.vertexBuffers) { var buffer = this.vertexBuffers[i]; buffer.delete(); } this.vertexBuffers = {}; for(var i in this.indexBuffers) { var buffer = this.indexBuffers[i]; buffer.delete(); } this.indexBuffers = {}; } Mesh.prototype.delete = Mesh.prototype.deleteBuffers; Mesh.prototype.bindBuffers = function( shader ) { // enable attributes as necessary. for (var name in this.vertexBuffers) { var buffer = this.vertexBuffers[ name ]; var attribute = buffer.attribute || name; var location = shader.attributes[ attribute ]; if (location == null || !buffer.buffer) continue; gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buffer); gl.enableVertexAttribArray(location); gl.vertexAttribPointer(location, buffer.buffer.spacing, buffer.buffer.gl_type, false, 0, 0); } } Mesh.prototype.unbindBuffers = function( shader ) { // disable attributes for (var name in this.vertexBuffers) { var buffer = this.vertexBuffers[ name ]; var attribute = buffer.attribute || name; var location = shader.attributes[ attribute ]; if (location == null || !buffer.buffer) continue; //ignore this buffer gl.disableVertexAttribArray( shader.attributes[attribute] ); } } /** * Creates a clone of the mesh, the datarrays are cloned too * @method clone */ Mesh.prototype.clone = function( gl ) { var gl = gl || global.gl; var vbs = {}; var ibs = {}; for(var i in this.vertexBuffers) { var b = this.vertexBuffers[i]; vbs[i] = new b.data.constructor( b.data ); //clone } for(var i in this.indexBuffers) { var b = this.indexBuffers[i]; ibs[i] = new b.data.constructor( b.data ); //clone } return new GL.Mesh( vbs, ibs, undefined, gl ); } /** * Creates a clone of the mesh, but the data-arrays are shared between both meshes (useful for sharing a mesh between contexts) * @method clone */ Mesh.prototype.cloneShared = function( gl ) { var gl = gl || global.gl; return new GL.Mesh( this.vertexBuffers, this.indexBuffers, undefined, gl ); } /** * Creates an object with the info of the mesh (useful to transfer to workers) * @method toObject */ Mesh.prototype.toObject = function() { var vbs = {}; var ibs = {}; for(var i in this.vertexBuffers) { var b = this.vertexBuffers[i]; vbs[i] = { spacing: b.spacing, data: new b.data.constructor( b.data ) //clone }; } for(var i in this.indexBuffers) { var b = this.indexBuffers[i]; ibs[i] = { data: new b.data.constructor( b.data ) //clone } } return { vertexBuffers: vbs, indexBuffers: ibs, info: this.info ? cloneObject( this.info ) : null, bounding: this._bounding.toJSON() }; } Mesh.prototype.toJSON = function() { var r = { vertexBuffers: {}, indexBuffers: {}, info: this.info ? cloneObject( this.info ) : null, bounding: this._bounding.toJSON() }; for(var i in this.vertexBuffers) r.vertexBuffers[i] = this.vertexBuffers[i].toJSON(); for(var i in this.indexBuffers) r.indexBuffers[i] = this.indexBuffers[i].toJSON(); return r; } Mesh.prototype.fromJSON = function(o) { this.vertexBuffers = {}; this.indexBuffers = {}; for(var i in o.vertexBuffers) { if(!o.vertexBuffers[i]) continue; var buffer = new GL.Buffer(); buffer.fromJSON( o.vertexBuffers[i] ); if(!buffer.attribute && GL.Mesh.common_buffers[i]) buffer.attribute = GL.Mesh.common_buffers[i].attribute; this.vertexBuffers[i] = buffer; } for(var i in o.indexBuffers) { if(!o.indexBuffers[i]) continue; var buffer = new GL.Buffer(); buffer.fromJSON( o.indexBuffers[i] ); this.indexBuffers[i] = buffer; } if(o.info) this.info = cloneObject( o.info ); if(o.bounding) this.bounding = o.bounding; //setter does the job } /** * Computes some data about the mesh * @method generateMetadata */ Mesh.prototype.generateMetadata = function() { var metadata = {}; var vertices = this.vertexBuffers["vertices"].data; var triangles = this.indexBuffers["triangles"].data; metadata.vertices = vertices.length / 3; if(triangles) metadata.faces = triangles.length / 3; else metadata.faces = vertices.length / 9; metadata.indexed = !!this.metadata.faces; this.metadata = metadata; } //never tested /* Mesh.prototype.draw = function(shader, mode, range_start, range_length) { if(range_length == 0) return; // Create and enable attribute pointers as necessary. var length = 0; for (var attribute in this.vertexBuffers) { var buffer = this.vertexBuffers[attribute]; var location = shader.attributes[attribute] || gl.getAttribLocation(shader.program, attribute); if (location == -1 || !buffer.buffer) continue; shader.attributes[attribute] = location; gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buffer); gl.enableVertexAttribArray(location); gl.vertexAttribPointer(location, buffer.buffer.spacing, gl.FLOAT, false, 0, 0); length = buffer.buffer.length / buffer.buffer.spacing; } //range rendering var offset = 0; if(arguments.length > 3) //render a polygon range offset = range_start * (this.indexBuffer ? this.indexBuffer.constructor.BYTES_PER_ELEMENT : 1); //in bytes (Uint16 == 2 bytes) if(arguments.length > 4) length = range_length; else if (this.indexBuffer) length = this.indexBuffer.buffer.length - offset; // Disable unused attribute pointers. for (var attribute in shader.attributes) { if (!(attribute in this.vertexBuffers)) { gl.disableVertexAttribArray(shader.attributes[attribute]); } } // Draw the geometry. if (length && (!this.indexBuffer || indexBuffer.buffer)) { if (this.indexBuffer) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer.buffer); gl.drawElements(mode, length, gl.UNSIGNED_SHORT, offset); } else { gl.drawArrays(mode, offset, length); } } return this; } */ /** * Creates a new index stream with wireframe * @method computeWireframe */ Mesh.prototype.computeWireframe = function() { var index_buffer = this.indexBuffers["triangles"]; var vertices = this.vertexBuffers["vertices"].data; var num_vertices = (vertices.length/3); if(!index_buffer) //unindexed { var num_triangles = num_vertices / 3; var buffer = num_vertices > 256*256 ? new Uint32Array( num_triangles * 6 ) : new Uint16Array( num_triangles * 6 ); for(var i = 0; i < num_vertices; i += 3) { buffer[i*2] = i; buffer[i*2+1] = i+1; buffer[i*2+2] = i+1; buffer[i*2+3] = i+2; buffer[i*2+4] = i+2; buffer[i*2+5] = i; } } else //indexed { var data = index_buffer.data; var indexer = new GL.Indexer(); for (var i = 0; i < data.length; i+=3) { var t = data.subarray(i,i+3); for (var j = 0; j < t.length; j++) { var a = t[j], b = t[(j + 1) % t.length]; indexer.add([Math.min(a, b), Math.max(a, b)]); } } //linearize var unique = indexer.unique; var buffer = num_vertices > 256*256 ? new Uint32Array( unique.length * 2 ) : new Uint16Array( unique.length * 2 ); for(var i = 0, l = unique.length; i < l; ++i) buffer.set(unique[i],i*2); } //create stream this.createIndexBuffer('wireframe', buffer); return this; } /** * Multiplies every normal by -1 and uploads it * @method flipNormals * @param {enum} stream_type default gl.STATIC_DRAW (other: gl.DYNAMIC_DRAW, gl.STREAM_DRAW) */ Mesh.prototype.flipNormals = function( stream_type ) { var normals_buffer = this.vertexBuffers["normals"]; if(!normals_buffer) return; var data = normals_buffer.data; var l = data.length; for(var i = 0; i < l; ++i) data[i] *= -1; normals_buffer.upload( stream_type ); //reverse indices too if( !this.indexBuffers["triangles"] ) this.computeIndices(); //create indices var triangles_buffer = this.indexBuffers["triangles"]; var data = triangles_buffer.data; var l = data.length; for(var i = 0; i < l; i += 3) { var tmp = data[i]; data[i] = data[i+1]; data[i+1] = tmp; //the [i+2] stays the same } triangles_buffer.upload( stream_type ); } /** * Compute indices for a mesh where vertices are shared * @method computeIndices */ Mesh.prototype.computeIndices = function() { //cluster by distance var new_vertices = []; var new_normals = []; var new_coords = []; var indices = []; var old_vertices_buffer = this.vertexBuffers["vertices"]; var old_normals_buffer = this.vertexBuffers["normals"]; var old_coords_buffer = this.vertexBuffers["coords"]; var old_vertices_data = old_vertices_buffer.data; var old_normals_data = null; if( old_normals_buffer ) old_normals_data = old_normals_buffer.data; var old_coords_data = null; if( old_coords_buffer ) old_coords_data = old_coords_buffer.data; var indexer = {}; var l = old_vertices_data.length / 3; for(var i = 0; i < l; ++i) { var v = old_vertices_data.subarray( i*3,(i+1)*3 ); var key = (v[0] * 1000)|0; //search in new_vertices var j = 0; var candidates = indexer[key]; if(candidates) { var l2 = candidates.length; for(; j < l2; j++) { var v2 = new_vertices[ candidates[j] ]; //same vertex if( vec3.sqrDist( v, v2 ) < 0.01 ) { indices.push(j); break; } } } /* var l2 = new_vertices.length; for(var j = 0; j < l2; j++) { //same vertex if( vec3.sqrDist( v, new_vertices[j] ) < 0.001 ) { indices.push(j); break; } } */ if(candidates && j != l2) continue; var index = j; new_vertices.push(v); if( indexer[ key ] ) indexer[ key ].push( index ); else indexer[ key ] = [ index ]; if(old_normals_data) new_normals.push( old_normals_data.subarray(i*3, (i+1)*3) ); if(old_coords_data) new_coords.push( old_coords_data.subarray(i*2, (i+1)*2) ); indices.push(index); } this.vertexBuffers = {}; //erase all //new buffers this.createVertexBuffer( 'vertices', GL.Mesh.common_buffers["vertices"].attribute, 3, linearizeArray( new_vertices ) ); if(old_normals_data) this.createVertexBuffer( 'normals', GL.Mesh.common_buffers["normals"].attribute, 3, linearizeArray( new_normals ) ); if(old_coords_data) this.createVertexBuffer( 'coords', GL.Mesh.common_buffers["coords"].attribute, 2, linearizeArray( new_coords ) ); this.createIndexBuffer( "triangles", indices ); } /** * Breaks the indices * @method explodeIndices */ Mesh.prototype.explodeIndices = function( buffer_name ) { buffer_name = buffer_name || "triangles"; var indices_buffer = this.getIndexBuffer( buffer_name ); if(!indices_buffer) return; var indices = indices_buffer.data; var new_buffers = {}; for(var i in this.vertexBuffers) { var info = GL.Mesh.common_buffers[i]; new_buffers[i] = new (info.type || Float32Array)( info.spacing * indices.length ); } for(var i = 0, l = indices.length; i < l; ++i) { var index = indices[i]; for(var j in this.vertexBuffers) { var buffer = this.vertexBuffers[j]; var info = GL.Mesh.common_buffers[j]; var spacing = buffer.spacing || info.spacing; var new_buffer = new_buffers[j]; new_buffer.set( buffer.data.subarray( index*spacing, index*spacing + spacing ), i*spacing ); } } /* //cluster by distance var new_vertices = new Float32Array(indices.length * 3); var new_normals = null; var new_coords = null; var old_vertices_buffer = this.vertexBuffers["vertices"]; var old_vertices = old_vertices_buffer.data; var old_normals_buffer = this.vertexBuffers["normals"]; var old_normals = null; if(old_normals_buffer) { old_normals = old_normals_buffer.data; new_normals = new Float32Array(indices.length * 3); } var old_coords_buffer = this.vertexBuffers["coords"]; var old_coords = null; if( old_coords_buffer ) { old_coords = old_coords_buffer.data; new_coords = new Float32Array(indices.length * 2); } for(var i = 0, l = indices.length; i < l; ++i) { var index = indices[i]; new_vertices.set( old_vertices.subarray( index*3, index*3 + 3 ), i*3 ); if(old_normals) new_normals.set( old_normals.subarray( index*3, index*3 + 3 ), i*3 ); if(old_coords) new_coords.set( old_coords.subarray( index*2, index*2 + 2 ), i*2 ); } //erase all this.vertexBuffers = {}; //new buffers this.createVertexBuffer( 'vertices', GL.Mesh.common_buffers["vertices"].attribute, 3, new_vertices ); if(new_normals) this.createVertexBuffer( 'normals', GL.Mesh.common_buffers["normals"].attribute, 3, new_normals ); if(new_coords) this.createVertexBuffer( 'coords', GL.Mesh.common_buffers["coords"].attribute, 2, new_coords ); */ for(var i in new_buffers) { var old = this.vertexBuffers[i]; this.createVertexBuffer( i, old.attribute, old.spacing, new_buffers[i] ); } delete this.indexBuffers[ buffer_name ]; } /** * Creates a stream with the normals * @method computeNormals * @param {enum} stream_type default gl.STATIC_DRAW (other: gl.DYNAMIC_DRAW, gl.STREAM_DRAW) */ Mesh.prototype.computeNormals = function( stream_type ) { var vertices_buffer = this.vertexBuffers["vertices"]; if(!vertices_buffer) return console.error("Cannot compute normals of a mesh without vertices"); var vertices = this.vertexBuffers["vertices"].data; var num_vertices = vertices.length / 3; //create because it is faster than filling it with zeros var normals = new Float32Array( vertices.length ); var triangles = null; if(this.indexBuffers["triangles"]) triangles = this.indexBuffers["triangles"].data; var temp = GL.temp_vec3; var temp2 = GL.temp2_vec3; var i1,i2,i3,v1,v2,v3,n1,n2,n3; //compute the plane normal var l = triangles ? triangles.length : vertices.length; for (var a = 0; a < l; a+=3) { if(triangles) { i1 = triangles[a]; i2 = triangles[a+1]; i3 = triangles[a+2]; v1 = vertices.subarray(i1*3,i1*3+3); v2 = vertices.subarray(i2*3,i2*3+3); v3 = vertices.subarray(i3*3,i3*3+3); n1 = normals.subarray(i1*3,i1*3+3); n2 = normals.subarray(i2*3,i2*3+3); n3 = normals.subarray(i3*3,i3*3+3); } else { v1 = vertices.subarray(a*3,a*3+3); v2 = vertices.subarray(a*3+3,a*3+6); v3 = vertices.subarray(a*3+6,a*3+9); n1 = normals.subarray(a*3,a*3+3); n2 = normals.subarray(a*3+3,a*3+6); n3 = normals.subarray(a*3+6,a*3+9); } vec3.sub( temp, v2, v1 ); vec3.sub( temp2, v3, v1 ); vec3.cross( temp, temp, temp2 ); vec3.normalize(temp,temp); //save vec3.add( n1, n1, temp ); vec3.add( n2, n2, temp ); vec3.add( n3, n3, temp ); } //normalize if vertices are shared if(triangles) for (var a = 0, l = normals.length; a < l; a+=3) { var n = normals.subarray(a,a+3); vec3.normalize(n,n); } var normals_buffer = this.vertexBuffers["normals"]; if(normals_buffer) { normals_buffer.data = normals; normals_buffer.upload( stream_type ); } else return this.createVertexBuffer('normals', GL.Mesh.common_buffers["normals"].attribute, 3, normals ); return normals_buffer; } /** * Creates a new stream with the tangents * @method computeTangents */ Mesh.prototype.computeTangents = function() { var vertices_buffer = this.vertexBuffers["vertices"]; if(!vertices_buffer) return console.error("Cannot compute tangents of a mesh without vertices"); var normals_buffer = this.vertexBuffers["normals"]; if(!normals_buffer) return console.error("Cannot compute tangents of a mesh without normals"); var uvs_buffer = this.vertexBuffers["coords"]; if(!uvs_buffer) return console.error("Cannot compute tangents of a mesh without uvs"); var triangles_buffer = this.indexBuffers["triangles"]; if(!triangles_buffer) return console.error("Cannot compute tangents of a mesh without indices"); var vertices = vertices_buffer.data; var normals = normals_buffer.data; var uvs = uvs_buffer.data; var triangles = triangles_buffer.data; if(!vertices || !normals || !uvs) return; var num_vertices = vertices.length / 3; var tangents = new Float32Array(num_vertices * 4); //temporary (shared) var tan1 = new Float32Array(num_vertices*3*2); var tan2 = tan1.subarray(num_vertices*3); var a,l; var sdir = vec3.create(); var tdir = vec3.create(); var temp = vec3.create(); var temp2 = vec3.create(); for (a = 0, l = triangles.length; a < l; a+=3) { var i1 = triangles[a]; var i2 = triangles[a+1]; var i3 = triangles[a+2]; var v1 = vertices.subarray(i1*3,i1*3+3); var v2 = vertices.subarray(i2*3,i2*3+3); var v3 = vertices.subarray(i3*3,i3*3+3); var w1 = uvs.subarray(i1*2,i1*2+2); var w2 = uvs.subarray(i2*2,i2*2+2); var w3 = uvs.subarray(i3*2,i3*2+2); var x1 = v2[0] - v1[0]; var x2 = v3[0] - v1[0]; var y1 = v2[1] - v1[1]; var y2 = v3[1] - v1[1]; var z1 = v2[2] - v1[2]; var z2 = v3[2] - v1[2]; var s1 = w2[0] - w1[0]; var s2 = w3[0] - w1[0]; var t1 = w2[1] - w1[1]; var t2 = w3[1] - w1[1]; var r; var den = (s1 * t2 - s2 * t1); if ( Math.abs(den) < 0.000000001 ) r = 0.0; else r = 1.0 / den; vec3.copy(sdir, [(t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r] ); vec3.copy(tdir, [(s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r] ); vec3.add( tan1.subarray( i1*3, i1*3+3), tan1.subarray( i1*3, i1*3+3), sdir); vec3.add( tan1.subarray( i2*3, i2*3+3), tan1.subarray( i2*3, i2*3+3), sdir); vec3.add( tan1.subarray( i3*3, i3*3+3), tan1.subarray( i3*3, i3*3+3), sdir); vec3.add( tan2.subarray( i1*3, i1*3+3), tan2.subarray( i1*3, i1*3+3), tdir); vec3.add( tan2.subarray( i2*3, i2*3+3), tan2.subarray( i2*3, i2*3+3), tdir); vec3.add( tan2.subarray( i3*3, i3*3+3), tan2.subarray( i3*3, i3*3+3), tdir); } for (a = 0, l = vertices.length; a < l; a+=3) { var n = normals.subarray(a,a+3); var t = tan1.subarray(a,a+3); // Gram-Schmidt orthogonalize vec3.subtract(temp, t, vec3.scale(temp, n, vec3.dot(n, t) ) ); vec3.normalize(temp,temp); // Calculate handedness var w = ( vec3.dot( vec3.cross(temp2, n, t), tan2.subarray(a,a+3) ) < 0.0) ? -1.0 : 1.0; tangents.set([temp[0], temp[1], temp[2], w],(a/3)*4); } this.createVertexBuffer('tangents', Mesh.common_buffers["tangents"].attribute, 4, tangents ); } /** * Creates texture coordinates using a triplanar aproximation * @method computeTextureCoordinates */ Mesh.prototype.computeTextureCoordinates = function( stream_type ) { var vertices_buffer = this.vertexBuffers["vertices"]; if(!vertices_buffer) return console.error("Cannot compute uvs of a mesh without vertices"); this.explodeIndices( "triangles" ); vertices_buffer = this.vertexBuffers["vertices"]; var vertices = vertices_buffer.data; var num_vertices = vertices.length / 3; var uvs_buffer = this.vertexBuffers["coords"]; var uvs = new Float32Array( num_vertices * 2 ); var triangles_buffer = this.indexBuffers["triangles"]; var triangles = null; if( triangles_buffer ) triangles = triangles_buffer.data; var plane_normal = vec3.create(); var side1 = vec3.create(); var side2 = vec3.create(); var bbox = this.getBoundingBox(); var bboxcenter = BBox.getCenter( bbox ); var bboxhs = vec3.create(); bboxhs.set( BBox.getHalfsize( bbox ) ); //careful, this is a reference vec3.scale( bboxhs, bboxhs, 2 ); var num = triangles ? triangles.length : vertices.length/3; for (var a = 0; a < num; a+=3) { if(triangles) { var i1 = triangles[a]; var i2 = triangles[a+1]; var i3 = triangles[a+2]; var v1 = vertices.subarray(i1*3,i1*3+3); var v2 = vertices.subarray(i2*3,i2*3+3); var v3 = vertices.subarray(i3*3,i3*3+3); var uv1 = uvs.subarray(i1*2,i1*2+2); var uv2 = uvs.subarray(i2*2,i2*2+2); var uv3 = uvs.subarray(i3*2,i3*2+2); } else { var v1 = vertices.subarray((a)*3,(a)*3+3); var v2 = vertices.subarray((a+1)*3,(a+1)*3+3); var v3 = vertices.subarray((a+2)*3,(a+2)*3+3); var uv1 = uvs.subarray((a)*2,(a)*2+2); var uv2 = uvs.subarray((a+1)*2,(a+1)*2+2); var uv3 = uvs.subarray((a+2)*2,(a+2)*2+2); } vec3.sub(side1, v1, v2 ); vec3.sub(side2, v1, v3 ); vec3.cross( plane_normal, side1, side2 ); //vec3.normalize( plane_normal, plane_normal ); //not necessary plane_normal[0] = Math.abs( plane_normal[0] ); plane_normal[1] = Math.abs( plane_normal[1] ); plane_normal[2] = Math.abs( plane_normal[2] ); if( plane_normal[0] > plane_normal[1] && plane_normal[0] > plane_normal[2]) { //X uv1[0] = (v1[2] - bboxcenter[2]) / bboxhs[2]; uv1[1] = (v1[1] - bboxcenter[1]) / bboxhs[1]; uv2[0] = (v2[2] - bboxcenter[2]) / bboxhs[2]; uv2[1] = (v2[1] - bboxcenter[1]) / bboxhs[1]; uv3[0] = (v3[2] - bboxcenter[2]) / bboxhs[2]; uv3[1] = (v3[1] - bboxcenter[1]) / bboxhs[1]; } else if ( plane_normal[1] > plane_normal[2]) { //Y uv1[0] = (v1[0] - bboxcenter[0]) / bboxhs[0]; uv1[1] = (v1[2] - bboxcenter[2]) / bboxhs[2]; uv2[0] = (v2[0] - bboxcenter[0]) / bboxhs[0]; uv2[1] = (v2[2] - bboxcenter[2]) / bboxhs[2]; uv3[0] = (v3[0] - bboxcenter[0]) / bboxhs[0]; uv3[1] = (v3[2] - bboxcenter[2]) / bboxhs[2]; } else { //Z uv1[0] = (v1[0] - bboxcenter[0]) / bboxhs[0]; uv1[1] = (v1[1] - bboxcenter[1]) / bboxhs[1]; uv2[0] = (v2[0] - bboxcenter[0]) / bboxhs[0]; uv2[1] = (v2[1] - bboxcenter[1]) / bboxhs[1]; uv3[0] = (v3[0] - bboxcenter[0]) / bboxhs[0]; uv3[1] = (v3[1] - bboxcenter[1]) / bboxhs[1]; } } if(uvs_buffer) { uvs_buffer.data = uvs; uvs_buffer.upload( stream_type ); } else this.createVertexBuffer('coords', Mesh.common_buffers["coords"].attribute, 2, uvs ); } /** * Computes the number of vertices * @method getVertexNumber */ Mesh.prototype.getNumVertices = function() { var b = this.vertexBuffers["vertices"]; if(!b) return 0; return b.data.length / b.spacing; } /** * Computes the number of triangles (takes into account indices) * @method getNumTriangles */ Mesh.prototype.getNumTriangles = function() { var indices_buffer = this.getIndexBuffer("triangles"); if(!indices_buffer) return this.getNumVertices() / 3; return indices_buffer.data.length / 3; } /** * Computes bounding information * @method Mesh.computeBoundingBox * @param {typed Array} vertices array containing all the vertices * @param {BBox} bb where to store the bounding box * @param {Array} mask [optional] to specify which vertices must be considered when creating the bbox, used to create BBox of a submesh */ Mesh.computeBoundingBox = function( vertices, bb, mask ) { if(!vertices) return; var start = 0; if(mask) { for(var i = 0; i < mask.length; ++i) if( mask[i] ) { start = i; break; } if(start == mask.length) { console.warn("mask contains only zeros, no vertices marked"); return; } } var min = vec3.clone( vertices.subarray( start*3, start*3 + 3) ); var max = vec3.clone( vertices.subarray( start*3, start*3 + 3) ); var v; for(var i = start*3; i < vertices.length; i+=3) { if( mask && !mask[i/3] ) continue; v = vertices.subarray(i,i+3); vec3.min( min,v, min); vec3.max( max,v, max); } if( isNaN(min[0]) || isNaN(min[1]) || isNaN(min[2]) || isNaN(max[0]) || isNaN(max[1]) || isNaN(max[2]) ) { min[0] = min[1] = min[2] = 0; max[0] = max[1] = max[2] = 0; console.warn("Warning: GL.Mesh has NaN values in vertices"); } var center = vec3.add( vec3.create(), min,max ); vec3.scale( center, center, 0.5); var half_size = vec3.subtract( vec3.create(), max, center ); return BBox.setCenterHalfsize( bb || BBox.create(), center, half_size ); } /** * returns the bounding box, if it is not computed, then computes it * @method getBoundingBox * @return {BBox} bounding box */ Mesh.prototype.getBoundingBox = function() { if(this._bounding) return this._bounding; this.updateBoundingBox(); return this._bounding; } /** * Update bounding information of this mesh * @method updateBoundingBox */ Mesh.prototype.updateBoundingBox = function() { var vertices = this.vertexBuffers["vertices"]; if(!vertices) return; GL.Mesh.computeBoundingBox( vertices.data, this._bounding ); if(this.info && this.info.groups && this.info.groups.length) this.computeGroupsBoundingBoxes(); } /** * Update bounding information for every group submesh * @method computeGroupsBoundingBoxes */ Mesh.prototype.computeGroupsBoundingBoxes = function() { var indices = null; var indices_buffer = this.getIndexBuffer("triangles"); if( indices_buffer ) indices = indices_buffer.data; var vertices_buffer = this.getVertexBuffer("vertices"); if(!vertices_buffer) return false; var vertices = vertices_buffer.data; if(!vertices.length) return false; var groups = this.info.groups; for(var i = 0; i < groups.length; ++i) { var group = groups[i]; group.bounding = group.bounding || BBox.create(); var submesh_vertices = null; if( indices ) { var mask = new Uint8Array( vertices.length / 3 ); var s = group.start; for( var j = 0, l = group.length; j < l; j += 3 ) { mask[ indices[s+j] ] = 1; mask[ indices[s+j+1] ] = 1; mask[ indices[s+j+2] ] = 1; } GL.Mesh.computeBoundingBox( vertices, group.bounding, mask ); } else { submesh_vertices = vertices.subarray( group.start * 3, ( group.start + group.length) * 3 ); GL.Mesh.computeBoundingBox( submesh_vertices, group.bounding ); } } return true; } /** * forces a bounding box to be set * @method setBoundingBox * @param {vec3} center center of the bounding box * @param {vec3} half_size vector from the center to positive corner */ Mesh.prototype.setBoundingBox = function( center, half_size ) { BBox.setCenterHalfsize( this._bounding, center, half_size ); } /** * Remove all local memory from the streams (leaving it only in the VRAM) to save RAM * @method freeData */ Mesh.prototype.freeData = function() { for (var attribute in this.vertexBuffers) { this.vertexBuffers[attribute].data = null; delete this[ this.vertexBuffers[attribute].name ]; //delete from the mesh itself } for (var name in this.indexBuffers) { this.indexBuffers[name].data = null; delete this[ this.indexBuffers[name].name ]; //delete from the mesh itself } } Mesh.prototype.configure = function( o, options ) { var vertex_buffers = {}; var index_buffers = {}; options = options || {}; for(var j in o) { if(!o[j]) continue; if(j == "vertexBuffers" || j == "vertex_buffers") //HACK: legacy code { for(i in o[j]) vertex_buffers[i] = o[j][i]; continue; } if(j == "indexBuffers" || j == "index_buffers") { for(i in o[j]) index_buffers[i] = o[j][i]; continue; } if(j == "indices" || j == "lines" || j == "wireframe" || j == "triangles") index_buffers[j] = o[j]; else if( GL.Mesh.common_buffers[j]) vertex_buffers[j] = o[j]; else //global data like bounding, info of groups, etc { options[j] = o[j]; } } this.addBuffers( vertex_buffers, index_buffers, options.stream_type ); for(var i in options) this[i] = options[i]; if(!options.bounding) this.updateBoundingBox(); } /** * Returns the amount of memory used by this mesh in bytes (sum of all buffers) * @method getMemory * @return {number} bytes */ Mesh.prototype.totalMemory = function() { var num = 0|0; for (var name in this.vertexBuffers) num += this.vertexBuffers[name].data.buffer.byteLength; for (var name in this.indexBuffers) num += this.indexBuffers[name].data.buffer.byteLength; return num; } Mesh.prototype.slice = function(start, length) { var new_vertex_buffers = {}; var indices_buffer = this.indexBuffers["triangles"]; if(!indices_buffer) { console.warn("splice in not indexed not supported yet"); return null; } var indices = indices_buffer.data; var new_triangles = []; var reindex = new Int32Array( indices.length ); reindex.fill(-1); var end = start + length; if(end >= indices.length) end = indices.length; var last_index = 0; for(var j = start; j < end; ++j) { var index = indices[j]; if( reindex[index] != -1 ) { new_triangles.push(reindex[index]); continue; } //new vertex var new_index = last_index++; reindex[index] = new_index; new_triangles.push(new_index); for( var i in this.vertexBuffers ) { var buffer = this.vertexBuffers[i]; var data = buffer.data; var spacing = buffer.spacing; if(!new_vertex_buffers[i]) new_vertex_buffers[i] = []; var new_buffer = new_vertex_buffers[i]; for(var k = 0; k < spacing; ++k) new_buffer.push( data[k + index*spacing] ); } } var new_mesh = new GL.Mesh( new_vertex_buffers, {triangles: new_triangles}, null,gl); new_mesh.updateBoundingBox(); return new_mesh; } /** * returns a low poly version of the mesh that takes much less memory (but breaks tiling of uvs and smoothing groups) * @method simplify * @return {Mesh} simplified mesh */ Mesh.prototype.simplify = function() { //compute bounding box var bb = this.getBoundingBox(); var min = BBox.getMin( bb ); var halfsize = BBox.getHalfsize( bb ); var range = vec3.scale( vec3.create(), halfsize, 2 ); var newmesh = new GL.Mesh(); var temp = vec3.create(); for(var i in this.vertexBuffers) { //take every vertex and normalize it to the bounding box var buffer = this.vertexBuffers[i]; var data = buffer.data; var new_data = new Float32Array( data.length ); if(i == "vertices") { for(var j = 0, l = data.length; j < l; j+=3 ) { var v = data.subarray(j,j+3); vec3.sub( temp, v, min ); vec3.div( temp, temp, range ); temp[0] = Math.round(temp[0] * 256) / 256; temp[1] = Math.round(temp[1] * 256) / 256; temp[2] = Math.round(temp[2] * 256) / 256; vec3.mul( temp, temp, range ); vec3.add( temp, temp, min ); new_data.set( temp, j ); } } else { } newmesh.addBuffer(); } //search for repeated vertices //compute the average normal and coord //reindex the triangles //return simplified mesh } /** * Static method for the class Mesh to create a mesh from a list of common streams * @method Mesh.load * @param {Object} buffers object will all the buffers * @param {Object} options [optional] * @param {Mesh} output_mesh [optional] mesh to store the mesh, otherwise is created * @param {WebGLContext} gl [optional] if omitted, the global.gl is used */ Mesh.load = function( buffers, options, output_mesh, gl ) { options = options || {}; if(options.no_gl) gl = null; var mesh = output_mesh || new GL.Mesh(null,null,null,gl); mesh.configure( buffers, options ); return mesh; } /** * Returns a mesh with all the meshes merged (you can apply transforms individually to every buffer) * @method Mesh.mergeMeshes * @param {Array} meshes array containing object like { mesh:, matrix:, texture_matrix: } * @param {Object} options { only_data: to get the mesh data without uploading it } * @return {GL.Mesh|Object} the mesh in GL.Mesh format or Object format (if options.only_data is true) */ Mesh.mergeMeshes = function( meshes, options ) { options = options || {}; var vertex_buffers = {}; var index_buffers = {}; var offsets = {}; //tells how many positions indices must be offseted var vertex_offsets = []; var current_vertex_offset = 0; var groups = []; //vertex buffers //compute size for(var i = 0; i < meshes.length; ++i) { var mesh_info = meshes[i]; var mesh = mesh_info.mesh; var offset = current_vertex_offset; vertex_offsets.push( offset ); var length = mesh.vertexBuffers["vertices"].data.length / 3; current_vertex_offset += length; for(var j in mesh.vertexBuffers) { if(!vertex_buffers[j]) vertex_buffers[j] = mesh.vertexBuffers[j].data.length; else vertex_buffers[j] += mesh.vertexBuffers[j].data.length; } for(var j in mesh.indexBuffers) { if(!index_buffers[j]) index_buffers[j] = mesh.indexBuffers[j].data.length; else index_buffers[j] += mesh.indexBuffers[j].data.length; } //groups var group = { name: "mesh_" + i, start: offset, length: length, material: "" }; groups.push( group ); } //allocate for(var j in vertex_buffers) { var datatype = options[j]; if(datatype === null) { delete vertex_buffers[j]; continue; } if(!datatype) datatype = Float32Array; vertex_buffers[j] = new datatype( vertex_buffers[j] ); offsets[j] = 0; } for(var j in index_buffers) { index_buffers[j] = new Uint32Array( index_buffers[j] ); offsets[j] = 0; } //store for(var i = 0; i < meshes.length; ++i) { var mesh_info = meshes[i]; var mesh = mesh_info.mesh; var offset = 0; var length = 0; for(var j in mesh.vertexBuffers) { if(!vertex_buffers[j]) continue; if(j == "vertices") length = mesh.vertexBuffers[j].data.length / 3; vertex_buffers[j].set( mesh.vertexBuffers[j].data, offsets[j] ); //apply transform if(mesh_info[ j + "_matrix"] ) { var matrix = mesh_info[ j + "_matrix" ]; if(matrix.length == 16) apply_transform( vertex_buffers[j], offsets[j], mesh.vertexBuffers[j].data.length, matrix ) else if(matrix.length == 9) apply_transform2D( vertex_buffers[j], offsets[j], mesh.vertexBuffers[j].data.length, matrix ) } offsets[j] += mesh.vertexBuffers[j].data.length; } for(var j in mesh.indexBuffers) { index_buffers[j].set( mesh.indexBuffers[j].data, offsets[j] ); apply_offset( index_buffers[j], offsets[j], mesh.indexBuffers[j].data.length, vertex_offsets[i] ); offsets[j] += mesh.indexBuffers[j].data.length; } } //useful functions function apply_transform( array, start, length, matrix ) { var l = start + length; for(var i = start; i < l; i+=3) { var v = array.subarray(i,i+3); vec3.transformMat4( v, v, matrix ); } } function apply_transform2D( array, start, length, matrix ) { var l = start + length; for(var i = start; i < l; i+=2) { var v = array.subarray(i,i+2); vec2.transformMat3( v, v, matrix ); } } function apply_offset( array, start, length, offset ) { var l = start + length; for(var i = start; i < l; ++i) array[i] += offset; } var extra = { info: { groups: groups } }; //return if( typeof(gl) != "undefined" || options.only_data ) return new GL.Mesh( vertex_buffers,index_buffers, extra ); return { vertexBuffers: vertex_buffers, indexBuffers: index_buffers, info: { groups: groups } }; } //Here we store all basic mesh parsers (OBJ, STL) and encoders Mesh.parsers = {}; Mesh.encoders = {}; Mesh.binary_file_formats = {}; //extensions that must be downloaded in binary Mesh.compressors = {}; //used to compress binary meshes Mesh.decompressors = {}; //used to decompress binary meshes /** * Returns am empty mesh and loads a mesh and parses it using the Mesh.parsers, by default only OBJ is supported * @method Mesh.fromOBJ * @param {Array} meshes array containing all the meshes */ Mesh.fromURL = function(url, on_complete, gl, options) { options = options || {}; gl = gl || global.gl; var mesh = new GL.Mesh(undefined,undefined,undefined,gl); mesh.ready = false; var pos = url.lastIndexOf("."); var extension = url.substr(pos+1).toLowerCase(); options.binary = Mesh.binary_file_formats[ extension ]; HttpRequest( url, null, function(data) { mesh.parse( data, extension ); delete mesh["ready"]; if(on_complete) on_complete.call(mesh,mesh, url); }, function(err){ if(on_complete) on_complete(null); }, options ); return mesh; } /** * given some data an information about the format, it search for a parser in Mesh.parsers and tries to extract the mesh information * Only obj supported now * @method parse * @param {*} data could be string or ArrayBuffer * @param {String} format parser file format name (p.e. "obj") * @return {?} depending on the parser */ Mesh.prototype.parse = function( data, format ) { format = format.toLowerCase(); var parser = GL.Mesh.parsers[ format ]; if(parser) return parser.call(null, data, {mesh: this}); throw("GL.Mesh.parse: no parser found for format " + format ); } /** * It returns the mesh data encoded in the format specified * Only obj supported now * @method encode * @param {String} format to encode the data to (p.e. "obj") * @return {?} String with the info */ Mesh.prototype.encode = function( format, options ) { format = format.toLowerCase(); var encoder = GL.Mesh.encoders[ format ]; if(encoder) return encoder.call(null, this, options ); throw("GL.Mesh.encode: no encoder found for format " + format ); } /** * Returns a shared mesh containing a quad to be used when rendering to the screen * Reusing the same quad helps not filling the memory * @method getScreenQuad * @return {GL.Mesh} the screen quad */ Mesh.getScreenQuad = function(gl) { gl = gl || global.gl; var mesh = gl.meshes[":screen_quad"]; if(mesh) return mesh; var vertices = new Float32Array([0,0,0, 1,1,0, 0,1,0, 0,0,0, 1,0,0, 1,1,0 ]); var coords = new Float32Array([0,0, 1,1, 0,1, 0,0, 1,0, 1,1 ]); mesh = new GL.Mesh({ vertices: vertices, coords: coords}, undefined, undefined, gl); return gl.meshes[":screen_quad"] = mesh; } function linearizeArray( array, typed_array_class ) { if(array.constructor === typed_array_class) return array; if(array.constructor !== Array) { typed_array_class = typed_array_class || Float32Array; return new typed_array_class(array); } typed_array_class = typed_array_class || Float32Array; var components = array[0].length; var size = array.length * components; var buffer = new typed_array_class(size); for (var i=0; i < array.length;++i) for(var j=0; j < components; ++j) buffer[i*components + j] = array[i][j]; return buffer; } GL.linearizeArray = linearizeArray; /* BINARY MESHES */ //Add some functions to the classes in LiteGL to allow store in binary GL.Mesh.EXTENSION = "wbin"; GL.Mesh.enable_wbin_compression = true; //this is used when a mesh is dynamic and constantly changes function DynamicMesh( size, normals, coords, colors, gl ) { size = size || 1024; if(GL.debug) console.log("GL.Mesh created"); if( gl !== null ) { gl = gl || global.gl; this.gl = gl; } //used to avoid problems with resources moving between different webgl context this._context_id = gl.context_id; this.vertexBuffers = {}; this.indexBuffers = {}; //here you can store extra info, like groups, which is an array of { name, start, length, material } this.info = { groups: [] }; this._bounding = BBox.create(); //here you can store a AABB in BBox format this.resize( size ); } DynamicMesh.DEFAULT_NORMAL = vec3.fromValues(0,1,0); DynamicMesh.DEFAULT_COORD = vec2.fromValues(0.5,0.5); DynamicMesh.DEFAULT_COLOR = vec4.fromValues(1,1,1,1); DynamicMesh.prototype.resize = function( size ) { var buffers = {}; this._vertex_data = new Float32Array( size * 3 ); buffers.vertices = this._vertex_data; if( normals ) buffers.normals = this._normal_data = new Float32Array( size * 3 ); if( coords ) buffers.coords = this._coord_data = new Float32Array( size * 2 ); if( colors ) buffers.colors = this._color_data = new Float32Array( size * 4 ); this.addBuffers( buffers ); this.current_pos = 0; this.max_size = size; this._must_update = true; } DynamicMesh.prototype.clear = function() { this.current_pos = 0; } DynamicMesh.prototype.addPoint = function( vertex, normal, coord, color ) { if (pos >= this.max_size) { console.warn("DynamicMesh: not enough space, reserve more"); return false; } var pos = this.current_pos++; this._vertex_data.set( vertex, pos*3 ); if(this._normal_data) this._normal_data.set( normal || DynamicMesh.DEFAULT_NORMAL, pos*3 ); if(this._coord_data) this._coord_data.set( coord || DynamicMesh.DEFAULT_COORD, pos*2 ); if(this._color_data) this._color_data.set( color || DynamicMesh.DEFAULT_COLOR, pos*4 ); this._must_update = true; return true; } DynamicMesh.prototype.update = function( force ) { if(!this._must_update && !force) return this.current_pos; this._must_update = false; this.getBuffer("vertices").upload( gl.STREAM_DRAW ); if(this._normal_data) this.getBuffer("normal").upload( gl.STREAM_DRAW ); if(this._coord_data) this.getBuffer("coord").upload( gl.STREAM_DRAW ); if(this._color_data) this.getBuffer("color").upload( gl.STREAM_DRAW ); return this.current_pos; } extendClass( DynamicMesh, Mesh ); /** * @class Mesh */ /** * Returns a planar mesh (you can choose how many subdivisions) * @method Mesh.plane * @param {Object} options valid options: detail, detailX, detailY, size, width, heigth, xz (horizontal plane) */ Mesh.plane = function(options, gl) { options = options || {}; options.triangles = []; var mesh = {}; var detailX = options.detailX || options.detail || 1; var detailY = options.detailY || options.detail || 1; var width = options.width || options.size || 1; var height = options.height || options.size || 1; var xz = options.xz; width *= 0.5; height *= 0.5; var triangles = []; var vertices = []; var coords = []; var normals = []; var N = vec3.fromValues(0,0,1); if(xz) N.set([0,1,0]); for (var y = 0; y <= detailY; y++) { var t = y / detailY; for (var x = 0; x <= detailX; x++) { var s = x / detailX; if(xz) vertices.push((2 * s - 1) * width, 0, -(2 * t - 1) * height); else vertices.push((2 * s - 1) * width, (2 * t - 1) * height, 0); coords.push(s, t); normals.push(N[0],N[1],N[2]); if (x < detailX && y < detailY) { var i = x + y * (detailX + 1); if(xz) //horizontal { triangles.push(i + 1, i + detailX + 1, i); triangles.push(i + 1, i + detailX + 2, i + detailX + 1); } else //vertical { triangles.push(i, i + 1, i + detailX + 1); triangles.push(i + detailX + 1, i + 1, i + detailX + 2); } } } } var bounding = BBox.fromCenterHalfsize( [0,0,0], xz ? [width,0,height] : [width,height,0] ); var mesh_info = {vertices:vertices, normals: normals, coords: coords, triangles: triangles }; return GL.Mesh.load( mesh_info, { bounding: bounding }, gl); }; /** * Returns a 2D Mesh (be careful, stream is vertices2D, used for 2D engines ) * @method Mesh.plane2D */ Mesh.plane2D = function(options, gl) { var vertices = new Float32Array([-1,1, 1,-1, 1,1, -1,1, -1,-1, 1,-1]); var coords = new Float32Array([0,1, 1,0, 1,1, 0,1, 0,0, 1,0]); if(options && options.size) { var s = options.size * 0.5; for(var i = 0; i < vertices.length; ++i) vertices[i] *= s; } return new GL.Mesh( {vertices2D: vertices, coords: coords },null,gl ); }; /** * Returns a point mesh * @method Mesh.point * @param {Object} options no options */ Mesh.point = function(options) { return new GL.Mesh( {vertices: [0,0,0]} ); } /** * Returns a cube mesh * @method Mesh.cube * @param {Object} options valid options: size */ Mesh.cube = function(options, gl) { options = options || {}; var halfsize = (options.size || 1) * 0.5; var buffers = {}; //[[-1,1,-1],[-1,-1,+1],[-1,1,1],[-1,1,-1],[-1,-1,-1],[-1,-1,+1],[1,1,-1],[1,1,1],[1,-1,+1],[1,1,-1],[1,-1,+1],[1,-1,-1],[-1,1,1],[1,-1,1],[1,1,1],[-1,1,1],[-1,-1,1],[1,-1,1],[-1,1,-1],[1,1,-1],[1,-1,-1],[-1,1,-1],[1,-1,-1],[-1,-1,-1],[-1,1,-1],[1,1,1],[1,1,-1],[-1,1,-1],[-1,1,1],[1,1,1],[-1,-1,-1],[1,-1,-1],[1,-1,1],[-1,-1,-1],[1,-1,1],[-1,-1,1]] buffers.vertices = new Float32Array([-1,1,-1,-1,-1,+1, -1,1,1,-1,1,-1, -1,-1,-1,-1,-1,+1, 1,1,-1,1,1,1,1,-1,+1,1,1,-1,1,-1,+1,1,-1,-1,-1,1,1,1,-1,1,1,1,1,-1,1,1,-1,-1,1,1,-1,1,-1,1,-1,1,1,-1,1,-1,-1,-1,1,-1,1,-1,-1,-1,-1,-1,-1,1,-1,1,1,1,1,1,-1,-1,1,-1,-1,1,1,1,1,1,-1,-1,-1,1,-1,-1,1,-1,1,-1,-1,-1,1,-1,1,-1,-1,1]); for(var i = 0, l = buffers.vertices.length; i < l; ++i) buffers.vertices[i] *= halfsize; //[[-1,0,0],[-1,0,0],[-1,0,0],[-1,0,0],[-1,0,0],[-1,0,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0],[0,0,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1],[0,0,-1],[0,0,-1],[0,0,-1],[0,0,-1],[0,0,-1],[0,0,-1],[0,1,0],[0,1,0],[0,1,0],[0,1,0],[0,1,0],[0,1,0],[0,-1,0],[0,-1,0],[0,-1,0],[0,-1,0],[0,-1,0],[0,-1,0]] //[[0,1],[1,0],[1,1],[0,1],[0,0],[1,0],[1,1],[0,1],[0,0],[1,1],[0,0],[1,0],[0,1],[1,0],[1,1],[0,1],[0,0],[1,0],[1,1],[0,1],[0,0],[1,1],[0,0],[1,0],[0,1],[1,0],[1,1],[0,1],[0,0],[1,0],[1,1],[0,1],[0,0],[1,1],[0,0],[1,0]]; buffers.normals = new Float32Array([-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0]); buffers.coords = new Float32Array([0,1,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,0,1,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,0,1,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0]); if(options.wireframe) buffers.wireframe = new Uint16Array([0,2, 2,5, 5,4, 4,0, 6,7, 7,10, 10,11, 11,6, 0,6, 2,7, 5,10, 4,11 ]); options.bounding = BBox.fromCenterHalfsize( [0,0,0], [halfsize,halfsize,halfsize] ); return GL.Mesh.load(buffers, options, gl); } /** * Returns a cube mesh of a given size * @method Mesh.cube * @param {Object} options valid options: size, sizex, sizey, sizez */ Mesh.box = function(options, gl) { options = options || {}; var sizex = options.sizex || 1; var sizey = options.sizey || 1; var sizez = options.sizez || 1; sizex *= 0.5; sizey *= 0.5; sizez *= 0.5; var buffers = {}; //[[-1,1,-1],[-1,-1,+1],[-1,1,1],[-1,1,-1],[-1,-1,-1],[-1,-1,+1],[1,1,-1],[1,1,1],[1,-1,+1],[1,1,-1],[1,-1,+1],[1,-1,-1],[-1,1,1],[1,-1,1],[1,1,1],[-1,1,1],[-1,-1,1],[1,-1,1],[-1,1,-1],[1,1,-1],[1,-1,-1],[-1,1,-1],[1,-1,-1],[-1,-1,-1],[-1,1,-1],[1,1,1],[1,1,-1],[-1,1,-1],[-1,1,1],[1,1,1],[-1,-1,-1],[1,-1,-1],[1,-1,1],[-1,-1,-1],[1,-1,1],[-1,-1,1]] buffers.vertices = new Float32Array([-1,1,-1,-1,-1,+1,-1,1,1,-1,1,-1,-1,-1,-1,-1,-1,+1,1,1,-1,1,1,1,1,-1,+1,1,1,-1,1,-1,+1,1,-1,-1,-1,1,1,1,-1,1,1,1,1,-1,1,1,-1,-1,1,1,-1,1,-1,1,-1,1,1,-1,1,-1,-1,-1,1,-1,1,-1,-1,-1,-1,-1,-1,1,-1,1,1,1,1,1,-1,-1,1,-1,-1,1,1,1,1,1,-1,-1,-1,1,-1,-1,1,-1,1,-1,-1,-1,1,-1,1,-1,-1,1]); //for(var i in options.vertices) for(var j in options.vertices[i]) options.vertices[i][j] *= size; for(var i = 0, l = buffers.vertices.length; i < l; i+=3) { buffers.vertices[i] *= sizex; buffers.vertices[i+1] *= sizey; buffers.vertices[i+2] *= sizez; } //[[-1,0,0],[-1,0,0],[-1,0,0],[-1,0,0],[-1,0,0],[-1,0,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0],[0,0,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1],[0,0,-1],[0,0,-1],[0,0,-1],[0,0,-1],[0,0,-1],[0,0,-1],[0,1,0],[0,1,0],[0,1,0],[0,1,0],[0,1,0],[0,1,0],[0,-1,0],[0,-1,0],[0,-1,0],[0,-1,0],[0,-1,0],[0,-1,0]] //[[0,1],[1,0],[1,1],[0,1],[0,0],[1,0],[1,1],[0,1],[0,0],[1,1],[0,0],[1,0],[0,1],[1,0],[1,1],[0,1],[0,0],[1,0],[1,1],[0,1],[0,0],[1,1],[0,0],[1,0],[0,1],[1,0],[1,1],[0,1],[0,0],[1,0],[1,1],[0,1],[0,0],[1,1],[0,0],[1,0]]; buffers.normals = new Float32Array([-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0]); buffers.coords = new Float32Array([0,1,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,0,1,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,0,1,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0]); if(options.wireframe) buffers.wireframe = new Uint16Array([0,2, 2,5, 5,4, 4,0, 6,7, 7,10, 10,11, 11,6, 0,6, 2,7, 5,10, 4,11 ]); options.bounding = BBox.fromCenterHalfsize( [0,0,0], [sizex,sizey,sizez] ); return GL.Mesh.load(buffers, options, gl); } /** * Returns a circle mesh * @method Mesh.circle * @param {Object} options valid options: size,radius, xz = in xz plane, otherwise xy plane */ Mesh.circle = function( options, gl ) { options = options || {}; var size = options.size || options.radius || 1; var slices = Math.ceil(options.slices || 24); var xz = options.xz || false; var empty = options.empty || false; if(slices < 3) slices = 3; var delta = (2 * Math.PI) / slices; var center = vec3.create(); var A = vec3.create(); var N = vec3.fromValues(0,0,1); var uv_center = vec2.fromValues(0.5,0.5); var uv = vec2.create(); if(xz) N.set([0,1,0]); var index = xz ? 2 : 1; var vertices = new Float32Array(3 * (slices + 1)); var normals = new Float32Array(3 * (slices + 1)); var coords = new Float32Array(2 * (slices + 1)); var triangles = null; //the center is always the same vertices.set(center, 0); normals.set(N, 0); coords.set(uv_center, 0); var sin = 0; var cos = 0; //compute vertices for(var i = 0; i < slices; ++i ) { sin = Math.sin( delta * i ); cos = Math.cos( delta * i ); A[0] = sin * size; A[index] = cos * size; uv[0] = sin * 0.5 + 0.5; uv[1] = cos * 0.5 + 0.5; vertices.set(A, i * 3 + 3); normals.set(N, i * 3 + 3); coords.set(uv, i * 2 + 2); } if(empty) { vertices = vertices.subarray(3); normals = vertices.subarray(3); coords = vertices.subarray(2); triangles = null; } else { var triangles = new Uint16Array(3 * slices); var offset = 2; var offset2 = 1; if(xz) { offset = 1; offset2 = 2; } //compute indices for(var i = 0; i < slices-1; ++i ) { triangles[i*3] = 0; triangles[i*3+1] = i+offset; triangles[i*3+2] = i+offset2; } triangles[i*3] = 0; if(xz) { triangles[i*3+1] = i+1; triangles[i*3+2] = 1; } else { triangles[i*3+1] = 1; triangles[i*3+2] = i+1; } } options.bounding = BBox.fromCenterHalfsize( [0,0,0], xz ? [size,0,size] : [size,size,0] ); var buffers = {vertices: vertices, normals: normals, coords: coords, triangles: triangles}; if(options.wireframe) { var wireframe = new Uint16Array(slices*2); for(var i = 0; i < slices; i++) { wireframe[i*2] = i; wireframe[i*2+1] = i+1; } wireframe[0] = slices; buffers.wireframe = wireframe; } return GL.Mesh.load( buffers, options, gl ); } /** * Returns a cube mesh * @method Mesh.cylinder * @param {Object} options valid options: radius, height, subdivisions */ Mesh.cylinder = function( options, gl ) { options = options || {}; var radius = options.radius || options.size || 1; var height = options.height || options.size || 2; var subdivisions = options.subdivisions || 64; var vertices = new Float32Array(subdivisions * 6 * 3 * 2 ); var normals = new Float32Array(subdivisions * 6 * 3 * 2 ); var coords = new Float32Array(subdivisions * 6 * 2 * 2 ); //not indexed because caps have different normals and uvs so... var delta = 2*Math.PI / subdivisions; var normal = null; for(var i = 0; i < subdivisions; ++i) { var angle = i * delta; normal = [ Math.sin(angle), 0, Math.cos(angle)]; vertices.set([ normal[0]*radius, height*0.5, normal[2]*radius], i*6*3); normals.set(normal, i*6*3 ); coords.set([i/subdivisions,1], i*6*2 ); normal = [ Math.sin(angle), 0, Math.cos(angle)]; vertices.set([ normal[0]*radius, height*-0.5, normal[2]*radius], i*6*3 + 3); normals.set(normal, i*6*3 + 3); coords.set([i/subdivisions,0], i*6*2 + 2); normal = [ Math.sin(angle+delta), 0, Math.cos(angle+delta)]; vertices.set([ normal[0]*radius, height*-0.5, normal[2]*radius], i*6*3 + 6); normals.set(normal, i*6*3 + 6); coords.set([(i+1)/subdivisions,0], i*6*2 + 4); normal = [ Math.sin(angle+delta), 0, Math.cos(angle+delta)]; vertices.set([ normal[0]*radius, height*0.5, normal[2]*radius], i*6*3 + 9); normals.set(normal, i*6*3 + 9); coords.set([(i+1)/subdivisions,1], i*6*2 + 6); normal = [ Math.sin(angle), 0, Math.cos(angle)]; vertices.set([ normal[0]*radius, height*0.5, normal[2]*radius], i*6*3 + 12); normals.set(normal, i*6*3 + 12); coords.set([i/subdivisions,1], i*6*2 + 8); normal = [ Math.sin(angle+delta), 0, Math.cos(angle+delta)]; vertices.set([ normal[0]*radius, height*-0.5, normal[2]*radius], i*6*3 + 15); normals.set(normal, i*6*3 + 15); coords.set([(i+1)/subdivisions,0], i*6*2 + 10); } var pos = i*6*3; var pos_uv = i*6*2; var caps_start = pos; //caps if( options.caps === false ) { //finalize arrays vertices = vertices.subarray(0,pos); normals = normals.subarray(0,pos); coords = coords.subarray(0,pos_uv); } else { var top_center = vec3.fromValues(0,height*0.5,0); var bottom_center = vec3.fromValues(0,height*-0.5,0); var up = vec3.fromValues(0,1,0); var down = vec3.fromValues(0,-1,0); for(var i = 0; i < subdivisions; ++i) { var angle = i * delta; var uv = vec3.fromValues( Math.sin(angle), 0, Math.cos(angle) ); var uv2 = vec3.fromValues( Math.sin(angle+delta), 0, Math.cos(angle+delta) ); vertices.set([ uv[0]*radius, height*0.5, uv[2]*radius], pos + i*6*3); normals.set(up, pos + i*6*3 ); coords.set( [ -uv[0] * 0.5 + 0.5,uv[2] * 0.5 + 0.5], pos_uv + i*6*2 ); vertices.set([ uv2[0]*radius, height*0.5, uv2[2]*radius], pos + i*6*3 + 3); normals.set(up, pos + i*6*3 + 3 ); coords.set( [ -uv2[0] * 0.5 + 0.5,uv2[2] * 0.5 + 0.5], pos_uv + i*6*2 + 2 ); vertices.set( top_center, pos + i*6*3 + 6 ); normals.set(up, pos + i*6*3 + 6); coords.set([0.5,0.5], pos_uv + i*6*2 + 4); //bottom vertices.set([ uv2[0]*radius, height*-0.5, uv2[2]*radius], pos + i*6*3 + 9); normals.set(down, pos + i*6*3 + 9); coords.set( [ uv2[0] * 0.5 + 0.5,uv2[2] * 0.5 + 0.5], pos_uv + i*6*2 + 6); vertices.set([ uv[0]*radius, height*-0.5, uv[2]*radius], pos + i*6*3 + 12); normals.set(down, pos + i*6*3 + 12 ); coords.set( [ uv[0] * 0.5 + 0.5,uv[2] * 0.5 + 0.5], pos_uv + i*6*2 + 8 ); vertices.set( bottom_center, pos + i*6*3 + 15 ); normals.set( down, pos + i*6*3 + 15); coords.set( [0.5,0.5], pos_uv + i*6*2 + 10); } } var buffers = { vertices: vertices, normals: normals, coords: coords } options.bounding = BBox.fromCenterHalfsize( [0,0,0], [radius,height*0.5,radius] ); options.info = { groups: [] }; if(options.caps !== false) { options.info.groups.push({ name:"side", start: 0, length: caps_start / 3}); options.info.groups.push({ name:"caps", start: caps_start / 3, length: (vertices.length - caps_start) / 3}); } return Mesh.load( buffers, options, gl ); } /** * Returns a cone mesh * @method Mesh.cone * @param {Object} options valid options: radius, height, subdivisions */ Mesh.cone = function( options, gl ) { options = options || {}; var radius = options.radius || options.size || 1; var height = options.height || options.size || 2; var subdivisions = options.subdivisions || 64; var vertices = new Float32Array(subdivisions * 3 * 3 * 2); var normals = new Float32Array(subdivisions * 3 * 3 * 2); var coords = new Float32Array(subdivisions * 2 * 3 * 2); //not indexed because caps have different normals and uvs so... var delta = 2*Math.PI / subdivisions; var normal = null; var normal_y = radius / height; var up = [0,1,0]; for(var i = 0; i < subdivisions; ++i) { var angle = i * delta; normal = [ Math.sin(angle+delta*0.5), normal_y, Math.cos(angle+delta*0.5)]; vec3.normalize(normal,normal); //normal = up; vertices.set([ 0, height, 0] , i*6*3); normals.set(normal, i*6*3 ); coords.set([i/subdivisions,1], i*6*2 ); normal = [ Math.sin(angle), normal_y, Math.cos(angle)]; vertices.set([ normal[0]*radius, 0, normal[2]*radius], i*6*3 + 3); vec3.normalize(normal,normal); normals.set(normal, i*6*3 + 3); coords.set([i/subdivisions,0], i*6*2 + 2); normal = [ Math.sin(angle+delta), normal_y, Math.cos(angle+delta)]; vertices.set([ normal[0]*radius, 0, normal[2]*radius], i*6*3 + 6); vec3.normalize(normal,normal); normals.set(normal, i*6*3 + 6); coords.set([(i+1)/subdivisions,0], i*6*2 + 4); } var pos = 0;//i*3*3; var pos_uv = 0;//i*3*2; //cap var bottom_center = vec3.fromValues(0,0,0); var down = vec3.fromValues(0,-1,0); for(var i = 0; i < subdivisions; ++i) { var angle = i * delta; var uv = vec3.fromValues( Math.sin(angle), 0, Math.cos(angle) ); var uv2 = vec3.fromValues( Math.sin(angle+delta), 0, Math.cos(angle+delta) ); //bottom vertices.set([ uv2[0]*radius, 0, uv2[2]*radius], pos + i*6*3 + 9); normals.set(down, pos + i*6*3 + 9); coords.set( [ uv2[0] * 0.5 + 0.5,uv2[2] * 0.5 + 0.5], pos_uv + i*6*2 + 6); vertices.set([ uv[0]*radius, 0, uv[2]*radius], pos + i*6*3 + 12); normals.set(down, pos + i*6*3 + 12 ); coords.set( [ uv[0] * 0.5 + 0.5,uv[2] * 0.5 + 0.5], pos_uv + i*6*2 + 8 ); vertices.set( bottom_center, pos + i*6*3 + 15 ); normals.set( down, pos + i*6*3 + 15); coords.set( [0.5,0.5], pos_uv + i*6*2 + 10); } var buffers = { vertices: vertices, normals: normals, coords: coords } options.bounding = BBox.fromCenterHalfsize( [0,height*0.5,0], [radius,height*0.5,radius] ); return Mesh.load( buffers, options, gl ); } /** * Returns a sphere mesh * @method Mesh.sphere * @param {Object} options valid options: radius, lat, long, subdivisions, hemi */ Mesh.sphere = function( options, gl ) { options = options || {}; var radius = options.radius || options.size || 1; var latitudeBands = options.lat || options.subdivisions || 16; var longitudeBands = options["long"] || options.subdivisions || 16; var vertexPositionData = new Float32Array( (latitudeBands+1)*(longitudeBands+1)*3 ); var normalData = new Float32Array( (latitudeBands+1)*(longitudeBands+1)*3 ); var textureCoordData = new Float32Array( (latitudeBands+1)*(longitudeBands+1)*2 ); var indexData = new Uint16Array( latitudeBands*longitudeBands*6 ); var latRange = options.hemi ? Math.PI * 0.5 : Math.PI; var i = 0, iuv = 0; for (var latNumber = 0; latNumber <= latitudeBands; latNumber++) { var theta = latNumber * latRange / latitudeBands; var sinTheta = Math.sin(theta); var cosTheta = Math.cos(theta); for (var longNumber = 0; longNumber <= longitudeBands; longNumber++) { var phi = longNumber * 2 * Math.PI / longitudeBands; var sinPhi = Math.sin(phi); var cosPhi = Math.cos(phi); var x = cosPhi * sinTheta; var y = cosTheta; var z = sinPhi * sinTheta; var u = 1- (longNumber / longitudeBands); var v = (1 - latNumber / latitudeBands); vertexPositionData.set([radius * x,radius * y,radius * z],i); normalData.set([x,y,z],i); textureCoordData.set([u,v], iuv ); i += 3; iuv += 2; } } i=0; for (var latNumber = 0; latNumber < latitudeBands; latNumber++) { for (var longNumber = 0; longNumber < longitudeBands; longNumber++) { var first = (latNumber * (longitudeBands + 1)) + longNumber; var second = first + longitudeBands + 1; indexData.set([second,first,first + 1], i); indexData.set([second + 1,second,first + 1], i+3); i += 6; } } var buffers = { vertices: vertexPositionData, normals: normalData, coords: textureCoordData, triangles: indexData }; if(options.wireframe) { var wireframe = new Uint16Array(longitudeBands*latitudeBands*4); var pos = 0; for(var i = 0; i < latitudeBands; i++) { for(var j = 0; j < longitudeBands; j++) { wireframe[pos] = i*(longitudeBands+1) + j; wireframe[pos + 1] = i*(longitudeBands+1) + j + 1; pos += 2; } wireframe[pos - longitudeBands*2] = i*(longitudeBands+1) + j; } for(var i = 0; i < longitudeBands; i++) for(var j = 0; j < latitudeBands; j++) { wireframe[pos] = j*(longitudeBands+1) + i; wireframe[pos + 1] = (j+1)*(longitudeBands+1) + i; pos += 2; } buffers.wireframe = wireframe; } if(options.hemi) options.bounding = BBox.fromCenterHalfsize( [0,radius*0.5,0], [radius,radius*0.5,radius], radius ); else options.bounding = BBox.fromCenterHalfsize( [0,0,0], [radius,radius,radius], radius ); return GL.Mesh.load( buffers, options, gl ); } /** * Returns a grid mesh (must be rendered using gl.LINES) * @method Mesh.grid * @param {Object} options valid options: size, lines */ Mesh.grid = function( options, gl ) { options = options || {}; var num_lines = options.lines || 11; if(num_lines < 0) num_lines = 1; var size = options.size || 10; var vertexPositionData = new Float32Array( num_lines*2*2*3 ); var hsize = size * 0.5; var pos = 0; var x = -hsize; var delta = size / (num_lines-1); for(var i = 0; i < num_lines; i++) { vertexPositionData[ pos ] = x; vertexPositionData[ pos+2 ] = -hsize; vertexPositionData[ pos+3 ] = x; vertexPositionData[ pos+5 ] = hsize; vertexPositionData[ pos+6 ] = hsize; vertexPositionData[ pos+8 ] = x vertexPositionData[ pos+9 ] = -hsize; vertexPositionData[ pos+11 ] = x x += delta; pos += 12; } return new GL.Mesh({vertices: vertexPositionData}, options, gl ); } /** * Returns a icosahedron mesh (useful to create spheres by subdivision) * @method Mesh.icosahedron * @param {Object} options valid options: radius, subdivisions (max: 6) */ Mesh.icosahedron = function( options, gl ) { options = options || {}; var radius = options.radius || options.size || 1; var subdivisions = options.subdivisions === undefined ? 0 : options.subdivisions; if(subdivisions > 6) //dangerous subdivisions = 6; var t = (1.0 + Math.sqrt(5)) / 2.0; var vertices = [-1,t,0, 1,t,0, -1,-t,0, 1,-t,0, 0,-1,t, 0,1,t, 0,-1,-t, 0,1,-t, t,0,-1, t,0,1, -t,0,-1, -t,0,1]; var normals = []; var coords = []; var indices = [0,11,5, 0,5,1, 0,1,7, 0,7,10, 0,10,11, 1,5,9, 5,11,4, 11,10,2, 10,7,6, 7,1,8, 3,9,4, 3,4,2, 3,2,6, 3,6,8, 3,8,9, 4,9,5, 2,4,11, 6,2,10, 8,6,7, 9,8,1 ]; //normalize var l = vertices.length; for(var i = 0; i < l; i+=3) { var mod = Math.sqrt( vertices[i]*vertices[i] + vertices[i+1]*vertices[i+1] + vertices[i+2]*vertices[i+2] ); var normalx = vertices[i] / mod; var normaly = vertices[i+1] / mod; var normalz = vertices[i+2] / mod; normals.push( normalx, normaly, normalz ); coords.push( Math.atan2( normalx, normalz ), Math.acos( normaly ) ); vertices[i] *= radius/mod; vertices[i+1] *= radius/mod; vertices[i+2] *= radius/mod; } var middles = {}; //A,B = index of vertex in vertex array function middlePoint( A, B ) { var key = indices[A] < indices[B] ? indices[A] + ":"+indices[B] : indices[B]+":"+indices[A]; var r = middles[key]; if(r) return r; var index = vertices.length / 3; vertices.push(( vertices[ indices[A]*3] + vertices[ indices[B]*3 ]) * 0.5, (vertices[ indices[A]*3+1] + vertices[ indices[B]*3+1 ]) * 0.5, (vertices[ indices[A]*3+2] + vertices[ indices[B]*3+2 ]) * 0.5); var mod = Math.sqrt( vertices[index*3]*vertices[index*3] + vertices[index*3+1]*vertices[index*3+1] + vertices[index*3+2]*vertices[index*3+2] ); var normalx = vertices[index*3] / mod; var normaly = vertices[index*3+1] / mod; var normalz = vertices[index*3+2] / mod; normals.push( normalx, normaly, normalz ); coords.push( (Math.atan2( normalx, normalz ) / Math.PI) * 0.5, (Math.acos( normaly ) / Math.PI) ); vertices[index*3] *= radius/mod; vertices[index*3+1] *= radius/mod; vertices[index*3+2] *= radius/mod; middles[key] = index; return index; } for (var iR = 0; iR < subdivisions; ++iR ) { var new_indices = []; var l = indices.length; for(var i = 0; i < l; i+=3) { var MA = middlePoint( i, i+1 ); var MB = middlePoint( i+1, i+2); var MC = middlePoint( i+2, i); new_indices.push(indices[i], MA, MC); new_indices.push(indices[i+1], MB, MA); new_indices.push(indices[i+2], MC, MB); new_indices.push(MA, MB, MC); } indices = new_indices; } options.bounding = BBox.fromCenterHalfsize( [0,0,0], [radius,radius,radius], radius ); return new GL.Mesh.load({vertices: vertices, coords: coords, normals: normals, triangles: indices},options,gl); } /** * @namespace GL */ /** * Texture class to upload images to the GPU, default is gl.TEXTURE_2D, gl.RGBA of gl.UNSIGNED_BYTE with filters set to gl.LINEAR and wrap to gl.CLAMP_TO_EDGE
    There is a list of options
    ==========================
    - texture_type: gl.TEXTURE_2D, gl.TEXTURE_CUBE_MAP, default gl.TEXTURE_2D
    - format: gl.RGB, gl.RGBA, gl.DEPTH_COMPONENT, default gl.RGBA
    - type: gl.UNSIGNED_BYTE, gl.UNSIGNED_SHORT, gl.HALF_FLOAT_OES, gl.FLOAT, default gl.UNSIGNED_BYTE
    - filter: filtering for mag and min: gl.NEAREST or gl.LINEAR, default gl.NEAREST
    - magFilter: magnifying filter: gl.NEAREST, gl.LINEAR, default gl.NEAREST
    - minFilter: minifying filter: gl.NEAREST, gl.LINEAR, gl.LINEAR_MIPMAP_LINEAR, default gl.NEAREST
    - wrap: texture wrapping: gl.CLAMP_TO_EDGE, gl.REPEAT, gl.MIRROR, default gl.CLAMP_TO_EDGE (also accepts wrapT and wrapS for separate settings)
    - pixel_data: ArrayBufferView with the pixel data to upload to the texture, otherwise the texture will be black (if cubemaps then pass an array[6] with the data for every face)
    - premultiply_alpha : multiply the color by the alpha value when uploading, default FALSE
    - no_flip : do not flip in Y, default TRUE
    - anisotropic : number of anisotropic fetches, default 0
    check for more info about formats: https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texImage2D * @class Texture * @param {number} width texture width (any supported but Power of Two allows to have mipmaps), 0 means no memory reserved till its filled * @param {number} height texture height (any supported but Power of Two allows to have mipmaps), 0 means no memory reserved till its filled * @param {Object} options Check the list in the description * @constructor */ global.Texture = GL.Texture = function Texture( width, height, options, gl ) { options = options || {}; //used to avoid problems with resources moving between different webgl context gl = gl || global.gl; this.gl = gl; this._context_id = gl.context_id; //round sizes width = parseInt(width); height = parseInt(height); if(GL.debug) console.log("GL.Texture created: ",width,height); //create texture handler this.handler = gl.createTexture(); //set settings this.width = width; this.height = height; if(options.depth) //for texture_3d this.depth = options.depth; this.texture_type = options.texture_type || gl.TEXTURE_2D; //or gl.TEXTURE_CUBE_MAP this.format = options.format || Texture.DEFAULT_FORMAT; //gl.RGBA (if gl.DEPTH_COMPONENT remember type: gl.UNSIGNED_SHORT) this.internalFormat = options.internalFormat; //LUMINANCE, and weird formats with bits this.type = options.type || Texture.DEFAULT_TYPE; //gl.UNSIGNED_BYTE, gl.UNSIGNED_SHORT, gl.FLOAT or gl.HALF_FLOAT_OES (or gl.HIGH_PRECISION_FORMAT which could be half or float) this.magFilter = options.magFilter || options.filter || Texture.DEFAULT_MAG_FILTER; this.minFilter = options.minFilter || options.filter || Texture.DEFAULT_MIN_FILTER; this.wrapS = options.wrap || options.wrapS || Texture.DEFAULT_WRAP_S; this.wrapT = options.wrap || options.wrapT || Texture.DEFAULT_WRAP_T; this.data = null; //where the data came from //precompute the max amount of texture units if(!Texture.MAX_TEXTURE_IMAGE_UNITS) Texture.MAX_TEXTURE_IMAGE_UNITS = gl.getParameter( gl.MAX_TEXTURE_IMAGE_UNITS ); this.has_mipmaps = false; if( this.format == gl.DEPTH_COMPONENT && gl.webgl_version == 1 && !gl.extensions["WEBGL_depth_texture"] ) throw("Depth Texture not supported"); if( this.type == gl.FLOAT && !gl.extensions["OES_texture_float"] && gl.webgl_version == 1 ) throw("Float Texture not supported"); if( this.type == gl.HALF_FLOAT_OES) { if( !gl.extensions["OES_texture_half_float"] && gl.webgl_version == 1 ) throw("Half Float Texture extension not supported."); else if( gl.webgl_version > 1 ) { console.warn("using HALF_FLOAT_OES in WebGL2 is deprecated, suing HALF_FLOAT instead"); this.type = this.format == gl.RGB ? gl.RGB16F : gl.RGBA16F; } } if( (!isPowerOfTwo(this.width) || !isPowerOfTwo(this.height)) && //non power of two ( (this.minFilter != gl.NEAREST && this.minFilter != gl.LINEAR) || //uses mipmaps (this.wrapS != gl.CLAMP_TO_EDGE || this.wrapT != gl.CLAMP_TO_EDGE) ) ) //uses wrap { if(!options.ignore_pot) throw("Cannot use texture-wrap or mipmaps in Non-Power-of-Two textures"); else { this.minFilter = this.magFilter = gl.LINEAR; this.wrapS = this.wrapT = gl.CLAMP_TO_EDGE; } } //empty textures are allowed to be created if(!width || !height) return; //because sometimes the internal format is not so obvious if(!this.internalFormat) this.computeInternalFormat(); //this is done because in some cases the user binds a texture to slot 0 and then creates a new one, which overrides slot 0 gl.activeTexture( gl.TEXTURE0 + Texture.MAX_TEXTURE_IMAGE_UNITS - 1); //I use an invalid gl enum to say this texture is a depth texture, ugly, I know... gl.bindTexture( this.texture_type, this.handler); gl.texParameteri( this.texture_type, gl.TEXTURE_MAG_FILTER, this.magFilter ); gl.texParameteri( this.texture_type, gl.TEXTURE_MIN_FILTER, this.minFilter ); gl.texParameteri( this.texture_type, gl.TEXTURE_WRAP_S, this.wrapS ); gl.texParameteri( this.texture_type, gl.TEXTURE_WRAP_T, this.wrapT ); if(options.anisotropic && gl.extensions["EXT_texture_filter_anisotropic"]) gl.texParameterf( GL.TEXTURE_2D, gl.extensions["EXT_texture_filter_anisotropic"].TEXTURE_MAX_ANISOTROPY_EXT, options.anisotropic); var type = this.type; var pixel_data = options.pixel_data; if(pixel_data && !pixel_data.buffer) { if( this.texture_type == GL.TEXTURE_CUBE_MAP ) { if(pixel_data[0].constructor === Number) //special case, specify just one face and copy it { pixel_data = toTypedArray( pixel_data ); pixel_data = [pixel_data,pixel_data,pixel_data,pixel_data,pixel_data,pixel_data]; } else for(var i = 0; i < pixel_data.length; ++i) pixel_data[i] = toTypedArray( pixel_data[i] ); } else pixel_data = toTypedArray( pixel_data ); this.data = pixel_data; } function toTypedArray( data ) { if(data.constructor !== Array) return data; if( type == GL.FLOAT) return new Float32Array( data ); if( type == GL.HALF_FLOAT_OES) return new Uint16Array( data ); return new Uint8Array( data ); } //gl.TEXTURE_1D is not supported by WebGL... //here we create all ********************************** if(this.texture_type == GL.TEXTURE_2D) { //create the texture gl.texImage2D( GL.TEXTURE_2D, 0, this.internalFormat, width, height, 0, this.format, this.type, pixel_data || null ); //generate empty mipmaps (necessary?) if ( GL.isPowerOfTwo(width) && GL.isPowerOfTwo(height) && options.minFilter && options.minFilter != gl.NEAREST && options.minFilter != gl.LINEAR) { gl.generateMipmap( this.texture_type ); this.has_mipmaps = true; } } else if(this.texture_type == GL.TEXTURE_CUBE_MAP) { for(var i = 0; i < 6; ++i) gl.texImage2D( gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, this.internalFormat, this.width, this.height, 0, this.format, this.type, pixel_data ? pixel_data[i] : null ); } else if(this.texture_type == GL.TEXTURE_3D) { if(this.gl.webgl_version == 1) throw("TEXTURE_3D not supported in WebGL 1. Enable WebGL 2 in the context by passing webgl2:true to the context"); if(!options.depth) throw("3d texture depth must be set in the options.depth"); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false ); //standard does not allow this flags for 3D textures gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false ); gl.texImage3D( GL.TEXTURE_3D, 0, this.internalFormat, width, height, options.depth, 0, this.format, this.type, pixel_data || null ); } gl.bindTexture(this.texture_type, null); //disable gl.activeTexture(gl.TEXTURE0); } Texture.DEFAULT_TYPE = GL.UNSIGNED_BYTE; Texture.DEFAULT_FORMAT = GL.RGBA; Texture.DEFAULT_MAG_FILTER = GL.LINEAR; Texture.DEFAULT_MIN_FILTER = GL.LINEAR; Texture.DEFAULT_WRAP_S = GL.CLAMP_TO_EDGE; Texture.DEFAULT_WRAP_T = GL.CLAMP_TO_EDGE; Texture.EXTENSION = "png"; //used when saving it to file //used for render to FBOs Texture.framebuffer = null; Texture.renderbuffer = null; Texture.loading_color = new Uint8Array([0,0,0,0]); Texture.use_renderbuffer_pool = true; //should improve performance //because usually you dont want to specify the internalFormat, this tries to guess it from its format //check https://webgl2fundamentals.org/webgl/lessons/webgl-data-textures.html for more info Texture.prototype.computeInternalFormat = function() { this.internalFormat = this.format; //default //automatic selection of internal format for depth textures to avoid problems between webgl1 and 2 if( this.format == GL.DEPTH_COMPONENT ) { this.minFilter = this.magFilter = GL.NEAREST; if( gl.webgl_version == 2 ) { if( this.type == GL.UNSIGNED_SHORT ) this.internalFormat = GL.DEPTH_COMPONENT16; else if( this.type == GL.UNSIGNED_INT ) this.internalFormat = GL.DEPTH_COMPONENT24; else if( this.type == GL.FLOAT ) this.internalFormat = GL.DEPTH_COMPONENT32F; else throw("unsupported type for a depth texture"); } else if( gl.webgl_version == 1 ) { if( this.type == GL.FLOAT ) throw("WebGL 1.0 does not support float depth textures"); this.internalFormat = GL.DEPTH_COMPONENT; } } else if( this.format == gl.RGBA ) { if( gl.webgl_version == 2 ) { if( this.type == GL.FLOAT ) this.internalFormat = GL.RGBA32F; else if( this.type == GL.HALF_FLOAT ) this.internalFormat = GL.RGBA16F; else if( this.type == GL.HALF_FLOAT_OES ) { console.warn("webgl 2 does not use HALF_FLOAT_OES, converting to HALF_FLOAT") this.type = GL.HALF_FLOAT; this.internalFormat = GL.RGBA16F; } /* else if( this.type == GL.UNSIGNED_SHORT ) { this.internalFormat = GL.RGBA16UI; this.format = gl.RGBA_INTEGER; } else if( this.type == GL.UNSIGNED_INT ) { this.internalFormat = GL.RGBA32UI; this.format = gl.RGBA_INTEGER; } */ } else if( gl.webgl_version == 1 ) { if( this.type == GL.HALF_FLOAT ) { console.warn("webgl 1 does not use HALF_FLOAT, converting to HALF_FLOAT_OES") this.type = GL.HALF_FLOAT_OES; } } } } /** * Free the texture memory from the GPU, sets the texture handler to null * @method delete */ Texture.prototype.delete = function() { gl.deleteTexture( this.handler ); this.handler = null; } Texture.prototype.getProperties = function() { return { width: this.width, height: this.height, type: this.type, format: this.format, texture_type: this.texture_type, magFilter: this.magFilter, minFilter: this.minFilter, wrapS: this.wrapS, wrapT: this.wrapT }; } Texture.prototype.hasSameProperties = function(t) { if(!t) return false; return t.width == this.width && t.height == this.height && t.type == this.type && t.format == this.format && t.texture_type == this.texture_type; } Texture.prototype.hasSameSize = function(t) { if(!t) return false; return t.width == this.width && t.height == this.height; } //textures cannot be stored in JSON Texture.prototype.toJSON = function() { return ""; } /** * Returns if depth texture is supported by the GPU * @method isDepthSupported * @return {Boolean} true if supported */ Texture.isDepthSupported = function() { return gl.extensions["WEBGL_depth_texture"] != null; } /** * Binds the texture to one texture unit * @method bind * @param {number} unit texture unit * @return {number} returns the texture unit */ Texture.prototype.bind = function( unit ) { if(unit == undefined) unit = 0; var gl = this.gl; //TODO: if the texture is not uploaded, must be upload now //bind gl.activeTexture(gl.TEXTURE0 + unit); gl.bindTexture( this.texture_type, this.handler ); return unit; } /** * Unbinds the texture * @method unbind * @param {number} unit texture unit * @return {number} returns the texture unit */ Texture.prototype.unbind = function(unit) { if(unit === undefined) unit = 0; var gl = this.gl; gl.activeTexture(gl.TEXTURE0 + unit ); gl.bindTexture(this.texture_type, null); } Texture.prototype.setParameter = function(param,value) { this.bind(0); this.gl.texParameteri( this.texture_type, param, value ); switch(param) { case this.gl.TEXTURE_MAG_FILTER: this.magFilter = value; break; case this.gl.TEXTURE_MIN_FILTER: this.minFilter = value; break; case this.gl.TEXTURE_WRAP_S: this.wrapS = value; break; case this.gl.TEXTURE_WRAP_T: this.wrapT = value; break; } } /** * Unbinds the texture * @method Texture.setUploadOptions * @param {Object} options a list of options to upload the texture * - premultiply_alpha : multiply the color by the alpha value, default FALSE * - no_flip : do not flip in Y, default TRUE */ Texture.setUploadOptions = function(options, gl) { gl = gl || global.gl; if(options) //options that are not stored in the texture should be passed again to avoid reusing unknown state { gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, !!(options.premultiply_alpha) ); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !(options.no_flip) ); } else { gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false ); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true ); } gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); } /** * Given an Image/Canvas/Video it uploads it to the GPU * @method uploadImage * @param {Image} img * @param {Object} options [optional] upload options (premultiply_alpha, no_flip) */ Texture.prototype.uploadImage = function( image, options ) { this.bind(); var gl = this.gl; if(!image) throw("uploadImage parameter must be Image"); Texture.setUploadOptions(options, gl); try { gl.texImage2D( gl.TEXTURE_2D, 0, this.format, this.format, this.type, image ); this.width = image.videoWidth || image.width; this.height = image.videoHeight || image.height; this.data = image; } catch (e) { if (location.protocol == 'file:') { throw 'image not loaded for security reasons (serve this page over "http://" instead)'; } else { throw 'image not loaded for security reasons (image must originate from the same ' + 'domain as this page or use Cross-Origin Resource Sharing)'; } } //TODO: add expand transparent pixels option //generate mipmaps if (this.minFilter && this.minFilter != gl.NEAREST && this.minFilter != gl.LINEAR) { gl.generateMipmap(this.texture_type); this.has_mipmaps = true; } gl.bindTexture(this.texture_type, null); //disable } /** * Uploads data to the GPU (data must have the appropiate size) * @method uploadData * @param {ArrayBuffer} data * @param {Object} options [optional] upload options (premultiply_alpha, no_flip, cubemap_face, mipmap_level) */ Texture.prototype.uploadData = function( data, options, skip_mipmaps ) { options = options || {}; if(!data) throw("no data passed"); var gl = this.gl; this.bind(); Texture.setUploadOptions(options, gl); var mipmap_level = options.mipmap_level || 0; var width = this.width; var height = this.height; width = width >> mipmap_level; height = height >> mipmap_level; var internal_format = this.internalFormat || this.format; if( this.type == GL.HALF_FLOAT_OES && data.constructor === Float32Array ) console.warn("cannot uploadData to a HALF_FLOAT texture from a Float32Array, must be Uint16Array. To upload it we recomment to create a FLOAT texture, upload data there and copy to your HALF_FLOAT."); if( this.texture_type == GL.TEXTURE_2D ) { if(gl.webgl_version == 1) { if(data.buffer && data.buffer.constructor == ArrayBuffer) gl.texImage2D(this.texture_type, mipmap_level, internal_format, width, height, 0, this.format, this.type, data); else gl.texImage2D(this.texture_type, mipmap_level, internal_format, this.format, this.type, data); } else if(gl.webgl_version == 2) //webgl forces to use width and height { if(data.buffer && data.buffer.constructor == ArrayBuffer) gl.texImage2D(this.texture_type, mipmap_level, internal_format, width, height, 0, this.format, this.type, data); else gl.texImage2D(this.texture_type, mipmap_level, internal_format, width, height, 0, this.format, this.type, data); } } else if( this.texture_type == GL.TEXTURE_3D ) { gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false ); //standard does not allow this flags for 3D textures gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false ); gl.texImage3D( this.texture_type, mipmap_level, internal_format, width, height, this.depth >> mipmap_level, 0, this.format, this.type, data); } else if( this.texture_type == GL.TEXTURE_CUBE_MAP ) gl.texImage2D( gl.TEXTURE_CUBE_MAP_POSITIVE_X + (options.cubemap_face || 0), mipmap_level, internal_format, width, height, 0, this.format, this.type, data); else throw("cannot uploadData for this texture type"); this.data = data; //should I clone it? if (!skip_mipmaps && this.minFilter && this.minFilter != gl.NEAREST && this.minFilter != gl.LINEAR) { gl.generateMipmap(this.texture_type); this.has_mipmaps = true; } gl.bindTexture(this.texture_type, null); //disable } //When creating cubemaps this is helpful /*THIS WORKS old Texture.cubemap_camera_parameters = [ { type:"posX", dir: vec3.fromValues(-1,0,0), up: vec3.fromValues(0,1,0), right: vec3.fromValues(0,0,-1) }, { type:"negX", dir: vec3.fromValues(1,0,0), up: vec3.fromValues(0,1,0), right: vec3.fromValues(0,0,1) }, { type:"posY", dir: vec3.fromValues(0,-1,0), up: vec3.fromValues(0,0,-1), right: vec3.fromValues(1,0,0) }, { type:"negY", dir: vec3.fromValues(0,1,0), up: vec3.fromValues(0,0,1), right: vec3.fromValues(-1,0,0) }, { type:"posZ", dir: vec3.fromValues(0,0,-1), up: vec3.fromValues(0,1,0), right: vec3.fromValues(1,0,0) }, { type:"negZ", dir: vec3.fromValues(0,0,1), up: vec3.fromValues(0,1,0), right: vec3.fromValues(-1,0,0) } ]; */ //THIS works Texture.cubemap_camera_parameters = [ { type:"posX", dir: vec3.fromValues(1,0,0), up: vec3.fromValues(0,1,0), right: vec3.fromValues(0,0,-1) }, { type:"negX", dir: vec3.fromValues(-1,0,0), up: vec3.fromValues(0,1,0), right: vec3.fromValues(0,0,1) }, { type:"posY", dir: vec3.fromValues(0,1,0), up: vec3.fromValues(0,0,-1), right: vec3.fromValues(1,0,0) }, { type:"negY", dir: vec3.fromValues(0,-1,0), up: vec3.fromValues(0,0,1), right: vec3.fromValues(1,0,0) }, { type:"posZ", dir: vec3.fromValues(0,0,1), up: vec3.fromValues(0,1,0), right: vec3.fromValues(1,0,0) }, { type:"negZ", dir: vec3.fromValues(0,0,-1), up: vec3.fromValues(0,1,0), right: vec3.fromValues(-1,0,0) } ]; /** * Render to texture using FBO, just pass the callback to a rendering function and the content of the texture will be updated * If the texture is a cubemap, the callback will be called six times, once per face, the number of the face is passed as a second parameter * for further info about how to set up the propper cubemap camera, check the GL.Texture.cubemap_camera_parameters with the direction and up vector for every face. * * Keep in mind that it tries to reuse the last renderbuffer for the depth, and if it cannot (different size) it creates a new one (throwing the old) * @method drawTo * @param {Function} callback function that does all the rendering inside this texture */ Texture.prototype.drawTo = function(callback, params) { var gl = this.gl; //if(this.format == gl.DEPTH_COMPONENT) // throw("cannot use drawTo in depth textures, use Texture.drawToColorAndDepth"); var v = gl.getViewport(); var now = GL.getTime(); var old_fbo = gl.getParameter( gl.FRAMEBUFFER_BINDING ); var framebuffer = gl._framebuffer = gl._framebuffer || gl.createFramebuffer(); gl.bindFramebuffer( gl.FRAMEBUFFER, framebuffer ); //this code allows to reuse old renderbuffers instead of creating and destroying them for every frame var renderbuffer = null; if( Texture.use_renderbuffer_pool ) //create a renderbuffer pool { if(!gl._renderbuffers_pool) gl._renderbuffers_pool = {}; //generate unique key for this renderbuffer var key = this.width + ":" + this.height; //reuse or create new one if( gl._renderbuffers_pool[ key ] ) //Reuse old { renderbuffer = gl._renderbuffers_pool[ key ]; renderbuffer.time = now; gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer ); } else { //create temporary buffer gl._renderbuffers_pool[ key ] = renderbuffer = gl.createRenderbuffer(); renderbuffer.time = now; renderbuffer.width = this.width; renderbuffer.height = this.height; gl.bindRenderbuffer( gl.RENDERBUFFER, renderbuffer ); //destroy after one minute setTimeout( inner_check_destroy.bind(renderbuffer), 1000*60 ); } } else { renderbuffer = gl._renderbuffer = gl._renderbuffer || gl.createRenderbuffer(); renderbuffer.width = this.width; renderbuffer.height = this.height; gl.bindRenderbuffer( gl.RENDERBUFFER, renderbuffer ); } //bind render buffer for depth or color if( this.format === gl.DEPTH_COMPONENT ) gl.renderbufferStorage( gl.RENDERBUFFER, gl.RGBA4, this.width, this.height); else gl.renderbufferStorage( gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.width, this.height); //clears memory from unused buffer function inner_check_destroy() { if( GL.getTime() - this.time >= 1000*60 ) { //console.log("Buffer cleared"); gl.deleteRenderbuffer( gl._renderbuffers_pool[ key ] ); delete gl._renderbuffers_pool[ key ]; } else setTimeout( inner_check_destroy.bind(this), 1000*60 ); } //create to store depth /* if (this.width != renderbuffer.width || this.height != renderbuffer.height ) { renderbuffer.width = this.width; renderbuffer.height = this.height; gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.width, this.height); } */ gl.viewport(0, 0, this.width, this.height); //if(gl._current_texture_drawto) // throw("Texture.drawTo: Cannot use drawTo from inside another drawTo"); gl._current_texture_drawto = this; gl._current_fbo_color = framebuffer; gl._current_fbo_depth = renderbuffer; if(this.texture_type == gl.TEXTURE_2D) { if( this.format !== gl.DEPTH_COMPONENT ) { gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.handler, 0 ); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer ); } else { gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, renderbuffer ); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this.handler, 0); } callback(this, params); } else if(this.texture_type == gl.TEXTURE_CUBE_MAP) { //bind the fixed ones out of the loop to save calls if( this.format !== gl.DEPTH_COMPONENT ) gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer ); else gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, renderbuffer ); //for every face of the cubemap for(var i = 0; i < 6; i++) { if( this.format !== gl.DEPTH_COMPONENT ) gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, this.handler, 0); else gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, this.handler, 0 ); callback(this,i, params); } } this.data = null; gl._current_texture_drawto = null; gl._current_fbo_color = null; gl._current_fbo_depth = null; gl.bindFramebuffer( gl.FRAMEBUFFER, old_fbo ); gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.viewport(v[0], v[1], v[2], v[3]); return this; } /** * Static version of drawTo meant to be used with several buffers * @method drawToColorAndDepth * @param {Texture} color_texture * @param {Texture} depth_texture * @param {Function} callback */ Texture.drawTo = function( color_textures, callback, depth_texture ) { var w = -1, h = -1, type = null; if(!color_textures && !depth_texture) throw("Textures missing in drawTo"); if(color_textures && color_textures.length) { for(var i = 0; i < color_textures.length; i++) { var t = color_textures[i]; if(w == -1) w = t.width; else if(w != t.width) throw("Cannot use Texture.drawTo if textures have different dimensions"); if(h == -1) h = t.height; else if(h != t.height) throw("Cannot use Texture.drawTo if textures have different dimensions"); if(type == null) //first one defines the type type = t.type; else if (type != t.type) throw("Cannot use Texture.drawTo if textures have different data type, all must have the same type"); } } else { w = depth_texture.width; h = depth_texture.height; } var ext = gl.extensions["WEBGL_draw_buffers"]; if(!ext && color_textures && color_textures.length > 1) throw("Rendering to several textures not supported"); var v = gl.getViewport(); gl._framebuffer = gl._framebuffer || gl.createFramebuffer(); gl.bindFramebuffer( gl.FRAMEBUFFER, gl._framebuffer ); gl.viewport( 0, 0, w, h ); var renderbuffer = null; if( depth_texture && depth_texture.format !== gl.DEPTH_COMPONENT || depth_texture.type != gl.UNSIGNED_INT ) throw("Depth texture must be of format: gl.DEPTH_COMPONENT and type: gl.UNSIGNED_INT"); if( depth_texture ) { gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depth_texture.handler, 0); } else //create a temporary depth renderbuffer { //create renderbuffer for depth renderbuffer = gl._renderbuffer = gl._renderbuffer || gl.createRenderbuffer(); renderbuffer.width = w; renderbuffer.height = h; gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer ); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer ); } if( color_textures ) { var order = []; //draw_buffers request the use of an array with the order of the attachments for(var i = 0; i < color_textures.length; i++) { var t = color_textures[i]; gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, t.handler, 0); order.push( gl.COLOR_ATTACHMENT0 + i ); } if(color_textures.length > 1) ext.drawBuffersWEBGL( order ); } else //create temporary color render buffer { var color_renderbuffer = this._color_renderbuffer = this._color_renderbuffer || gl.createRenderbuffer(); color_renderbuffer.width = w; color_renderbuffer.height = h; gl.bindRenderbuffer( gl.RENDERBUFFER, color_renderbuffer ); gl.renderbufferStorage( gl.RENDERBUFFER, gl.RGBA4, w, h ); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, color_renderbuffer ); } var complete = gl.checkFramebufferStatus( gl.FRAMEBUFFER ); if(complete !== gl.FRAMEBUFFER_COMPLETE) throw("FBO not complete: " + complete); callback(); //clear data if(color_textures.length) for(var i = 0; i < color_textures.length; ++i) color_textures[i].data = null; gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(v[0], v[1], v[2], v[3]); } /** * Similar to drawTo but it also stores the depth in a depth texture * @method drawToColorAndDepth * @param {Texture} color_texture * @param {Texture} depth_texture * @param {Function} callback */ Texture.drawToColorAndDepth = function( color_texture, depth_texture, callback ) { var gl = color_texture.gl; //static function if(depth_texture.width != color_texture.width || depth_texture.height != color_texture.height) throw("Different size between color texture and depth texture"); var v = gl.getViewport(); gl._framebuffer = gl._framebuffer || gl.createFramebuffer(); gl.bindFramebuffer( gl.FRAMEBUFFER, gl._framebuffer); gl.viewport(0, 0, color_texture.width, color_texture.height); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, color_texture.handler, 0); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depth_texture.handler, 0); callback(); color_texture.data = null; depth_texture.data = null; gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(v[0], v[1], v[2], v[3]); } /** * Copy content of one texture into another * TODO: check using copyTexImage2D * @method copyTo * @param {GL.Texture} target_texture * @param {GL.Shader} [shader=null] optional shader to apply while copying * @param {Object} [uniforms=null] optional uniforms for the shader */ Texture.prototype.copyTo = function( target_texture, shader, uniforms ) { var that = this; var gl = this.gl; if(!target_texture) throw("target_texture required"); //save state var previous_fbo = gl.getParameter( gl.FRAMEBUFFER_BINDING ); var viewport = gl.getViewport(); if(!shader) shader = this.texture_type == gl.TEXTURE_2D ? GL.Shader.getScreenShader() : GL.Shader.getCubemapCopyShader(); //render gl.disable( gl.BLEND ); gl.disable( gl.DEPTH_TEST ); if(shader && uniforms) shader.uniforms( uniforms ); //reuse fbo var fbo = gl.__copy_fbo; if(!fbo) fbo = gl.__copy_fbo = gl.createFramebuffer(); gl.bindFramebuffer( gl.FRAMEBUFFER, fbo ); gl.viewport(0,0,target_texture.width, target_texture.height); if(this.texture_type == gl.TEXTURE_2D) { //regular color texture if(this.format !== gl.DEPTH_COMPONENT && this.format !== gl.DEPTH_STENCIL ) { /* doesnt work if( this.width == target_texture.width && this.height == target_texture.height && this.format == target_texture.format) { gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.handler, 0); gl.bindTexture( target_texture.texture_type, target_texture.handler ); gl.copyTexImage2D( target_texture.texture_type, 0, this.format, 0, 0, target_texture.width, target_texture.height, 0); } else */ { gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target_texture.handler, 0); this.toViewport( shader ); } } else //copying a depth texture is harder { var color_renderbuffer = gl._color_renderbuffer = gl._color_renderbuffer || gl.createRenderbuffer(); var w = color_renderbuffer.width = target_texture.width; var h = color_renderbuffer.height = target_texture.height; //attach color render buffer gl.bindRenderbuffer( gl.RENDERBUFFER, color_renderbuffer ); gl.renderbufferStorage( gl.RENDERBUFFER, gl.RGBA4, w, h ); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, color_renderbuffer ); //attach depth texture var attachment_point = target_texture.format == gl.DEPTH_STENCIL ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT; gl.framebufferTexture2D( gl.FRAMEBUFFER, attachment_point, gl.TEXTURE_2D, target_texture.handler, 0); var complete = gl.checkFramebufferStatus( gl.FRAMEBUFFER ); if(complete !== gl.FRAMEBUFFER_COMPLETE) throw("FBO not complete: " + complete); //enable depth test? gl.enable( gl.DEPTH_TEST ); gl.depthFunc( gl.ALWAYS ); gl.colorMask( false,false,false,false ); //call shader that overwrites depth values shader = GL.Shader.getCopyDepthShader(); this.toViewport( shader ); gl.colorMask( true,true,true,true ); gl.disable( gl.DEPTH_TEST ); gl.depthFunc( gl.LEQUAL ); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, null ); gl.framebufferTexture2D( gl.FRAMEBUFFER, attachment_point, gl.TEXTURE_2D, null, 0); } } else if(this.texture_type == gl.TEXTURE_CUBE_MAP) { shader.uniforms({u_texture: 0}); var rot_matrix = GL.temp_mat3; for(var i = 0; i < 6; i++) { gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, target_texture.handler, 0); var face_info = GL.Texture.cubemap_camera_parameters[ i ]; mat3.identity( rot_matrix ); rot_matrix.set( face_info.right, 0 ); rot_matrix.set( face_info.up, 3 ); rot_matrix.set( face_info.dir, 6 ); //mat3.invert(rot_matrix,rot_matrix); this.toViewport( shader,{ u_rotation: rot_matrix }); } } //restore previous state gl.setViewport(viewport); //restore viewport gl.bindFramebuffer( gl.FRAMEBUFFER, previous_fbo ); //restore fbo //generate mipmaps when needed if (target_texture.minFilter && target_texture.minFilter != gl.NEAREST && target_texture.minFilter != gl.LINEAR) { target_texture.bind(); gl.generateMipmap(target_texture.texture_type); target_texture.has_mipmaps = true; } target_texture.data = null; gl.bindTexture( target_texture.texture_type, null ); //disable return this; } /** * Similar to CopyTo, but more specific, only for color texture_2D. It doesnt change the blend flag * @method blit * @param {GL.Texture} target_texture * @param {GL.Shader} [shader=null] optional shader to apply while copying * @param {Object} [uniforms=null] optional uniforms for the shader */ Texture.prototype.blit = (function(){ var viewport = new Float32Array(4); return function( target_texture, shader, uniforms ) { var that = this; var gl = this.gl; if ( this.texture_type != gl.TEXTURE_2D || this.format === gl.DEPTH_COMPONENT || this.format === gl.DEPTH_STENCIL ) throw("blit only support TEXTURE_2D of RGB or RGBA. use copyTo instead"); //save state var previous_fbo = gl.getParameter( gl.FRAMEBUFFER_BINDING ); viewport.set( gl.viewport_data ); shader = shader || GL.Shader.getScreenShader(); if(shader && uniforms) shader.uniforms( uniforms ); //reuse fbo var fbo = gl.__copy_fbo; if(!fbo) fbo = gl.__copy_fbo = gl.createFramebuffer(); gl.bindFramebuffer( gl.FRAMEBUFFER, fbo ); gl.viewport(0,0,target_texture.width, target_texture.height); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target_texture.handler, 0); this.bind(0); shader.draw( GL.Mesh.getScreenQuad(), gl.TRIANGLES ); //restore previous state gl.setViewport(viewport); //restore viewport gl.bindFramebuffer( gl.FRAMEBUFFER, previous_fbo ); //restore fbo target_texture.data = null; gl.bindTexture( target_texture.texture_type, null ); //disable return this; } })(); /** * Render texture in a quad to full viewport size * @method toViewport * @param {Shader} shader to apply, otherwise a default textured shader is applied [optional] * @param {Object} uniforms for the shader if needed [optional] */ Texture.prototype.toViewport = function(shader, uniforms) { shader = shader || Shader.getScreenShader(); var mesh = Mesh.getScreenQuad(); this.bind(0); //shader.uniforms({u_texture: 0}); //never changes if(uniforms) shader.uniforms(uniforms); shader.draw( mesh, gl.TRIANGLES ); } /** * Fills the texture with a constant color (uses gl.clear) * @method fill * @param {vec4} color rgba * @param {boolean} skip_mipmaps if true the mipmaps wont be updated */ Texture.prototype.fill = function(color, skip_mipmaps ) { var old_color = gl.getParameter( gl.COLOR_CLEAR_VALUE ); gl.clearColor( color[0], color[1], color[2], color[3] ); this.drawTo( function() { gl.clear( gl.COLOR_BUFFER_BIT ); }); gl.clearColor( old_color[0], old_color[1], old_color[2], old_color[3] ); if (!skip_mipmaps && this.minFilter && this.minFilter != gl.NEAREST && this.minFilter != gl.LINEAR ) { this.bind(); gl.generateMipmap( this.texture_type ); this.has_mipmaps = true; } } /** * Render texture in a quad of specified area * @method renderQuad * @param {number} x * @param {number} y * @param {number} width * @param {number} height */ Texture.prototype.renderQuad = (function() { //static variables: less garbage var identity = mat3.create(); var pos = vec2.create(); var size = vec2.create(); var white = vec4.fromValues(1,1,1,1); return (function(x,y,w,h, shader, uniforms) { pos[0] = x; pos[1] = y; size[0] = w; size[1] = h; shader = shader || Shader.getQuadShader(this.gl); var mesh = Mesh.getScreenQuad(this.gl); this.bind(0); shader.uniforms({u_texture: 0, u_position: pos, u_color: white, u_size: size, u_viewport: gl.viewport_data.subarray(2,4), u_transform: identity }); if(uniforms) shader.uniforms(uniforms); shader.draw( mesh, gl.TRIANGLES ); }); })(); /** * Applies a blur filter of 5x5 pixels to the texture (be careful using it, it is slow) * @method applyBlur * @param {Number} offsetx scalar that multiplies the offset when fetching pixels horizontally (default 1) * @param {Number} offsety scalar that multiplies the offset when fetching pixels vertically (default 1) * @param {Number} intensity scalar that multiplies the result (default 1) * @param {Texture} output_texture [optional] if not passed the output is the own texture * @param {Texture} temp_texture blur needs a temp texture, if not supplied it will use the temporary textures pool * @return {Texture} returns the temp_texture in case you want to reuse it */ Texture.prototype.applyBlur = function( offsetx, offsety, intensity, output_texture, temp_texture ) { var that = this; var gl = this.gl; if(offsetx === undefined) offsetx = 1; if(offsety === undefined) offsety = 1; gl.disable( gl.DEPTH_TEST ); gl.disable( gl.BLEND ); output_texture = output_texture || this; var is_temp = !temp_texture; //if(this === output_texture && this.texture_type === gl.TEXTURE_CUBE_MAP ) // throw("cannot use applyBlur in a texture with itself when blurring a CUBE_MAP"); if(temp_texture === output_texture) throw("cannot use applyBlur in a texture using as temporary itself"); if(output_texture && this.texture_type !== output_texture.texture_type ) throw("cannot use applyBlur with textures of different texture_type"); //if(this.width != output_texture.width || this.height != output_texture.height) // throw("cannot use applyBlur with an output texture of different size, it doesnt work"); //save state var current_fbo = gl.getParameter( gl.FRAMEBUFFER_BINDING ); var viewport = gl.getViewport(); //reuse fbo var fbo = gl.__copy_fbo; if(!fbo) fbo = gl.__copy_fbo = gl.createFramebuffer(); gl.bindFramebuffer( gl.FRAMEBUFFER, fbo ); gl.viewport(0,0, this.width, this.height); if( this.texture_type === gl.TEXTURE_2D ) { var shader = GL.Shader.getBlurShader(); if(!temp_texture) temp_texture = GL.Texture.getTemporary( this.width, this.height, this ); //horizontal blur gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, temp_texture.handler, 0); this.toViewport( shader, {u_texture: 0, u_intensity: intensity, u_offset: [0, offsety / this.height ] }); //vertical blur gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, output_texture.handler, 0); gl.viewport(0,0,output_texture.width, output_texture.height); temp_texture.toViewport( shader, {u_intensity: intensity, u_offset: [offsetx / temp_texture.width, 0] }); if(is_temp) GL.Texture.releaseTemporary( temp_texture ); } else if( this.texture_type === gl.TEXTURE_CUBE_MAP ) { //var weights = new Float32Array([ 0.16/0.98, 0.15/0.98, 0.12/0.98, 0.09/0.98, 0.05/0.98 ]); //var weights = new Float32Array([ 0.05/0.98, 0.09/0.98, 0.12/0.98, 0.15/0.98, 0.16/0.98, 0.15/0.98, 0.12/0.98, 0.09/0.98, 0.05/0.98, 0.0 ]); //extra 0 to avoid mat3 var shader = GL.Shader.getCubemapBlurShader(); shader.uniforms({u_texture: 0, u_intensity: intensity, u_offset: [ offsetx / this.width, offsety / this.height ] }); this.bind(0); var mesh = Mesh.getScreenQuad(); mesh.bindBuffers( shader ); shader.bind(); var destination = null; if(!temp_texture && output_texture == this) //we need a temporary texture destination = temp_texture = GL.Texture.getTemporary( output_texture.width, output_texture.height, output_texture ); else destination = output_texture; //blur directly to output texture var rot_matrix = GL.temp_mat3; for(var i = 0; i < 6; ++i) { gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, destination.handler, 0); var face_info = GL.Texture.cubemap_camera_parameters[ i ]; mat3.identity(rot_matrix); rot_matrix.set( face_info.right, 0 ); rot_matrix.set( face_info.up, 3 ); rot_matrix.set( face_info.dir, 6 ); //mat3.invert(rot_matrix,rot_matrix); shader._setUniform( "u_rotation", rot_matrix ); gl.drawArrays( gl.TRIANGLES, 0, 6 ); } mesh.unbindBuffers( shader ); if(temp_texture) //copy back temp_texture.copyTo( output_texture ); if(temp_texture && is_temp) //release temp GL.Texture.releaseTemporary( temp_texture ); } //restore previous state gl.setViewport(viewport); //restore viewport gl.bindFramebuffer( gl.FRAMEBUFFER, current_fbo ); //restore fbo output_texture.data = null; //generate mipmaps when needed if (output_texture.minFilter && output_texture.minFilter != gl.NEAREST && output_texture.minFilter != gl.LINEAR) { output_texture.bind(); gl.generateMipmap(output_texture.texture_type); output_texture.has_mipmaps = true; } gl.bindTexture( output_texture.texture_type, null ); //disable } /** * Loads and uploads a texture from a url * @method Texture.fromURL * @param {String} url * @param {Object} options * @param {Function} on_complete * @return {Texture} the texture */ Texture.fromURL = function( url, options, on_complete, gl ) { gl = gl || global.gl; options = options || {}; options = Object.create(options); //creates a new options using the old one as prototype var texture = options.texture || new GL.Texture(1, 1, options, gl); if(url.length < 64) texture.url = url; texture.bind(); var default_color = options.temp_color || Texture.loading_color; //Texture.setUploadOptions(options); gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4); var temp_color = options.type == gl.FLOAT ? new Float32Array(default_color) : new Uint8Array(default_color); gl.texImage2D( gl.TEXTURE_2D, 0, texture.format, texture.width, texture.height, 0, texture.format, texture.type, temp_color ); gl.bindTexture( texture.texture_type, null ); //disable texture.ready = false; var ext = null; if( options.extension ) //to force format ext = options.extension; if(!ext && url.length < 512) //avoid base64 urls { var base = url; var pos = url.indexOf("?"); if(pos != -1) base = url.substr(0,pos); pos = base.lastIndexOf("."); if(pos != -1) ext = base.substr(pos+1).toLowerCase(); } if( ext == "dds") { var ext = gl.getExtension("WEBKIT_WEBGL_compressed_texture_s3tc") || gl.getExtension("WEBGL_compressed_texture_s3tc"); var new_texture = new GL.Texture(0,0, options, gl); DDS.loadDDSTextureEx(gl, ext, url, new_texture.handler, true, function(t) { texture.texture_type = t.texture_type; texture.handler = t; delete texture["ready"]; //texture.ready = true; if(on_complete) on_complete(texture, url); }); } else if( ext == "tga" ) { HttpRequest( url, null, function(data) { var img_data = GL.Texture.parseTGA(data); if(!img_data) return; options.texture = texture; if(img_data.format == "RGB") texture.format = gl.RGB; texture = GL.Texture.fromMemory( img_data.width, img_data.height, img_data.pixels, options ); delete texture["ready"]; //texture.ready = true; if(on_complete) on_complete( texture, url ); },null,{ binary: true }); } else //png,jpg,webp,... { var image = new Image(); image.src = url; var that = this; image.onload = function() { options.texture = texture; GL.Texture.fromImage(this, options); delete texture["ready"]; //texture.ready = true; if(on_complete) on_complete(texture, url); } image.onerror = function() { if(on_complete) on_complete(null); } } return texture; }; Texture.parseTGA = function(data) { if(!data || data.constructor !== ArrayBuffer) throw( "TGA: data must be ArrayBuffer"); data = new Uint8Array(data); var TGAheader = new Uint8Array( [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0] ); var TGAcompare = data.subarray(0,12); for(var i = 0; i < TGAcompare.length; i++) if(TGAheader[i] != TGAcompare[i]) { console.error("TGA header is not valid"); return null; //not a TGA } var header = data.subarray(12,18); var img = {}; img.width = header[1] * 256 + header[0]; img.height = header[3] * 256 + header[2]; img.bpp = header[4]; img.bytesPerPixel = img.bpp / 8; img.imageSize = img.width * img.height * img.bytesPerPixel; img.pixels = data.subarray(18,18+img.imageSize); img.pixels = new Uint8Array( img.pixels ); //clone if( (header[5] & (1<<4)) == 0) //hack, needs swap { //TGA comes in BGR format so we swap it, this is slooooow for(var i = 0; i < img.imageSize; i+= img.bytesPerPixel) { var temp = img.pixels[i]; img.pixels[i] = img.pixels[i+2]; img.pixels[i+2] = temp; } header[5] |= 1<<4; //mark as swaped img.format = img.bpp == 32 ? "RGBA" : "RGB"; } else img.format = img.bpp == 32 ? "RGBA" : "RGB"; //some extra bytes to avoid alignment problems //img.pixels = new Uint8Array( img.imageSize + 14); //img.pixels.set( data.subarray(18,18+img.imageSize), 0); img.flipY = true; //img.format = img.bpp == 32 ? "BGRA" : "BGR"; //trace("TGA info: " + img.width + "x" + img.height ); return img; } /** * Create a texture from an Image * @method Texture.fromImage * @param {Image} image * @param {Object} options * @return {Texture} the texture */ Texture.fromImage = function( image, options ) { options = options || {}; var texture = options.texture || new GL.Texture( image.width, image.height, options); texture.uploadImage( image, options ); texture.bind(); gl.texParameteri(texture.texture_type, gl.TEXTURE_MAG_FILTER, texture.magFilter ); gl.texParameteri(texture.texture_type, gl.TEXTURE_MIN_FILTER, texture.minFilter ); gl.texParameteri(texture.texture_type, gl.TEXTURE_WRAP_S, texture.wrapS ); gl.texParameteri(texture.texture_type, gl.TEXTURE_WRAP_T, texture.wrapT ); if (GL.isPowerOfTwo(texture.width) && GL.isPowerOfTwo(texture.height) ) { if( options.minFilter && options.minFilter != gl.NEAREST && options.minFilter != gl.LINEAR) { texture.bind(); gl.generateMipmap(texture.texture_type); texture.has_mipmaps = true; } } else { //no mipmaps supported gl.texParameteri(texture.texture_type, gl.TEXTURE_MIN_FILTER, GL.LINEAR ); gl.texParameteri(texture.texture_type, gl.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE ); gl.texParameteri(texture.texture_type, gl.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE ); texture.has_mipmaps = false; } gl.bindTexture(texture.texture_type, null); //disable texture.data = image; if(options.keep_image) texture.img = image; return texture; }; /** * Create a texture from a Video * @method Texture.fromVideo * @param {Video} video * @param {Object} options * @return {Texture} the texture */ Texture.fromVideo = function(video, options) { options = options || {}; var texture = options.texture || new GL.Texture(video.videoWidth, video.videoHeight, options); texture.bind(); texture.uploadImage( video, options ); if (options.minFilter && options.minFilter != gl.NEAREST && options.minFilter != gl.LINEAR) { texture.bind(); gl.generateMipmap(texture.texture_type); texture.has_mipmaps = true; texture.data = video; } gl.bindTexture(texture.texture_type, null); //disable return texture; }; /** * Create a clone of a texture * @method Texture.fromTexture * @param {Texture} old_texture * @param {Object} options * @return {Texture} the texture */ Texture.fromTexture = function( old_texture, options) { options = options || {}; var texture = new GL.Texture( old_texture.width, old_texture.height, options ); old_texture.copyTo( texture ); return texture; }; Texture.prototype.clone = function( options ) { var old_options = this.getProperties(); if(options) for(var i in options) old_options[i] = options[i]; return Texture.fromTexture( this, old_options); } /** * Create a texture from an ArrayBuffer containing the pixels * @method Texture.fromTexture * @param {number} width * @param {number} height * @param {ArrayBuffer} pixels * @param {Object} options * @return {Texture} the texture */ Texture.fromMemory = function( width, height, pixels, options) //format in options as format { options = options || {}; var texture = options.texture || new GL.Texture(width, height, options); Texture.setUploadOptions(options); texture.bind(); if(pixels.constructor === Array) { if(options.type == gl.FLOAT) pixels = new Float32Array( pixels ); else if(options.type == GL.HALF_FLOAT || options.type == GL.HALF_FLOAT_OES) pixels = new Uint16Array( pixels ); //gl.UNSIGNED_SHORT_4_4_4_4 is only for texture that are SHORT per pixel, not per channel! else pixels = new Uint8Array( pixels ); } gl.texImage2D( gl.TEXTURE_2D, 0, texture.format, width, height, 0, texture.format, texture.type, pixels ); texture.width = width; texture.height = height; texture.data = pixels; if (options.minFilter && options.minFilter != gl.NEAREST && options.minFilter != gl.LINEAR) { gl.generateMipmap(gl.TEXTURE_2D); texture.has_mipmaps = true; } gl.bindTexture(texture.texture_type, null); //disable return texture; }; /** * Create a texture from an ArrayBuffer containing the pixels * @method Texture.fromDDSInMemory * @param {ArrayBuffer} DDS data * @param {Object} options * @return {Texture} the texture */ Texture.fromDDSInMemory = function(data, options) //format in options as format { options = options || {}; var texture = options.texture || new GL.Texture(0, 0, options); GL.Texture.setUploadOptions(options); texture.bind(); var ext = gl.getExtension("WEBKIT_WEBGL_compressed_texture_s3tc") || gl.getExtension("WEBGL_compressed_texture_s3tc"); DDS.loadDDSTextureFromMemoryEx(gl, ext, data, texture, true ); gl.bindTexture(texture.texture_type, null); //disable return texture; }; /** * Create a generative texture from a shader ( must GL.Shader.getScreenShader as reference for the shader ) * @method Texture.fromShader * @param {number} width * @param {number} height * @param {Shader} shader * @param {Object} options * @return {Texture} the texture */ Texture.fromShader = function(width, height, shader, options) { options = options || {}; var texture = new GL.Texture( width, height, options ); //copy content texture.drawTo(function() { gl.disable( gl.BLEND ); gl.disable( gl.DEPTH_TEST ); gl.disable( gl.CULL_FACE ); var mesh = Mesh.getScreenQuad(); shader.draw( mesh ); }); return texture; }; /** * Create a cubemap texture from a set of 6 images * @method Texture.cubemapFromImages * @param {Array} images * @param {Object} options * @return {Texture} the texture */ Texture.cubemapFromImages = function(images, options) { options = options || {}; if(images.length != 6) throw "missing images to create cubemap"; var width = images[0].width; var height = images[0].height; options.texture_type = gl.TEXTURE_CUBE_MAP; var texture = null; if(options.texture) { texture = options.texture; texture.width = width; texture.height = height; } else texture = new GL.Texture( width, height, options ); Texture.setUploadOptions(options); texture.bind(); try { for(var i = 0; i < 6; i++) gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X+i, 0, texture.format, texture.format, texture.type, images[i]); texture.data = images; } catch (e) { if (location.protocol == 'file:') { throw 'image not loaded for security reasons (serve this page over "http://" instead)'; } else { throw 'image not loaded for security reasons (image must originate from the same ' + 'domain as this page or use Cross-Origin Resource Sharing)'; } } if (options.minFilter && options.minFilter != gl.NEAREST && options.minFilter != gl.LINEAR) { gl.generateMipmap(gl.TEXTURE_CUBE_MAP); texture.has_mipmaps = true; } texture.unbind(); return texture; }; /** * Create a cubemap texture from a single image that contains all six images * If it is a cross, it must be horizontally aligned, and options.is_cross must be equal to the column where the top and bottom are located (usually 1 or 2) * otherwise it assumes the 6 images are arranged vertically, in the order of OpenGL: +X, -X, +Y, -Y, +Z, -Z * @method Texture.cubemapFromImage * @param {Image} image * @param {Object} options * @return {Texture} the texture */ Texture.cubemapFromImage = function( image, options ) { options = options || {}; if(image.width != (image.height / 6) && image.height % 6 != 0 && !options.faces && !options.is_polar ) { console.error( "Cubemap image not valid, only 1x6 (vertical) or 6x3 (cross) formats. Check size:", image.width, image.height ); return null; } var width = image.width; var height = image.height; if(options.is_polar) { var size = options.size || GL.nearestPowerOfTwo( image.height ); var temp_tex = GL.Texture.fromImage( image, { ignore_pot:true, wrap: gl.REPEAT, filter: gl.LINEAR } ); var cubemap = new GL.Texture( size, size, { texture_type: gl.TEXTURE_CUBE_MAP, format: gl.RGBA }); if(options.texture) { var old_tex = options.texture; for(var i in cubemap) old_tex[i] = cubemap[i]; cubemap = old_tex; } var rot_matrix = mat3.create(); var uniforms = { u_texture:0, u_rotation: rot_matrix }; gl.disable( gl.DEPTH_TEST ); gl.disable( gl.BLEND ); var shader = GL.Shader.getPolarToCubemapShader(); cubemap.drawTo(function(t,i){ var face_info = GL.Texture.cubemap_camera_parameters[ i ]; mat3.identity( rot_matrix ); rot_matrix.set( face_info.right, 0 ); rot_matrix.set( face_info.up, 3 ); rot_matrix.set( face_info.dir, 6 ); temp_tex.toViewport( shader, uniforms ); }); if(options.keep_image) cubemap.img = image; return cubemap; } else if(options.is_cross !== undefined) { options.faces = Texture.generateCubemapCrossFacesInfo(image.width, options.is_cross); width = height = image.width / 4; } else if(options.faces) { width = options.width || options.faces[0].width; height = options.height || options.faces[0].height; } else height /= 6; if(width != height) { console.log("Texture not valid, width and height for every face must be square"); return null; } var size = width; options.no_flip = true; var images = []; for(var i = 0; i < 6; i++) { var canvas = createCanvas( size, size ); var ctx = canvas.getContext("2d"); if(options.faces) ctx.drawImage(image, options.faces[i].x, options.faces[i].y, options.faces[i].width || size, options.faces[i].height || size, 0,0, size, size ); else ctx.drawImage(image, 0, height*i, width, height, 0,0, size, size ); images.push(canvas); //document.body.appendChild(canvas); //debug } var texture = Texture.cubemapFromImages(images, options); if(options.keep_image) texture.img = image; return texture; }; /** * Given the width and the height of an image, and in which column is the top and bottom sides of the cubemap, it gets the info to pass to Texture.cubemapFromImage in options.faces * @method Texture.generateCubemapCrossFaces * @param {number} width of the CROSS image (not the side image) * @param {number} column the column where the top and the bottom is located * @return {Object} object to pass to Texture.cubemapFromImage in options.faces */ Texture.generateCubemapCrossFacesInfo = function(width, column) { if(column === undefined) column = 1; var s = width / 4; return [ { x: 2*s, y: s, width: s, height: s }, //+x { x: 0, y: s, width: s, height: s }, //-x { x: column*s, y: 0, width: s, height: s }, //+y { x: column*s, y: 2*s, width: s, height: s }, //-y { x: s, y: s, width: s, height: s }, //+z { x: 3*s, y: s, width: s, height: s } //-z ]; } /** * Create a cubemap texture from a single image url that contains the six images * if it is a cross, it must be horizontally aligned, and options.is_cross must be equal to the column where the top and bottom are located (usually 1 or 2) * otherwise it assumes the 6 images are arranged vertically. * @method Texture.cubemapFromURL * @param {Image} image * @param {Object} options * @param {Function} on_complete callback * @return {Texture} the texture */ Texture.cubemapFromURL = function( url, options, on_complete ) { options = options || {}; options = Object.create(options); //creates a new options using the old one as prototype options.texture_type = gl.TEXTURE_CUBE_MAP; var texture = options.texture || new GL.Texture(1, 1, options); texture.bind(); Texture.setUploadOptions(options); var default_color = options.temp_color || [0,0,0,255]; var temp_color = options.type == gl.FLOAT ? new Float32Array(default_color) : new Uint8Array(default_color); for(var i = 0; i < 6; i++) gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X+i, 0, texture.format, 1, 1, 0, texture.format, texture.type, temp_color); gl.bindTexture(texture.texture_type, null); //disable texture.ready = false; var image = new Image(); image.src = url; var that = this; image.onload = function() { options.texture = texture; texture = GL.Texture.cubemapFromImage(this, options); if(texture) delete texture["ready"]; //texture.ready = true; if(on_complete) on_complete(texture); } return texture; }; /** * returns an ArrayBuffer with the pixels in the texture, they are fliped in Y * Warn: If cubemap it only returns the pixels of the first face! use getCubemapPixels instead * @method getPixels * @param {number} cubemap_face [optional] the index of the cubemap face to read (ignore if texture_2D) * @param {number} mipmap level [optional, default is 0] * @return {ArrayBuffer} the data ( Uint8Array, Uint16Array or Float32Array ) */ Texture.prototype.getPixels = function( cubemap_face, mipmap_level ) { mipmap_level = mipmap_level || 0; var gl = this.gl; var v = gl.getViewport(); var old_fbo = gl.getParameter( gl.FRAMEBUFFER_BINDING ); if(this.format == gl.DEPTH_COMPONENT) throw("cannot use getPixels in depth textures"); gl.disable( gl.DEPTH_TEST ); //reuse fbo var fbo = gl.__copy_fbo; if(!fbo) fbo = gl.__copy_fbo = gl.createFramebuffer(); gl.bindFramebuffer( gl.FRAMEBUFFER, fbo ); var buffer = null; var width = this.width >> mipmap_level; var height = this.height >> mipmap_level; gl.viewport(0, 0, width, height); if(this.texture_type == gl.TEXTURE_2D) gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.handler, mipmap_level); else if(this.texture_type == gl.TEXTURE_CUBE_MAP) gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + (cubemap_face || 0), this.handler, mipmap_level); var channels = this.format == gl.RGB ? 3 : 4; channels = 4; //WEBGL DOES NOT SUPPORT READING 3 CHANNELS ONLY, YET... var type = this.type; //type = gl.UNSIGNED_BYTE; //WEBGL DOES NOT SUPPORT READING FLOAT seems, YET... 23/5/18 now it seems it does now if(type == gl.UNSIGNED_BYTE) buffer = new Uint8Array( width * height * channels ); else if(type == GL.HALF_FLOAT || type == GL.HALF_FLOAT_OES) //previously half float couldnot be read buffer = new Uint16Array( width * height * channels ); //gl.UNSIGNED_SHORT_4_4_4_4 is only for texture that are SHORT per pixel, not per channel! else buffer = new Float32Array( width * height * channels ); gl.readPixels( 0,0, width, height, channels == 3 ? gl.RGB : gl.RGBA, type, buffer ); //NOT SUPPORTED FLOAT or RGB BY WEBGL YET //restore gl.bindFramebuffer(gl.FRAMEBUFFER, old_fbo ); gl.viewport(v[0], v[1], v[2], v[3]); return buffer; } /** * uploads some pixels to the texture (see uploadData method for more options) * @method setPixels * @param {ArrayBuffer} data gl.UNSIGNED_BYTE or gl.FLOAT data * @param {Boolean} no_flip do not flip in Y * @param {Boolean} skip_mipmaps do not update mipmaps when possible * @param {Number} cubemap_face if the texture is a cubemap, which face */ Texture.prototype.setPixels = function( data, no_flip, skip_mipmaps, cubemap_face ) { var options = { no_flip: no_flip }; if(cubemap_face) options.cubemap_face = cubemap_face; this.uploadData( data, options, skip_mipmaps ); } /** * returns an array with six arrays containing the pixels of every cubemap face * @method getCubemapPixels * @return {Array} the array that has 6 typed arrays containing the pixels */ Texture.prototype.getCubemapPixels = function() { if(this.texture_type !== gl.TEXTURE_CUBE_MAP) throw("this texture is not a cubemap"); return [ this.getPixels(0), this.getPixels(1), this.getPixels(2), this.getPixels(3), this.getPixels(4), this.getPixels(5) ]; } /** * fills a cubemap given an array with typed arrays containing the pixels of 6 faces * @method setCubemapPixels * @param {Array} data array that has 6 typed arrays containing the pixels * @param {bool} noflip if pixels should not be flipped according to Y */ Texture.prototype.setCubemapPixels = function( data_array, no_flip ) { if(this.texture_type !== gl.TEXTURE_CUBE_MAP) throw("this texture is not a cubemap, it should be created with { texture_type: gl.TEXTURE_CUBE_MAP }"); for(var i = 0; i < 6; ++i) this.setPixels( data_array[i], no_flip, i != 5, i ); } /** * Copy texture content to a canvas * @method toCanvas * @param {Canvas} canvas must have the same size, if different the canvas will be resized * @param {boolean} flip_y optional, flip vertically * @param {Number} max_size optional, if it is supplied the canvas wont be bigger of max_size (the image will be scaled down) */ Texture.prototype.toCanvas = function( canvas, flip_y, max_size ) { max_size = max_size || 8192; var gl = this.gl; var w = Math.min( this.width, max_size ); var h = Math.min( this.height, max_size ); //cross if(this.texture_type == gl.TEXTURE_CUBE_MAP) { w = w * 4; h = h * 3; } canvas = canvas || createCanvas( w, h ); if(canvas.width != w) canvas.width = w; if(canvas.height != h) canvas.height = h; var buffer = null; if(this.texture_type == gl.TEXTURE_2D ) { if(this.width != w || this.height != h || this.type != gl.UNSIGNED_BYTE) //resize image to fit the canvas { //create a temporary texture var temp = new GL.Texture(w,h,{ format: gl.RGBA, filter: gl.NEAREST }); this.copyTo( temp ); buffer = temp.getPixels(); } else buffer = this.getPixels(); var ctx = canvas.getContext("2d"); var pixels = ctx.getImageData(0,0,w,h); pixels.data.set( buffer ); ctx.putImageData(pixels,0,0); if(flip_y) { var temp = createCanvas(w,h); var temp_ctx = temp.getContext("2d"); temp_ctx.translate(0,temp.height); temp_ctx.scale(1,-1); temp_ctx.drawImage( canvas, 0, 0, temp.width, temp.height ); ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); ctx.drawImage( temp, 0, 0 ); } } else if(this.texture_type == gl.TEXTURE_CUBE_MAP ) { var temp_canvas = createCanvas( this.width, this.height ); var temp_ctx = temp_canvas.getContext("2d"); var info = GL.Texture.generateCubemapCrossFacesInfo( canvas.width, 1 ); var ctx = canvas.getContext("2d"); ctx.fillStyle = "black"; ctx.fillRect(0,0,canvas.width, canvas.height ); var cubemap = this; if(this.type != gl.UNSIGNED_BYTE) //convert pixels to uint8 as it is the only supported format by the canvas { //create a temporary texture cubemap = new GL.Texture( this.width, this.height, { format: gl.RGBA, texture_type: gl.TEXTURE_CUBE_MAP, filter: gl.NEAREST, type: gl.UNSIGNED_BYTE }); this.copyTo( cubemap ); } for(var i = 0; i < 6; i++) { var pixels = temp_ctx.getImageData(0,0, temp_canvas.width, temp_canvas.height ); buffer = cubemap.getPixels(i); pixels.data.set( buffer ); temp_ctx.putImageData(pixels,0,0); ctx.drawImage( temp_canvas, info[i].x, info[i].y, temp_canvas.width, temp_canvas.height ); } } return canvas; } /** * returns the texture file in binary format * @method toBinary * @param {Boolean} flip_y * @return {ArrayBuffer} the arraybuffer of the file containing the image */ Texture.binary_extension = "png"; Texture.prototype.toBinary = function(flip_y, type) { //dump to canvas var canvas = this.toCanvas(null,flip_y); //use the slow method (because its sync) var data = canvas.toDataURL( type ); var index = data.indexOf(","); var base64_data = data.substr(index+1); var binStr = atob( base64_data ); var len = binStr.length, arr = new Uint8Array(len); for (var i=0; i 0 ) console.warn("this texture is already in the textures pool"); var pool = gl._texture_pool; if(!pool) pool = gl._texture_pool = []; tex._pool = getTime(); pool.push( tex ); //do not store too much textures in the textures pool if( pool.length > 20 ) { pool.sort( function(a,b) { return b._pool - a._pool } ); //sort by time //pool.sort( function(a,b) { return a._key - b._key } ); //sort by size var tex = pool.pop(); //free the last one tex._pool = 0; tex.delete(); } } //returns the next power of two bigger than size Texture.nextPOT = function( size ) { return Math.pow( 2, Math.ceil( Math.log(size) / Math.log(2) ) ); } /** * FBO for FrameBufferObjects, FBOs are used to store the render inside one or several textures * Supports multibuffer and depthbuffer texture, useful for deferred rendering * @namespace GL * @class FBO * @param {Array} color_textures an array containing the color textures, if not supplied a render buffer will be used * @param {GL.Texture} depth_texture the depth texture, if not supplied a render buffer will be used * @param {Bool} stencil create a stencil buffer? * @constructor */ function FBO( textures, depth_texture, stencil, gl ) { gl = gl || global.gl; this.gl = gl; this._context_id = gl.context_id; if(textures && textures.constructor !== Array) throw("FBO textures must be an Array"); this.handler = null; this.width = -1; this.height = -1; this.color_textures = []; this.depth_texture = null; this.stencil = !!stencil; this._stencil_enabled = false; this._num_binded_textures = 0; //assign textures if((textures && textures.length) || depth_texture) this.setTextures( textures, depth_texture ); //save state this._old_fbo_handler = null; this._old_viewport = new Float32Array(4); this.order = null; } GL.FBO = FBO; /** * Changes the textures binded to this FBO * @method setTextures * @param {Array} color_textures an array containing the color textures, if not supplied a render buffer will be used * @param {GL.Texture} depth_texture the depth texture, if not supplied a render buffer will be used * @param {Boolean} skip_disable it doenst try to go back to the previous FBO enabled in case there was one */ FBO.prototype.setTextures = function( color_textures, depth_texture, skip_disable ) { //test depth if( depth_texture && depth_texture.constructor === GL.Texture ) { if( depth_texture.format !== GL.DEPTH_COMPONENT && depth_texture.format !== GL.DEPTH_STENCIL && depth_texture.format !== GL.DEPTH_COMPONENT16 && depth_texture.format !== GL.DEPTH_COMPONENT24 && depth_texture.format !== GL.DEPTH_COMPONENT32F ) throw("FBO Depth texture must be of format: gl.DEPTH_COMPONENT, gl.DEPTH_STENCIL or gl.DEPTH_COMPONENT16/24/32F (only in webgl2)"); if( depth_texture.type != GL.UNSIGNED_SHORT && depth_texture.type != GL.UNSIGNED_INT && depth_texture.type != GL.UNSIGNED_INT_24_8_WEBGL && depth_texture.type != GL.FLOAT) throw("FBO Depth texture must be of type: gl.UNSIGNED_SHORT, gl.UNSIGNED_INT, gl.UNSIGNED_INT_24_8_WEBGL"); } //test if is already binded var same = this.depth_texture == depth_texture; if( same && color_textures ) { if( color_textures.constructor !== Array ) throw("FBO: color_textures parameter must be an array containing all the textures to be binded in the color"); if( color_textures.length == this.color_textures.length ) { for(var i = 0; i < color_textures.length; ++i) if( color_textures[i] != this.color_textures[i] ) { same = false; break; } } else same = false; } if(this._stencil_enabled !== this.stencil) same = false; if(same) return; //copy textures in place this.color_textures.length = color_textures ? color_textures.length : 0; if(color_textures) for(var i = 0; i < color_textures.length; ++i) this.color_textures[i] = color_textures[i]; this.depth_texture = depth_texture; //update GPU FBO this.update( skip_disable ); } /** * Updates the FBO with the new set of textures and buffers * @method update * @param {Boolean} skip_disable it doenst try to go back to the previous FBO enabled in case there was one */ FBO.prototype.update = function( skip_disable ) { //save state to restore afterwards this._old_fbo_handler = gl.getParameter( gl.FRAMEBUFFER_BINDING ); if(!this.handler) this.handler = gl.createFramebuffer(); var w = -1, h = -1, type = null; var color_textures = this.color_textures; var depth_texture = this.depth_texture; //compute the W and H (and check they have the same size) if(color_textures && color_textures.length) for(var i = 0; i < color_textures.length; i++) { var t = color_textures[i]; if(t.constructor !== GL.Texture) throw("FBO can only bind instances of GL.Texture"); if(w == -1) w = t.width; else if(w != t.width) throw("Cannot bind textures with different dimensions"); if(h == -1) h = t.height; else if(h != t.height) throw("Cannot bind textures with different dimensions"); if(type == null) //first one defines the type type = t.type; else if (type != t.type) throw("Cannot bind textures to a FBO with different pixel formats"); if (t.texture_type != gl.TEXTURE_2D) throw("Cannot bind a Cubemap to a FBO"); } else { w = depth_texture.width; h = depth_texture.height; } this.width = w; this.height = h; gl.bindFramebuffer( gl.FRAMEBUFFER, this.handler ); //draw_buffers allow to have more than one color texture binded in a FBO var ext = gl.extensions["WEBGL_draw_buffers"]; if( gl.webgl_version == 1 && !ext && color_textures && color_textures.length > 1) throw("Rendering to several textures not supported by your browser"); var target = gl.webgl_version == 1 ? gl.FRAMEBUFFER : gl.DRAW_FRAMEBUFFER; //detach anything bindede gl.framebufferRenderbuffer( target, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, null ); gl.framebufferRenderbuffer( target, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, null ); //detach color too? //bind a buffer for the depth if( depth_texture && depth_texture.constructor === GL.Texture ) { if(gl.webgl_version == 1 && !gl.extensions["WEBGL_depth_texture"] ) throw("Rendering to depth texture not supported by your browser"); if(this.stencil && depth_texture.format !== gl.DEPTH_STENCIL ) console.warn("Stencil cannot be enabled if there is a depth texture with a DEPTH_STENCIL format"); if( depth_texture.format == gl.DEPTH_STENCIL ) gl.framebufferTexture2D( target, gl.DEPTH_STENCIL_ATTACHMENT, gl.TEXTURE_2D, depth_texture.handler, 0); else gl.framebufferTexture2D( target, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depth_texture.handler, 0); } else //create a renderbuffer to store depth { var depth_renderbuffer = null; //allows to reuse a renderbuffer between FBOs if( depth_texture && depth_texture.constructor === WebGLRenderbuffer && depth_texture.width == w && depth_texture.height == h ) depth_renderbuffer = this._depth_renderbuffer = depth_texture; else { //create one depth_renderbuffer = this._depth_renderbuffer = this._depth_renderbuffer || gl.createRenderbuffer(); depth_renderbuffer.width = w; depth_renderbuffer.height = h; } gl.bindRenderbuffer( gl.RENDERBUFFER, depth_renderbuffer ); if(this.stencil) { gl.renderbufferStorage( gl.RENDERBUFFER, gl.DEPTH_STENCIL, w, h ); gl.framebufferRenderbuffer( target, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depth_renderbuffer ); } else { gl.renderbufferStorage( gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h ); gl.framebufferRenderbuffer( target, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depth_renderbuffer ); } } //bind buffers for the colors if(color_textures && color_textures.length) { this.order = []; //draw_buffers request the use of an array with the order of the attachments for(var i = 0; i < color_textures.length; i++) { var t = color_textures[i]; //not a bug, gl.COLOR_ATTACHMENT0 + i because COLOR_ATTACHMENT is sequential numbers gl.framebufferTexture2D( target, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, t.handler, 0 ); this.order.push( gl.COLOR_ATTACHMENT0 + i ); } } else //create renderbuffer to store color { var color_renderbuffer = this._color_renderbuffer = this._color_renderbuffer || gl.createRenderbuffer(); color_renderbuffer.width = w; color_renderbuffer.height = h; gl.bindRenderbuffer( gl.RENDERBUFFER, color_renderbuffer ); gl.renderbufferStorage( gl.RENDERBUFFER, gl.RGBA4, w, h ); gl.framebufferRenderbuffer( target, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, color_renderbuffer ); } //detach old ones (only if is reusing a FBO with a different set of textures) var num = color_textures ? color_textures.length : 0; for(var i = num; i < this._num_binded_textures; ++i) gl.framebufferTexture2D( target, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, null, 0); this._num_binded_textures = num; this._stencil_enabled = this.stencil; /* does not work, must be used with the depth_stencil if(this.stencil && !depth_texture) { var stencil_buffer = this._stencil_buffer = this._stencil_buffer || gl.createRenderbuffer(); stencil_buffer.width = w; stencil_buffer.height = h; gl.bindRenderbuffer( gl.RENDERBUFFER, stencil_buffer ); gl.renderbufferStorage( gl.RENDERBUFFER, gl.STENCIL_INDEX8, w, h); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, stencil_buffer ); this._stencil_enabled = true; } else { this._stencil_buffer = null; this._stencil_enabled = false; } */ //when using more than one texture you need to use the multidraw extension if(color_textures && color_textures.length > 1) { if( ext ) ext.drawBuffersWEBGL( this.order ); else gl.drawBuffers( this.order ); } //check completion var complete = gl.checkFramebufferStatus( target ); if(complete !== gl.FRAMEBUFFER_COMPLETE) //36054: GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT throw("FBO not complete: " + complete); //restore state gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); if(!skip_disable) gl.bindFramebuffer( target, this._old_fbo_handler ); } /** * Enables this FBO (from now on all the render will be stored in the textures attached to this FBO) * It stores the previous viewport to restore it afterwards, and changes it to full FBO size * @method bind * @param {boolean} keep_old keeps the previous FBO is one was attached to restore it afterwards */ FBO.prototype.bind = function( keep_old ) { if(!this.color_textures.length && !this.depth_texture) throw("FBO: no textures attached to FBO"); this._old_viewport.set( gl.viewport_data ); if(keep_old) this._old_fbo_handler = gl.getParameter( gl.FRAMEBUFFER_BINDING ); else this._old_fbo_handler = null; if(this._old_fbo_handler != this.handler ) gl.bindFramebuffer( gl.FRAMEBUFFER, this.handler ); //mark them as in use in the FBO for(var i = 0; i < this.color_textures.length; ++i) this.color_textures[i]._in_current_fbo = true; if(this.depth_texture) this.depth_texture._in_current_fbo = true; gl.viewport( 0,0, this.width, this.height ); FBO.current = this; } /** * Disables this FBO, if it was binded with keep_old then the old FBO is enabled, otherwise it will render to the screen * Restores viewport to previous * @method unbind */ FBO.prototype.unbind = function() { gl.bindFramebuffer( gl.FRAMEBUFFER, this._old_fbo_handler ); this._old_fbo_handler = null; gl.setViewport( this._old_viewport ); //mark the textures as no longer in use for(var i = 0; i < this.color_textures.length; ++i) this.color_textures[i]._in_current_fbo = false; if(this.depth_texture) this.depth_texture._in_current_fbo = false; FBO.current = null; } //binds another FBO without switch back to previous (faster) FBO.prototype.switchTo = function( next_fbo ) { next_fbo._old_fbo_handler = this._old_fbo_handler; next_fbo._old_viewport.set( this._old_viewport ); gl.bindFramebuffer( gl.FRAMEBUFFER, next_fbo.handler ); this._old_fbo_handler = null; gl.viewport( 0,0, this.width, this.height ); //mark the textures as no longer in use for(var i = 0; i < this.color_textures.length; ++i) this.color_textures[i]._in_current_fbo = false; if(this.depth_texture) this.depth_texture._in_current_fbo = false; //mark them as in use in the FBO for(var i = 0; i < next_fbo.color_textures.length; ++i) next_fbo.color_textures[i]._in_current_fbo = true; if(next_fbo.depth_texture) next_fbo.depth_texture._in_current_fbo = true; FBO.current = next_fbo; } FBO.prototype.delete = function() { gl.deleteFramebuffer( this.handler ); this.handler = null; } //WebGL 1.0 support for certaing FBOs is not very clear and can crash sometimes FBO.supported = {}; //type: gl.FLOAT, format: gl.RGBA FBO.testSupport = function( type, format ) { var name = type +":" + format; if( FBO.supported[ name ] != null ) return FBO.supported[ name ]; var tex = new GL.Texture(1,1,{ format: format, type: type }); try { var fbo = new GL.FBO([tex]); } catch (err) { console.warn("This browser WEBGL implementation doesn't support this FBO format: " + GL.reverse[type] + " " + GL.reverse[format] ); return FBO.supported[ name ] = false; } FBO.supported[ name ] = true; return true; } FBO.prototype.toSingle = function() { if( this.color_textures.length < 2 ) return; //nothing to do var ext = gl.extensions.WEBGL_draw_buffers; if( ext ) ext.drawBuffersWEBGL( [ this.order[0] ] ); else gl.drawBuffers( [ this.order[0] ] ); } FBO.prototype.toMulti = function() { if( this.color_textures.length < 2 ) return; //nothing to do var ext = gl.extensions.WEBGL_draw_buffers; if( ext ) ext.drawBuffersWEBGL( this.order ); else gl.drawBuffers( this.order ); } //clears only the secondary buffers (not the main one) FBO.prototype.clearSecondary = function( color ) { if(!this.order || this.order.length < 2) return; var ext = gl.extensions.WEBGL_draw_buffers; var new_order = [gl.NONE]; for(var i = 1; i < this.order.length; ++i) new_order.push(this.order[i]); if(ext) ext.drawBuffersWEBGL( new_order ); else gl.drawBuffers( new_order ); gl.clearColor( color[0],color[1],color[2],color[3] ); gl.clear( gl.COLOR_BUFFER_BIT ); if(ext) ext.drawBuffersWEBGL( this.order ); else gl.drawBuffers( this.order ); } /** * @namespace GL */ /** * Shader class to upload programs to the GPU * @class Shader * @constructor * @param {String} vertexSource (it also allows to pass a compiled vertex shader) * @param {String} fragmentSource (it also allows to pass a compiled fragment shader) * @param {Object} macros (optional) precompiler macros to be applied when compiling */ global.Shader = GL.Shader = function Shader( vertexSource, fragmentSource, macros ) { if(GL.debug) console.log("GL.Shader created"); if( !vertexSource || !fragmentSource ) throw("GL.Shader source code parameter missing"); //used to avoid problems with resources moving between different webgl context this._context_id = global.gl.context_id; var gl = this.gl = global.gl; //expand macros var extra_code = Shader.expandMacros( macros ); var final_vertexSource = vertexSource.constructor === String ? Shader.injectCode( extra_code, vertexSource, gl ) : vertexSource; var final_fragmentSource = fragmentSource.constructor === String ? Shader.injectCode( extra_code, fragmentSource, gl ) : fragmentSource; this.program = gl.createProgram(); var vs = vertexSource.constructor === String ? GL.Shader.compileSource( gl.VERTEX_SHADER, final_vertexSource ) : vertexSource; var fs = fragmentSource.constructor === String ? GL.Shader.compileSource( gl.FRAGMENT_SHADER, final_fragmentSource ) : fragmentSource; gl.attachShader( this.program, vs, gl ); gl.attachShader( this.program, fs, gl ); gl.linkProgram(this.program); if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { throw 'link error: ' + gl.getProgramInfoLog(this.program); } this.vs_shader = vs; this.fs_shader = fs; //Extract info from the shader this.attributes = {}; this.uniformInfo = {}; this.samplers = {}; //extract info about the shader to speed up future processes this.extractShaderInfo(); } Shader.expandMacros = function(macros) { var extra_code = ""; //add here preprocessor directives that should be above everything if(macros) for(var i in macros) extra_code += "#define " + i + " " + (macros[i] ? macros[i] : "") + "\n"; return extra_code; } //this is done to avoid problems with the #version which must be in the first line Shader.injectCode = function( inject_code, code, gl ) { var index = code.indexOf("\n"); var version = ( gl ? "#define WEBGL" + gl.webgl_version + "\n" : ""); var first_line = code.substr(0,index).trim(); if( first_line.indexOf("#version") == -1 ) return version + inject_code + code; return first_line + "\n" + version + inject_code + code.substr(index); } /** * Compiles one single shader source (could be gl.VERTEX_SHADER or gl.FRAGMENT_SHADER) and returns the webgl shader handler * Used internaly to compile the vertex and fragment shader. * It throws an exception if there is any error in the code * @method Shader.compileSource * @param {Number} type could be gl.VERTEX_SHADER or gl.FRAGMENT_SHADER * @param {String} source the source file to compile * @return {WebGLShader} the handler from webgl */ Shader.compileSource = function( type, source, gl, shader ) { gl = gl || global.gl; shader = shader || gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { throw (type == gl.VERTEX_SHADER ? "Vertex" : "Fragment") + ' shader compile error: ' + gl.getShaderInfoLog(shader); } return shader; } Shader.parseError = function( error_str, vs_code, fs_code ) { if(!error_str) return null; var t = error_str.split(" "); var nums = t[5].split(":"); return { type: t[0], line_number: parseInt( nums[1] ), line_pos: parseInt( nums[0] ), line_code: ( t[0] == "Fragment" ? fs_code : vs_code ).split("\n")[ parseInt( nums[1] ) ], err: error_str }; } /** * It updates the code inside one shader * @method updateShader * @param {String} vertexSource * @param {String} fragmentSource * @param {Object} macros [optional] */ Shader.prototype.updateShader = function( vertexSource, fragmentSource, macros ) { var gl = this.gl || global.gl; //expand macros var extra_code = Shader.expandMacros( macros ); if(!this.program) this.program = gl.createProgram(); else { gl.detachShader( this.program, this.vs_shader ); gl.detachShader( this.program, this.fs_shader ); } var extra_code = Shader.expandMacros( macros ); var final_vertexSource = vertexSource.constructor === String ? Shader.injectCode( extra_code, vertexSource, gl ) : vertexSource; var final_fragmentSource = fragmentSource.constructor === String ? Shader.injectCode( extra_code, fragmentSource, gl ) : fragmentSource; var vs = vertexSource.constructor === String ? GL.Shader.compileSource( gl.VERTEX_SHADER, final_vertexSource ) : vertexSource; var fs = fragmentSource.constructor === String ? GL.Shader.compileSource( gl.FRAGMENT_SHADER, final_fragmentSource ) : fragmentSource; gl.attachShader( this.program, vs, gl ); gl.attachShader( this.program, fs, gl ); gl.linkProgram( this.program ); if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { throw 'link error: ' + gl.getProgramInfoLog( this.program ); } //store shaders separated this.vs_shader = vs; this.fs_shader = fs; //Extract info from the shader this.attributes = {}; this.uniformInfo = {}; this.samplers = {}; //extract info about the shader to speed up future processes this.extractShaderInfo(); } /** * It extract all the info about the compiled shader program, all the info about uniforms and attributes. * This info is stored so it works faster during rendering. * @method extractShaderInfo */ Shader.prototype.extractShaderInfo = function() { var gl = this.gl; var l = gl.getProgramParameter( this.program, gl.ACTIVE_UNIFORMS ); //extract uniforms info for(var i = 0; i < l; ++i) { var data = gl.getActiveUniform( this.program, i); if(!data) break; var uniformName = data.name; //arrays have uniformName[0], strip the [] (also data.size tells you if it is an array) var pos = uniformName.indexOf("["); if(pos != -1) { var pos2 = uniformName.indexOf("]."); //leave array of structs though if(pos2 == -1) uniformName = uniformName.substr(0,pos); } //store texture samplers if(data.type == gl.SAMPLER_2D || data.type == gl.SAMPLER_CUBE || data.type == GL.SAMPLER_3D) this.samplers[ uniformName ] = data.type; //get which function to call when uploading this uniform var func = Shader.getUniformFunc(data); var is_matrix = false; if(data.type == gl.FLOAT_MAT2 || data.type == gl.FLOAT_MAT3 || data.type == gl.FLOAT_MAT4) is_matrix = true; var type_length = GL.TYPE_LENGTH[ data.type ] || 1; //save the info so the user doesnt have to specify types when uploading data to the shader this.uniformInfo[ uniformName ] = { type: data.type, func: func, size: data.size, type_length: type_length, is_matrix: is_matrix, loc: gl.getUniformLocation(this.program, uniformName), data: new Float32Array( type_length * data.size ) //prealloc space to assign uniforms that are not typed }; } //extract attributes info for(var i = 0, l = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES); i < l; ++i) { var data = gl.getActiveAttrib( this.program, i); if(!data) break; var func = Shader.getUniformFunc(data); var type_length = GL.TYPE_LENGTH[ data.type ] || 1; this.uniformInfo[ data.name ] = { type: data.type, func: func, type_length: type_length, size: data.size, loc: null }; //gl.getAttribLocation( this.program, data.name ) this.attributes[ data.name ] = gl.getAttribLocation(this.program, data.name ); } } /** * Returns if this shader has a uniform with the given name * @method hasUniform * @param {String} name name of the uniform * @return {Boolean} */ Shader.prototype.hasUniform = function(name) { return this.uniformInfo[name]; } /** * Returns if this shader has an attribute with the given name * @method hasAttribute * @param {String} name name of the attribute * @return {Boolean} */ Shader.prototype.hasAttribute = function(name) { return this.attributes[name]; } /** * Tells you which function to call when uploading a uniform according to the data type in the shader * Used internally from extractShaderInfo to optimize calls * @method Shader.getUniformFunc * @param {Object} data info about the uniform * @return {Function} */ Shader.getUniformFunc = function( data ) { var func = null; switch (data.type) { case GL.FLOAT: if(data.size == 1) func = gl.uniform1f; else func = gl.uniform1fv; break; case GL.FLOAT_MAT2: func = gl.uniformMatrix2fv; break; case GL.FLOAT_MAT3: func = gl.uniformMatrix3fv; break; case GL.FLOAT_MAT4: func = gl.uniformMatrix4fv; break; case GL.FLOAT_VEC2: func = gl.uniform2fv; break; case GL.FLOAT_VEC3: func = gl.uniform3fv; break; case GL.FLOAT_VEC4: func = gl.uniform4fv; break; case GL.UNSIGNED_INT: case GL.INT: if(data.size == 1) func = gl.uniform1i; else func = gl.uniform1iv; break; case GL.INT_VEC2: func = gl.uniform2iv; break; case GL.INT_VEC3: func = gl.uniform3iv; break; case GL.INT_VEC4: func = gl.uniform4iv; break; case GL.SAMPLER_2D: case GL.SAMPLER_3D: case GL.SAMPLER_CUBE: func = gl.uniform1i; break; default: func = gl.uniform1f; break; } return func; } /** * Create a shader from two urls. While the system is fetching the two urls, the shader contains a dummy shader that renders black. * @method Shader.fromURL * @param {String} vs_path the url to the vertex shader * @param {String} fs_path the url to the fragment shader * @param {Function} on_complete [Optional] a callback to call once the shader is ready. * @return {Shader} */ Shader.fromURL = function( vs_path, fs_path, on_complete ) { //create simple shader first var vs_code = "\n\ precision highp float;\n\ attribute vec3 a_vertex;\n\ attribute mat4 u_mvp;\n\ void main() { \n\ gl_Position = u_mvp * vec4(a_vertex,1.0); \n\ }\n\ "; var fs_code = "\n\ precision highp float;\n\ void main() {\n\ gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\n\ }\n\ "; var shader = new GL.Shader(vs_code, fs_code); shader.ready = false; var true_vs = null; var true_fs = null; HttpRequest( vs_path, null, function(vs_code) { true_vs = vs_code; if(true_fs) compileShader(); }); HttpRequest( fs_path, null, function(fs_code) { true_fs = fs_code; if(true_vs) compileShader(); }); function compileShader() { var true_shader = new GL.Shader(true_vs, true_fs); for(var i in true_shader) shader[i] = true_shader[i]; shader.ready = true; } return shader; } /** * enables the shader (calls useProgram) * @method bind */ Shader.prototype.bind = function() { var gl = this.gl; gl.useProgram( this.program ); gl._current_shader = this; } /** * Returns the location of a uniform or attribute * @method getLocation * @param {String} name * @return {WebGLUniformLocation} location */ Shader.prototype.getLocation = function( name ) { var info = this.uniformInfo[name]; if(info) return this.uniformInfo[name].loc; return null; } /** * Uploads a set of uniforms to the Shader. You dont need to specify types, they are infered from the shader info. * @method uniforms * @param {Object} uniforms */ Shader._temp_uniform = new Float32Array(16); Shader.prototype.uniforms = function(uniforms) { var gl = this.gl; gl.useProgram(this.program); gl._current_shader = this; for (var name in uniforms) { var info = this.uniformInfo[ name ]; if (!info) continue; this._setUniform( name, uniforms[name] ); //this.setUniform( name, uniforms[name] ); //this._assing_uniform(uniforms, name, gl ); } return this; }//uniforms Shader.prototype.uniformsArray = function(array) { var gl = this.gl; gl.useProgram( this.program ); gl._current_shader = this; for(var i = 0, l = array.length; i < l; ++i) { var uniforms = array[i]; for (var name in uniforms) this._setUniform( name, uniforms[name] ); //this._assing_uniform(uniforms, name, gl ); } return this; } /** * Uploads a uniform to the Shader. You dont need to specify types, they are infered from the shader info. Shader must be binded! * @method setUniform * @param {string} name * @param {*} value */ Shader.prototype.setUniform = (function(){ return (function(name, value) { if( this.gl._current_shader != this ) this.bind(); var info = this.uniformInfo[name]; if (!info) return; if(info.loc === null) return; if(value == null) //strict? return; if(value.constructor === Array) { info.data.set( value ); value = info.data; } if(info.is_matrix) info.func.call( this.gl, info.loc, false, value ); else info.func.call( this.gl, info.loc, value ); }); })(); //skips enabling shader Shader.prototype._setUniform = (function(){ return (function(name, value) { var info = this.uniformInfo[ name ]; if (!info) return; if(info.loc === null) return; //if(info.loc.constructor !== Function) // return; if(value == null) return; if(value.constructor === Array) { info.data.set( value ); value = info.data; } if(info.is_matrix) info.func.call( this.gl, info.loc, false, value ); else info.func.call( this.gl, info.loc, value ); }); })(); /** * Renders a mesh using this shader, remember to use the function uniforms before to enable the shader * @method draw * @param {Mesh} mesh * @param {number} mode could be gl.LINES, gl.POINTS, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN * @param {String} index_buffer_name the name of the index buffer, if not provided triangles will be assumed */ Shader.prototype.draw = function( mesh, mode, index_buffer_name ) { index_buffer_name = index_buffer_name === undefined ? (mode == gl.LINES ? 'lines' : 'triangles') : index_buffer_name; this.drawBuffers( mesh.vertexBuffers, index_buffer_name ? mesh.indexBuffers[ index_buffer_name ] : null, arguments.length < 2 ? gl.TRIANGLES : mode); } /** * Renders a range of a mesh using this shader * @method drawRange * @param {Mesh} mesh * @param {number} mode could be gl.LINES, gl.POINTS, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN * @param {number} start first primitive to render * @param {number} length number of primitives to render * @param {String} index_buffer_name the name of the index buffer, if not provided triangles will be assumed */ Shader.prototype.drawRange = function(mesh, mode, start, length, index_buffer_name ) { index_buffer_name = index_buffer_name === undefined ? (mode == gl.LINES ? 'lines' : 'triangles') : index_buffer_name; this.drawBuffers( mesh.vertexBuffers, index_buffer_name ? mesh.indexBuffers[ index_buffer_name ] : null, mode, start, length); } /** * render several buffers with a given index buffer * @method drawBuffers * @param {Object} vertexBuffers an object containing all the buffers * @param {IndexBuffer} indexBuffer * @param {number} mode could be gl.LINES, gl.POINTS, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN * @param {number} range_start first primitive to render * @param {number} range_length number of primitives to render */ //this two variables are a hack to avoid memory allocation on drawCalls var temp_attribs_array = new Uint8Array(16); var temp_attribs_array_zero = new Uint8Array(16); //should be filled with zeros always Shader.prototype.drawBuffers = function( vertexBuffers, indexBuffer, mode, range_start, range_length ) { if(range_length == 0) return; var gl = this.gl; gl.useProgram(this.program); //this could be removed assuming every shader is called with some uniforms // enable attributes as necessary. var length = 0; var attribs_in_use = temp_attribs_array; //hack to avoid garbage attribs_in_use.set( temp_attribs_array_zero ); //reset for (var name in vertexBuffers) { var buffer = vertexBuffers[name]; var attribute = buffer.attribute || name; //precompute attribute locations in shader var location = this.attributes[attribute];// || gl.getAttribLocation(this.program, attribute); if (location == null || !buffer.buffer) //-1 changed for null continue; //ignore this buffer attribs_in_use[location] = 1; //mark it as used //this.attributes[attribute] = location; gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buffer); gl.enableVertexAttribArray(location); gl.vertexAttribPointer(location, buffer.buffer.spacing, buffer.buffer.gl_type, false, 0, 0); length = buffer.buffer.length / buffer.buffer.spacing; } //range rendering var offset = 0; //in bytes if(range_start > 0) //render a polygon range offset = range_start; //in bytes (Uint16 == 2 bytes) if (indexBuffer) length = indexBuffer.buffer.length - offset; if(range_length > 0 && range_length < length) //to avoid problems length = range_length; var BYTES_PER_ELEMENT = (indexBuffer && indexBuffer.data) ? indexBuffer.data.constructor.BYTES_PER_ELEMENT : 1; offset *= BYTES_PER_ELEMENT; // Force to disable buffers in this shader that are not in this mesh for (var attribute in this.attributes) { var location = this.attributes[attribute]; if (!(attribs_in_use[location])) { gl.disableVertexAttribArray(this.attributes[attribute]); } } // Draw the geometry. if (length && (!indexBuffer || indexBuffer.buffer)) { if (indexBuffer) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer.buffer); gl.drawElements( mode, length, indexBuffer.buffer.gl_type, offset); //gl.UNSIGNED_SHORT gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); } else { gl.drawArrays(mode, offset, length); } } return this; } Shader._instancing_arrays = []; Shader.prototype.drawInstanced = function( mesh, primitive, indices, instanced_uniforms, range_start, range_length, num_instances ) { if(range_length === 0) return; //bind buffers var gl = this.gl; if( gl.webgl_version == 1 && !gl.extensions.ANGLE_instanced_arrays ) throw("instancing not supported"); gl.useProgram(this.program); //this could be removed assuming every shader is called with some uniforms // enable attributes as necessary. var length = 0; var attribs_in_use = temp_attribs_array; //hack to avoid garbage attribs_in_use.set( temp_attribs_array_zero ); //reset var vertexBuffers = mesh.vertexBuffers; for (var name in vertexBuffers) { var buffer = vertexBuffers[name]; var attribute = buffer.attribute || name; //precompute attribute locations in shader var location = this.attributes[attribute];// || gl.getAttribLocation(this.program, attribute); if (location == null || !buffer.buffer) //-1 changed for null continue; //ignore this buffer attribs_in_use[location] = 1; //mark it as used //this.attributes[attribute] = location; gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buffer); gl.enableVertexAttribArray(location); gl.vertexAttribPointer(location, buffer.buffer.spacing, buffer.buffer.gl_type, false, 0, 0); length = buffer.buffer.length / buffer.buffer.spacing; } var indexBuffer = null; if(indices) { if(indices.constructor === GL.Buffer) indexBuffer = indices; else indexBuffer = mesh.getIndexBuffer( indices ); } //range rendering var offset = 0; //in bytes if(range_start > 0) //render a polygon range offset = range_start; if (indexBuffer) length = indexBuffer.buffer.length - offset; if(range_length > 0 && range_length < length) //to avoid problems length = range_length; var BYTES_PER_ELEMENT = (indexBuffer && indexBuffer.data) ? indexBuffer.data.constructor.BYTES_PER_ELEMENT : 1; offset *= BYTES_PER_ELEMENT; // Force to disable buffers in this shader that are not in this mesh for (var attribute in this.attributes) { var location = this.attributes[attribute]; if (!(attribs_in_use[location])) { gl.disableVertexAttribArray(this.attributes[attribute]); } } var ext = gl.extensions.ANGLE_instanced_arrays; var batch_length = 0; //pack the instanced uniforms var index = 0; for(var uniform in instanced_uniforms) { var values = instanced_uniforms[ uniform ]; batch_length = values.length; var uniformLocation = this.attributes[ uniform ]; if( uniformLocation == null ) return; //not found var element_size = 0; var total_size = 0; if( values.constructor === Array ) { element_size = values[0].constructor === Number ? 1 : values[0].length; total_size = element_size * values.length; } else //typed array { element_size = this.uniformInfo[ uniform ].type_length; total_size = values.length; batch_length = total_size / element_size; } var data_array = Shader._instancing_arrays[ index ]; if( !data_array || data_array.data.length < total_size ) data_array = Shader._instancing_arrays[ index ] = { data: new Float32Array( total_size ), buffer: gl.createBuffer() }; data_array.uniform = uniform; data_array.element_size = element_size; if( values.constructor === Array ) for(var j = 0; j < values.length; ++j) data_array.data.set( values[j], j*element_size ); //flatten array else data_array.data.set( values ); //copy gl.bindBuffer( gl.ARRAY_BUFFER, data_array.buffer ); gl.bufferData( gl.ARRAY_BUFFER, data_array.data, gl.STREAM_DRAW ); if(element_size == 16) //mat4 { for(var k = 0; k < 4; ++k) { gl.enableVertexAttribArray( uniformLocation+k ); gl.vertexAttribPointer( uniformLocation+k, 4, gl.FLOAT , false, 16*4, k*4*4 ); //4 bytes per float if( ext ) //webgl 1 ext.vertexAttribDivisorANGLE( uniformLocation+k, 1 ); // This makes it instanced! else gl.vertexAttribDivisor( uniformLocation+k, 1 ); // This makes it instanced! } } else //others { gl.enableVertexAttribArray( uniformLocation ); gl.vertexAttribPointer( uniformLocation, element_size, gl.FLOAT, false, element_size*4, 0 ); //4 bytes per float, 0 offset if( ext ) //webgl 1 ext.vertexAttribDivisorANGLE( uniformLocation, 1 ); // This makes it instanced! else gl.vertexAttribDivisor( uniformLocation, 1 ); // This makes it instanced! } index+=1; } if( num_instances ) batch_length = num_instances; if( ext ) //webgl 1.0 { if(indexBuffer) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer.buffer); ext.drawElementsInstancedANGLE( primitive, length, indexBuffer.buffer.gl_type, offset, batch_length ); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null ); } else ext.drawArraysInstancedANGLE( primitive, offset, length, batch_length); } else { if(indexBuffer) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer.buffer); gl.drawElementsInstanced( primitive, length, indexBuffer.buffer.gl_type, offset, batch_length ); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null ); } else gl.drawArraysInstanced( primitive, offset, length, batch_length); } //disable instancing buffers for(var i = 0; i < index; ++i) { var info = Shader._instancing_arrays[ i ]; var uniformLocation = this.attributes[ info.uniform ]; var element_size = info.element_size; if( element_size == 16) //mat4 { for(var k = 0; k < 4; ++k) { gl.disableVertexAttribArray( uniformLocation+k ); if( ext ) //webgl 1 ext.vertexAttribDivisorANGLE( uniformLocation+k, 0 ); else gl.vertexAttribDivisor( uniformLocation+k, 0 ); } } else //others { gl.enableVertexAttribArray( uniformLocation ); if( ext ) //webgl 1 ext.vertexAttribDivisorANGLE( uniformLocation, 0 ); else gl.vertexAttribDivisor( uniformLocation, 0 ); } } return this; } /** * Given a source code with the directive #import it expands it inserting the code using Shader.files to fetch for import files. * Warning: Imports are evaluated only the first inclusion, the rest are ignored to avoid double inclusion of functions * Also, imports cannot have other imports inside. * @method Shader.expandImports * @param {String} code the source code * @param {Object} files [Optional] object with files to import from (otherwise Shader.files is used) * @return {String} the code with the lines #import removed and replaced by the code */ Shader.expandImports = function(code, files) { files = files || Shader.files; var already_imported = {}; //avoid to import two times the same code if( !files ) throw("Shader.files not initialized, assign files there"); var replace_import = function(v) { var token = v.split("\""); var id = token[1]; if( already_imported[id] ) return "//already imported: " + id + "\n"; var file = files[id]; already_imported[ id ] = true; if(file) return file + "\n"; return "//import code not found: " + id + "\n"; } //return code.replace(/#import\s+\"(\w+)\"\s*\n/g, replace_import ); return code.replace(/#import\s+\"([a-zA-Z0-9_\.]+)\"\s*\n/g, replace_import ); } Shader.dumpErrorToConsole = function(err, vscode, fscode) { console.error(err); var msg = err.msg; var code = null; if(err.indexOf("Fragment") != -1) code = fscode; else code = vscode; var lines = code.split("\n"); for(var i in lines) lines[i] = i + "| " + lines[i]; console.groupCollapsed("Shader code"); console.log( lines.join("\n") ); console.groupEnd(); } Shader.convertTo100 = function(code,type) { //in VERTEX //change in for attribute //change out for varying //add #extension GL_OES_standard_derivatives //in FRAGMENT //change in for varying //remove out vec4 _gl_FragColor //rename _gl_FragColor for gl_FragColor //in both //change #version 300 es for #version 100 //replace 'texture(' for 'texture2D(' } Shader.convertTo300 = function(code,type) { //in VERTEX //change attribute for in //change varying for out //remove #extension GL_OES_standard_derivatives //in FRAGMENT //change varying for in //rename gl_FragColor for _gl_FragColor //rename gl_FragData[0] for _gl_FragColor //add out vec4 _gl_FragColor //in both //replace texture2D for texture } //helps to check if a variable value is valid to an specific uniform in a shader Shader.validateValue = function( value, uniform_info ) { if(value === null || value === undefined) return false; switch (uniform_info.type) { //used to validate shaders case GL.INT: case GL.FLOAT: case GL.SAMPLER_2D: case GL.SAMPLER_CUBE: return isNumber(value); case GL.INT_VEC2: case GL.FLOAT_VEC2: return value.length === 2; case GL.INT_VEC3: case GL.FLOAT_VEC3: return value.length === 3; case GL.INT_VEC4: case GL.FLOAT_VEC4: case GL.FLOAT_MAT2: return value.length === 4; case GL.FLOAT_MAT3: return value.length === 8; case GL.FLOAT_MAT4: return value.length === 16; } return true; } //**************** SHADERS *********************************** Shader.DEFAULT_VERTEX_SHADER = "\n\ precision highp float;\n\ attribute vec3 a_vertex;\n\ attribute vec3 a_normal;\n\ attribute vec2 a_coord;\n\ varying vec3 v_position;\n\ varying vec3 v_normal;\n\ varying vec2 v_coord;\n\ uniform mat4 u_model;\n\ uniform mat4 u_mvp;\n\ void main() {\n\ v_position = (u_model * vec4(a_vertex,1.0)).xyz;\n\ v_normal = (u_model * vec4(a_normal,0.0)).xyz;\n\ v_coord = a_coord;\n\ gl_Position = u_mvp * vec4(a_vertex,1.0);\n\ }\n\ "; Shader.SCREEN_VERTEX_SHADER = "\n\ precision highp float;\n\ attribute vec3 a_vertex;\n\ attribute vec2 a_coord;\n\ varying vec2 v_coord;\n\ void main() { \n\ v_coord = a_coord; \n\ gl_Position = vec4(a_coord * 2.0 - 1.0, 0.0, 1.0); \n\ }\n\ "; Shader.SCREEN_FRAGMENT_SHADER = "\n\ precision highp float;\n\ uniform sampler2D u_texture;\n\ varying vec2 v_coord;\n\ void main() {\n\ gl_FragColor = texture2D(u_texture, v_coord);\n\ }\n\ "; //used in createFX Shader.SCREEN_FRAGMENT_FX = "\n\ precision highp float;\n\ uniform sampler2D u_texture;\n\ varying vec2 v_coord;\n\ #ifdef FX_UNIFORMS\n\ FX_UNIFORMS\n\ #endif\n\ void main() {\n\ vec2 uv = v_coord;\n\ vec4 color = texture2D(u_texture, uv);\n\ #ifdef FX_CODE\n\ FX_CODE ;\n\ #endif\n\ gl_FragColor = color;\n\ }\n\ "; Shader.SCREEN_COLORED_FRAGMENT_SHADER = "\n\ precision highp float;\n\ uniform sampler2D u_texture;\n\ uniform vec4 u_color;\n\ varying vec2 v_coord;\n\ void main() {\n\ gl_FragColor = u_color * texture2D(u_texture, v_coord);\n\ }\n\ "; Shader.BLEND_FRAGMENT_SHADER = "\n\ precision highp float;\n\ uniform sampler2D u_texture;\n\ uniform sampler2D u_texture2;\n\ uniform float u_factor;\n\ varying vec2 v_coord;\n\ void main() {\n\ gl_FragColor = mix( texture2D(u_texture, v_coord), texture2D(u_texture2, v_coord), u_factor);\n\ }\n\ "; //used to paint quads Shader.QUAD_VERTEX_SHADER = "\n\ precision highp float;\n\ attribute vec3 a_vertex;\n\ attribute vec2 a_coord;\n\ varying vec2 v_coord;\n\ uniform vec2 u_position;\n\ uniform vec2 u_size;\n\ uniform vec2 u_viewport;\n\ uniform mat3 u_transform;\n\ void main() { \n\ vec3 pos = vec3(u_position + vec2(a_coord.x,1.0 - a_coord.y) * u_size, 1.0);\n\ v_coord = a_coord; \n\ pos = u_transform * pos;\n\ pos.z = 0.0;\n\ //normalize\n\ pos.x = (2.0 * pos.x / u_viewport.x) - 1.0;\n\ pos.y = -((2.0 * pos.y / u_viewport.y) - 1.0);\n\ gl_Position = vec4(pos, 1.0); \n\ }\n\ "; Shader.QUAD_FRAGMENT_SHADER = "\n\ precision highp float;\n\ uniform sampler2D u_texture;\n\ uniform vec4 u_color;\n\ varying vec2 v_coord;\n\ void main() {\n\ gl_FragColor = u_color * texture2D(u_texture, v_coord);\n\ }\n\ "; //used to render partially a texture Shader.QUAD2_FRAGMENT_SHADER = "\n\ precision highp float;\n\ uniform sampler2D u_texture;\n\ uniform vec4 u_color;\n\ uniform vec4 u_texture_area;\n\ varying vec2 v_coord;\n\ void main() {\n\ vec2 uv = vec2( mix(u_texture_area.x, u_texture_area.z, v_coord.x), 1.0 - mix(u_texture_area.w, u_texture_area.y, v_coord.y) );\n\ gl_FragColor = u_color * texture2D(u_texture, uv);\n\ }\n\ "; Shader.PRIMITIVE2D_VERTEX_SHADER = "\n\ precision highp float;\n\ attribute vec3 a_vertex;\n\ uniform vec2 u_viewport;\n\ uniform mat3 u_transform;\n\ void main() { \n\ vec3 pos = a_vertex;\n\ pos = u_transform * pos;\n\ pos.z = 0.0;\n\ //normalize\n\ pos.x = (2.0 * pos.x / u_viewport.x) - 1.0;\n\ pos.y = -((2.0 * pos.y / u_viewport.y) - 1.0);\n\ gl_Position = vec4(pos, 1.0); \n\ }\n\ "; Shader.FLAT_VERTEX_SHADER = "\n\ precision highp float;\n\ attribute vec3 a_vertex;\n\ uniform mat4 u_mvp;\n\ void main() { \n\ gl_Position = u_mvp * vec4(a_vertex,1.0); \n\ }\n\ "; Shader.FLAT_FRAGMENT_SHADER = "\n\ precision highp float;\n\ uniform vec4 u_color;\n\ void main() {\n\ gl_FragColor = u_color;\n\ }\n\ "; Shader.SCREEN_FLAT_FRAGMENT_SHADER = Shader.FLAT_FRAGMENT_SHADER; //legacy /** * Allows to create a simple shader meant to be used to process a texture, instead of having to define the generic Vertex & Fragment Shader code * @method Shader.createFX * @param {string} code string containg code, like "color = color * 2.0;" * @param {string} [uniforms=null] string containg extra uniforms, like "uniform vec3 u_pos;" */ Shader.createFX = function(code, uniforms, shader) { //remove comments code = GL.Shader.removeComments( code, true ); //remove comments and breaklines to avoid problems with the macros var macros = { FX_CODE: code, FX_UNIFORMS: uniforms || "" } if(!shader) return new GL.Shader( GL.Shader.SCREEN_VERTEX_SHADER, GL.Shader.SCREEN_FRAGMENT_FX, macros ); shader.updateShader( GL.Shader.SCREEN_VERTEX_SHADER, GL.Shader.SCREEN_FRAGMENT_FX, macros ); return shader; } /** * Given a shader code with some vars inside (like {{varname}}) and an object with the variable values, it will replace them. * @method Shader.replaceCodeUsingContext * @param {string} code string containg code and vars in {{varname}} format * @param {object} context object containing all var values */ Shader.replaceCodeUsingContext = function( code_template, context ) { return code_template.replace(/\{\{[a-zA-Z0-9_]*\}\}/g, function(v){ v = v.replace( /[\{\}]/g, "" ); return context[v] || ""; }); } Shader.removeComments = function(code, one_line) { if(!code) return ""; var rx = /(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(\/\/.*)/g; var code = code.replace( rx ,""); var lines = code.split("\n"); var result = []; for(var i = 0; i < lines.length; ++i) { var line = lines[i]; var pos = line.indexOf("//"); if(pos != -1) line = lines[i].substr(0,pos); line = line.trim(); if(line.length) result.push(line); } return result.join( one_line ? "" : "\n" ); } /** * Renders a fullscreen quad with this shader applied * @method toViewport * @param {object} uniforms */ Shader.prototype.toViewport = function(uniforms) { var mesh = GL.Mesh.getScreenQuad(); if(uniforms) this.uniforms(uniforms); this.draw( mesh ); } //Now some common shaders everybody needs /** * Returns a shader ready to render a textured quad in fullscreen, use with Mesh.getScreenQuad() mesh * shader params: sampler2D u_texture * @method Shader.getScreenShader */ Shader.getScreenShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":screen"]; if(shader) return shader; shader = gl.shaders[":screen"] = new GL.Shader( Shader.SCREEN_VERTEX_SHADER, Shader.SCREEN_FRAGMENT_SHADER ); return shader.uniforms({u_texture:0}); //do it the first time so I dont have to do it every time } /** * Returns a shader ready to render a flat color quad in fullscreen, use with Mesh.getScreenQuad() mesh * shader params: vec4 u_color * @method Shader.getFlatScreenShader */ Shader.getFlatScreenShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":flat_screen"]; if(shader) return shader; shader = gl.shaders[":flat_screen"] = new GL.Shader( Shader.SCREEN_VERTEX_SHADER, Shader.FLAT_FRAGMENT_SHADER ); return shader.uniforms({u_color:[1,1,1,1]}); //do it the first time so I dont have to do it every time } /** * Returns a shader ready to render a colored textured quad in fullscreen, use with Mesh.getScreenQuad() mesh * shader params vec4 u_color and sampler2D u_texture * @method Shader.getColoredScreenShader */ Shader.getColoredScreenShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":colored_screen"]; if(shader) return shader; shader = gl.shaders[":colored_screen"] = new GL.Shader( Shader.SCREEN_VERTEX_SHADER, Shader.SCREEN_COLORED_FRAGMENT_SHADER ); return shader.uniforms({u_texture:0, u_color: vec4.fromValues(1,1,1,1) }); //do it the first time so I dont have to do it every time } /** * Returns a shader ready to render a quad with transform, use with Mesh.getScreenQuad() mesh * shader must have: u_position, u_size, u_viewport, u_transform (mat3) * @method Shader.getQuadShader */ Shader.getQuadShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":quad"]; if(shader) return shader; return gl.shaders[":quad"] = new GL.Shader( Shader.QUAD_VERTEX_SHADER, Shader.QUAD_FRAGMENT_SHADER ); } /** * Returns a shader ready to render part of a texture into the viewport * shader must have: u_position, u_size, u_viewport, u_transform, u_texture_area (vec4) * @method Shader.getPartialQuadShader */ Shader.getPartialQuadShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":quad2"]; if(shader) return shader; return gl.shaders[":quad2"] = new GL.Shader( Shader.QUAD_VERTEX_SHADER, Shader.QUAD2_FRAGMENT_SHADER ); } /** * Returns a shader that blends two textures * shader must have: u_factor, u_texture, u_texture2 * @method Shader.getBlendShader */ Shader.getBlendShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":blend"]; if(shader) return shader; return gl.shaders[":blend"] = new GL.Shader( Shader.SCREEN_VERTEX_SHADER, Shader.BLEND_FRAGMENT_SHADER ); } /** * Returns a shader used to apply gaussian blur to one texture in one axis (you should use it twice to get a gaussian blur) * shader params are: vec2 u_offset, float u_intensity * @method Shader.getBlurShader */ Shader.getBlurShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":blur"]; if(shader) return shader; var shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER,"\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\ 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 += texture2D(u_texture, v_coord) * 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\ "); return gl.shaders[":blur"] = shader; } //shader to copy a depth texture into another one Shader.getCopyDepthShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":copy_depth"]; if(shader) return shader; var shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER,"\n\ #extension GL_EXT_frag_depth : enable\n\ precision highp float;\n\ varying vec2 v_coord;\n\ uniform sampler2D u_texture;\n\ void main() {\n\ gl_FragDepthEXT = texture2D( u_texture, v_coord ).x;\n\ gl_FragColor = vec4(1.0);\n\ }\n\ "); return gl.shaders[":copy_depth"] = shader; } Shader.getCubemapShowShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":show_cubemap"]; if(shader) return shader; var shader = new GL.Shader( Shader.DEFAULT_VERTEX_SHADER,"\n\ precision highp float;\n\ varying vec3 v_normal;\n\ uniform samplerCube u_texture;\n\ void main() {\n\ gl_FragColor = textureCube( u_texture, v_normal );\n\ }\n\ "); shader.uniforms({u_texture:0}); return gl.shaders[":show_cubemap"] = shader; } //shader to copy a cubemap into another Shader.getPolarToCubemapShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":polar_to_cubemap"]; if(shader) return shader; var shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER,"\n\ precision highp float;\n\ varying vec2 v_coord;\n\ uniform sampler2D u_texture;\n\ uniform mat3 u_rotation;\n\ void main() {\n\ vec2 uv = vec2( v_coord.x, 1.0 - v_coord.y );\n\ vec3 dir = normalize( vec3( uv - vec2(0.5), 0.5 ));\n\ dir = u_rotation * dir;\n\ float u = atan(dir.x,dir.z) / 6.28318531;\n\ float v = (asin(dir.y) / 1.57079633) * 0.5 + 0.5;\n\ u = mod(u,1.0);\n\ v = mod(v,1.0);\n\ gl_FragColor = texture2D( u_texture, vec2(u,v) );\n\ }\n\ "); return gl.shaders[":polar_to_cubemap"] = shader; } //shader to copy a cubemap into another Shader.getCubemapCopyShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":copy_cubemap"]; if(shader) return shader; var shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER,"\n\ precision highp float;\n\ varying vec2 v_coord;\n\ uniform samplerCube u_texture;\n\ uniform mat3 u_rotation;\n\ void main() {\n\ vec2 uv = vec2( v_coord.x, 1.0 - v_coord.y );\n\ vec3 dir = vec3( uv - vec2(0.5), 0.5 );\n\ dir = u_rotation * dir;\n\ gl_FragColor = textureCube( u_texture, dir );\n\ }\n\ "); return gl.shaders[":copy_cubemap"] = shader; } //shader to blur a cubemap Shader.getCubemapBlurShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":blur_cubemap"]; if(shader) return shader; var shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER,"\n\ #ifndef NUM_SAMPLES\n\ #define NUM_SAMPLES 4\n\ #endif\n\ \n\ precision highp float;\n\ varying vec2 v_coord;\n\ uniform samplerCube u_texture;\n\ uniform mat3 u_rotation;\n\ uniform vec2 u_offset;\n\ uniform float u_intensity;\n\ void main() {\n\ vec4 sum = vec4(0.0);\n\ vec2 uv = vec2( v_coord.x, 1.0 - v_coord.y ) - vec2(0.5);\n\ vec3 dir = vec3(0.0);\n\ vec4 color = vec4(0.0);\n\ for( int x = -2; x <= 2; x++ )\n\ {\n\ for( int y = -2; y <= 2; y++ )\n\ {\n\ dir.xy = uv + vec2( u_offset.x * float(x), u_offset.y * float(y)) * 0.5;\n\ dir.z = 0.5;\n\ dir = u_rotation * dir;\n\ color = textureCube( u_texture, dir );\n\ color.xyz = color.xyz * color.xyz;/*linearize*/\n\ sum += color;\n\ }\n\ }\n\ sum /= 25.0;\n\ gl_FragColor = vec4( sqrt( sum.xyz ), sum.w ) ;\n\ }\n\ "); return gl.shaders[":blur_cubemap"] = shader; } //shader to do FXAA (antialiasing) Shader.FXAA_FUNC = "\n\ uniform vec2 u_viewportSize;\n\ uniform vec2 u_iViewportSize;\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 u_iViewportSize = vec2(1.0 / u_viewportSize.x, 1.0 / u_viewportSize.y);*/\n\ vec3 rgbNW = texture2D(tex, (fragCoord + vec2(-1.0, -1.0)) * u_iViewportSize).xyz;\n\ vec3 rgbNE = texture2D(tex, (fragCoord + vec2(1.0, -1.0)) * u_iViewportSize).xyz;\n\ vec3 rgbSW = texture2D(tex, (fragCoord + vec2(-1.0, 1.0)) * u_iViewportSize).xyz;\n\ vec3 rgbSE = texture2D(tex, (fragCoord + vec2(1.0, 1.0)) * u_iViewportSize).xyz;\n\ vec3 rgbM = texture2D(tex, fragCoord * u_iViewportSize).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)) * u_iViewportSize;\n\ \n\ vec3 rgbA = 0.5 * (texture2D(tex, fragCoord * u_iViewportSize + dir * (1.0 / 3.0 - 0.5)).xyz + \n\ texture2D(tex, fragCoord * u_iViewportSize + dir * (2.0 / 3.0 - 0.5)).xyz);\n\ vec3 rgbB = rgbA * 0.5 + 0.25 * (texture2D(tex, fragCoord * u_iViewportSize + dir * -0.5).xyz + \n\ texture2D(tex, fragCoord * u_iViewportSize + 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\ return color;\n\ }\n\ "; /** * Returns a shader to apply FXAA antialiasing * params are vec2 u_viewportSize, vec2 u_iViewportSize or you can call shader.setup() * @method Shader.getFXAAShader */ Shader.getFXAAShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":fxaa"]; if(shader) return shader; var shader = new GL.Shader( Shader.SCREEN_VERTEX_SHADER,"\n\ precision highp float;\n\ varying vec2 v_coord;\n\ uniform sampler2D u_texture;\n\ " + Shader.FXAA_FUNC + "\n\ \n\ void main() {\n\ gl_FragColor = applyFXAA( u_texture, v_coord * u_viewportSize) ;\n\ }\n\ "); var viewport = vec2.fromValues( gl.viewport_data[2], gl.viewport_data[3] ); var iviewport = vec2.fromValues( 1/gl.viewport_data[2], 1/gl.viewport_data[3] ); shader.setup = function() { viewport[0] = gl.viewport_data[2]; viewport[1] = gl.viewport_data[3]; iviewport[0] = 1/gl.viewport_data[2]; iviewport[1] = 1/gl.viewport_data[3]; this.uniforms({ u_viewportSize: viewport, u_iViewportSize: iviewport }); } return gl.shaders[":fxaa"] = shader; } /** * Returns a flat shader (useful to render lines) * @method Shader.getFlatShader */ Shader.getFlatShader = function(gl) { gl = gl || global.gl; var shader = gl.shaders[":flat"]; if(shader) return shader; var shader = new GL.Shader( Shader.FLAT_VERTEX_SHADER,Shader.FLAT_FRAGMENT_SHADER); shader.uniforms({u_color:[1,1,1,1]}); return gl.shaders[":flat"] = shader; } /** * The global scope that contains all the classes from LiteGL and also all the enums of WebGL so you dont need to create a context to use the values. * @class GL */ /** * creates a new WebGL context (it can create the canvas or use an existing one) * @method create * @param {Object} options supported are: * - width * - height * - canvas * - container (string or element) * @return {WebGLRenderingContext} webgl context with all the extra functions (check gl in the doc for more info) */ GL.create = function(options) { options = options || {}; var canvas = null; if(options.canvas) { if(typeof(options.canvas) == "string") { canvas = document.getElementById( options.canvas ); if(!canvas) throw("Canvas element not found: " + options.canvas ); } else canvas = options.canvas; } else { var root = null; if(options.container) root = options.container.constructor === String ? document.querySelector( options.container ) : options.container; if(root && !options.width) { var rect = root.getBoundingClientRect(); options.width = rect.width; options.height = rect.height; } canvas = createCanvas( options.width || 800, options.height || 600 ); if(root) root.appendChild(canvas); } if (!('alpha' in options)) options.alpha = false; /** * the webgl context returned by GL.create, its a WebGLRenderingContext with some extra methods added * @class gl */ var gl = null; var seq = null; if(options.version == 2) seq = ['webgl2','experimental-webgl2']; else if(options.version == 1 || options.version === undefined) //default seq = ['webgl','experimental-webgl']; else if(options.version === 0) //latest seq = ['webgl2','experimental-webgl2','webgl','experimental-webgl']; if(!seq) throw 'Incorrect WebGL version, must be 1 or 2'; var context_options = { alpha: options.alpha === undefined ? true : options.alpha, depth: options.depth === undefined ? true : options.depth, stencil: options.stencil === undefined ? true : options.stencil, antialias: options.antialias === undefined ? true : options.antialias, premultipliedAlpha: options.premultipliedAlpha === undefined ? true : options.premultipliedAlpha, preserveDrawingBuffer: options.preserveDrawingBuffer === undefined ? true : options.preserveDrawingBuffer }; for(var i = 0; i < seq.length; ++i) { try { gl = canvas.getContext( seq[i], context_options ); } catch (e) {} if(gl) break; } if (!gl) { if( canvas.getContext( "webgl" ) ) throw 'WebGL supported but not with those parameters'; throw 'WebGL not supported'; } //context globals gl.webgl_version = gl.constructor.name === "WebGL2RenderingContext" ? 2 : 1; global.gl = gl; canvas.is_webgl = true; canvas.gl = gl; gl.context_id = this.last_context_id++; //get all supported extensions var supported_extensions = gl.getSupportedExtensions(); gl.extensions = {}; for(var i in supported_extensions) gl.extensions[ supported_extensions[i] ] = gl.getExtension( supported_extensions[i] ); gl.derivatives_supported = gl.extensions['OES_standard_derivatives'] != null || gl.webgl_version > 1; /* gl.extensions["OES_standard_derivatives"] = gl.derivatives_supported = gl.getExtension('OES_standard_derivatives') || false; gl.extensions["WEBGL_depth_texture"] = gl.getExtension("WEBGL_depth_texture") || gl.getExtension("WEBKIT_WEBGL_depth_texture") || gl.getExtension("MOZ_WEBGL_depth_texture"); gl.extensions["OES_element_index_uint"] = gl.getExtension("OES_element_index_uint"); gl.extensions["WEBGL_draw_buffers"] = gl.getExtension("WEBGL_draw_buffers"); gl.extensions["EXT_shader_texture_lod"] = gl.getExtension("EXT_shader_texture_lod"); gl.extensions["EXT_sRGB"] = gl.getExtension("EXT_sRGB"); gl.extensions["EXT_texture_filter_anisotropic"] = gl.getExtension("EXT_texture_filter_anisotropic") || gl.getExtension("WEBKIT_EXT_texture_filter_anisotropic") || gl.getExtension("MOZ_EXT_texture_filter_anisotropic"); gl.extensions["EXT_frag_depth"] = gl.getExtension("EXT_frag_depth") || gl.getExtension("WEBKIT_EXT_frag_depth") || gl.getExtension("MOZ_EXT_frag_depth"); gl.extensions["WEBGL_lose_context"] = gl.getExtension("WEBGL_lose_context") || gl.getExtension("WEBKIT_WEBGL_lose_context") || gl.getExtension("MOZ_WEBGL_lose_context"); gl.extensions["ANGLE_instanced_arrays"] = gl.getExtension("ANGLE_instanced_arrays"); gl.extensions["disjoint_timer_query"] = gl.getExtension("EXT_disjoint_timer_query"); //for float textures gl.extensions["OES_texture_float_linear"] = gl.getExtension("OES_texture_float_linear"); if(gl.extensions["OES_texture_float_linear"]) gl.extensions["OES_texture_float"] = gl.getExtension("OES_texture_float"); gl.extensions["EXT_color_buffer_float"] = gl.getExtension("EXT_color_buffer_float"); //for half float textures in webgl 1 require extension gl.extensions["OES_texture_half_float_linear"] = gl.getExtension("OES_texture_half_float_linear"); if(gl.extensions["OES_texture_half_float_linear"]) gl.extensions["OES_texture_half_float"] = gl.getExtension("OES_texture_half_float"); */ if( gl.webgl_version == 1 ) gl.HIGH_PRECISION_FORMAT = gl.extensions["OES_texture_half_float"] ? GL.HALF_FLOAT_OES : (gl.extensions["OES_texture_float"] ? GL.FLOAT : GL.UNSIGNED_BYTE); //because Firefox dont support half float else gl.HIGH_PRECISION_FORMAT = GL.HALF_FLOAT_OES; gl.max_texture_units = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); //viewport hack to retrieve it without using getParameter (which is slow and generates garbage) if(!gl._viewport_func) { gl._viewport_func = gl.viewport; gl.viewport_data = new Float32Array([0,0,gl.canvas.width,gl.canvas.height]); //32000 max viewport, I guess its fine gl.viewport = function(a,b,c,d) { var v = this.viewport_data; v[0] = a|0; v[1] = b|0; v[2] = c|0; v[3] = d|0; this._viewport_func(a,b,c,d); } gl.getViewport = function(v) { if(v) { v[0] = gl.viewport_data[0]; v[1] = gl.viewport_data[1]; v[2] = gl.viewport_data[2]; v[3] = gl.viewport_data[3]; return v; } return new Float32Array( gl.viewport_data ); }; gl.setViewport = function( v, flip_y ) { gl.viewport_data.set(v); if(flip_y) gl.viewport_data[1] = this.drawingBufferHeight-v[1]-v[3]; this._viewport_func(v[0],gl.viewport_data[1],v[2],v[3]); }; } else console.warn("Creating LiteGL context over the same canvas twice"); //reverse names helper (assuming no names repeated) if(!GL.reverse) { GL.reverse = {}; for(var i in gl) if( gl[i] && gl[i].constructor === Number ) GL.reverse[ gl[i] ] = i; } //just some checks if(typeof(glMatrix) == "undefined") throw("glMatrix not found, LiteGL requires glMatrix to be included"); var last_click_time = 0; //some global containers, use them to reuse assets gl.shaders = {}; gl.textures = {}; gl.meshes = {}; /** * sets this context as the current global gl context (in case you have more than one) * @method makeCurrent */ gl.makeCurrent = function() { global.gl = this; } /** * executes callback inside this webgl context * @method execute * @param {Function} callback */ gl.execute = function(callback) { var old_gl = global.gl; global.gl = this; callback(); global.gl = old_gl; } /** * Launch animation loop (calls gl.onupdate and gl.ondraw every frame) * example: gl.ondraw = function(){ ... } or gl.onupdate = function(dt) { ... } * @method animate */ gl.animate = function(v) { if(v === false) { global.cancelAnimationFrame( this._requestFrame_id ); this._requestFrame_id = null; return; } var post = global.requestAnimationFrame; var time = getTime(); var context = this; //loop only if browser tab visible function loop() { if(gl.destroyed) //to stop rendering once it is destroyed return; context._requestFrame_id = post(loop); //do it first, in case it crashes var now = getTime(); var dt = (now - time) * 0.001; if(context.mouse) context.mouse.last_buttons = context.mouse.buttons; if (context.onupdate) context.onupdate(dt); LEvent.trigger( context, "update", dt); if (context.ondraw) { //make sure the ondraw is called using this gl context (in case there is more than one) var old_gl = global.gl; global.gl = context; //call ondraw context.ondraw(); LEvent.trigger(context,"draw"); //restore old context global.gl = old_gl; } time = now; } this._requestFrame_id = post(loop); //launch main loop } //store binded to be able to remove them if destroyed /* var _binded_events = []; function addEvent(object, type, callback) { _binded_events.push(object,type,callback); } */ /** * Destroy this WebGL context (removes also the Canvas from the DOM) * @method destroy */ gl.destroy = function() { //unbind global events if(onkey_handler) { document.removeEventListener("keydown", onkey_handler ); document.removeEventListener("keyup", onkey_handler ); } if(this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas); this.destroyed = true; if(global.gl == this) global.gl = null; } var mouse = gl.mouse = { buttons: 0, //this should always be up-to-date with mouse state last_buttons: 0, //button state in the previous frame left_button: false, middle_button: false, right_button: false, position: new Float32Array(2), x:0, //in canvas coordinates y:0, deltax: 0, deltay: 0, clientx:0, //in client coordinates clienty:0, isInsideRect: function(x,y,w,h, flip_y ) { var mouse_y = this.y; if(flip_y) mouse_y = gl.canvas.height - mouse_y; if( this.x > x && this.x < x + w && mouse_y > y && mouse_y < y + h) return true; return false; }, /** * returns true if button num is pressed (where num could be GL.LEFT_MOUSE_BUTTON, GL.RIGHT_MOUSE_BUTTON, GL.MIDDLE_MOUSE_BUTTON * @method captureMouse * @param {boolean} capture_wheel capture also the mouse wheel */ isButtonPressed: function(num) { if(num == GL.LEFT_MOUSE_BUTTON) return this.buttons & GL.LEFT_MOUSE_BUTTON_MASK; if(num == GL.MIDDLE_MOUSE_BUTTON) return this.buttons & GL.MIDDLE_MOUSE_BUTTON_MASK; if(num == GL.RIGHT_MOUSE_BUTTON) return this.buttons & GL.RIGHT_MOUSE_BUTTON_MASK; }, wasButtonPressed: function(num) { var mask = 0; if(num == GL.LEFT_MOUSE_BUTTON) mask = GL.LEFT_MOUSE_BUTTON_MASK; else if(num == GL.MIDDLE_MOUSE_BUTTON) mask = GL.MIDDLE_MOUSE_BUTTON_MASK; else if(num == GL.RIGHT_MOUSE_BUTTON) mask = GL.RIGHT_MOUSE_BUTTON_MASK; return (this.buttons & mask) && !(this.last_buttons & mask); } }; /** * Tells the system to capture mouse events on the canvas. * This will trigger onmousedown, onmousemove, onmouseup, onmousewheel callbacks assigned in the gl context * example: gl.onmousedown = function(e){ ... } * The event is a regular MouseEvent with some extra parameters * @method captureMouse * @param {boolean} capture_wheel capture also the mouse wheel */ gl.captureMouse = function(capture_wheel, translate_touchs ) { canvas.addEventListener("mousedown", onmouse); canvas.addEventListener("mousemove", onmouse); canvas.addEventListener("dragstart", onmouse); //canvas.addEventListener("mouseup", onmouse); ?? if(capture_wheel) { canvas.addEventListener("mousewheel", onmouse, false); canvas.addEventListener("wheel", onmouse, false); //canvas.addEventListener("DOMMouseScroll", onmouse, false); //deprecated or non-standard } //prevent right click context menu canvas.addEventListener("contextmenu", function(e) { e.preventDefault(); return false; }); if( translate_touchs ) this.captureTouch( true ); } function onmouse(e) { if(gl.ignore_events) return; //console.log(e.type); //debug var old_mouse_mask = gl.mouse.buttons; GL.augmentEvent(e, canvas); e.eventType = e.eventType || e.type; //type cannot be overwritten, so I make a clone to allow me to overwrite var now = getTime(); //gl.mouse info mouse.dragging = e.dragging; mouse.position[0] = e.canvasx; mouse.position[1] = e.canvasy; mouse.x = e.canvasx; mouse.y = e.canvasy; mouse.mousex = e.mousex; mouse.mousey = e.mousey; mouse.canvasx = e.canvasx; mouse.canvasy = e.canvasy; mouse.clientx = e.mousex; mouse.clienty = e.mousey; mouse.buttons = e.buttons; mouse.left_button = !!(mouse.buttons & GL.LEFT_MOUSE_BUTTON_MASK); mouse.middle_button = !!(mouse.buttons & GL.MIDDLE_MOUSE_BUTTON_MASK); mouse.right_button = !!(mouse.buttons & GL.RIGHT_MOUSE_BUTTON_MASK); if(e.eventType == "mousedown") { if(old_mouse_mask == 0) //no mouse button was pressed till now { canvas.removeEventListener("mousemove", onmouse); var doc = canvas.ownerDocument; doc.addEventListener("mousemove", onmouse); doc.addEventListener("mouseup", onmouse); } last_click_time = now; if(gl.onmousedown) gl.onmousedown(e); LEvent.trigger(gl,"mousedown"); } else if(e.eventType == "mousemove") { if(gl.onmousemove) gl.onmousemove(e); LEvent.trigger(gl,"mousemove",e); } else if(e.eventType == "mouseup") { //console.log("mouseup"); if(gl.mouse.buttons == 0) //no more buttons pressed { canvas.addEventListener("mousemove", onmouse); var doc = canvas.ownerDocument; doc.removeEventListener("mousemove", onmouse); doc.removeEventListener("mouseup", onmouse); } e.click_time = now - last_click_time; //last_click_time = now; //commented to avoid reseting click time when unclicking two mouse buttons if(gl.onmouseup) gl.onmouseup(e); LEvent.trigger(gl,"mouseup",e); } else if((e.eventType == "mousewheel" || e.eventType == "wheel" || e.eventType == "DOMMouseScroll")) { e.eventType = "mousewheel"; if(e.type == "wheel") e.wheel = -e.deltaY; //in firefox deltaY is 1 while in Chrome is 120 else e.wheel = (e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60); //from stack overflow //firefox doesnt have wheelDelta e.delta = e.wheelDelta !== undefined ? (e.wheelDelta/40) : (e.deltaY ? -e.deltaY/3 : 0); //console.log(e.delta); if(gl.onmousewheel) gl.onmousewheel(e); LEvent.trigger(gl, "mousewheel", e); } else if(e.eventType == "dragstart") { if(gl.ondragstart) gl.ondragstart(e); LEvent.trigger(gl, "dragstart", e); } if(gl.onmouse) gl.onmouse(e); if(!e.skip_preventDefault) { if(e.eventType != "mousemove") e.stopPropagation(); e.preventDefault(); return false; } } var translate_touches = false; gl.captureTouch = function( translate_to_mouse_events ) { translate_touches = translate_to_mouse_events; canvas.addEventListener("touchstart", ontouch, true); canvas.addEventListener("touchmove", ontouch, true); canvas.addEventListener("touchend", ontouch, true); canvas.addEventListener("touchcancel", ontouch, true); canvas.addEventListener('gesturestart', ongesture ); canvas.addEventListener('gesturechange', ongesture ); canvas.addEventListener('gestureend', ongesture ); } //translates touch events in mouseevents function ontouch( e ) { var touches = e.changedTouches, first = touches[0], type = ""; if( gl.ontouch && gl.ontouch(e) === true ) return; if( LEvent.trigger( gl, e.type, e ) === true ) return; if(!translate_touches) return; //ignore secondary touches if(e.touches.length && e.changedTouches[0].identifier !== e.touches[0].identifier) return; if(touches > 1) return; switch(e.type) { case "touchstart": type = "mousedown"; break; case "touchmove": type = "mousemove"; break; case "touchend": type = "mouseup"; break; default: return; } 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); simulatedEvent.originalEvent = simulatedEvent; simulatedEvent.is_touch = true; first.target.dispatchEvent( simulatedEvent ); e.preventDefault(); } function ongesture(e) { e.eventType = e.type; if(gl.ongesture && gl.ongesture(e) === false ) return; if( LEvent.trigger( gl, e.type, e ) === false ) return; e.preventDefault(); } var keys = gl.keys = {}; /** * Tells the system to capture key events on the canvas. This will trigger onkey * @method captureKeys * @param {boolean} prevent_default prevent default behaviour (like scroll on the web, etc) * @param {boolean} only_canvas only caches keyboard events if they happen when the canvas is in focus */ var onkey_handler = null; gl.captureKeys = function( prevent_default, only_canvas ) { if(onkey_handler) return; gl.keys = {}; var target = only_canvas ? gl.canvas : document; document.addEventListener("keydown", inner ); document.addEventListener("keyup", inner ); function inner(e) { onkey(e, prevent_default); } onkey_handler = inner; } function onkey(e, prevent_default) { e.eventType = e.type; //type cannot be overwritten, so I make a clone to allow me to overwrite var target_element = e.target.nodeName.toLowerCase(); if(target_element === "input" || target_element === "textarea" || target_element === "select") return; e.character = String.fromCharCode(e.keyCode).toLowerCase(); var prev_state = false; var key = GL.mapKeyCode(e.keyCode); if(!key) //this key doesnt look like an special key key = e.character; //regular key if (!e.altKey && !e.ctrlKey && !e.metaKey) { if (key) gl.keys[key] = e.type == "keydown"; prev_state = gl.keys[e.keyCode]; gl.keys[e.keyCode] = e.type == "keydown"; } //avoid repetition if key stays pressed if(prev_state != gl.keys[e.keyCode]) { if(e.type == "keydown" && gl.onkeydown) gl.onkeydown(e); else if(e.type == "keyup" && gl.onkeyup) gl.onkeyup(e); LEvent.trigger(gl, e.type, e); } if(gl.onkey) gl.onkey(e); if(prevent_default && (e.isChar || GL.blockable_keys[e.keyIdentifier || e.key ]) ) e.preventDefault(); } //gamepads gl.gamepads = null; /* function onButton(e, pressed) { console.log(e); if(pressed && gl.onbuttondown) gl.onbuttondown(e); else if(!pressed && gl.onbuttonup) gl.onbuttonup(e); if(gl.onbutton) gl.onbutton(e); LEvent.trigger(gl, pressed ? "buttondown" : "buttonup", e ); } function onGamepad(e) { console.log(e); if(gl.ongamepad) gl.ongamepad(e); } */ /** * Tells the system to capture gamepad events on the canvas. * @method captureGamepads */ gl.captureGamepads = function() { var getGamepads = navigator.getGamepads || navigator.webkitGetGamepads || navigator.mozGetGamepads; if(!getGamepads) return; this.gamepads = getGamepads.call(navigator); //only in firefox, so I cannot rely on this /* window.addEventListener("gamepadButtonDown", function(e) { onButton(e, true); }, false); window.addEventListener("MozGamepadButtonDown", function(e) { onButton(e, true); }, false); window.addEventListener("WebkitGamepadButtonDown", function(e) { onButton(e, true); }, false); window.addEventListener("gamepadButtonUp", function(e) { onButton(e, false); }, false); window.addEventListener("MozGamepadButtonUp", function(e) { onButton(e, false); }, false); window.addEventListener("WebkitGamepadButtonUp", function(e) { onButton(e, false); }, false); window.addEventListener("gamepadconnected", onGamepad, false); window.addEventListener("gamepaddisconnected", onGamepad, false); */ } /** * returns the detected gamepads on the system * @method getGamepads * @param {bool} skip_mapping if set to true it returns the basic gamepad, otherwise it returns a class with mapping info to XBOX controller */ gl.getGamepads = function(skip_mapping) { //gamepads var getGamepads = navigator.getGamepads || navigator.webkitGetGamepads || navigator.mozGetGamepads; if(!getGamepads) return; var gamepads = getGamepads.call(navigator); if(!this.gamepads) this.gamepads = []; //iterate to generate events for(var i = 0; i < 4; i++) { var gamepad = gamepads[i]; //current state if(gamepad && !skip_mapping) addGamepadXBOXmapping(gamepad); //launch connected gamepads events if(gamepad && !gamepad.prev_buttons) { gamepad.prev_buttons = new Uint8Array(32); var event = new CustomEvent("gamepadconnected"); event.eventType = event.type; event.gamepad = gamepad; if(this.ongamepadconnected) this.ongamepadconnected(event); LEvent.trigger(gl,"gamepadconnected",event); } /* else if(old_gamepad && !gamepad) { var event = new CustomEvent("gamepaddisconnected"); event.eventType = event.type; event.gamepad = old_gamepad; if(this.ongamepaddisconnected) this.ongamepaddisconnected(event); LEvent.trigger(gl,"gamepaddisconnected",event); } */ //seek buttons changes to trigger events if(gamepad) { for(var j = 0; j < gamepad.buttons.length; ++j) { var button = gamepad.buttons[j]; button.was_pressed = false; if( button.pressed && !gamepad.prev_buttons[j] ) { button.was_pressed = true; var event = new CustomEvent("gamepadButtonDown"); event.eventType = event.type; event.button = button; event.which = j; event.gamepad = gamepad; if(gl.onbuttondown) gl.onbuttondown(event); LEvent.trigger(gl,"buttondown",event); } else if( !button.pressed && gamepad.prev_buttons[j] ) { var event = new CustomEvent("gamepadButtonUp"); event.eventType = event.type; event.button = button; event.which = j; event.gamepad = gamepad; if(gl.onbuttondown) gl.onbuttondown(event); LEvent.trigger(gl,"buttonup",event); } gamepad.prev_buttons[j] = button.pressed ? 1 : 0; } } } this.gamepads = gamepads; return gamepads; } function addGamepadXBOXmapping(gamepad) { //xbox controller mapping var xbox = gamepad.xbox || { axes:[], buttons:{}, hat: ""}; 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["triggers"] = gamepad.axes[4]; for(var i = 0; i < gamepad.buttons.length; i++) { switch(i) //I use a switch to ensure that a player with another gamepad could play { case 0: xbox.buttons["a"] = gamepad.buttons[i].pressed; break; case 1: xbox.buttons["b"] = gamepad.buttons[i].pressed; break; case 2: xbox.buttons["x"] = gamepad.buttons[i].pressed; break; case 3: xbox.buttons["y"] = gamepad.buttons[i].pressed; break; case 4: xbox.buttons["lb"] = gamepad.buttons[i].pressed; break; case 5: xbox.buttons["rb"] = gamepad.buttons[i].pressed; break; case 6: xbox.buttons["lt"] = gamepad.buttons[i].pressed; break; case 7: xbox.buttons["rt"] = gamepad.buttons[i].pressed; break; case 8: xbox.buttons["back"] = gamepad.buttons[i].pressed; break; case 9: xbox.buttons["start"] = gamepad.buttons[i].pressed; break; case 10: xbox.buttons["ls"] = gamepad.buttons[i].pressed; break; case 11: xbox.buttons["rs"] = gamepad.buttons[i].pressed; break; case 12: if( gamepad.buttons[i].pressed) xbox.hat += "up"; break; case 13: if( gamepad.buttons[i].pressed) xbox.hat += "down"; break; case 14: if( gamepad.buttons[i].pressed) xbox.hat += "left"; break; case 15: if( gamepad.buttons[i].pressed) xbox.hat += "right"; break; case 16: xbox.buttons["home"] = gamepad.buttons[i].pressed; break; default: } } gamepad.xbox = xbox; } /** * launches de canvas in fullscreen mode * @method fullscreen */ gl.fullscreen = function() { var canvas = this.canvas; if(canvas.requestFullScreen) canvas.requestFullScreen(); else if(canvas.webkitRequestFullScreen) canvas.webkitRequestFullScreen(); else if(canvas.mozRequestFullScreen) canvas.mozRequestFullScreen(); else console.error("Fullscreen not supported"); } /** * returns a canvas with a snapshot of an area * this is safer than using the canvas itself due to internals of webgl * @method snapshot * @param {Number} startx viewport x coordinate * @param {Number} starty viewport y coordinate from bottom * @param {Number} areax viewport area width * @param {Number} areay viewport area height * @return {Canvas} canvas */ gl.snapshot = function(startx, starty, areax, areay, skip_reverse) { var canvas = createCanvas(areax,areay); var ctx = canvas.getContext("2d"); var pixels = ctx.getImageData(0,0,canvas.width,canvas.height); var buffer = new Uint8Array(areax * areay * 4); gl.readPixels(startx, starty, canvas.width, canvas.height, gl.RGBA,gl.UNSIGNED_BYTE, buffer); pixels.data.set( buffer ); ctx.putImageData(pixels,0,0); if(skip_reverse) return canvas; //flip image var final_canvas = createCanvas(areax,areay); var ctx = final_canvas.getContext("2d"); ctx.translate(0,areay); ctx.scale(1,-1); ctx.drawImage(canvas,0,0); return final_canvas; } //from https://webgl2fundamentals.org/webgl/lessons/webgl1-to-webgl2.html function getAndApplyExtension( gl, name ) { var ext = gl.getExtension(name); if (!ext) { return false; } var suffix = name.split("_")[0]; var prefix = suffix = '_'; var suffixRE = new RegExp(suffix + '$'); var prefixRE = new RegExp('^' + prefix); for (var key in ext) { var val = ext[key]; if (typeof(val) === 'function') { // remove suffix (eg: bindVertexArrayOES -> bindVertexArray) var unsuffixedKey = key.replace(suffixRE, ''); if (key.substing) gl[unprefixedKey] = ext[key].bind(ext); } else { var unprefixedKey = key.replace(prefixRE, ''); gl[unprefixedKey] = ext[key]; } } } //mini textures manager var loading_textures = {}; /** * returns a texture and caches it inside gl.textures[] * @method loadTexture * @param {String} url * @param {Object} options (same options as when creating a texture) * @param {Function} callback function called once the texture is loaded * @return {Texture} texture */ gl.loadTexture = function(url, options, on_load) { if(this.textures[ url ]) return this.textures[url]; if( loading_textures[url] ) return null; var img = new Image(); img.url = url; img.onload = function() { var texture = GL.Texture.fromImage(this, options); texture.img = this; gl.textures[this.url] = texture; delete loading_textures[this.url]; if(on_load) on_load(texture); } img.src = url; loading_textures[url] = true; return null; } /** * draws a texture to the viewport * @method drawTexture * @param {Texture} texture * @param {number} x in viewport coordinates * @param {number} y in viewport coordinates * @param {number} w in viewport coordinates * @param {number} h in viewport coordinates * @param {number} tx texture x in texture coordinates * @param {number} ty texture y in texture coordinates * @param {number} tw texture width in texture coordinates * @param {number} th texture height in texture coordinates */ gl.drawTexture = (function() { //static variables: less garbage var identity = mat3.create(); var pos = vec2.create(); var size = vec2.create(); var area = vec4.create(); var white = vec4.fromValues(1,1,1,1); var viewport = vec2.create(); var _uniforms = {u_texture: 0, u_position: pos, u_color: white, u_size: size, u_texture_area: area, u_viewport: viewport, u_transform: identity }; return (function(texture, x,y, w,h, tx,ty, tw,th, shader, uniforms) { pos[0] = x; pos[1] = y; if(w === undefined) w = texture.width; if(h === undefined) h = texture.height; size[0] = w; size[1] = h; if(tx === undefined) tx = 0; if(ty === undefined) ty = 0; if(tw === undefined) tw = texture.width; if(th === undefined) th = texture.height; area[0] = tx / texture.width; area[1] = ty / texture.height; area[2] = (tx + tw) / texture.width; area[3] = (ty + th) / texture.height; viewport[0] = this.viewport_data[2]; viewport[1] = this.viewport_data[3]; shader = shader || Shader.getPartialQuadShader(this); var mesh = Mesh.getScreenQuad(this); texture.bind(0); shader.uniforms( _uniforms ); if( uniforms ) shader.uniforms( uniforms ); shader.draw( mesh, gl.TRIANGLES ); }); })(); gl.canvas.addEventListener("webglcontextlost", function(e) { e.preventDefault(); gl.context_lost = true; if(gl.onlosecontext) gl.onlosecontext(e); }, false); /** * use it to reset the the initial gl state * @method gl.reset */ gl.reset = function() { //viewport gl.viewport(0,0, this.canvas.width, this.canvas.height ); //flags gl.disable( gl.BLEND ); gl.disable( gl.CULL_FACE ); gl.disable( gl.DEPTH_TEST ); gl.frontFace( gl.CCW ); //texture gl._current_texture_drawto = null; gl._current_fbo_color = null; gl._current_fbo_depth = null; } gl.dump = function() { console.log("userAgent: ", navigator.userAgent ); console.log("Supported extensions:"); var extensions = gl.getSupportedExtensions(); console.log( extensions.join(",") ); var info = [ "VENDOR", "VERSION", "MAX_VERTEX_ATTRIBS", "MAX_VARYING_VECTORS", "MAX_VERTEX_UNIFORM_VECTORS", "MAX_VERTEX_TEXTURE_IMAGE_UNITS", "MAX_FRAGMENT_UNIFORM_VECTORS", "MAX_TEXTURE_SIZE", "MAX_TEXTURE_IMAGE_UNITS" ]; console.log("WebGL info:"); for(var i in info) console.log(" * " + info[i] + ": " + gl.getParameter( gl[info[i]] )); console.log("*************************************************") } //Reset state gl.reset(); //Return return gl; } GL.mapKeyCode = function(code) { var named = { 8: 'BACKSPACE', 9: 'TAB', 13: 'ENTER', 16: 'SHIFT', 17: 'CTRL', 27: 'ESCAPE', 32: 'SPACE', 37: 'LEFT', 38: 'UP', 39: 'RIGHT', 40: 'DOWN' }; return named[code] || (code >= 65 && code <= 90 ? String.fromCharCode(code) : null); } //add useful info to the event GL.dragging = false; GL.last_pos = [0,0]; //adds extra info to the MouseEvent (coordinates in canvas axis, deltas and button state) GL.augmentEvent = function(e, root_element) { var offset_left = 0; var offset_top = 0; var b = null; root_element = root_element || e.target || gl.canvas; b = root_element.getBoundingClientRect(); e.mousex = e.clientX - b.left; e.mousey = e.clientY - b.top; e.canvasx = e.mousex; e.canvasy = b.height - e.mousey; e.deltax = 0; e.deltay = 0; if(e.type == "mousedown") { this.dragging = true; //gl.mouse.buttons |= (1 << e.which); //enable } else if (e.type == "mousemove") { } else if (e.type == "mouseup") { //gl.mouse.buttons = gl.mouse.buttons & ~(1 << e.which); if(e.buttons == 0) this.dragging = false; } if( e.movementX !== undefined && !GL.isMobile() ) //pointer lock (mobile gives always zero) { //console.log( e.movementX ) e.deltax = e.movementX; e.deltay = e.movementY; } else { e.deltax = e.mousex - this.last_pos[0]; e.deltay = e.mousey - this.last_pos[1]; } this.last_pos[0] = e.mousex; this.last_pos[1] = e.mousey; //insert info in event e.dragging = this.dragging; e.leftButton = !!(gl.mouse.buttons & GL.LEFT_MOUSE_BUTTON_MASK); e.middleButton = !!(gl.mouse.buttons & GL.MIDDLE_MOUSE_BUTTON_MASK); e.rightButton = !!(gl.mouse.buttons & GL.RIGHT_MOUSE_BUTTON_MASK); //shitty non-coherent API, e.buttons use 1:left,2:right,4:middle) but e.button uses (0:left,1:middle,2:right) e.buttons_mask = 0; if( e.leftButton ) e.buttons_mask = 1; if( e.middleButton ) e.buttons_mask |= 2; if( e.rightButton ) e.buttons_mask |= 4; e.isButtonPressed = function(num) { return this.buttons_mask & (1<= 0; --j) //iterate backwards to avoid problems after removing { if( array[j][1] != target_instance || (callback && callback !== array[j][0]) ) continue; array.splice(j,1);//remove } } }, /** * Unbinds all callbacks associated to one specific event from this instance * @method LEvent.unbindAll * @param {Object} instance where the events are binded * @param {String} event name of the event you want to remove all binds **/ unbindAllEvent: function( instance, event_type ) { if(!instance) throw("cannot unbind events in null"); var events = instance.__levents; if(!events) return; delete events[ event_type ]; if( instance.onLEventUnbindAll ) instance.onLEventUnbindAll( event_type, target_instance, callback ); return; }, /** * Tells if there is a binded callback that matches the criteria * @method LEvent.isBind * @param {Object} instance where the are the events binded * @param {String} event_name string defining the event name * @param {function} callback the callback * @param {Object} target_instance [Optional] instance binded to callback **/ isBind: function( instance, event_type, callback, target_instance ) { if(!instance) throw("LEvent cannot have null as instance"); var events = instance.__levents; if( !events ) return; if( !events.hasOwnProperty(event_type) ) return false; for(var i = 0, l = events[event_type].length; i < l; ++i) { var v = events[event_type][i]; if(v[0] === callback && v[1] === target_instance) return true; } return false; }, /** * Tells if there is any callback binded to this event * @method LEvent.hasBind * @param {Object} instance where the are the events binded * @param {String} event_name string defining the event name * @return {boolean} true is there is at least one **/ hasBind: function( instance, event_type ) { if(!instance) throw("LEvent cannot have null as instance"); var events = instance.__levents; if(!events || !events.hasOwnProperty( event_type ) || !events[event_type].length) return false; return true; }, /** * Tells if there is any callback binded to this object pointing to a method in the target object * @method LEvent.hasBindTo * @param {Object} instance where there are the events binded * @param {Object} target instance to check to * @return {boolean} true is there is at least one **/ hasBindTo: function( instance, target ) { if(!instance) throw("LEvent cannot have null as instance"); var events = instance.__levents; //no events binded if(!events) return false; for(var j in events) { var binds = events[j]; for(var i = 0; i < binds.length; ++i) { if(binds[i][1] === target) //one found return true; } } return false; }, /** * Triggers and event in an instance * If the callback returns true then it will stop the propagation and return true * @method LEvent.trigger * @param {Object} instance that triggers the event * @param {String} event_name string defining the event name * @param {*} parameters that will be received by the binded function * @param {bool} reverse_order trigger in reverse order (binded last get called first) * @param {bool} expand_parameters parameters are passed not as one single parameter, but as many * return {bool} true if the event passed was blocked by any binded callback **/ trigger: function( instance, event_type, params, reverse_order, expand_parameters ) { if(!instance) throw("cannot trigger event from null"); if(instance.constructor === String ) throw("cannot bind event to a string"); var events = instance.__levents; if( !events || !events.hasOwnProperty(event_type) ) return false; var inst = events[event_type]; if( reverse_order ) { for(var i = inst.length - 1; i >= 0; --i) { var v = inst[i]; if(expand_parameters) { if( v && v[0].apply( v[1], params ) === true)// || event.stop) return true; //stopPropagation } else { if( v && v[0].call( v[1], event_type, params) === true)// || event.stop) return true; //stopPropagation } } } else { for(var i = 0, l = inst.length; i < l; ++i) { var v = inst[i]; if( expand_parameters ) { if( v && v[0].apply( v[1], params ) === true)// || event.stop) return true; //stopPropagation } else { if( v && v[0].call(v[1], event_type, params) === true)// || event.stop) return true; //stopPropagation } } } return false; }, /** * Triggers and event to every element in an array. * If the event returns true, it must be intercepted * @method LEvent.triggerArray * @param {Array} array contains all instances to triggers the event * @param {String} event_name string defining the event name * @param {*} parameters that will be received by the binded function * @param {bool} reverse_order trigger in reverse order (binded last get called first) * @param {bool} expand_parameters parameters are passed not as one single parameter, but as many * return {bool} false **/ triggerArray: function( instances, event_type, params, reverse_order, expand_parameters ) { var blocked = false; for(var i = 0, l = instances.length; i < l; ++i) { var instance = instances[i]; if(!instance) throw("cannot trigger event from null"); if(instance.constructor === String ) throw("cannot bind event to a string"); var events = instance.__levents; if( !events || !events.hasOwnProperty( event_type ) ) continue; if( reverse_order ) { for(var j = events[event_type].length - 1; j >= 0; --j) { var v = events[event_type][j]; if(expand_parameters) { if( v[0].apply(v[1], params ) === true)// || event.stop) { blocked = true; break; //stopPropagation } } else { if( v[0].call(v[1], event_type, params) === true)// || event.stop) { blocked = true; break; //stopPropagation } } } } else { for(var j = 0, ll = events[event_type].length; j < ll; ++j) { var v = events[event_type][j]; if(expand_parameters) { if( v[0].apply(v[1], params ) === true)// || event.stop) { blocked = true; break; //stopPropagation } } else { if( v[0].call(v[1], event_type, params) === true)// || event.stop) { blocked = true; break; //stopPropagation } } } } } return blocked; }, extendObject: function( object ) { object.bind = function( event_type, callback, instance ){ return LEvent.bind( this, event_type, callback, instance ); }; object.trigger = function( event_type, params ){ return LEvent.trigger( this, event_type, params ); }; object.unbind = function( event_type, callback, target_instance ) { return LEvent.unbind( this, event_type, callback, instance ); }; object.unbindAll = function( target_instance, callback ) { return LEvent.unbindAll( this, target_instance, callback ); }; }, /** * Adds the methods to bind, trigger and unbind to this class prototype * @method LEvent.extendClass * @param {Object} constructor **/ extendClass: function( constructor ) { this.extendObject( constructor.prototype ); } }; /* geometric utilities */ global.CLIP_INSIDE = GL.CLIP_INSIDE = 0; global.CLIP_OUTSIDE = GL.CLIP_OUTSIDE = 1; global.CLIP_OVERLAP = GL.CLIP_OVERLAP = 2; /** * @namespace */ /** * Computational geometry algorithms, is a static class * @class geo */ global.geo = { /** * Returns a float4 containing the info about a plane with normal N and that passes through point P * @method createPlane * @param {vec3} P * @param {vec3} N * @return {vec4} plane values */ createPlane: function(P,N) { return new Float32Array([N[0],N[1],N[2],-vec3.dot(P,N)]); }, /** * Computes the distance between the point and the plane * @method distancePointToPlane * @param {vec3} point * @param {vec4} plane * @return {Number} distance */ distancePointToPlane: function(point, plane) { return (vec3.dot(point,plane) + plane[3])/Math.sqrt(plane[0]*plane[0] + plane[1]*plane[1] + plane[2]*plane[2]); }, /** * Computes the square distance between the point and the plane * @method distance2PointToPlane * @param {vec3} point * @param {vec4} plane * @return {Number} distance*distance */ distance2PointToPlane: function(point, plane) { return (vec3.dot(point,plane) + plane[3])/(plane[0]*plane[0] + plane[1]*plane[1] + plane[2]*plane[2]); }, /** * Projects a 3D point on a 3D line * @method projectPointOnLine * @param {vec3} P * @param {vec3} A line start * @param {vec3} B line end * @param {vec3} result to store result (optional) * @return {vec3} projectec point */ projectPointOnLine: function( P, A, B, result ) { result = result || vec3.create(); //A + dot(AP,AB) / dot(AB,AB) * AB var AP = vec3.fromValues( P[0] - A[0], P[1] - A[1], P[2] - A[2]); var AB = vec3.fromValues( B[0] - A[0], B[1] - A[1], B[2] - A[2]); var div = vec3.dot(AP,AB) / vec3.dot(AB,AB); result[0] = A[0] + div[0] * AB[0]; result[1] = A[1] + div[1] * AB[1]; result[2] = A[2] + div[2] * AB[2]; return result; }, /** * Projects a 2D point on a 2D line * @method project2DPointOnLine * @param {vec2} P * @param {vec2} A line start * @param {vec2} B line end * @param {vec2} result to store result (optional) * @return {vec2} projectec point */ project2DPointOnLine: function( P, A, B, result ) { result = result || vec2.create(); //A + dot(AP,AB) / dot(AB,AB) * AB var AP = vec2.fromValues(P[0] - A[0], P[1] - A[1]); var AB = vec2.fromValues(B[0] - A[0], B[1] - A[1]); var div = vec2.dot(AP,AB) / vec2.dot(AB,AB); result[0] = A[0] + div[0] * AB[0]; result[1] = A[1] + div[1] * AB[1]; return result; }, /** * Projects point on plane * @method projectPointOnPlane * @param {vec3} point * @param {vec3} P plane point * @param {vec3} N plane normal * @param {vec3} result to store result (optional) * @return {vec3} projectec point */ projectPointOnPlane: function(point, P, N, result) { result = result || vec3.create(); var v = vec3.subtract( vec3.create(), point, P ); var dist = vec3.dot(v,N); return vec3.subtract( result, point , vec3.scale( vec3.create(), N, dist ) ); }, /** * Finds the reflected point over a plane (useful for reflecting camera position when rendering reflections) * @method reflectPointInPlane * @param {vec3} point point to reflect * @param {vec3} P point where the plane passes * @param {vec3} N normal of the plane * @return {vec3} reflected point */ reflectPointInPlane: function(point, P, N) { var d = -1 * (P[0] * N[0] + P[1] * N[1] + P[2] * N[2]); var t = -(d + N[0]*point[0] + N[1]*point[1] + N[2]*point[2]) / (N[0]*N[0] + N[1]*N[1] + N[2]*N[2]); //trace("T:" + t); //var closest = [ point[0]+t*N[0], point[1]+t*N[1], point[2]+t*N[2] ]; //trace("Closest:" + closest); return vec3.fromValues( point[0]+t*N[0]*2, point[1]+t*N[1]*2, point[2]+t*N[2]*2 ); }, /** * test a ray plane collision and retrieves the collision point * @method testRayPlane * @param {vec3} start ray start * @param {vec3} direction ray direction * @param {vec3} P point where the plane passes * @param {vec3} N normal of the plane * @param {vec3} result collision position * @return {boolean} returns if the ray collides the plane or the ray is parallel to the plane */ testRayPlane: function(start, direction, P, N, result) { var D = vec3.dot( P, N ); var numer = D - vec3.dot(N, start); var denom = vec3.dot(N, direction); if( Math.abs(denom) < EPSILON) return false; var t = (numer / denom); if(t < 0.0) return false; //behind the ray if(result) vec3.add( result, start, vec3.scale( result, direction, t) ); return true; }, /** * test collision between segment and plane and retrieves the collision point * @method testSegmentPlane * @param {vec3} start segment start * @param {vec3} end segment end * @param {vec3} P point where the plane passes * @param {vec3} N normal of the plane * @param {vec3} result collision position * @return {boolean} returns if the segment collides the plane or it is parallel to the plane */ testSegmentPlane: (function() { var temp = vec3.create(); return function(start, end, P, N, result) { var D = vec3.dot( P, N ); var numer = D - vec3.dot(N, start); var direction = vec3.sub( temp, end, start ); var denom = vec3.dot(N, direction); if( Math.abs(denom) < EPSILON) return false; //parallel var t = (numer / denom); if(t < 0.0) return false; //behind the start if(t > 1.0) return false; //after the end if(result) vec3.add( result, start, vec3.scale( result, direction, t) ); return true; }; })(), /** * test a ray sphere collision and retrieves the collision point * @method testRaySphere * @param {vec3} start ray start * @param {vec3} direction ray direction (normalized) * @param {vec3} center center of the sphere * @param {number} radius radius of the sphere * @param {vec3} result [optional] collision position * @param {number} max_dist not fully tested * @return {boolean} returns if the ray collides the sphere */ testRaySphere: (function() { var temp = vec3.create(); return function(start, direction, center, radius, result, max_dist) { // sphere equation (centered at origin) x2+y2+z2=r2 // ray equation x(t) = p0 + t*dir // substitute x(t) into sphere equation // solution below: // transform ray origin into sphere local coordinates var orig = vec3.subtract( temp , start, center); var a = direction[0]*direction[0] + direction[1]*direction[1] + direction[2]*direction[2]; var b = 2*orig[0]*direction[0] + 2*orig[1]*direction[1] + 2*orig[2]*direction[2]; var c = orig[0]*orig[0] + orig[1]*orig[1] + orig[2]*orig[2] - radius*radius; //return quadraticFormula(a,b,c,t0,t1) ? 2 : 0; var q = b*b - 4*a*c; if( q < 0.0 ) return false; if(result) { var sq = Math.sqrt(q); var d = 1 / (2*a); var r1 = ( -b + sq ) * d; var r2 = ( -b - sq ) * d; var t = r1 < r2 ? r1 : r2; if(max_dist !== undefined && t > max_dist) return false; vec3.add(result, start, vec3.scale( result, direction, t ) ); } return true;//real roots }; })(), /** * test a ray cylinder collision (only vertical cylinders) and retrieves the collision point [not fully tested] * @method testRayCylinder * @param {vec3} start ray start * @param {vec3} direction ray direction * @param {vec3} p center of the cylinder * @param {number} q height of the cylinder * @param {number} r radius of the cylinder * @param {vec3} result collision position * @return {boolean} returns if the ray collides the cylinder */ testRayCylinder: function(start, direction, p, q, r, result) { var sa = vec3.clone(start); var sb = vec3.add(vec3.create(), start, vec3.scale( vec3.create(), direction, 100000) ); var t = 0; var d = vec3.subtract(vec3.create(),q,p); var m = vec3.subtract(vec3.create(),sa,p); var n = vec3.subtract(vec3.create(),sb,sa); //var n = vec3.create(direction); var md = vec3.dot(m, d); var nd = vec3.dot(n, d); var dd = vec3.dot(d, d); // Test if segment fully outside either endcap of cylinder if (md < 0.0 && md + nd < 0.0) return false; // Segment outside p side of cylinder if (md > dd && md + nd > dd) return false; // Segment outside q side of cylinder var nn = vec3.dot(n, n); var mn = vec3.dot(m, n); var a = dd * nn - nd * nd; var k = vec3.dot(m,m) - r*r; var c = dd * k - md * md; if (Math.abs(a) < EPSILON) { // Segment runs parallel to cylinder axis if (c > 0.0) return false; // a and thus the segment lie outside cylinder // Now known that segment intersects cylinder; figure out how it intersects if (md < 0.0) t = -mn/nn; // Intersect segment against p endcap else if (md > dd) t=(nd-mn)/nn; // Intersect segment against q endcap else t = 0.0; // a lies inside cylinder if(result) vec3.add(result, sa, vec3.scale(result, n,t) ); return true; } var b = dd * mn - nd * md; var discr = b*b - a*c; if (discr < 0.0) return false; // No real roots; no intersection t = (-b - Math.sqrt(discr)) / a; if (t < 0.0 || t > 1.0) return false; // Intersection lies outside segment if(md+t*nd < 0.0) { // Intersection outside cylinder on p side if (nd <= 0.0) return false; // Segment pointing away from endcap t = -md / nd; // Keep intersection if Dot(S(t) - p, S(t) - p) <= r^2 if(result) vec3.add(result, sa, vec3.scale(result, n,t) ); return k+2*t*(mn+t*nn) <= 0.0; } else if (md+t*nd>dd) { // Intersection outside cylinder on q side if (nd >= 0.0) return false; //Segment pointing away from endcap t = (dd - md) / nd; // Keep intersection if Dot(S(t) - q, S(t) - q) <= r^2 if(result) vec3.add(result, sa, vec3.scale(result, n,t) ); return k+dd - 2*md+t*(2*(mn - nd)+t*nn) <= 0.0; } // Segment intersects cylinder between the endcaps; t is correct if(result) vec3.add(result, sa, vec3.scale(result, n,t) ); return true; }, /** * test a ray bounding-box collision and retrieves the collision point, the BB must be Axis Aligned * @method testRayBox * @param {vec3} start ray start * @param {vec3} direction ray direction * @param {vec3} minB minimum position of the bounding box * @param {vec3} maxB maximim position of the bounding box * @param {vec3} result collision position * @return {boolean} returns if the ray collides the box */ testRayBox: (function() { var quadrant = new Float32Array(3); var candidatePlane = new Float32Array(3); var maxT = new Float32Array(3); return function(start, direction, minB, maxB, result, max_dist) { //#define NUMDIM 3 //#define RIGHT 0 //#define LEFT 1 //#define MIDDLE 2 max_dist = max_dist || Number.MAX_VALUE; var inside = true; var i = 0|0; var whichPlane; quadrant.fill(0); maxT.fill(0); candidatePlane.fill(0); /* Find candidate planes; this loop can be avoided if rays cast all from the eye(assume perpsective view) */ for (i=0; i < 3; ++i) if(start[i] < minB[i]) { quadrant[i] = 1; candidatePlane[i] = minB[i]; inside = false; }else if (start[i] > maxB[i]) { quadrant[i] = 0; candidatePlane[i] = maxB[i]; inside = false; }else { quadrant[i] = 2; } /* Ray origin inside bounding box */ if(inside) { if(result) vec3.copy(result, start); return true; } /* Calculate T distances to candidate planes */ for (i = 0; i < 3; ++i) if (quadrant[i] != 2 && direction[i] != 0.) maxT[i] = (candidatePlane[i] - start[i]) / direction[i]; else maxT[i] = -1.; /* Get largest of the maxT's for final choice of intersection */ whichPlane = 0; for (i = 1; i < 3; i++) if (maxT[whichPlane] < maxT[i]) whichPlane = i; /* Check final candidate actually inside box */ if (maxT[whichPlane] < 0.) return false; if (maxT[whichPlane] > max_dist) return false; //NOT TESTED for (i = 0; i < 3; ++i) if (whichPlane != i) { var res = start[i] + maxT[whichPlane] * direction[i]; if (res < minB[i] || res > maxB[i]) return false; if(result) result[i] = res; } else { if(result) result[i] = candidatePlane[i]; } return true; /* ray hits box */ } })(), /** * test a ray bounding-box collision, it uses the BBox class and allows to use non-axis aligned bbox * @method testRayBBox * @param {vec3} origin ray origin * @param {vec3} direction ray direction * @param {BBox} box in BBox format * @param {mat4} model transformation of the BBox [optional] * @param {vec3} result collision position in world space unless in_local is true * @return {boolean} returns if the ray collides the box */ testRayBBox: (function(){ var inv = mat4.create(); var end = vec3.create(); var origin2 = vec3.create(); return function( origin, direction, box, model, result, max_dist, in_local ) { if(!origin || !direction || !box) throw("parameters missing"); if(model) { mat4.invert( inv, model ); vec3.add( end, origin, direction ); origin = vec3.transformMat4( origin2, origin, inv); vec3.transformMat4( end, end, inv ); vec3.sub( end, end, origin ); direction = vec3.normalize( end, end ); } var r = this.testRayBox( origin, direction, box.subarray(6,9), box.subarray(9,12), result, max_dist ); if(!in_local && model && result) vec3.transformMat4(result, result, model); return r; } })(), /** * test if a 3d point is inside a BBox * @method testPointBBox * @param {vec3} point * @param {BBox} bbox * @return {boolean} true if it is inside */ testPointBBox: function(point, bbox) { if(point[0] < bbox[6] || point[0] > bbox[9] || point[1] < bbox[7] || point[0] > bbox[10] || point[2] < bbox[8] || point[0] > bbox[11]) return false; return true; }, /** * test if a BBox overlaps another BBox * @method testBBoxBBox * @param {BBox} a * @param {BBox} b * @return {boolean} true if it overlaps */ testBBoxBBox: function(a, b) { var tx = Math.abs( b[0] - a[0]); if (tx > (a[3] + b[3])) return false; //outside var ty = Math.abs(b[1] - a[1]); if (ty > (a[4] + b[4])) return false; //outside var tz = Math.abs( b[2] - a[2]); if (tz > (a[5] + b[5]) ) return false; //outside var vmin = BBox.getMin(b); if ( geo.testPointBBox(vmin, a) ) { var vmax = BBox.getMax(b); if (geo.testPointBBox(vmax, a)) { return true;// INSIDE;// this instance contains b } } return true; //OVERLAP; // this instance overlaps with b }, /** * test if a sphere overlaps a BBox * @method testSphereBBox * @param {vec3} point * @param {float} radius * @param {BBox} bounding_box * @return {boolean} true if it overlaps */ testSphereBBox: function(center, radius, bbox) { // arvo's algorithm from gamasutra // http://www.gamasutra.com/features/19991018/Gomez_4.htm var s, d = 0.0; //find the square of the distance //from the sphere to the box var vmin = BBox.getMin( bbox ); var vmax = BBox.getMax( bbox ); for(var i = 0; i < 3; ++i) { if( center[i] < vmin[i] ) { s = center[i] - vmin[i]; d += s*s; } else if( center[i] > vmax[i] ) { s = center[i] - vmax[i]; d += s*s; } } //return d <= r*r var radiusSquared = radius * radius; if (d <= radiusSquared) { return true; /* // this is used just to know if it overlaps or is just inside, but I dont care // make an aabb aabb test with the sphere aabb to test inside state var halfsize = vec3.fromValues( radius, radius, radius ); var sphere_bbox = BBox.fromCenterHalfsize( center, halfsize ); if ( geo.testBBoxBBox(bbox, sphere_bbox) ) return INSIDE; return OVERLAP; */ } return false; //OUTSIDE; }, closestPointBetweenLines: function(a0,a1, b0,b1, p_a, p_b) { var u = vec3.subtract( vec3.create(), a1, a0 ); var v = vec3.subtract( vec3.create(), b1, b0 ); var w = vec3.subtract( vec3.create(), a0, b0 ); var a = vec3.dot(u,u); // always >= 0 var b = vec3.dot(u,v); var c = vec3.dot(v,v); // always >= 0 var d = vec3.dot(u,w); var e = vec3.dot(v,w); var D = a*c - b*b; // always >= 0 var sc, tc; // compute the line parameters of the two closest points if (D < EPSILON) { // the lines are almost parallel sc = 0.0; tc = (b>c ? d/b : e/c); // use the largest denominator } else { sc = (b*e - c*d) / D; tc = (a*e - b*d) / D; } // get the difference of the two closest points if(p_a) vec3.add(p_a, a0, vec3.scale(vec3.create(),u,sc)); if(p_b) vec3.add(p_b, b0, vec3.scale(vec3.create(),v,tc)); var dP = vec3.add( vec3.create(), w, vec3.subtract( vec3.create(), vec3.scale(vec3.create(),u,sc) , vec3.scale(vec3.create(),v,tc)) ); // = L1(sc) - L2(tc) return vec3.length(dP); // return the closest distance }, /** * extract frustum planes given a view-projection matrix * @method extractPlanes * @param {mat4} viewprojection matrix * @return {Float32Array} returns all 6 planes in a float32array[24] */ extractPlanes: function(vp, planes) { var planes = planes || new Float32Array(4*6); //right planes.set( [vp[3] - vp[0], vp[7] - vp[4], vp[11] - vp[8], vp[15] - vp[12] ], 0); normalize(0); //left planes.set( [vp[3] + vp[0], vp[ 7] + vp[ 4], vp[11] + vp[ 8], vp[15] + vp[12] ], 4); normalize(4); //bottom planes.set( [ vp[ 3] + vp[ 1], vp[ 7] + vp[ 5], vp[11] + vp[ 9], vp[15] + vp[13] ], 8); normalize(8); //top planes.set( [ vp[ 3] - vp[ 1], vp[ 7] - vp[ 5], vp[11] - vp[ 9], vp[15] - vp[13] ],12); normalize(12); //back planes.set( [ vp[ 3] - vp[ 2], vp[ 7] - vp[ 6], vp[11] - vp[10], vp[15] - vp[14] ],16); normalize(16); //front planes.set( [ vp[ 3] + vp[ 2], vp[ 7] + vp[ 6], vp[11] + vp[10], vp[15] + vp[14] ],20); normalize(20); return planes; function normalize(pos) { var N = planes.subarray(pos,pos+3); var l = vec3.length(N); if(l === 0) return; l = 1.0 / l; planes[pos] *= l; planes[pos+1] *= l; planes[pos+2] *= l; planes[pos+3] *= l; } }, /** * test a BBox against the frustum * @method frustumTestBox * @param {Float32Array} planes frustum planes * @param {BBox} boundindbox in BBox format * @return {enum} CLIP_INSIDE, CLIP_OVERLAP, CLIP_OUTSIDE */ frustumTestBox: function(planes, box) { var flag = 0, o = 0; flag = planeBoxOverlap(planes.subarray(0,4),box); if (flag == CLIP_OUTSIDE) return CLIP_OUTSIDE; o+= flag; flag = planeBoxOverlap(planes.subarray(4,8),box); if (flag == CLIP_OUTSIDE) return CLIP_OUTSIDE; o+= flag; flag = planeBoxOverlap(planes.subarray(8,12),box); if (flag == CLIP_OUTSIDE) return CLIP_OUTSIDE; o+= flag; flag = planeBoxOverlap(planes.subarray(12,16),box); if (flag == CLIP_OUTSIDE) return CLIP_OUTSIDE; o+= flag; flag = planeBoxOverlap(planes.subarray(16,20),box); if (flag == CLIP_OUTSIDE) return CLIP_OUTSIDE; o+= flag; flag = planeBoxOverlap(planes.subarray(20,24),box); if (flag == CLIP_OUTSIDE) return CLIP_OUTSIDE; o+= flag; return o == 0 ? CLIP_INSIDE : CLIP_OVERLAP; }, /** * test a Sphere against the frustum * @method frustumTestSphere * @param {vec3} center sphere center * @param {number} radius sphere radius * @return {enum} CLIP_INSIDE, CLIP_OVERLAP, CLIP_OUTSIDE */ frustumTestSphere: function(planes, center, radius) { var dist; var overlap = false; dist = distanceToPlane( planes.subarray(0,4), center ); if( dist < -radius ) return CLIP_OUTSIDE; else if(dist >= -radius && dist <= radius) overlap = true; dist = distanceToPlane( planes.subarray(4,8), center ); if( dist < -radius ) return CLIP_OUTSIDE; else if(dist >= -radius && dist <= radius) overlap = true; dist = distanceToPlane( planes.subarray(8,12), center ); if( dist < -radius ) return CLIP_OUTSIDE; else if(dist >= -radius && dist <= radius) overlap = true; dist = distanceToPlane( planes.subarray(12,16), center ); if( dist < -radius ) return CLIP_OUTSIDE; else if(dist >= -radius && dist <= radius) overlap = true; dist = distanceToPlane( planes.subarray(16,20), center ); if( dist < -radius ) return CLIP_OUTSIDE; else if(dist >= -radius && dist <= radius) overlap = true; dist = distanceToPlane( planes.subarray(20,24), center ); if( dist < -radius ) return CLIP_OUTSIDE; else if(dist >= -radius && dist <= radius) overlap = true; return overlap ? CLIP_OVERLAP : CLIP_INSIDE; }, /** * test if a 2d point is inside a 2d polygon * @method testPoint2DInPolygon * @param {Array} poly array of 2d points * @param {vec2} point * @return {boolean} true if it is inside */ testPoint2DInPolygon: function(poly, pt) { for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1] < poly[i][1])) && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) && (c = !c); return c; } }; /** * BBox is a class to create BoundingBoxes but it works as glMatrix, creating Float32Array with the info inside instead of objects * The bounding box is stored as center,halfsize,min,max,radius (total of 13 floats) * @class BBox */ global.BBox = GL.BBox = { center:0, halfsize:3, min:6, max:9, radius:12, data_length: 13, //corners: new Float32Array([1,1,1, 1,1,-1, 1,-1,1, 1,-1,-1, -1,1,1, -1,1,-1, -1,-1,1, -1,-1,-1 ]), corners: [ vec3.fromValues(1,1,1), vec3.fromValues(1,1,-1), vec3.fromValues(1,-1,1), vec3.fromValues(1,-1,-1), vec3.fromValues(-1,1,1), vec3.fromValues(-1,1,-1), vec3.fromValues(-1,-1,1), vec3.fromValues(-1,-1,-1) ] , /** * create an empty bbox * @method create * @return {BBox} returns a float32array with the bbox */ create: function() { return new Float32Array(13); }, /** * create an bbox copy from another one * @method clone * @return {BBox} returns a float32array with the bbox */ clone: function(bb) { return new Float32Array(bb); }, /** * copy one bbox into another * @method copy * @param {BBox} out where to store the result * @param {BBox} where to read the bbox * @return {BBox} returns out */ copy: function(out,bb) { out.set(bb); return out; }, /** * create a bbox from one point * @method fromPoint * @param {vec3} point * @return {BBox} returns a float32array with the bbox */ fromPoint: function(point) { var bb = this.create(); bb.set(point, 0); //center bb.set(point, 6); //min bb.set(point, 9); //max return bb; }, /** * create a bbox from min and max points * @method fromMinMax * @param {vec3} min * @param {vec3} max * @return {BBox} returns a float32array with the bbox */ fromMinMax: function(min,max) { var bb = this.create(); this.setMinMax(bb, min, max); return bb; }, /** * create a bbox from center and halfsize * @method fromCenterHalfsize * @param {vec3} center * @param {vec3} halfsize * @return {BBox} returns a float32array with the bbox */ fromCenterHalfsize: function(center, halfsize) { var bb = this.create(); this.setCenterHalfsize(bb, center, halfsize); return bb; }, /** * create a bbox from a typed-array containing points * @method fromPoints * @param {Float32Array} points * @return {BBox} returns a float32array with the bbox */ fromPoints: function(points) { var bb = this.create(); this.setFromPoints(bb, points); return bb; }, /** * set the values to a BB from a set of points * @method setFromPoints * @param {BBox} out where to store the result * @param {Float32Array} points * @return {BBox} returns a float32array with the bbox */ setFromPoints: function(bb, points) { var min = bb.subarray(6,9); var max = bb.subarray(9,12); min[0] = points[0]; //min.set( points.subarray(0,3) ); min[1] = points[1]; min[2] = points[2]; max.set( min ); var v = 0; for(var i = 3, l = points.length; i < l; i+=3) { var x = points[i]; var y = points[i+1]; var z = points[i+2]; if( x < min[0] ) min[0] = x; else if( x > max[0] ) max[0] = x; if( y < min[1] ) min[1] = y; else if( y > max[1] ) max[1] = y; if( z < min[2] ) min[2] = z; else if( z > max[2] ) max[2] = z; /* v = points.subarray(i,i+3); vec3.min( min, v, min); vec3.max( max, v, max); */ } //center bb[0] = (min[0] + max[0]) * 0.5; bb[1] = (min[1] + max[1]) * 0.5; bb[2] = (min[2] + max[2]) * 0.5; //halfsize bb[3] = max[0] - bb[0]; bb[4] = max[1] - bb[1]; bb[5] = max[2] - bb[2]; bb[12] = Math.sqrt( bb[3]*bb[3] + bb[4]*bb[4] + bb[5]*bb[5] ); /* var center = vec3.add( bb.subarray(0,3), min, max ); vec3.scale( center, center, 0.5); vec3.subtract( bb.subarray(3,6), max, center ); bb[12] = vec3.length(bb.subarray(3,6)); //radius */ return bb; }, /** * set the values to a BB from min and max * @method setMinMax * @param {BBox} out where to store the result * @param {vec3} min * @param {vec3} max * @return {BBox} returns out */ setMinMax: function(bb, min, max) { bb[6] = min[0]; bb[7] = min[1]; bb[8] = min[2]; bb[9] = max[0]; bb[10] = max[1]; bb[11] = max[2]; //halfsize var halfsize = bb.subarray(3,6); vec3.sub( halfsize, max, min ); //range vec3.scale( halfsize, halfsize, 0.5 ); //center bb[0] = max[0] - halfsize[0]; bb[1] = max[1] - halfsize[1]; bb[2] = max[2] - halfsize[2]; bb[12] = vec3.length(bb.subarray(3,6)); //radius return bb; }, /** * set the values to a BB from center and halfsize * @method setCenterHalfsize * @param {BBox} out where to store the result * @param {vec3} min * @param {vec3} max * @param {number} radius [optional] (the minimum distance from the center to the further point) * @return {BBox} returns out */ setCenterHalfsize: function(bb, center, halfsize, radius) { bb[0] = center[0]; bb[1] = center[1]; bb[2] = center[2]; bb[3] = halfsize[0]; bb[4] = halfsize[1]; bb[5] = halfsize[2]; bb[6] = bb[0] - bb[3]; bb[7] = bb[1] - bb[4]; bb[8] = bb[2] - bb[5]; bb[9] = bb[0] + bb[3]; bb[10] = bb[1] + bb[4]; bb[11] = bb[2] + bb[5]; if(radius) bb[12] = radius; else bb[12] = vec3.length(halfsize); return bb; }, /** * Apply a matrix transformation to the BBox (applies to every corner and recomputes the BB) * @method transformMat4 * @param {BBox} out where to store the result * @param {BBox} bb bbox you want to transform * @param {mat4} mat transformation * @return {BBox} returns out */ transformMat4: (function(){ var hsx = 0; var hsy = 0; var hsz = 0; var points_buffer = new Float32Array(8*3); var points = []; for(var i = 0; i < 24; i += 3 ) points.push( points_buffer.subarray( i, i+3 ) ); return function( out, bb, mat ) { var centerx = bb[0]; var centery = bb[1]; var centerz = bb[2]; hsx = bb[3]; hsy = bb[4]; hsz = bb[5]; var corners = this.corners; for(var i = 0; i < 8; ++i) { var corner = corners[i]; var result = points[i]; result[0] = hsx * corner[0] + centerx; result[1] = hsy * corner[1] + centery; result[2] = hsz * corner[2] + centerz; mat4.multiplyVec3( result, mat, result ); } return this.setFromPoints( out, points_buffer ); } })(), /** * Computes the eight corners of the BBox and returns it * @method getCorners * @param {BBox} bb the bounding box * @param {Float32Array} result optional, should be 8 * 3 * @return {Float32Array} returns the 8 corners */ getCorners: function( bb, result ) { var center = bb; //.subarray(0,3); AVOID GC var halfsize = bb.subarray(3,6); var corners = null; if(result) { result.set(this.corners); corners = result; } else corners = new Float32Array( this.corners ); for(var i = 0; i < 8; ++i) { var corner = corners.subarray(i*3, i*3+3); vec3.multiply( corner, halfsize, corner ); vec3.add( corner, corner, center ); } return corners; }, merge: function( out, a, b ) { var min = out.subarray(6,9); var max = out.subarray(9,12); vec3.min( min, a.subarray(6,9), b.subarray(6,9) ); vec3.max( max, a.subarray(9,12), b.subarray(9,12) ); return BBox.setMinMax( out, min, max ); }, extendToPoint: function( out, p ) { if( p[0] < out[6] ) out[6] = p[0]; else if( p[0] > out[9] ) out[9] = p[0]; if( p[1] < out[7] ) out[7] = p[1]; else if( p[1] > out[10] ) out[10] = p[1]; if( p[2] < out[8] ) out[8] = p[2]; else if( p[2] > out[11] ) out[11] = p[2]; //recompute var min = out.subarray(6,9); var max = out.subarray(9,12); var center = vec3.add( out.subarray(0,3), min, max ); vec3.scale( center, center, 0.5); vec3.subtract( out.subarray(3,6), max, center ); out[12] = vec3.length( out.subarray(3,6) ); //radius return out; }, clampPoint: function(out, box, point) { out[0] = Math.clamp( point[0], box[0] - box[3], box[0] + box[3]); out[1] = Math.clamp( point[1], box[1] - box[4], box[1] + box[4]); out[2] = Math.clamp( point[2], box[2] - box[5], box[2] + box[5]); }, isPointInside: function( bbox, point ) { if( (bbox[0] - bbox[3]) > point[0] || (bbox[1] - bbox[4]) > point[1] || (bbox[2] - bbox[5]) > point[2] || (bbox[0] + bbox[3]) < point[0] || (bbox[1] + bbox[4]) < point[1] || (bbox[2] + bbox[5]) < point[2] ) return false; return true; }, getCenter: function(bb) { return bb.subarray(0,3); }, getHalfsize: function(bb) { return bb.subarray(3,6); }, getMin: function(bb) { return bb.subarray(6,9); }, getMax: function(bb) { return bb.subarray(9,12); }, getRadius: function(bb) { return bb[12]; } //setCenter,setHalfsize not coded, too much work to update all } global.distanceToPlane = GL.distanceToPlane = function distanceToPlane(plane, point) { return vec3.dot(plane,point) + plane[3]; } global.planeBoxOverlap = GL.planeBoxOverlap = function planeBoxOverlap(plane, box) { var n = plane; //.subarray(0,3); var d = plane[3]; //hack, to avoif GC I use indices directly var center = box; //.subarray(0,3); var halfsize = box; //.subarray(3,6); var radius = Math.abs( halfsize[3] * n[0] ) + Math.abs( halfsize[4] * n[1] ) + Math.abs( halfsize[5] * n[2] ); var distance = vec3.dot(n,center) + d; if (distance <= -radius) return CLIP_OUTSIDE; else if (distance <= radius) return CLIP_OVERLAP; return CLIP_INSIDE; } /** * @namespace GL */ /** * Octree generator for fast ray triangle collision with meshes * Dependencies: glmatrix.js (for vector and matrix operations) * @class Octree * @constructor * @param {Mesh} mesh object containing vertices buffer (indices buffer optional) */ global.Octree = GL.Octree = function Octree( mesh ) { this.root = null; this.total_depth = 0; this.total_nodes = 0; if(mesh) { this.buildFromMesh(mesh); this.total_nodes = this.trim(); } } Octree.MAX_NODE_TRIANGLES_RATIO = 0.1; Octree.MAX_OCTREE_DEPTH = 8; Octree.OCTREE_MARGIN_RATIO = 0.01; Octree.OCTREE_MIN_MARGIN = 0.1; var octree_tested_boxes = 0; var octree_tested_triangles = 0; Octree.prototype.buildFromMesh = function( mesh ) { this.total_depth = 0; this.total_nodes = 0; var vertices = mesh.getBuffer("vertices").data; var triangles = mesh.getIndexBuffer("triangles"); if(triangles) triangles = triangles.data; //get the internal data var root = this.computeAABB(vertices); this.root = root; this.total_nodes = 1; this.total_triangles = triangles ? triangles.length / 3 : vertices.length / 9; this.max_node_triangles = this.total_triangles * Octree.MAX_NODE_TRIANGLES_RATIO; var margin = vec3.create(); vec3.scale( margin, root.size, Octree.OCTREE_MARGIN_RATIO ); if(margin[0] < Octree.OCTREE_MIN_MARGIN) margin[0] = Octree.OCTREE_MIN_MARGIN; if(margin[1] < Octree.OCTREE_MIN_MARGIN) margin[1] = Octree.OCTREE_MIN_MARGIN; if(margin[2] < Octree.OCTREE_MIN_MARGIN) margin[2] = Octree.OCTREE_MIN_MARGIN; vec3.sub(root.min, root.min, margin); vec3.add(root.max, root.max, margin); root.faces = []; root.inside = 0; //indexed if(triangles) { for(var i = 0; i < triangles.length; i+=3) { var face = new Float32Array([vertices[triangles[i]*3], vertices[triangles[i]*3+1],vertices[triangles[i]*3+2], vertices[triangles[i+1]*3], vertices[triangles[i+1]*3+1],vertices[triangles[i+1]*3+2], vertices[triangles[i+2]*3], vertices[triangles[i+2]*3+1],vertices[triangles[i+2]*3+2],i/3]); this.addToNode( face,root,0); } } else { for(var i = 0; i < vertices.length; i+=9) { var face = new Float32Array( 10 ); face.set( vertices.subarray(i,i+9) ); face[9] = i/9; this.addToNode(face,root,0); } } return root; } Octree.prototype.addToNode = function( face, node, depth ) { node.inside += 1; //has children if(node.c) { var aabb = this.computeAABB(face); var added = false; for(var i in node.c) { var child = node.c[i]; if (Octree.isInsideAABB(aabb,child)) { this.addToNode(face,child, depth+1); added = true; break; } } if(!added) { if(node.faces == null) node.faces = []; node.faces.push(face); } } else //add till full, then split { if(node.faces == null) node.faces = []; node.faces.push(face); //split if(node.faces.length > this.max_node_triangles && depth < Octree.MAX_OCTREE_DEPTH) { this.splitNode(node); if(this.total_depth < depth + 1) this.total_depth = depth + 1; var faces = node.faces.concat(); node.faces = null; //redistribute all nodes for(var i in faces) { var face = faces[i]; var aabb = this.computeAABB(face); var added = false; for(var j in node.c) { var child = node.c[j]; if (Octree.isInsideAABB(aabb,child)) { this.addToNode(face,child, depth+1); added = true; break; } } if (!added) { if(node.faces == null) node.faces = []; node.faces.push(face); } } } } }; Octree.prototype.octree_pos_ref = [[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]]; Octree.prototype.splitNode = function(node) { node.c = []; var half = [(node.max[0] - node.min[0]) * 0.5, (node.max[1] - node.min[1]) * 0.5, (node.max[2] - node.min[2]) * 0.5]; for(var i in this.octree_pos_ref) { var ref = this.octree_pos_ref[i]; var newnode = {}; this.total_nodes += 1; newnode.min = [ node.min[0] + half[0] * ref[0], node.min[1] + half[1] * ref[1], node.min[2] + half[2] * ref[2]]; newnode.max = [newnode.min[0] + half[0], newnode.min[1] + half[1], newnode.min[2] + half[2]]; newnode.faces = null; newnode.inside = 0; node.c.push(newnode); } } Octree.prototype.computeAABB = function(vertices) { var min = new Float32Array([ vertices[0], vertices[1], vertices[2] ]); var max = new Float32Array([ vertices[0], vertices[1], vertices[2] ]); for(var i = 0; i < vertices.length; i+=3) { for(var j = 0; j < 3; j++) { if(min[j] > vertices[i+j]) min[j] = vertices[i+j]; if(max[j] < vertices[i+j]) max[j] = vertices[i+j]; } } return {min: min, max: max, size: vec3.sub( vec3.create(), max, min) }; } //remove empty nodes Octree.prototype.trim = function(node) { node = node || this.root; if(!node.c) return 1; var num = 1; var valid = []; var c = node.c; for(var i = 0; i < c.length; ++i) { if(c[i].inside) { valid.push(c[i]); num += this.trim(c[i]); } } node.c = valid; return num; } /** * Test collision between ray and triangles in the octree * @method testRay * @param {vec3} origin ray origin position * @param {vec3} direction ray direction position * @param {number} dist_min * @param {number} dist_max * @return {HitTest} object containing pos and normal */ Octree.prototype.testRay = (function(){ var origin_temp = vec3.create(); var direction_temp = vec3.create(); var min_temp = vec3.create(); var max_temp = vec3.create(); return function(origin, direction, dist_min, dist_max, test_backfaces ) { octree_tested_boxes = 0; octree_tested_triangles = 0; if(!this.root) { throw("Error: octree not build"); } origin_temp.set( origin ); direction_temp.set( direction ); min_temp.set( this.root.min ); max_temp.set( this.root.max ); var test = Octree.hitTestBox( origin_temp, direction_temp, min_temp, max_temp ); if(!test) //no collision with mesh bounding box return null; var test = Octree.testRayInNode( this.root, origin_temp, direction_temp, test_backfaces ); if(test != null) { var pos = vec3.scale( vec3.create(), direction, test.t ); vec3.add( pos, pos, origin ); test.pos = pos; return test; } return null; } })(); /** * test collision between sphere and the triangles in the octree (only test if there is any vertex inside the sphere) * @method testSphere * @param {vec3} origin sphere center * @param {number} radius * @return {Boolean} true if the sphere collided with the mesh */ Octree.prototype.testSphere = function( origin, radius ) { origin = vec3.clone(origin); octree_tested_boxes = 0; octree_tested_triangles = 0; if(!this.root) throw("Error: octree not build"); //better to use always the radius squared, because all the calculations are going to do that var rr = radius * radius; if( !Octree.testSphereBox( origin, rr, vec3.clone(this.root.min), vec3.clone(this.root.max) ) ) return false; //out of the box return Octree.testSphereInNode( this.root, origin, rr ); } //WARNING: cannot use static here, it uses recursion Octree.testRayInNode = function( node, origin, direction, test_backfaces ) { var test = null; var prev_test = null; octree_tested_boxes += 1; //test faces if(node.faces) for(var i = 0, l = node.faces.length; i < l; ++i) { var face = node.faces[i]; octree_tested_triangles += 1; test = Octree.hitTestTriangle( origin, direction, face.subarray(0,3) , face.subarray(3,6), face.subarray(6,9), test_backfaces ); if (test==null) continue; test.face = face; if(prev_test) prev_test.mergeWith( test ); else prev_test = test; } //WARNING: cannot use statics here, this function uses recursion var child_min = vec3.create(); var child_max = vec3.create(); //test children nodes faces var child; if(node.c) for(var i = 0; i < node.c.length; ++i) { child = node.c[i]; child_min.set( child.min ); child_max.set( child.max ); //test with node box test = Octree.hitTestBox( origin, direction, child_min, child_max ); if( test == null ) continue; //nodebox behind current collision, then ignore node if(prev_test && test.t > prev_test.t) continue; //test collision with node test = Octree.testRayInNode( child, origin, direction, test_backfaces ); if(test == null) continue; if(prev_test) prev_test.mergeWith( test ); else prev_test = test; } return prev_test; } //WARNING: cannot use static here, it uses recursion Octree.testSphereInNode = function( node, origin, radius2 ) { var test = null; var prev_test = null; octree_tested_boxes += 1; //test faces if(node.faces) for(var i = 0, l = node.faces.length; i < l; ++i) { var face = node.faces[i]; octree_tested_triangles += 1; if( Octree.testSphereTriangle( origin, radius2, face.subarray(0,3) , face.subarray(3,6), face.subarray(6,9) ) ) return true; } //WARNING: cannot use statics here, this function uses recursion var child_min = vec3.create(); var child_max = vec3.create(); //test children nodes faces var child; if(node.c) for(var i = 0; i < node.c.length; ++i) { child = node.c[i]; child_min.set( child.min ); child_max.set( child.max ); //test with node box if( !Octree.testSphereBox( origin, radius2, child_min, child_max ) ) continue; //test collision with node content if( Octree.testSphereInNode( child, origin, radius2 ) ) return true; } return false; } //test if one bounding is inside or overlapping another bounding Octree.isInsideAABB = function(a,b) { if(a.min[0] < b.min[0] || a.min[1] < b.min[1] || a.min[2] < b.min[2] || a.max[0] > b.max[0] || a.max[1] > b.max[1] || a.max[2] > b.max[2]) return false; return true; } Octree.hitTestBox = (function(){ var tMin = vec3.create(); var tMax = vec3.create(); var inv = vec3.create(); var t1 = vec3.create(); var t2 = vec3.create(); var tmp = vec3.create(); var epsilon = 1.0e-6; var eps = vec3.fromValues( epsilon,epsilon,epsilon ); return function( origin, ray, box_min, box_max ) { vec3.subtract( tMin, box_min, origin ); vec3.subtract( tMax, box_max, origin ); if( vec3.maxValue(tMin) < 0 && vec3.minValue(tMax) > 0) return new HitTest(0,origin,ray); inv[0] = 1/ray[0]; inv[1] = 1/ray[1]; inv[2] = 1/ray[2]; vec3.multiply(tMin, tMin, inv); vec3.multiply(tMax, tMax, inv); vec3.min(t1, tMin, tMax); vec3.max(t2, tMin, tMax); var tNear = vec3.maxValue(t1); var tFar = vec3.minValue(t2); if (tNear > 0 && tNear < tFar) { var hit = vec3.add( vec3.create(), vec3.scale(tmp, ray, tNear ), origin); vec3.add( box_min, box_min, eps); vec3.subtract(box_min, box_min, eps); return new HitTest(tNear, hit, vec3.fromValues( (hit[0] > box_max[0]) - (hit[0] < box_min[0]), (hit[1] > box_max[1]) - (hit[1] < box_min[1]), (hit[2] > box_max[2]) - (hit[2] < box_min[2]) )); } return null; } })(); Octree.hitTestTriangle = (function(){ var AB = vec3.create(); var AC = vec3.create(); var toHit = vec3.create(); var tmp = vec3.create(); return function( origin, ray, A, B, C, test_backfaces ) { vec3.subtract( AB, B, A ); vec3.subtract( AC, C, A ); var normal = vec3.cross( vec3.create(), AB, AC ); //returned vec3.normalize( normal, normal ); if( !test_backfaces && vec3.dot(normal,ray) > 0) return null; //ignore backface var t = vec3.dot(normal, vec3.subtract( tmp, A, origin )) / vec3.dot(normal,ray); if (t > 0) { var hit = vec3.scale(vec3.create(), ray, t); //returned vec3.add(hit, hit, origin); vec3.subtract( toHit, hit, A ); var dot00 = vec3.dot(AC,AC); var dot01 = vec3.dot(AC,AB); var dot02 = vec3.dot(AC,toHit); var dot11 = vec3.dot(AB,AB); var dot12 = vec3.dot(AB,toHit); var divide = dot00 * dot11 - dot01 * dot01; var u = (dot11 * dot02 - dot01 * dot12) / divide; var v = (dot00 * dot12 - dot01 * dot02) / divide; if (u >= 0 && v >= 0 && u + v <= 1) return new HitTest(t, hit, normal); } return null; }; })(); //from http://realtimecollisiondetection.net/blog/?p=103 //radius must be squared Octree.testSphereTriangle = (function(){ var A = vec3.create(); var B = vec3.create(); var C = vec3.create(); var AB = vec3.create(); var AC = vec3.create(); var BC = vec3.create(); var CA = vec3.create(); var V = vec3.create(); return function( P, rr, A_, B_, C_ ) { vec3.sub( A, A_, P ); vec3.sub( B, B_, P ); vec3.sub( C, C_, P ); vec3.sub( AB, B, A ); vec3.sub( AC, C, A ); vec3.cross( V, AB, AC ); var d = vec3.dot( A, V ); var e = vec3.dot( V, V ); var sep1 = d * d > rr * e; var aa = vec3.dot(A, A); var ab = vec3.dot(A, B); var ac = vec3.dot(A, C); var bb = vec3.dot(B, B); var bc = vec3.dot(B, C); var cc = vec3.dot(C, C); var sep2 = (aa > rr) & (ab > aa) & (ac > aa); var sep3 = (bb > rr) & (ab > bb) & (bc > bb); var sep4 = (cc > rr) & (ac > cc) & (bc > cc); var d1 = ab - aa; var d2 = bc - bb; var d3 = ac - cc; vec3.sub( BC, C, B ); vec3.sub( CA, A, C ); var e1 = vec3.dot(AB, AB); var e2 = vec3.dot(BC, BC); var e3 = vec3.dot(CA, CA); var Q1 = vec3.scale(vec3.create(), A, e1); vec3.sub( Q1, Q1, vec3.scale(vec3.create(), AB, d1) ); var Q2 = vec3.scale(vec3.create(), B, e2); vec3.sub( Q2, Q2, vec3.scale(vec3.create(), BC, d2) ); var Q3 = vec3.scale(vec3.create(), C, e3); vec3.sub( Q3, Q3, vec3.scale(vec3.create(), CA, d3) ); var QC = vec3.scale( vec3.create(), C, e1 ); QC = vec3.sub( QC, QC, Q1 ); var QA = vec3.scale( vec3.create(), A, e2 ); QA = vec3.sub( QA, QA, Q2 ); var QB = vec3.scale( vec3.create(), B, e3 ); QB = vec3.sub( QB, QB, Q3 ); var sep5 = ( vec3.dot(Q1, Q1) > rr * e1 * e1) & (vec3.dot(Q1, QC) > 0 ); var sep6 = ( vec3.dot(Q2, Q2) > rr * e2 * e2) & (vec3.dot(Q2, QA) > 0 ); var sep7 = ( vec3.dot(Q3, Q3) > rr * e3 * e3) & (vec3.dot(Q3, QB) > 0 ); var separated = sep1 | sep2 | sep3 | sep4 | sep5 | sep6 | sep7 return !separated; }; })(); Octree.testSphereBox = function( center, radius2, box_min, box_max ) { // arvo's algorithm from gamasutra // http://www.gamasutra.com/features/19991018/Gomez_4.htm var s, d = 0.0; //find the square of the distance //from the sphere to the box for(var i = 0; i < 3; ++i) { if( center[i] < box_min[i] ) { s = center[i] - box_min[i]; d += s*s; } else if( center[i] > box_max[i] ) { s = center[i] - box_max[i]; d += s*s; } } //return d <= r*r if (d <= radius2) { return true; /* // this is used just to know if it overlaps or is just inside, but I dont care // make an aabb aabb test with the sphere aabb to test inside state var halfsize = vec3.fromValues( radius, radius, radius ); var sphere_bbox = BBox.fromCenterHalfsize( center, halfsize ); if ( geo.testBBoxBBox(bbox, sphere_bbox) ) return INSIDE; return OVERLAP; */ } return false; //OUTSIDE; }; // Provides a convenient raytracing interface. // ### new GL.HitTest([t, hit, normal]) // // This is the object used to return hit test results. If there are no // arguments, the constructed argument represents a hit infinitely far // away. global.HitTest = GL.HitTest = function HitTest(t, hit, normal) { this.t = arguments.length ? t : Number.MAX_VALUE; this.hit = hit; this.normal = normal; this.face = null; } // ### .mergeWith(other) // // Changes this object to be the closer of the two hit test results. HitTest.prototype = { mergeWith: function(other) { if (other.t > 0 && other.t < this.t) { this.t = other.t; this.hit = other.hit; this.normal = other.normal; this.face = other.face; } } }; // ### new GL.Ray( origin, direction ) global.Ray = GL.Ray = function Ray( origin, direction ) { this.origin = vec3.create(); this.direction = vec3.create(); this.collision_point = vec3.create(); if(origin) this.origin.set( origin ); if(direction) this.direction.set( direction ); } Ray.prototype.testPlane = function( P, N ) { return geo.testRayPlane( this.origin, this.direction, P, N, this.collision_point ); } Ray.prototype.testSphere = function( center, radius, max_dist ) { return geo.testRaySphere( this.origin, this.direction, center, radius, this.collision_point, max_dist ); } // ### new GL.Raytracer() // // This will read the current modelview matrix, projection matrix, and viewport, // reconstruct the eye position, and store enough information to later generate // per-pixel rays using `getRayForPixel()`. // // Example usage: // // var tracer = new GL.Raytracer(); // var ray = tracer.getRayForPixel( // gl.canvas.width / 2, // gl.canvas.height / 2); // var result = GL.Raytracer.hitTestSphere( // tracer.eye, ray, new GL.Vector(0, 0, 0), 1); global.Raytracer = GL.Raytracer = function Raytracer( viewprojection_matrix, viewport ) { this.viewport = vec4.create(); this.ray00 = vec3.create(); this.ray10 = vec3.create(); this.ray01 = vec3.create(); this.ray11 = vec3.create(); this.eye = vec3.create(); this.setup( viewprojection_matrix, viewport ); } Raytracer.prototype.setup = function( viewprojection_matrix, viewport ) { viewport = viewport || gl.viewport_data; this.viewport.set( viewport ); var minX = viewport[0], maxX = minX + viewport[2]; var minY = viewport[1], maxY = minY + viewport[3]; vec3.set( this.ray00, minX, minY, 1 ); vec3.set( this.ray10, maxX, minY, 1 ); vec3.set( this.ray01, minX, maxY, 1 ); vec3.set( this.ray11, maxX, maxY, 1 ); vec3.unproject( this.ray00, this.ray00, viewprojection_matrix, viewport); vec3.unproject( this.ray10, this.ray10, viewprojection_matrix, viewport); vec3.unproject( this.ray01, this.ray01, viewprojection_matrix, viewport); vec3.unproject( this.ray11, this.ray11, viewprojection_matrix, viewport); var eye = this.eye; vec3.unproject(eye, eye, viewprojection_matrix, viewport); vec3.subtract(this.ray00, this.ray00, eye); vec3.subtract(this.ray10, this.ray10, eye); vec3.subtract(this.ray01, this.ray01, eye); vec3.subtract(this.ray11, this.ray11, eye); } // ### .getRayForPixel(x, y) // // Returns the ray direction originating from the camera and traveling through the pixel `x, y`. Raytracer.prototype.getRayForPixel = (function(){ var ray0 = vec3.create(); var ray1 = vec3.create(); return function(x, y, out) { out = out || vec3.create(); x = (x - this.viewport[0]) / this.viewport[2]; y = 1 - (y - this.viewport[1]) / this.viewport[3]; vec3.lerp(ray0, this.ray00, this.ray10, x); vec3.lerp(ray1, this.ray01, this.ray11, x); vec3.lerp( out, ray0, ray1, y) return vec3.normalize( out, out ); } })(); // ### GL.Raytracer.hitTestBox(origin, ray, min, max) // // Traces the ray starting from `origin` along `ray` against the axis-aligned box // whose coordinates extend from `min` to `max`. Returns a `HitTest` with the // information or `null` for no intersection. // // This implementation uses the [slab intersection method](http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtinter3.htm). var _hittest_inv = mat4.create(); Raytracer.hitTestBox = function(origin, ray, min, max, model) { var _hittest_v3 = new Float32Array(10*3); //reuse memory to speedup if(model) { var inv = mat4.invert( _hittest_inv, model ); origin = mat4.multiplyVec3( _hittest_v3.subarray(3,6), inv, origin ); ray = mat4.rotateVec3( _hittest_v3.subarray(6,9), inv, ray ); } var tMin = vec3.subtract( _hittest_v3.subarray(9,12), min, origin ); vec3.divide( tMin, tMin, ray ); var tMax = vec3.subtract( _hittest_v3.subarray(12,15), max, origin ); vec3.divide( tMax, tMax, ray ); var t1 = vec3.min( _hittest_v3.subarray(15,18), tMin, tMax); var t2 = vec3.max( _hittest_v3.subarray(18,21), tMin, tMax); var tNear = vec3.maxValue(t1); var tFar = vec3.minValue(t2); if (tNear > 0 && tNear <= tFar) { var epsilon = 1.0e-6; var hit = vec3.scale( _hittest_v3.subarray(21,24), ray, tNear); vec3.add( hit, origin, hit ); vec3.addValue(_hittest_v3.subarray(24,27), min, epsilon); vec3.subValue(_hittest_v3.subarray(27,30), max, epsilon); return new HitTest(tNear, hit, vec3.fromValues( (hit[0] > max[0]) - (hit[0] < min[0]), (hit[1] > max[1]) - (hit[1] < min[1]), (hit[2] > max[2]) - (hit[2] < min[2]) )); } return null; }; // ### GL.Raytracer.hitTestSphere(origin, ray, center, radius) // // Traces the ray starting from `origin` along `ray` against the sphere defined // by `center` and `radius`. Returns a `HitTest` with the information or `null` // for no intersection. Raytracer.hitTestSphere = function(origin, ray, center, radius) { var offset = vec3.subtract( vec3.create(), origin,center); var a = vec3.dot(ray,ray); var b = 2 * vec3.dot(ray,offset); var c = vec3.dot(offset,offset) - radius * radius; var discriminant = b * b - 4 * a * c; if (discriminant > 0) { var t = (-b - Math.sqrt(discriminant)) / (2 * a), hit = vec3.add(vec3.create(),origin, vec3.scale(vec3.create(), ray, t)); return new HitTest(t, hit, vec3.scale( vec3.create(), vec3.subtract(vec3.create(), hit,center), 1.0/radius)); } return null; }; // ### GL.Raytracer.hitTestTriangle(origin, ray, a, b, c) // // Traces the ray starting from `origin` along `ray` against the triangle defined // by the points `a`, `b`, and `c`. Returns a `HitTest` with the information or // `null` for no intersection. Raytracer.hitTestTriangle = function(origin, ray, a, b, c) { var ab = vec3.subtract(vec3.create(), b,a ); var ac = vec3.subtract(vec3.create(), c,a ); var normal = vec3.cross( vec3.create(), ab,ac); vec3.normalize( normal, normal ); var t = vec3.dot(normal, vec3.subtract( vec3.create(), a,origin)) / vec3.dot(normal,ray); if (t > 0) { var hit = vec3.add( vec3.create(), origin, vec3.scale(vec3.create(), ray,t)); var toHit = vec3.subtract( vec3.create(), hit, a); var dot00 = vec3.dot(ac,ac); var dot01 = vec3.dot(ac,ab); var dot02 = vec3.dot(ac,toHit); var dot11 = vec3.dot(ab,ab); var dot12 = vec3.dot(ab,toHit); var divide = dot00 * dot11 - dot01 * dot01; var u = (dot11 * dot02 - dot01 * dot12) / divide; var v = (dot00 * dot12 - dot01 * dot02) / divide; if (u >= 0 && v >= 0 && u + v <= 1) return new HitTest(t, hit, normal); } return null; }; //***** OBJ parser adapted from SpiderGL implementation ***************** /** * Parses a OBJ string and returns an object with the info ready to be passed to GL.Mesh.load * @method Mesh.parseOBJ * @param {String} data all the OBJ info to be parsed * @param {Object} options * @return {Object} mesh information (vertices, coords, normals, indices) */ Mesh.parseOBJ = function( text, options ) { options = options || {}; //final arrays (packed, lineal [ax,ay,az, bx,by,bz ...]) var positionsArray = [ ]; var texcoordsArray = [ ]; var normalsArray = [ ]; var indicesArray = [ ]; //unique arrays (not packed, lineal) var positions = [ ]; var texcoords = [ ]; var normals = [ ]; var facemap = { }; var index = 0; var line = null; var f = null; var pos = 0; var tex = 0; var nor = 0; var x = 0.0; var y = 0.0; var z = 0.0; var tokens = null; var hasPos = false; var hasTex = false; var hasNor = false; var parsingFaces = false; var indices_offset = 0; var negative_offset = -1; //used for weird objs with negative indices var max_index = 0; var skip_indices = options.noindex ? options.noindex : (text.length > 10000000 ? true : false); //trace("SKIP INDICES: " + skip_indices); var flip_axis = options.flipAxis; var flip_normals = (flip_axis || options.flipNormals); //used for mesh groups (submeshes) var group = null; var groups = []; var materials_found = {}; var V_CODE = 1; var VT_CODE = 2; var VN_CODE = 3; var F_CODE = 4; var G_CODE = 5; var O_CODE = 6; var codes = { v: V_CODE, vt: VT_CODE, vn: VN_CODE, f: F_CODE, g: G_CODE, o: O_CODE }; var lines = text.split("\n"); var length = lines.length; for (var lineIndex = 0; lineIndex < length; ++lineIndex) { line = lines[lineIndex].replace(/[ \t]+/g, " ").replace(/\s\s*$/, ""); //trim if (line[0] == "#") continue; if(line == "") continue; tokens = line.split(" "); var code = codes[ tokens[0] ]; if(parsingFaces && code == V_CODE) //another mesh? { indices_offset = index; parsingFaces = false; //trace("multiple meshes: " + indices_offset); } //read and parse numbers if( code <= VN_CODE ) //v,vt,vn { x = parseFloat(tokens[1]); y = parseFloat(tokens[2]); if( code != VT_CODE ) { if(tokens[3] == '\\') //super weird case, OBJ allows to break lines with slashes... { //HACK! only works if the var is the thirth position... ++lineIndex; line = lines[lineIndex].replace(/[ \t]+/g, " ").replace(/\s\s*$/, ""); //better than trim z = parseFloat(line); } else z = parseFloat(tokens[3]); } } if (code == V_CODE) { if(flip_axis) //maya and max notation style positions.push(-1*x,z,y); else positions.push(x,y,z); } else if (code == VT_CODE) { texcoords.push(x,y); } else if (code == VN_CODE) { if(flip_normals) //maya and max notation style normals.push(-y,-z,x); else normals.push(x,y,z); } else if (code == F_CODE) { parsingFaces = true; if (tokens.length < 4) continue; //faces with less that 3 vertices? nevermind //for every corner of this polygon var polygon_indices = []; for (var i=1; i < tokens.length; ++i) { if (!(tokens[i] in facemap) || skip_indices) { f = tokens[i].split("/"); if (f.length == 1) { //unpacked pos = parseInt(f[0]) - 1; tex = pos; nor = pos; } else if (f.length == 2) { //no normals pos = parseInt(f[0]) - 1; tex = parseInt(f[1]) - 1; nor = -1; } else if (f.length == 3) { //all three indexed pos = parseInt(f[0]) - 1; tex = parseInt(f[1]) - 1; nor = parseInt(f[2]) - 1; } else { console.err("Problem parsing: unknown number of values per face"); return false; } if(i > 3 && skip_indices) //break polygon in triangles { //first var pl = positionsArray.length; positionsArray.push( positionsArray[pl - (i-3)*9], positionsArray[pl - (i-3)*9 + 1], positionsArray[pl - (i-3)*9 + 2]); positionsArray.push( positionsArray[pl - 3], positionsArray[pl - 2], positionsArray[pl - 1]); pl = texcoordsArray.length; texcoordsArray.push( texcoordsArray[pl - (i-3)*6], texcoordsArray[pl - (i-3)*6 + 1]); texcoordsArray.push( texcoordsArray[pl - 2], texcoordsArray[pl - 1]); pl = normalsArray.length; normalsArray.push( normalsArray[pl - (i-3)*9], normalsArray[pl - (i-3)*9 + 1], normalsArray[pl - (i-3)*9 + 2]); normalsArray.push( normalsArray[pl - 3], normalsArray[pl - 2], normalsArray[pl - 1]); } //add new vertex x = 0.0; y = 0.0; z = 0.0; if ((pos * 3 + 2) < positions.length) { hasPos = true; x = positions[pos*3+0]; y = positions[pos*3+1]; z = positions[pos*3+2]; } positionsArray.push(x,y,z); //add new texture coordinate x = 0.0; y = 0.0; if ((tex * 2 + 1) < texcoords.length) { hasTex = true; x = texcoords[tex*2+0]; y = texcoords[tex*2+1]; } texcoordsArray.push(x,y); //add new normal x = 0.0; y = 0.0; z = 1.0; if(nor != -1) { if ((nor * 3 + 2) < normals.length) { hasNor = true; x = normals[nor*3+0]; y = normals[nor*3+1]; z = normals[nor*3+2]; } normalsArray.push(x,y,z); } //Save the string "10/10/10" and tells which index represents it in the arrays if(!skip_indices) facemap[tokens[i]] = index++; }//end of 'if this token is new (store and index for later reuse)' //store key for this triplet if(!skip_indices) { var final_index = facemap[tokens[i]]; polygon_indices.push(final_index); if(max_index < final_index) max_index = final_index; } } //end of for every token on a 'f' line //polygons (not just triangles) if(!skip_indices) { for(var iP = 2; iP < polygon_indices.length; iP++) { indicesArray.push( polygon_indices[0], polygon_indices[iP-1], polygon_indices[iP] ); //indicesArray.push( [polygon_indices[0], polygon_indices[iP-1], polygon_indices[iP]] ); } } } else if (code == G_CODE) { //tokens[0] == "usemtl" negative_offset = positions.length / 3 - 1; if(tokens.length > 1) { if(group != null) { group.length = indicesArray.length - group.start; if(group.length > 0) groups.push(group); } group = { name: tokens[1], start: indicesArray.length, length: -1, material: "" }; } } else if (tokens[0] == "usemtl") { if(group) group.material = tokens[1]; } } if(!positions.length) { console.error("OBJ doesnt have vertices, maybe the file is not a OBJ"); return null; } if(group && (indicesArray.length - group.start) > 1) { group.length = indicesArray.length - group.start; groups.push(group); } //deindex streams if((max_index > 256*256 || skip_indices ) && indicesArray.length > 0) { console.log("Deindexing mesh...") var finalVertices = new Float32Array(indicesArray.length * 3); var finalNormals = normalsArray && normalsArray.length ? new Float32Array(indicesArray.length * 3) : null; var finalTexCoords = texcoordsArray && texcoordsArray.length ? new Float32Array(indicesArray.length * 2) : null; for(var i = 0; i < indicesArray.length; i += 1) { finalVertices.set( positionsArray.slice( indicesArray[i]*3,indicesArray[i]*3 + 3), i*3 ); if(finalNormals) finalNormals.set( normalsArray.slice( indicesArray[i]*3,indicesArray[i]*3 + 3 ), i*3 ); if(finalTexCoords) finalTexCoords.set( texcoordsArray.slice(indicesArray[i]*2,indicesArray[i]*2 + 2 ), i*2 ); } positionsArray = finalVertices; if(finalNormals) normalsArray = finalNormals; if(finalTexCoords) texcoordsArray = finalTexCoords; indicesArray = null; } //Create final mesh object var mesh = {}; //create typed arrays if (hasPos) mesh.vertices = new Float32Array(positionsArray); if (hasNor && normalsArray.length > 0) mesh.normals = new Float32Array(normalsArray); if (hasTex && texcoordsArray.length > 0) mesh.coords = new Float32Array(texcoordsArray); if (indicesArray && indicesArray.length > 0) mesh.triangles = new Uint16Array(indicesArray); var info = {}; if(groups.length > 1) info.groups = groups; mesh.info = info; if(options.only_data) return mesh; //creates and returns a GL.Mesh var final_mesh = null; final_mesh = Mesh.load( mesh, null, options.mesh ); final_mesh.updateBoundingBox(); return final_mesh; } Mesh.parsers["obj"] = Mesh.parseOBJ; Mesh.encoders["obj"] = function( mesh, options ) { //store vertices var verticesBuffer = mesh.getBuffer("vertices"); if(!verticesBuffer) return null; var lines = []; lines.push("# Generated with liteGL.js by Javi Agenjo\n"); var vertices = verticesBuffer.data; for (var i = 0; i < vertices.length; i+=3) lines.push("v " + vertices[i].toFixed(4) + " " + vertices[i+1].toFixed(4) + " " + vertices[i+2].toFixed(4)); //store normals var normalsBuffer = mesh.getBuffer("normals"); if(normalsBuffer) { lines.push(""); var normals = normalsBuffer.data; for (var i = 0; i < normals.length; i+=3) lines.push("vn " + normals[i].toFixed(4) + " " + normals[i+1].toFixed(4) + " " + normals[i+2].toFixed(4) ); } //store uvs var coordsBuffer = mesh.getBuffer("coords"); if(coordsBuffer) { lines.push(""); var coords = coordsBuffer.data; for (var i = 0; i < coords.length; i+=2) lines.push("vt " + coords[i].toFixed(4) + " " + coords[i+1].toFixed(4) + " " + " 0.0000"); } var groups = mesh.info.groups; //store faces var indicesBuffer = mesh.getIndexBuffer("triangles"); if(indicesBuffer) { var indices = indicesBuffer.data; if(!groups || !groups.length) groups = [{start:0, length: indices.length, name:"mesh"}]; for(var j = 0; j < groups.length; ++j) { var group = groups[j]; lines.push("g " + group.name ); lines.push("usemtl " + (group.material || ("mat_"+j))); var start = group.start; var end = start + group.length; for (var i = start; i < end; i+=3) lines.push("f " + (indices[i]+1) + "/" + (indices[i]+1) + "/" + (indices[i]+1) + " " + (indices[i+1]+1) + "/" + (indices[i+1]+1) + "/" + (indices[i+1]+1) + " " + (indices[i+2]+1) + "/" + (indices[i+2]+1) + "/" + (indices[i+2]+1) ); } } else //no indices { if(!groups || !groups.length) groups = [{start:0, length: (vertices.length / 3), name:"mesh"}]; for(var j = 0; j < groups.length; ++j) { var group = groups[j]; lines.push("g " + group.name); lines.push("usemtl " + (group.material || ("mat_"+j))); var start = group.start; var end = start + group.length; for (var i = start; i < end; i+=3) lines.push( "f " + (i+1) + "/" + (i+1) + "/" + (i+1) + " " + (i+2) + "/" + (i+2) + "/" + (i+2) + " " + (i+3) + "/" + (i+3) + "/" + (i+3) ); } } return lines.join("\n"); } //simple format to output meshes in ASCII Mesh.parsers["mesh"] = function( text, options ) { var mesh = {}; var lines = text.split("\n"); for(var i = 0; i < lines.length; ++i) { var line = lines[i]; var type = line[0]; var t = line.substr(1).split(","); var name = t[0]; if(type == "-") //buffer { var data = new Float32Array( Number(t[1]) ); for(var j = 0; j < data.length; ++j) data[j] = Number(t[j+2]); mesh[name] = data; } else if(type == "*") //index { var data = Number(t[1]) > 256*256 ? new Uint32Array( Number(t[1]) ) : new Uint16Array( Number(t[1]) ); for(var j = 0; j < data.length; ++j) data[j] = Number(t[j+2]); mesh[name] = data; } else if(type == "@") //info { if(name == "bones") { var bones = []; var num_bones = Number(t[1]); for(var j = 0; j < num_bones; ++j) { var m = (t.slice(3 + j*17, 3 + (j+1)*17 - 1)).map(Number); bones.push( [ t[2 + j*17], m ] ); } mesh.bones = bones; } else if(name == "bind_matrix") mesh.bind_matrix = t.slice(1,17).map(Number); else if(name == "groups") { mesh.info = { groups: [] }; var num_groups = Number(t[1]); for(var j = 0; j < num_groups; ++j) { var group = { name: t[2+j*4], material: t[2+j*4+1], start: Number(t[2+j*4+2]), length: Number(t[2+j*4+3]) }; mesh.info.groups.push(group); } } } else console.warn("type unknown: " + t[0] ); } if(options.only_data) return mesh; //creates and returns a GL.Mesh var final_mesh = null; final_mesh = Mesh.load( mesh, null, options.mesh ); final_mesh.updateBoundingBox(); return final_mesh; } Mesh.encoders["mesh"] = function( mesh, options ) { var lines = []; for(var i in mesh.vertexBuffers ) { var buffer = mesh.vertexBuffers[i]; var line = ["-"+i, buffer.data.length, buffer.data, typedArrayToArray( buffer.data ) ]; lines.push(line.join(",")); } for(var i in mesh.indexBuffers ) { var buffer = mesh.indexBuffers[i]; var line = [ "*" + i, buffer.data.length, buffer.data, typedArrayToArray( buffer.data ) ]; lines.push(line.join(",")); } if(mesh.bounding) lines.push( ["@bounding", typedArrayToArray(mesh.bounding.subarray(0,6))].join(",") ); if(mesh.info && mesh.info.groups) { var groups_info = []; for(var j = 0; j < mesh.info.groups.length; ++j) { var group = mesh.info.groups[j]; groups_info.push( group.name, group.material, group.start, group.length ); } lines.push( ["@groups", mesh.info.groups.length ].concat( groups_info ).join(",") ); } if(mesh.bones) lines.push( ["@bones", mesh.bones.length, mesh.bones.flat()].join(",") ); if(mesh.bind_matrix) lines.push( ["@bind_matrix", typedArrayToArray(mesh.bind_matrix) ].join(",") ); return lines.join("\n"); } /* BINARY FORMAT ************************************/ if(global.WBin) global.WBin.classes["Mesh"] = Mesh; Mesh.binary_file_formats["wbin"] = true; Mesh.parsers["wbin"] = Mesh.fromBinary = function( data_array, options ) { if(!global.WBin) throw("To use binary meshes you need to install WBin.js from https://github.com/jagenjo/litescene.js/blob/master/src/utils/wbin.js "); options = options || {}; var o = null; if( data_array.constructor == ArrayBuffer ) o = WBin.load( data_array, true ); else o = data_array; if(!o.info) console.warn("This WBin doesn't seem to contain a mesh. Classname: ", o["@classname"] ); if( o.format ) GL.Mesh.decompress( o ); var vertex_buffers = {}; if(o.vertex_buffers) { for(var i in o.vertex_buffers) vertex_buffers[ o.vertex_buffers[i] ] = o[ o.vertex_buffers[i] ]; } else { if(o.vertices) vertex_buffers.vertices = o.vertices; if(o.normals) vertex_buffers.normals = o.normals; if(o.coords) vertex_buffers.coords = o.coords; if(o.weights) vertex_buffers.weights = o.weights; if(o.bone_indices) vertex_buffers.bone_indices = o.bone_indices; } var index_buffers = {}; if( o.index_buffers ) { for(var i in o.index_buffers) index_buffers[ o.index_buffers[i] ] = o[ o.index_buffers[i] ]; } else { if(o.triangles) index_buffers.triangles = o.triangles; if(o.wireframe) index_buffers.wireframe = o.wireframe; } var mesh = { vertex_buffers: vertex_buffers, index_buffers: index_buffers, bounding: o.bounding, info: o.info }; if(o.bones) { mesh.bones = o.bones; //restore Float32array for(var i = 0; i < mesh.bones.length; ++i) mesh.bones[i][1] = mat4.clone(mesh.bones[i][1]); if(o.bind_matrix) mesh.bind_matrix = mat4.clone( o.bind_matrix ); } if(o.morph_targets) mesh.morph_targets = o.morph_targets; if(options.only_data) return mesh; //build mesh object var final_mesh = options.mesh || new GL.Mesh(); final_mesh.configure( mesh ); return final_mesh; } Mesh.encoders["wbin"] = function( mesh, options ) { return mesh.toBinary( options ); } Mesh.prototype.toBinary = function( options ) { if(!global.WBin) throw("to use Mesh.toBinary you need to have WBin included. Check the repository for wbin.js"); if(!this.info) this.info = {}; //clean data var o = { object_class: "Mesh", info: this.info, groups: this.groups }; if(this.bones) { var bones = []; //convert to array for(var i = 0; i < this.bones.length; ++i) bones.push([ this.bones[i][0], mat4.toArray( this.bones[i][1] ) ]); o.bones = bones; if(this.bind_matrix) o.bind_matrix = this.bind_matrix; } //bounding box if(!this.bounding) this.updateBoundingBox(); o.bounding = this.bounding; var vertex_buffers = []; var index_buffers = []; for(var i in this.vertexBuffers) { var stream = this.vertexBuffers[i]; o[ stream.name ] = stream.data; vertex_buffers.push( stream.name ); if(stream.name == "vertices") o.info.num_vertices = stream.data.length / 3; } for(var i in this.indexBuffers) { var stream = this.indexBuffers[i]; o[i] = stream.data; index_buffers.push( i ); } o.vertex_buffers = vertex_buffers; o.index_buffers = index_buffers; //compress wbin using the bounding if( GL.Mesh.enable_wbin_compression ) //apply compression GL.Mesh.compress( o ); //create pack file var bin = WBin.create( o, "Mesh" ); return bin; } Mesh.compress = function( o, format ) { format = format || "bounding_compressed"; o.format = { type: format }; var func = Mesh.compressors[ format ]; if(!func) throw("compression format not supported:" + format ); return func( o ); } Mesh.decompress = function( o ) { if(!o.format) return; var func = Mesh.decompressors[ o.format.type ]; if(!func) throw("decompression format not supported:" + o.format.type ); return func( o ); } Mesh.compressors["bounding_compressed"] = function(o) { if(!o.vertex_buffers) throw("buffers not found"); var min = BBox.getMin( o.bounding ); var max = BBox.getMax( o.bounding ); var range = vec3.sub( vec3.create(), max, min ); var vertices = o.vertices; var new_vertices = new Uint16Array( vertices.length ); for(var i = 0; i < vertices.length; i+=3) { new_vertices[i] = ((vertices[i] - min[0]) / range[0]) * 65535; new_vertices[i+1] = ((vertices[i+1] - min[1]) / range[1]) * 65535; new_vertices[i+2] = ((vertices[i+2] - min[2]) / range[2]) * 65535; } o.vertices = new_vertices; if( o.normals ) { var normals = o.normals; var new_normals = new Uint8Array( normals.length ); var normals_range = new_normals.constructor == Uint8Array ? 255 : 65535; for(var i = 0; i < normals.length; i+=3) { new_normals[i] = (normals[i] * 0.5 + 0.5) * normals_range; new_normals[i+1] = (normals[i+1] * 0.5 + 0.5) * normals_range; new_normals[i+2] = (normals[i+2] * 0.5 + 0.5) * normals_range; } o.normals = new_normals; } if( o.coords ) { //compute uv bounding: [minu,minv,maxu,maxv] var coords = o.coords; var uvs_bounding = [10000,10000,-10000,-10000]; for(var i = 0; i < coords.length; i+=2) { var u = coords[i]; if( uvs_bounding[0] > u ) uvs_bounding[0] = u; else if( uvs_bounding[2] < u ) uvs_bounding[2] = u; var v = coords[i+1]; if( uvs_bounding[1] > v ) uvs_bounding[1] = v; else if( uvs_bounding[3] < v ) uvs_bounding[3] = v; } o.format.uvs_bounding = uvs_bounding; var new_coords = new Uint16Array( coords.length ); var range = [ uvs_bounding[2] - uvs_bounding[0], uvs_bounding[3] - uvs_bounding[1] ]; for(var i = 0; i < coords.length; i+=2) { new_coords[i] = ((coords[i] - uvs_bounding[0]) / range[0]) * 65535; new_coords[i+1] = ((coords[i+1] - uvs_bounding[1]) / range[1]) * 65535; } o.coords = new_coords; } if( o.weights ) { var weights = o.weights; var new_weights = new Uint16Array( weights.length ); //using only one byte distorts the meshes a lot var weights_range = new_weights.constructor == Uint8Array ? 255 : 65535; for(var i = 0; i < weights.length; i+=4) { new_weights[i] = weights[i] * weights_range; new_weights[i+1] = weights[i+1] * weights_range; new_weights[i+2] = weights[i+2] * weights_range; new_weights[i+3] = weights[i+3] * weights_range; } o.weights = new_weights; } } Mesh.decompressors["bounding_compressed"] = function(o) { var bounding = o.bounding; if(!bounding) throw("error in mesh decompressing data: bounding not found, cannot use the bounding decompression."); var min = BBox.getMin( bounding ); var max = BBox.getMax( bounding ); var range = vec3.sub( vec3.create(), max, min ); var format = o.format; var inv8 = 1 / 255; var inv16 = 1 / 65535; var vertices = o.vertices; var new_vertices = new Float32Array( vertices.length ); for( var i = 0, l = vertices.length; i < l; i += 3 ) { new_vertices[i] = ((vertices[i] * inv16) * range[0]) + min[0]; new_vertices[i+1] = ((vertices[i+1] * inv16) * range[1]) + min[1]; new_vertices[i+2] = ((vertices[i+2] * inv16) * range[2]) + min[2]; } o.vertices = new_vertices; if( o.normals && o.normals.constructor != Float32Array ) { var normals = o.normals; var new_normals = new Float32Array( normals.length ); var inormals_range = normals.constructor == Uint8Array ? inv8 : inv16; for( var i = 0, l = normals.length; i < l; i += 3 ) { new_normals[i] = (normals[i] * inormals_range) * 2.0 - 1.0; new_normals[i+1] = (normals[i+1] * inormals_range) * 2.0 - 1.0; new_normals[i+2] = (normals[i+2] * inormals_range) * 2.0 - 1.0; var N = new_normals.subarray(i,i+3); vec3.normalize(N,N); } o.normals = new_normals; } if( o.coords && format.uvs_bounding && o.coords.constructor != Float32Array ) { var coords = o.coords; var uvs_bounding = format.uvs_bounding; var range = [ uvs_bounding[2] - uvs_bounding[0], uvs_bounding[3] - uvs_bounding[1] ]; var new_coords = new Float32Array( coords.length ); for( var i = 0, l = coords.length; i < l; i += 2 ) { new_coords[i] = (coords[i] * inv16) * range[0] + uvs_bounding[0]; new_coords[i+1] = (coords[i+1] * inv16) * range[1] + uvs_bounding[1]; } o.coords = new_coords; } //bones are already in Uint8 format so dont need to compress them further, but weights yes if( o.weights && o.weights.constructor != Float32Array ) //do we really need to unpack them? what if we use them like this? { var weights = o.weights; var new_weights = new Float32Array( weights.length ); var iweights_range = weights.constructor == Uint8Array ? inv8 : inv16; for(var i = 0, l = weights.length; i < l; i += 4 ) { new_weights[i] = weights[i] * iweights_range; new_weights[i+1] = weights[i+1] * iweights_range; new_weights[i+2] = weights[i+2] * iweights_range; new_weights[i+3] = weights[i+3] * iweights_range; } o.weights = new_weights; } } //footer.js })( typeof(window) != "undefined" ? window : (typeof(self) != "undefined" ? self : global ) ); ================================================ FILE: editor/js/libs/midi-parser.js ================================================ /* Project Name : midi-parser-js Project Url : https://github.com/colxi/midi-parser-js/ Author : colxi Author URL : http://www.colxi.info/ Description : MidiParser library reads .MID binary files, Base64 encoded MIDI Data, or UInt8 Arrays, and outputs as a readable and structured JS object. */ (function(){ 'use strict'; /** * CROSSBROWSER & NODEjs POLYFILL for ATOB() - * By: https://github.com/MaxArt2501 (modified) * @param {string} string [description] * @return {[type]} [description] */ const _atob = function(string) { // base64 character set, plus padding character (=) let b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; // Regular expression to check formal correctness of base64 encoded strings let b64re = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/; // remove data type signatures at the begining of the string // eg : "data:audio/mid;base64," string = string.replace( /^.*?base64,/ , ''); // atob can work with strings with whitespaces, even inside the encoded part, // but only \t, \n, \f, \r and ' ', which can be stripped. string = String(string).replace(/[\t\n\f\r ]+/g, ''); if (!b64re.test(string)) throw new TypeError('Failed to execute _atob() : The string to be decoded is not correctly encoded.'); // Adding the padding if missing, for semplicity string += '=='.slice(2 - (string.length & 3)); let bitmap, result = ''; let r1, r2, i = 0; for (; i < string.length;) { bitmap = b64.indexOf(string.charAt(i++)) << 18 | b64.indexOf(string.charAt(i++)) << 12 | (r1 = b64.indexOf(string.charAt(i++))) << 6 | (r2 = b64.indexOf(string.charAt(i++))); result += r1 === 64 ? String.fromCharCode(bitmap >> 16 & 255) : r2 === 64 ? String.fromCharCode(bitmap >> 16 & 255, bitmap >> 8 & 255) : String.fromCharCode(bitmap >> 16 & 255, bitmap >> 8 & 255, bitmap & 255); } return result; }; /** * [MidiParser description] * @type {Object} */ const MidiParser = { // debug (bool), when enabled will log in console unimplemented events // warnings and internal handled errors. debug: false, /** * [parse description] * @param {[type]} input [description] * @param {[type]} _callback [description] * @return {[type]} [description] */ parse: function(input, _callback){ if(input instanceof Uint8Array) return MidiParser.Uint8(input); else if(typeof input === 'string') return MidiParser.Base64(input); else if(input instanceof HTMLElement && input.type === 'file') return MidiParser.addListener(input , _callback); else throw new Error('MidiParser.parse() : Invalid input provided'); }, /** * addListener() should be called in order attach a listener to the INPUT HTML element * that will provide the binary data automating the conversion, and returning * the structured data to the provided callback function. */ addListener: function(_fileElement, _callback){ if(!File || !FileReader) throw new Error('The File|FileReader APIs are not supported in this browser. Use instead MidiParser.Base64() or MidiParser.Uint8()'); // validate provided element if( _fileElement === undefined || !(_fileElement instanceof HTMLElement) || _fileElement.tagName !== 'INPUT' || _fileElement.type.toLowerCase() !== 'file' ){ console.warn('MidiParser.addListener() : Provided element is not a valid FILE INPUT element'); return false; } _callback = _callback || function(){}; _fileElement.addEventListener('change', function(InputEvt){ // set the 'file selected' event handler if (!InputEvt.target.files.length) return false; // return false if no elements where selected console.log('MidiParser.addListener() : File detected in INPUT ELEMENT processing data..'); let reader = new FileReader(); // prepare the file Reader reader.readAsArrayBuffer(InputEvt.target.files[0]); // read the binary data reader.onload = function(e){ _callback( MidiParser.Uint8(new Uint8Array(e.target.result))); // encode data with Uint8Array and call the parser }; }); }, /** * Base64() : convert baset4 string into uint8 array buffer, before performing the * parsing subroutine. */ Base64 : function(b64String){ b64String = String(b64String); let raw = _atob(b64String); let rawLength = raw.length; let t_array = new Uint8Array(new ArrayBuffer(rawLength)); for(let i=0; i 1){ for(let i=1; i<= (_bytes-1); i++){ value += this.data.getUint8(this.pointer) * Math.pow(256, (_bytes - i)); this.pointer++; } } value += this.data.getUint8(this.pointer); this.pointer++; return value; }, readStr: function(_bytes){ // read as ASCII chars, the followoing _bytes let text = ''; for(let char=1; char <= _bytes; char++) text += String.fromCharCode(this.readInt(1)); return text; }, readIntVLV: function(){ // read a variable length value let value = 0; if ( this.pointer >= this.data.byteLength ){ return -1; // EOF }else if(this.data.getUint8(this.pointer) < 128){ // ...value in a single byte value = this.readInt(1); }else{ // ...value in multiple bytes let FirstBytes = []; while(this.data.getUint8(this.pointer) >= 128){ FirstBytes.push(this.readInt(1) - 128); } let lastByte = this.readInt(1); for(let dt = 1; dt <= FirstBytes.length; dt++){ value += FirstBytes[FirstBytes.length - dt] * Math.pow(128, dt); } value += lastByte; } return value; } }; file.data = new DataView(FileAsUint8Array.buffer, FileAsUint8Array.byteOffset, FileAsUint8Array.byteLength); // 8 bits bytes file data array // ** read FILE HEADER if(file.readInt(4) !== 0x4D546864){ console.warn('Header validation failed (not MIDI standard or file corrupt.)'); return false; // Header validation failed (not MIDI standard or file corrupt.) } let headerSize = file.readInt(4); // header size (unused var), getted just for read pointer movement let MIDI = {}; // create new midi object MIDI.formatType = file.readInt(2); // get MIDI Format Type MIDI.tracks = file.readInt(2); // get ammount of track chunks MIDI.track = []; // create array key for track data storing let timeDivisionByte1 = file.readInt(1); // get Time Division first byte let timeDivisionByte2 = file.readInt(1); // get Time Division second byte if(timeDivisionByte1 >= 128){ // discover Time Division mode (fps or tpf) MIDI.timeDivision = []; MIDI.timeDivision[0] = timeDivisionByte1 - 128; // frames per second MODE (1st byte) MIDI.timeDivision[1] = timeDivisionByte2; // ticks in each frame (2nd byte) }else MIDI.timeDivision = (timeDivisionByte1 * 256) + timeDivisionByte2;// else... ticks per beat MODE (2 bytes value) // ** read TRACK CHUNK for(let t=1; t <= MIDI.tracks; t++){ MIDI.track[t-1] = {event: []}; // create new Track entry in Array let headerValidation = file.readInt(4); if ( headerValidation === -1 ) break; // EOF if(headerValidation !== 0x4D54726B) return false; // Track chunk header validation failed. file.readInt(4); // move pointer. get chunk size (bytes length) let e = 0; // init event counter let endOfTrack = false; // FLAG for track reading secuence breaking // ** read EVENT CHUNK let statusByte; let laststatusByte; while(!endOfTrack){ e++; // increase by 1 event counter MIDI.track[t-1].event[e-1] = {}; // create new event object, in events array MIDI.track[t-1].event[e-1].deltaTime = file.readIntVLV(); // get DELTA TIME OF MIDI event (Variable Length Value) statusByte = file.readInt(1); // read EVENT TYPE (STATUS BYTE) if(statusByte === -1) break; // EOF else if(statusByte >= 128) laststatusByte = statusByte; // NEW STATUS BYTE DETECTED else{ // 'RUNNING STATUS' situation detected statusByte = laststatusByte; // apply last loop, Status Byte file.movePointer(-1); // move back the pointer (cause readed byte is not status byte) } // // ** IS META EVENT // if(statusByte === 0xFF){ // Meta Event type MIDI.track[t-1].event[e-1].type = 0xFF; // assign metaEvent code to array MIDI.track[t-1].event[e-1].metaType = file.readInt(1); // assign metaEvent subtype let metaEventLength = file.readIntVLV(); // get the metaEvent length switch(MIDI.track[t-1].event[e-1].metaType){ case 0x2F: // end of track, has no data byte case -1: // EOF endOfTrack = true; // change FLAG to force track reading loop breaking break; case 0x01: // Text Event case 0x02: // Copyright Notice case 0x03: case 0x04: // Instrument Name case 0x05: // Lyrics) case 0x07: // Cue point // Sequence/Track Name (documentation: http://www.ta7.de/txt/musik/musi0006.htm) case 0x06: // Marker MIDI.track[t-1].event[e-1].data = file.readStr(metaEventLength); break; case 0x21: // MIDI PORT case 0x59: // Key Signature case 0x51: // Set Tempo MIDI.track[t-1].event[e-1].data = file.readInt(metaEventLength); break; case 0x54: // SMPTE Offset MIDI.track[t-1].event[e-1].data = []; MIDI.track[t-1].event[e-1].data[0] = file.readInt(1); MIDI.track[t-1].event[e-1].data[1] = file.readInt(1); MIDI.track[t-1].event[e-1].data[2] = file.readInt(1); MIDI.track[t-1].event[e-1].data[3] = file.readInt(1); MIDI.track[t-1].event[e-1].data[4] = file.readInt(1); break; case 0x58: // Time Signature MIDI.track[t-1].event[e-1].data = []; MIDI.track[t-1].event[e-1].data[0] = file.readInt(1); MIDI.track[t-1].event[e-1].data[1] = file.readInt(1); MIDI.track[t-1].event[e-1].data[2] = file.readInt(1); MIDI.track[t-1].event[e-1].data[3] = file.readInt(1); break; default : // if user provided a custom interpreter, call it // and assign to event the returned data if( this.customInterpreter !== null){ MIDI.track[t-1].event[e-1].data = this.customInterpreter( MIDI.track[t-1].event[e-1].metaType, file, metaEventLength); } // if no customInterpretr is provided, or returned // false (=apply default), perform default action if(this.customInterpreter === null || MIDI.track[t-1].event[e-1].data === false){ file.readInt(metaEventLength); MIDI.track[t-1].event[e-1].data = file.readInt(metaEventLength); if (this.debug) console.info('Unimplemented 0xFF meta event! data block readed as Integer'); } } } // // IS REGULAR EVENT // else{ // MIDI Control Events OR System Exclusive Events statusByte = statusByte.toString(16).split(''); // split the status byte HEX representation, to obtain 4 bits values if(!statusByte[1]) statusByte.unshift('0'); // force 2 digits MIDI.track[t-1].event[e-1].type = parseInt(statusByte[0], 16);// first byte is EVENT TYPE ID MIDI.track[t-1].event[e-1].channel = parseInt(statusByte[1], 16);// second byte is channel switch(MIDI.track[t-1].event[e-1].type){ case 0xF:{ // System Exclusive Events // if user provided a custom interpreter, call it // and assign to event the returned data if( this.customInterpreter !== null){ MIDI.track[t-1].event[e-1].data = this.customInterpreter( MIDI.track[t-1].event[e-1].type, file , false); } // if no customInterpretr is provided, or returned // false (=apply default), perform default action if(this.customInterpreter === null || MIDI.track[t-1].event[e-1].data === false){ let event_length = file.readIntVLV(); MIDI.track[t-1].event[e-1].data = file.readInt(event_length); if (this.debug) console.info('Unimplemented 0xF exclusive events! data block readed as Integer'); } break; } case 0xA: // Note Aftertouch case 0xB: // Controller case 0xE: // Pitch Bend Event case 0x8: // Note off case 0x9: // Note On MIDI.track[t-1].event[e-1].data = []; MIDI.track[t-1].event[e-1].data[0] = file.readInt(1); MIDI.track[t-1].event[e-1].data[1] = file.readInt(1); break; case 0xC: // Program Change case 0xD: // Channel Aftertouch MIDI.track[t-1].event[e-1].data = file.readInt(1); break; case -1: // EOF endOfTrack = true; // change FLAG to force track reading loop breaking break; default: // if user provided a custom interpreter, call it // and assign to event the returned data if( this.customInterpreter !== null){ MIDI.track[t-1].event[e-1].data = this.customInterpreter( MIDI.track[t-1].event[e-1].metaType, file , false); } // if no customInterpretr is provided, or returned // false (=apply default), perform default action if(this.customInterpreter === null || MIDI.track[t-1].event[e-1].data === false){ console.log('Unknown EVENT detected... reading cancelled!'); return false; } } } } } return MIDI; }, /** * custom function to handle unimplemented, or custom midi messages. * If message is a meta-event, the value of metaEventLength will be >0. * Function must return the value to store, and pointer of dataView needs * to be manually increased * If you want default action to be performed, return false */ customInterpreter : null // function( e_type , arrayByffer, metaEventLength){ return e_data_int } }; // if running in NODE export module if(typeof module !== 'undefined') module.exports = MidiParser; else{ // if running in Browser, set a global variable. let _global = typeof window === 'object' && window.self === window && window || typeof self === 'object' && self.self === self && self || typeof global === 'object' && global.global === global && global; _global.MidiParser = MidiParser; } })(); ================================================ FILE: editor/style.css ================================================ html,body { width: 100%; height: 100%; margin: 0; padding: 0; } body { background-color: #333; color: #EEE; font: 14px Tahoma; } h1 { font-family: "Metro Light",Tahoma; } h2 { font-family: "Metro Light"; } #main { width: 100%; height: 100%; background-color: #222; } #status { position: absolute; top: 10px; right: 10px; color: #FAA; font-size: 18px; padding: 5px; /*border-radius: 5px;*/ width: -moz-calc( 50% - 30px); min-height: 30px; overflow: hidden; background-color: #644; } #help-message { padding: 2px; font-size: 0.8em; background-color: #464; /*border-radius: 2px;*/ } #content { position: relative; min-height: 500px; overflow: hidden; } .fullscreen #content { min-height: -moz-calc(100% - 80px); min-height: -webkit-calc(100% - 80px); min-height: calc(100% - 80px); } .info-section p { padding-left: 20px; margin: 2px; } .info-section strong { color: #FEA; } #visual { position: absolute; top: 0; left: 0; background-color: black; width: 100%; height: 100%; } .item-list .item { margin: 5px; padding: 5px; font-size: 1.2em; background-color: transparent; color: #999; padding-left: 5px; transition: background-color 300ms, color 300ms, padding-left 300ms; -moz-transition: background-color 300ms, color 300ms, padding-left 300ms; -webkit-transition: background-color 300ms, color 300ms, padding-left 300ms; } .item-list .item:hover { background-color: #33A; /*border-radius: 4px;*/ color: white; padding-left: 15px; transition: background-color 300ms, color 300ms, padding-left 300ms; -moz-transition: background-color 300ms, color 300ms, padding-left 300ms; -webkit-transition: background-color 300ms, color 300ms, padding-left 300ms; cursor: pointer; } #gallery .item-list .item:hover { background-color: #A83; } .item-list .item strong { display: inline-block; width: 200px; } .form label { font-size: 1.2em; width: 200px; display: inline-block; text-align: right; } label { font-weight: bold; color: #AAF; } .header input { color: #EEE; background-color: #555; font-size: 1.2em; border: 1px solid black; /*border-radius: 4px;*/ padding: 2px; /*box-shadow: inset 0 0 3px #333; */ font-family: Verdana; width: 250px; } textarea { vertical-align: top; } #block-app { width:100%; height:100%; position: absolute; top: 0; left: 0; background-color: rgba(0,0,0,0.5); text-align: center; z-index: 6; } #block-app span { display: block; font-size: 30px; margin: auto; margin-top: 300px; } #block-app span a { display: inline-block; /*border-radius: 4px;*/ text-decoration: none; color: black; background-color: red; padding: 0 4px 0 4px; } ::-webkit-scrollbar { height: 12px; width: 6px; background: #222; } ::-webkit-scrollbar-thumb { background: rgba(200,200,200,0.4); } ::-webkit-scrollbar-corner { background: #766; } #editor { position: relative; width: 50%; height: 100%; display: inline-block; margin: 0; padding: 0; } #editor .toolsbar { width: 100%; height: 30px; background-color: #262626; margin: 0; padding: 0; } #editor .toolsbar button { padding: 2px; padding-left: 10px; padding-right: 10px; margin: 3px 0 0 3px; } #editor .toolsbar button.enabled { background-color: #66A; } #world { position: absolute; top: 0; right: 0; width: 50%; height: 100%; } #worldcanvas { background-color: #343; } .popup { position: absolute; top: 0; background-color: rgba(50,50,90,0.8); width: 100%; height: 100%; } .popup .header, .nodepanel .header { width: 100%; height: 30px; font-size: 20px; padding: 2px; } #help { color: #eee; } #help p { margin: 10px; } .selector { font-size: 1.8em; } .selector select { color: white; background-color: black; border: 0; font-size: 1em; } .graphcanvas{ /*touch-action: manipulation; WONT WORK*/ /*touch-action: none; DESIDERABLE: implement zoom and pan*/ touch-action: pinch-zoom; } ================================================ FILE: gruntfile.js ================================================ module.exports = function (grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), projectFiles: ['src/litegraph.js', 'src/nodes/base.js', 'src/nodes/events.js', 'src/nodes/interface.js', 'src/nodes/input.js', 'src/nodes/math.js', 'src/nodes/logic.js', 'src/nodes/image.js', 'src/nodes/gltextures.js', 'src/nodes/glfx.js', 'src/nodes/midi.js', 'src/nodes/audio.js', 'src/nodes/network.js' ], concat: { build: { src: '<%= projectFiles %>', dest: 'build/litegraph.js' } }, closureCompiler: { options: { compilerFile: 'node_modules/google-closure-compiler/compiler.jar', compilerOpts: { formatting: 'pretty_print', warning_level: 'default' }, d32: false, // will use 'java -client -d32 -jar compiler.jar' TieredCompilation: false// will use 'java -server -XX:+TieredCompilation -jar compiler.jar', // ,output_wrapper: '"var LiteGraph = (function(){%output% return LiteGraph;}).call(this);"' //* Make container for all }, targetName: { src: '<%= projectFiles %>', dest: 'build/litegraph.min.js' } } }) grunt.loadNpmTasks('grunt-contrib-concat') grunt.loadNpmTasks('grunt-closure-tools') grunt.registerTask('build', ['concat:build', 'closureCompiler']) } ================================================ FILE: guides/README.md ================================================ # LiteGraph Here is a list of useful info when working with LiteGraph. The library is divided in four levels: * **LGraphNode**: the base class of a node (this library uses is own system of inheritance) * **LGraph**: the container of a whole graph made of nodes * **LGraphCanvas**: the class in charge of rendering/interaction with the nodes inside the browser. And in ```the src/``` folder there is also another class included: * **LiteGraph.Editor**: A wrapper around LGraphCanvas that adds buttons around it. ## LGraphNode LGraphNode is the base class used for all the nodes classes. To extend the other classes all the methods contained in LGraphNode.prototype are copied to the classes when registered. When you create a new node type you do not have to inherit from that class, when the node is registered all the methods are copied to your node prototype. This is done inside the functions ```LiteGraph.registerNodeType(...)```. Here is an example of how to create your own node: ```javascript //your node constructor class function MyAddNode() { //add some input slots this.addInput("A","number"); this.addInput("B","number"); //add some output slots this.addOutput("A+B","number"); //add some properties this.properties = { precision: 1 }; } //name to show on the canvas MyAddNode.title = "Sum"; //function to call when the node is executed MyAddNode.prototype.onExecute = function() { //retrieve data from inputs var A = this.getInputData(0); if( A === undefined ) A = 0; var B = this.getInputData(1); if( B === undefined ) B = 0; //assing data to outputs this.setOutputData( 0, A + B ); } //register in the system LiteGraph.registerNodeType("basic/sum", MyAddNode ); ``` ## Node settings There are several settings that could be defined or modified per node: * **size**: ```[width,height]``` the size of the area inside the node (excluding title). Every row is LiteGraph.NODE_SLOT_HEIGHT pixels height. * **properties**: object containing the properties that could be configured by the user, and serialized when saving the graph * **shape**: the shape of the object (could be LiteGraph.BOX_SHAPE,LiteGraph.ROUND_SHAPE,LiteGraph.CARD_SHAPE) * **flags**: flags that can be changed by the user and will be stored when serialized * **collapsed**: if it is shown collapsed (small) * **redraw_on_mouse**: forces a redraw if the mouse passes over the widget * **widgets_up**: widgets do not start after the slots * **widgets_start_y**: widgets should start being drawn from this Y * **clip_area**: clips the content when rendering the node * **resizable**: if it can be resized dragging the corner * **horizontal**: if the slots should be placed horizontally on the top and bottom of the node There are several callbacks that could be defined by the user: * **onAdded**: called when added to graph * **onRemoved**: called when removed from graph * **onStart**: called when the graph starts playing * **onStop**: called when the graph stops playing * **onDrawBackground**: render custom node content on canvas (not visible in Live mode) * **onDrawForeground**: render custom node content on canvas (on top of slots) * **onMouseDown,onMouseMove,onMouseUp,onMouseEnter,onMouseLeave** to catch mouse events * **onDblClick**: double clicked in the editor * **onExecute**: called when it is time to 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 in the form of [ ["name","type"], [...], [...] ] * **onGetOutputs**: returns an array of possible outputs * **onSerialize**: before serializing, receives an object where to store data * **onSelected**: selected in the editor, receives an object where to read data * **onDeselected**: deselected from the editor * **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 ) ### Node slots Every node could have several slots, stored in node.inputs and node.outputs. You can add new slots by calling node.addInput or node.addOutput The main difference between inputs and outputs is that an input can only have one connection link while outputs could have several. To get information about an slot you can access node.inputs[ slot_index ] or node.outputs[ slot_index ] Slots have the next information: * **name**: string with the name of the slot (used also to show in the canvas) * **type**: string specifying the data type traveling through this link * **link or links**: depending if the slot is input or output contains the id of the link or an array of ids * **label**: optional, string used to rename the name as shown in the canvas. * **dir**: optional, could be LiteGraph.UP, LiteGraph.RIGHT, LiteGraph.DOWN, LiteGraph.LEFT * **color_on**: color to render when it is connected * **color_off**: color to render when it is not connected To retrieve the data traveling through a link you can call ```node.getInputData``` or ```node.getOutputData``` ### Define your Graph Node When creating a class for a graph node here are some useful points: - The constructor should create the default inputs and outputs (use ```addInput``` and ```addOutput```) - Properties that can be edited are stored in ```this.properties = {};``` - the ```onExecute``` is the method that will be called when the graph is executed - you can catch if a property was changed defining a ```onPropertyChanged``` - you must register your node using ```LiteGraph.registerNodeType("type/name", MyGraphNodeClass );``` - you can alter the default priority of execution by defining the ```MyGraphNodeClass.priority``` (default is 0) - you can overwrite how the node is rendered using the ```onDrawBackground``` and ```onDrawForeground``` ### Custom Node Appearance You can configure the node shape or the title color if you want it to be different from the body color: ```js MyNodeClass.title_color = "#345"; MyNodeClass.shape = LiteGraph.ROUND_SHAPE; ``` You can draw something inside a node using the callbacks ```onDrawForeground``` and ```onDrawBackground```. The only difference is that onDrawForeground gets called in Live Mode and onDrawBackground not. Both functions receive the [Canvas2D rendering context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) and the LGraphCanvas instance where the node is being rendered. You do not have to worry about the coordinates system, (0,0) is the top-left corner of the node content area (not the title). ```js node.onDrawForeground = function(ctx, graphcanvas) { if(this.flags.collapsed) return; ctx.save(); ctx.fillColor = "black"; ctx.fillRect(0,0,10,this.size[1]); ctx.restore(); } ``` ### Custom Node Behaviour You can also grab events from the mouse in case your node has some sort of special interactivity. The second parameter is the position in node coordinates, where 0,0 represents the top-left corner of the node content (below the title). ```js node.onMouseDown = function( event, pos, graphcanvas ) { return true; //return true is the event was used by your node, to block other behaviours } ``` Other methods are: - onMouseMove - onMouseUp - onMouseEnter - onMouseLeave - onKey ### Node Widgets You can add widgets inside the node to edit text, values, etc. To do so you must create them in the constructor by calling ```node.addWidget```, the returned value is the object containing all the info about the widget, it is handy to store it in case you want to change the value later from code. The syntax is: ```js function MyNodeType() { this.slider_widget = this.addWidget("slider","Slider", 0.5, function(value, widget, node){ /* do something with the value */ }, { min: 0, max: 1} ); } ``` This is the list of supported widgets: * **"number"** to change a value of a number, the syntax is ```this.addWidget("number","Number", current_value, callback, { min: 0, max: 100, step: 1, precision: 3 } );``` * **"slider"** to change a number by dragging the mouse, the syntax is the same as number. * **"combo"** to select between multiple choices, the syntax is: ```this.addWidget("combo","Combo", "red", callback, { values:["red","green","blue"]} );``` or if you want to use objects: ```this.addWidget("combo","Combo", value1, callback, { values: { "title1":value1, "title2":value2 } } );``` * **"text"** to edit a short string * **"toggle"** like a checkbox * **"button"** The fourth optional parameter could be options for the widget, the parameters accepted are: * **property**: specifies the name of a property to modify when the widget changes * **min**: min value * **max**: max value * **precision**: set the number of digits after decimal point * **callback**: function to call when the value changes. Widget's value is not serialized by default when storing the node state, but if you want to store the value of widgets just set serialize_widgets to true: ```js function MyNode() { this.addWidget("text","name",""); this.serialize_widgets = true; } ``` Or if you want to associate a widget with a property of the node, then specify it in the options: ```js function MyNode() { this.properties = { surname: "smith" }; this.addWidget("text","Surname","", { property: "surname"}); //this will modify the node.properties } ``` ## LGraphCanvas LGraphCanvas is the class in charge of rendering/interaction with the nodes inside the browser. ## LGraphCanvas settings There are graph canvas settings that could be defined or modified to change behaviour: * **allow_interaction**: when set to `false` disable interaction with the canvas (`flags.allow_interaction` on node can be used to override graph canvas setting) ### Canvas Shortcuts * Space - Holding space key while moving the cursor moves the canvas around. It works when holding the mouse button down so it is easier to connect different nodes when the canvas gets too large. * Ctrl/Shift + Click - Add clicked node to selection. * Ctrl + A - Select all nodes * Ctrl + C/Ctrl + V - Copy and paste selected nodes, without maintaining the connection to the outputs of unselected nodes. * Ctrl + C/Ctrl + Shift + V - Copy and paste selected nodes, and maintaining the connection from the outputs of unselected nodes to the inputs of the newly pasted nodes. * Holding Shift and drag selected nodes - Move multiple selected nodes at the same time. # Execution Flow To execute a graph you must call ```graph.runStep()```. This function will call the method ```node.onExecute()``` for every node in the graph. The order of execution is determined by the system according to the morphology of the graph (nodes without inputs are considered level 0, then nodes connected to nodes of level 0 are level 1, and so on). This order is computed only when the graph morphology changes (new nodes are created, connections change). It is up to the developer to decide how to handle inputs and outputs from inside the node. The data send through outputs using ```this.setOutputData(0,data)``` is stored in the link, so if the node connected through that link does ```this.getInputData(0)``` it will receive the same data sent. For rendering, the nodes are executed according to their order in the ```graph._nodes``` array, which changes when the user interact with the GraphCanvas (clicked nodes are moved to the back of the array so they are rendered the last). ## Integration To integrate in you HTML application: ```js var graph = new LiteGraph.LGraph(); var graph_canvas = new LiteGraph.LGraphCanvas( canvas, graph ); ``` If you want to start the graph then: ```js graph.start(); ``` ## Events When we run a step in a graph (using ```graph.runStep()```) every node onExecute method will be called. But sometimes you want that actions are only performed when some trigger is activated, for this situations you can use Events. Events allow to trigger executions in nodes only when an event is dispatched from one node. To define slots for nodes you must use the type LiteGraph.ACTION for inputs, and LIteGraph.EVENT for outputs: ```js function MyNode() { this.addInput("play", LiteGraph.ACTION ); this.addInput("onFinish", LiteGraph.EVENT ); } ``` Now to execute some code when an event is received from an input, you must define the method onAction: ```js MyNode.prototype.onAction = function(action, data) { if(action == "play") { //do your action... } } ``` And the last thing is to trigger events when something in your node happens. You could trigger them from inside the onExecute or from any other interaction: ```js MyNode.prototype.onAction = function(action, data) { if( this.button_was_clicked ) this.triggerSlot(0); //triggers event in slot 0 } ``` There are some nodes already available to handle events, like delaying, counting, etc. ### Customising Link Tooltips When hovering over a link that connects two nodes together, a tooltip will be shown allowing the user to see the data that is being output from one node to the other. Sometimes, you may have a node that outputs an object, rather than a primitive value that can be easily represented (like a string). In these instances, the tooltip will default to showing `[Object]`. If you need a more descriptive tooltip, you can achieve this by adding a `toToolTip` function to your object which returns the text you wish to display in the tooltip. For example, to ensure the link from output slot 0 shows `A useful description`, the output object would look like this: ```javascript this.setOutputData(0, { complexObject: { yes: true, }, toToolTip: () => 'A useful description', }); ``` ================================================ FILE: index.html ================================================ litegraph.js

    litegraph.js

    Litegraph.js is a library that allows to create modular graphs on the web, similar to PureData.

    Graphs can be used to create workflows, image processing, audio, or any kind of network of modules interacting with each other.

    Some of the main features:

    • Automatic sorting of modules according to basic rules.
    • Dynamic number of input/outputs per module.
    • Persistence, graphs can be serialized in a JSON.
    • Optimized render in a HTML5 Canvas (supports hundres of modules on the screen).
    • Allows to run the graphs without the need of the canvas (standalone mode).
    • Simple API. It is very easy to create new modules.
    • Edit and Live mode, (in live mode only modules with widgets are rendered.
    • Contextual menu in the editor.

    Usage

    Include the library

    <script type="text/javascript" src="../src/litegraph.js"></script>

    Create a graph

    var graph = new LGraph();

    Create a canvas renderer

    var canvas = new LGraphCanvas("#mycanvas");

    Add some nodes to the graph

    var node_const = LiteGraph.createNode("basic/const");
    node_const.pos = [200,200];
    graph.add(node_const);
    node_const.setValue(4.5);
    
    var node_watch = LiteGraph.createNode("basic/watch");
    node_watch.pos = [700,200];
    graph.add(node_watch);

    Connect them

    node_const.connect(0, node_watch, 0 );

    Run the graph

    graph.start();

    Example of editor

    Documentation

    Here you can check automatically generated documentation.

    ================================================ FILE: jest.config.js ================================================ /* * For a detailed explanation regarding each configuration property, visit: * https://jestjs.io/docs/configuration */ module.exports = { // All imported modules in your tests should be mocked automatically // automock: false, // Stop running tests after `n` failures // bail: 0, // The directory where Jest should store its cached dependency information // cacheDirectory: "/tmp/jest_rs", // Automatically clear mock calls, instances, contexts and results before every test // clearMocks: false, // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, // An array of glob patterns indicating a set of files for which coverage information should be collected // collectCoverageFrom: undefined, // The directory where Jest should output its coverage files coverageDirectory: "coverage", // An array of regexp pattern strings used to skip coverage collection // coveragePathIgnorePatterns: [ // "/node_modules/" // ], // Indicates which provider should be used to instrument code for coverage // coverageProvider: "babel", // A list of reporter names that Jest uses when writing coverage reports // coverageReporters: [ // "json", // "text", // "lcov", // "clover" // ], // An object that configures minimum threshold enforcement for coverage results // coverageThreshold: undefined, // A path to a custom dependency extractor // dependencyExtractor: undefined, // Make calling deprecated APIs throw helpful error messages // errorOnDeprecated: false, // The default configuration for fake timers // fakeTimers: { // "enableGlobally": false // }, // Force coverage collection from ignored files using an array of glob patterns // forceCoverageMatch: [], // A path to a module which exports an async function that is triggered once before all test suites // globalSetup: undefined, // A path to a module which exports an async function that is triggered once after all test suites // globalTeardown: undefined, // A set of global variables that need to be available in all test environments // globals: {}, // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. // maxWorkers: "50%", // An array of directory names to be searched recursively up from the requiring module's location // moduleDirectories: [ // "node_modules" // ], // An array of file extensions your modules use // moduleFileExtensions: [ // "js", // "mjs", // "cjs", // "jsx", // "ts", // "tsx", // "json", // "node" // ], // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // moduleNameMapper: {}, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // modulePathIgnorePatterns: [], // Activates notifications for test results // notify: false, // An enum that specifies notification mode. Requires { notify: true } // notifyMode: "failure-change", // A preset that is used as a base for Jest's configuration // preset: undefined, // Run tests from one or more projects // projects: undefined, // Use this configuration option to add custom reporters to Jest // reporters: undefined, // Automatically reset mock state before every test // resetMocks: false, // Reset the module registry before running each individual test // resetModules: false, // A path to a custom resolver // resolver: undefined, // Automatically restore mock state and implementation before every test // restoreMocks: false, // The root directory that Jest should scan for tests and modules within // rootDir: undefined, // A list of paths to directories that Jest should use to search for files in // roots: [ // "" // ], // Allows you to use a custom runner instead of Jest's default test runner // runner: "jest-runner", // The paths to modules that run some code to configure or set up the testing environment before each test // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test // setupFilesAfterEnv: [], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, // A list of paths to snapshot serializer modules Jest should use for snapshot testing // snapshotSerializers: [], // The test environment that will be used for testing // testEnvironment: "jest-environment-node", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, // Adds a location field to test results // testLocationInResults: false, // The glob patterns Jest uses to detect test files // testMatch: [ // "**/__tests__/**/*.[jt]s?(x)", // "**/?(*.)+(spec|test).[tj]s?(x)" // ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ // "/node_modules/" // ], // The regexp pattern or array of patterns that Jest uses to detect test files // testRegex: [], // This option allows the use of a custom results processor // testResultsProcessor: undefined, // This option allows use of a custom test runner // testRunner: "jest-circus/runner", // A map from regular expressions to paths to transformers // transform: undefined, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // transformIgnorePatterns: [ // "/node_modules/", // "\\.pnp\\.[^\\/]+$" // ], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, // Indicates whether each individual test should be reported during the run // verbose: undefined, // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode // watchPathIgnorePatterns: [], // Whether to use watchman for file crawling // watchman: true, }; ================================================ FILE: package.json ================================================ { "name": "litegraph.js", "version": "0.7.14", "description": "A graph node editor similar to PD or UDK Blueprints. It works in an HTML5 Canvas and allows to export graphs to be included in applications.", "main": "build/litegraph.js", "types": "src/litegraph.d.ts", "directories": { "doc": "doc" }, "private": false, "scripts": { "prebuild": "rimraf build", "build": "grunt build", "start": "nodemon utils/server.js", "test": "jest", "test:allVersions": "./utils/test.sh", "prettier": "npx prettier --write src/**/*.* css/**/*.*", "lint": "npx eslint src", "lint:fix": "npx eslint --fix src" }, "repository": { "type": "git", "url": "git+https://github.com/jagenjo/litegraph.js.git" }, "author": "jagenjo", "license": "MIT", "files": [ "build", "css/litegraph.css", "src/litegraph.d.ts" ], "bugs": { "url": "https://github.com/jagenjo/litegraph.js/issues" }, "homepage": "https://github.com/jagenjo/litegraph.js#readme", "devDependencies": { "@types/jest": "^28.1.3", "eslint": "^8.37.0 ", "eslint-plugin-jest": "^27.2.1", "express": "^4.17.1", "google-closure-compiler": "^20230411.0.0", "grunt": "^1.1.0", "grunt-cli": "^1.2.0", "grunt-closure-tools": "^1.0.0", "grunt-contrib-concat": "^2.1.0", "jest": "^28.1.3", "jest-cli": "^28.1.3", "nodemon": "^2.0.22", "rimraf": "^5.0.0", "yuidocjs": "^0.10.2" } } ================================================ FILE: src/litegraph-editor.js ================================================ //Creates an interface to access extra features from a graph (like play, stop, live, etc) function Editor(container_id, options) { options = options || {}; //fill container var html = "
    "; html += "
    "; html += ""; var root = document.createElement("div"); this.root = root; root.className = "litegraph litegraph-editor"; root.innerHTML = html; this.tools = root.querySelector(".tools"); this.content = root.querySelector(".content"); this.footer = root.querySelector(".footer"); var canvas = this.canvas = root.querySelector(".graphcanvas"); //create graph var graph = (this.graph = new LGraph()); var graphcanvas = this.graphcanvas = new LGraphCanvas(canvas, graph); graphcanvas.background_image = "imgs/grid.png"; graph.onAfterExecute = function() { graphcanvas.draw(true); }; graphcanvas.onDropItem = this.onDropItem.bind(this); //add stuff //this.addToolsButton("loadsession_button","Load","imgs/icon-load.png", this.onLoadButton.bind(this), ".tools-left" ); //this.addToolsButton("savesession_button","Save","imgs/icon-save.png", this.onSaveButton.bind(this), ".tools-left" ); this.addLoadCounter(); this.addToolsButton( "playnode_button", "Play", "imgs/icon-play.png", this.onPlayButton.bind(this), ".tools-right" ); this.addToolsButton( "playstepnode_button", "Step", "imgs/icon-playstep.png", this.onPlayStepButton.bind(this), ".tools-right" ); if (!options.skip_livemode) { this.addToolsButton( "livemode_button", "Live", "imgs/icon-record.png", this.onLiveButton.bind(this), ".tools-right" ); } if (!options.skip_maximize) { this.addToolsButton( "maximize_button", "", "imgs/icon-maximize.png", this.onFullscreenButton.bind(this), ".tools-right" ); } if (options.miniwindow) { this.addMiniWindow(300, 200); } //append to DOM var parent = document.getElementById(container_id); if (parent) { parent.appendChild(root); } graphcanvas.resize(); //graphcanvas.draw(true,true); } Editor.prototype.addLoadCounter = function() { var meter = document.createElement("div"); meter.className = "headerpanel loadmeter toolbar-widget"; var html = "
    CPU
    "; html += "
    GFX
    "; meter.innerHTML = html; this.root.querySelector(".header .tools-left").appendChild(meter); var self = this; setInterval(function() { meter.querySelector(".cpuload .fgload").style.width = 2 * self.graph.execution_time * 90 + "px"; if (self.graph.status == LGraph.STATUS_RUNNING) { meter.querySelector(".gpuload .fgload").style.width = self.graphcanvas.render_time * 10 * 90 + "px"; } else { meter.querySelector(".gpuload .fgload").style.width = 4 + "px"; } }, 200); }; Editor.prototype.addToolsButton = function( id, name, icon_url, callback, container ) { if (!container) { container = ".tools"; } var button = this.createButton(name, icon_url, callback); button.id = id; this.root.querySelector(container).appendChild(button); }; Editor.prototype.createButton = function(name, icon_url, callback) { var button = document.createElement("button"); if (icon_url) { button.innerHTML = " "; } button.classList.add("btn"); button.innerHTML += name; if(callback) button.addEventListener("click", callback ); return button; }; Editor.prototype.onLoadButton = function() { var panel = this.graphcanvas.createPanel("Load session",{closable:true}); //TO DO this.root.appendChild(panel); }; Editor.prototype.onSaveButton = function() {}; Editor.prototype.onPlayButton = function() { var graph = this.graph; var button = this.root.querySelector("#playnode_button"); if (graph.status == LGraph.STATUS_STOPPED) { button.innerHTML = " Stop"; graph.start(); } else { button.innerHTML = " Play"; graph.stop(); } }; Editor.prototype.onPlayStepButton = function() { var graph = this.graph; graph.runStep(1); this.graphcanvas.draw(true, true); }; Editor.prototype.onLiveButton = function() { var is_live_mode = !this.graphcanvas.live_mode; this.graphcanvas.switchLiveMode(true); this.graphcanvas.draw(); var url = this.graphcanvas.live_mode ? "imgs/gauss_bg_medium.jpg" : "imgs/gauss_bg.jpg"; var button = this.root.querySelector("#livemode_button"); button.innerHTML = !is_live_mode ? " Live" : " Edit"; }; Editor.prototype.onDropItem = function(e) { var that = this; for(var i = 0; i < e.dataTransfer.files.length; ++i) { var file = e.dataTransfer.files[i]; var ext = LGraphCanvas.getFileExtension(file.name); var reader = new FileReader(); if(ext == "json") { reader.onload = function(event) { var data = JSON.parse( event.target.result ); that.graph.configure(data); }; reader.readAsText(file); } } } Editor.prototype.goFullscreen = function() { if (this.root.requestFullscreen) { this.root.requestFullscreen(Element.ALLOW_KEYBOARD_INPUT); } else if (this.root.mozRequestFullscreen) { this.root.requestFullscreen(Element.ALLOW_KEYBOARD_INPUT); } else if (this.root.webkitRequestFullscreen) { this.root.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); } else { throw "Fullscreen not supported"; } var self = this; setTimeout(function() { self.graphcanvas.resize(); }, 100); }; Editor.prototype.onFullscreenButton = function() { this.goFullscreen(); }; Editor.prototype.addMiniWindow = function(w, h) { var miniwindow = document.createElement("div"); miniwindow.className = "litegraph miniwindow"; miniwindow.innerHTML = ""; var canvas = miniwindow.querySelector("canvas"); var that = this; var graphcanvas = new LGraphCanvas( canvas, this.graph ); graphcanvas.show_info = false; graphcanvas.background_image = "imgs/grid.png"; graphcanvas.scale = 0.25; graphcanvas.allow_dragnodes = false; graphcanvas.allow_interaction = false; graphcanvas.render_shadows = false; graphcanvas.max_zoom = 0.25; this.miniwindow_graphcanvas = graphcanvas; graphcanvas.onClear = function() { graphcanvas.scale = 0.25; graphcanvas.allow_dragnodes = false; graphcanvas.allow_interaction = false; }; graphcanvas.onRenderBackground = function(canvas, ctx) { ctx.strokeStyle = "#567"; var tl = that.graphcanvas.convertOffsetToCanvas([0, 0]); var br = that.graphcanvas.convertOffsetToCanvas([ that.graphcanvas.canvas.width, that.graphcanvas.canvas.height ]); tl = this.convertCanvasToOffset(tl); br = this.convertCanvasToOffset(br); ctx.lineWidth = 1; ctx.strokeRect( Math.floor(tl[0]) + 0.5, Math.floor(tl[1]) + 0.5, Math.floor(br[0] - tl[0]), Math.floor(br[1] - tl[1]) ); }; miniwindow.style.position = "absolute"; miniwindow.style.top = "4px"; miniwindow.style.right = "4px"; var close_button = document.createElement("div"); close_button.className = "corner-button"; close_button.innerHTML = "❌"; close_button.addEventListener("click", function(e) { graphcanvas.setGraph(null); miniwindow.parentNode.removeChild(miniwindow); }); miniwindow.appendChild(close_button); this.root.querySelector(".content").appendChild(miniwindow); }; Editor.prototype.addMultiview = function() { var canvas = this.canvas; this.graphcanvas.ctx.fillStyle = "black"; this.graphcanvas.ctx.fillRect(0,0,canvas.width,canvas.height); this.graphcanvas.viewport = [0,0,canvas.width*0.5-2,canvas.height]; var graphcanvas = new LGraphCanvas( canvas, this.graph ); graphcanvas.background_image = "imgs/grid.png"; this.graphcanvas2 = graphcanvas; this.graphcanvas2.viewport = [canvas.width*0.5,0,canvas.width*0.5,canvas.height]; } LiteGraph.Editor = Editor; ================================================ FILE: src/litegraph.d.ts ================================================ // Type definitions for litegraph.js 0.7.0 // Project: litegraph.js // Definitions by: NateScarlet export type Vector2 = [number, number]; export type Vector4 = [number, number, number, number]; export type widgetTypes = | "number" | "slider" | "combo" | "text" | "toggle" | "button"; export type SlotShape = | typeof LiteGraph.BOX_SHAPE | typeof LiteGraph.CIRCLE_SHAPE | typeof LiteGraph.ARROW_SHAPE | typeof LiteGraph.SQUARE_SHAPE | number; // For custom shapes /** https://github.com/jagenjo/litegraph.js/tree/master/guides#node-slots */ export interface INodeSlot { name: string; type: string | -1; label?: string; dir?: | typeof LiteGraph.UP | typeof LiteGraph.RIGHT | typeof LiteGraph.DOWN | typeof LiteGraph.LEFT; color_on?: string; color_off?: string; shape?: SlotShape; locked?: boolean; nameLocked?: boolean; } export interface INodeInputSlot extends INodeSlot { link: LLink["id"] | null; } export interface INodeOutputSlot extends INodeSlot { links: LLink["id"][] | null; } export type WidgetCallback = ( this: T, value: T["value"], graphCanvas: LGraphCanvas, node: LGraphNode, pos: Vector2, event?: MouseEvent ) => void; export interface IWidget { name: string | null; value: TValue; options?: TOptions; type?: widgetTypes; y?: number; property?: string; last_y?: number; clicked?: boolean; marker?: boolean; callback?: WidgetCallback; /** Called by `LGraphCanvas.drawNodeWidgets` */ draw?( ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, posY: number, height: number ): void; /** * Called by `LGraphCanvas.processNodeWidgets` * https://github.com/jagenjo/litegraph.js/issues/76 */ mouse?( event: MouseEvent, pos: Vector2, node: LGraphNode ): boolean; /** Called by `LGraphNode.computeSize` */ computeSize?(width: number): [number, number]; } export interface IButtonWidget extends IWidget { type: "button"; } export interface IToggleWidget extends IWidget { type: "toggle"; } export interface ISliderWidget extends IWidget { type: "slider"; } export interface INumberWidget extends IWidget { type: "number"; } export interface IComboWidget extends IWidget< string[], { values: | string[] | ((widget: IComboWidget, node: LGraphNode) => string[]); } > { type: "combo"; } export interface ITextWidget extends IWidget { type: "text"; } export interface IContextMenuItem { content: string; callback?: ContextMenuEventListener; /** Used as innerHTML for extra child element */ title?: string; disabled?: boolean; has_submenu?: boolean; submenu?: { options: ContextMenuItem[]; } & IContextMenuOptions; className?: string; } export interface IContextMenuOptions { callback?: ContextMenuEventListener; ignore_item_callbacks?: Boolean; event?: MouseEvent | CustomEvent; parentMenu?: ContextMenu; autoopen?: boolean; title?: string; extra?: any; } export type ContextMenuItem = IContextMenuItem | null; export type ContextMenuEventListener = ( value: ContextMenuItem, options: IContextMenuOptions, event: MouseEvent, parentMenu: ContextMenu | undefined, node: LGraphNode ) => boolean | void; export const LiteGraph: { VERSION: number; CANVAS_GRID_SIZE: number; NODE_TITLE_HEIGHT: number; NODE_TITLE_TEXT_Y: number; NODE_SLOT_HEIGHT: number; NODE_WIDGET_HEIGHT: number; NODE_WIDTH: number; NODE_MIN_WIDTH: number; NODE_COLLAPSED_RADIUS: number; NODE_COLLAPSED_WIDTH: number; NODE_TITLE_COLOR: string; NODE_TEXT_SIZE: number; NODE_TEXT_COLOR: string; NODE_SUBTEXT_SIZE: number; NODE_DEFAULT_COLOR: string; NODE_DEFAULT_BGCOLOR: string; NODE_DEFAULT_BOXCOLOR: string; NODE_DEFAULT_SHAPE: string; DEFAULT_SHADOW_COLOR: string; DEFAULT_GROUP_FONT: number; LINK_COLOR: string; EVENT_LINK_COLOR: string; CONNECTING_LINK_COLOR: string; MAX_NUMBER_OF_NODES: number; //avoid infinite loops DEFAULT_POSITION: Vector2; //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; SQUARE_SHAPE: 6; //enums INPUT: 1; OUTPUT: 2; EVENT: -1; //for outputs ACTION: -1; //for inputs ALWAYS: 0; ON_EVENT: 1; NEVER: 2; ON_TRIGGER: 3; UP: 1; DOWN: 2; LEFT: 3; RIGHT: 4; CENTER: 5; STRAIGHT_LINK: 0; LINEAR_LINK: 1; SPLINE_LINK: 2; NORMAL_TITLE: 0; NO_TITLE: 1; TRANSPARENT_TITLE: 2; AUTOHIDE_TITLE: 3; node_images_path: string; debug: boolean; catch_exceptions: boolean; throw_errors: boolean; /** 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 */ allow_scripts: boolean; /** node types by string */ registered_node_types: Record; /** used for dropping files in the canvas */ node_types_by_file_extension: Record; /** node types by class name */ Nodes: Record; /** used to add extra features to the search box */ searchbox_extras: Record< string, { data: { outputs: string[][]; title: string }; desc: string; type: string; } >; createNode(type: string): T; /** Register a node class so it can be listed when the user wants to create a new one */ registerNodeType(type: string, base: { new (): LGraphNode }): void; /** removes a node type from the system */ unregisterNodeType(type: string): void; /** Removes all previously registered node's types. */ clearRegisteredTypes(): void; /** * Create a new node type 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. * @param name node name with namespace (p.e.: 'math/sum') * @param func * @param param_types an array containing the type of every parameter, otherwise parameters will accept any type * @param return_type string with the return type, otherwise it will be generic * @param properties properties to be configurable * @return {LGraphNode} */ wrapFunctionAsNode( name: string, func: (...args: any[]) => any, param_types?: string[], return_type?: string, properties?: object ): LGraphNode; /** * Adds this method to all node types, existing and to be created * (You can add it to LGraphNode.prototype but then existing node types wont have it) */ addNodeMethod(name: string, func: (...args: any[]) => any): void; /** * Create a node of a given type with a name. The node is not attached to any graph yet. * @param type full name of the node class. p.e. "math/sin" * @param name a name to distinguish from other nodes * @param options to set options */ createNode( type: string, title: string, options: object ): T; /** * Returns a registered node type with a given name * @param type full name of the node class. p.e. "math/sin" */ getNodeType(type: string): LGraphNodeConstructor; /** * Returns a list of node types matching one category * @method getNodeTypesInCategory * @param {String} category category name * @param {String} filter only nodes with ctor.filter equal can be shown * @return {Array} array with all the node classes */ getNodeTypesInCategory( category: string, filter: string ): LGraphNodeConstructor[]; /** * 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(filter: string): string[]; /** debug purposes: reloads all the js scripts that matches a wildcard */ reloadNodes(folder_wildcard: string): void; getTime(): number; LLink: typeof LLink; LGraph: typeof LGraph; DragAndScale: typeof DragAndScale; compareObjects(a: object, b: object): boolean; distance(a: Vector2, b: Vector2): number; colorToString(c: string): string; isInsideRectangle( x: number, y: number, left: number, top: number, width: number, height: number ): boolean; growBounding(bounding: Vector4, x: number, y: number): Vector4; isInsideBounding(p: Vector2, bb: Vector4): boolean; hex2num(hex: string): [number, number, number]; num2hex(triplet: [number, number, number]): string; ContextMenu: typeof ContextMenu; extendClass(target: A, origin: B): A & B; getParameterNames(func: string): string[]; }; export type serializedLGraph< TNode = ReturnType, // https://github.com/jagenjo/litegraph.js/issues/74 TLink = [number, number, number, number, number, string], TGroup = ReturnType > = { last_node_id: LGraph["last_node_id"]; last_link_id: LGraph["last_link_id"]; nodes: TNode[]; links: TLink[]; groups: TGroup[]; config: LGraph["config"]; version: typeof LiteGraph.VERSION; }; export declare class LGraph { static supported_types: string[]; static STATUS_STOPPED: 1; static STATUS_RUNNING: 2; constructor(o?: object); filter: string; catch_errors: boolean; /** custom data */ config: object; elapsed_time: number; fixedtime: number; fixedtime_lapse: number; globaltime: number; inputs: any; iteration: number; last_link_id: number; last_node_id: number; last_update_time: number; links: Record; list_of_graphcanvas: LGraphCanvas[]; outputs: any; runningtime: number; starttime: number; status: typeof LGraph.STATUS_RUNNING | typeof LGraph.STATUS_STOPPED; private _nodes: LGraphNode[]; private _groups: LGraphGroup[]; private _nodes_by_id: Record; /** nodes that are executable sorted in execution order */ private _nodes_executable: | (LGraphNode & { onExecute: NonNullable }[]) | null; /** nodes that contain onExecute */ private _nodes_in_order: LGraphNode[]; private _version: number; getSupportedTypes(): string[]; /** Removes all nodes from this graph */ clear(): void; /** Attach Canvas to this graph */ attachCanvas(graphCanvas: LGraphCanvas): void; /** Detach Canvas to this graph */ detachCanvas(graphCanvas: LGraphCanvas): void; /** * Starts running this graph every interval milliseconds. * @param interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate */ start(interval?: number): void; /** Stops the execution loop of the graph */ stop(): void; /** * Run N steps (cycles) of the graph * @param num number of steps to run, default is 1 */ runStep(num?: number, do_not_catch_errors?: boolean): void; /** * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than * nodes with only inputs. */ updateExecutionOrder(): void; /** This is more internal, it computes the executable nodes in order and returns it */ computeExecutionOrder(only_onExecute: boolean, set_level: any): T; /** * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. * It doesn't include the node itself * @return an array with all the LGraphNodes that affect this node, in order of execution */ getAncestors(node: LGraphNode): LGraphNode[]; /** * Positions every node in a more readable manner */ arrange(margin?: number,layout?: string): void; /** * Returns the amount of time the graph has been running in milliseconds * @return number of milliseconds the graph has been running */ getTime(): number; /** * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant * @return number of milliseconds the graph has been running */ getFixedTime(): number; /** * 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 * @return number of milliseconds it took the last cycle */ getElapsedTime(): number; /** * Sends an event to all the nodes, useful to trigger stuff * @param eventName the name of the event (function to be called) * @param params parameters in array format */ sendEventToAllNodes(eventName: string, params: any[], mode?: any): void; sendActionToCanvas(action: any, params: any[]): void; /** * Adds a new node instance to this graph * @param node the instance of the node */ add(node: LGraphNode, skip_compute_order?: boolean): void; /** * Called when a new node is added * @param node the instance of the node */ onNodeAdded(node: LGraphNode): void; /** Removes a node from the graph */ remove(node: LGraphNode): void; /** Returns a node by its id. */ getNodeById(id: number): LGraphNode | undefined; /** * Returns a list of nodes that matches a class * @param classObject the class itself (not an string) * @return a list with all the nodes of this type */ findNodesByClass( classObject: LGraphNodeConstructor ): T[]; /** * Returns a list of nodes that matches a type * @param type the name of the node type * @return a list with all the nodes of this type */ findNodesByType(type: string): T[]; /** * Returns the first node that matches a name in its title * @param title the name of the node to search * @return the node or null */ findNodeByTitle(title: string): T | null; /** * Returns a list of nodes that matches a name * @param title the name of the node to search * @return a list with all the nodes with this name */ findNodesByTitle(title: string): T[]; /** * Returns the top-most node in this position of the canvas * @param x the x coordinate in canvas space * @param y the y coordinate in canvas space * @param nodes_list a list with all the nodes to search from, by default is all the nodes in the graph * @return the node at this position or null */ getNodeOnPos( x: number, y: number, node_list?: LGraphNode[], margin?: number ): T | null; /** * Returns the top-most group in that position * @param x the x coordinate in canvas space * @param y the y coordinate in canvas space * @return the group or null */ getGroupOnPos(x: number, y: number): LGraphGroup | null; onAction(action: any, param: any): void; trigger(action: any, param: any): void; /** Tell this graph it has a global graph input of this type */ addInput(name: string, type: string, value?: any): void; /** Assign a data to the global graph input */ setInputData(name: string, data: any): void; /** Returns the current value of a global graph input */ getInputData(name: string): T; /** Changes the name of a global graph input */ renameInput(old_name: string, name: string): false | undefined; /** Changes the type of a global graph input */ changeInputType(name: string, type: string): false | undefined; /** Removes a global graph input */ removeInput(name: string): boolean; /** Creates a global graph output */ addOutput(name: string, type: string, value: any): void; /** Assign a data to the global output */ setOutputData(name: string, value: string): void; /** Returns the current value of a global graph output */ getOutputData(name: string): T; /** Renames a global graph output */ renameOutput(old_name: string, name: string): false | undefined; /** Changes the type of a global graph output */ changeOutputType(name: string, type: string): false | undefined; /** Removes a global graph output */ removeOutput(name: string): boolean; triggerInput(name: string, value: any): void; setCallback(name: string, func: (...args: any[]) => any): void; beforeChange(info?: LGraphNode): void; afterChange(info?: LGraphNode): void; connectionChange(node: LGraphNode): void; /** returns if the graph is in live mode */ isLive(): boolean; /** clears the triggered slot animation in all links (stop visual animation) */ clearTriggeredSlots(): void; /* Called when something visually changed (not the graph!) */ change(): void; setDirtyCanvas(fg: boolean, bg: boolean): void; /** Destroys a link */ removeLink(link_id: number): void; /** Creates a Object containing all the info about this graph, it can be serialized */ serialize(): T; /** * Configure a graph from a JSON string * @param data configure a graph from a JSON string * @returns if there was any error parsing */ configure(data: object, keep_old?: boolean): boolean | undefined; load(url: string): void; } export type SerializedLLink = [number, string, number, number, number, number]; export declare class LLink { id: number; type: string; origin_id: number; origin_slot: number; target_id: number; target_slot: number; constructor( id: number, type: string, origin_id: number, origin_slot: number, target_id: number, target_slot: number ); configure(o: LLink | SerializedLLink): void; serialize(): SerializedLLink; } export type SerializedLGraphNode = { id: T["id"]; type: T["type"]; pos: T["pos"]; size: T["size"]; flags: T["flags"]; mode: T["mode"]; inputs: T["inputs"]; outputs: T["outputs"]; title: T["title"]; properties: T["properties"]; widgets_values?: IWidget["value"][]; }; /** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */ export declare class LGraphNode { static title_color: string; static title: string; static type: null | string; static widgets_up: boolean; constructor(title?: string); title: string; type: null | string; size: Vector2; graph: null | LGraph; graph_version: number; pos: Vector2; is_selected: boolean; mouseOver: boolean; id: number; //inputs available: array of inputs inputs: INodeInputSlot[]; outputs: INodeOutputSlot[]; connections: any[]; //local data properties: Record; properties_info: any[]; flags: Partial<{ collapsed: boolean }>; color: string; bgcolor: string; boxcolor: string; shape: | typeof LiteGraph.BOX_SHAPE | typeof LiteGraph.ROUND_SHAPE | typeof LiteGraph.CIRCLE_SHAPE | typeof LiteGraph.CARD_SHAPE | typeof LiteGraph.ARROW_SHAPE; serialize_widgets: boolean; skip_list: boolean; /** Used in `LGraphCanvas.onMenuNodeMode` */ mode?: | typeof LiteGraph.ON_EVENT | typeof LiteGraph.ON_TRIGGER | typeof LiteGraph.NEVER | typeof LiteGraph.ALWAYS; /** If set to true widgets do not start after the slots */ widgets_up: boolean; /** widgets start at y distance from the top of the node */ widgets_start_y: number; /** if you render outside the node, it will be clipped */ clip_area: boolean; /** if set to false it wont be resizable with the mouse */ resizable: boolean; /** slots are distributed horizontally */ horizontal: boolean; /** if true, the node will show the bgcolor as 'red' */ has_errors?: boolean; /** configure a node from an object containing the serialized info */ configure(info: SerializedLGraphNode): void; /** serialize the content */ serialize(): SerializedLGraphNode; /** Creates a clone of this node */ clone(): this; /** serialize and stringify */ toString(): string; /** get the title string */ getTitle(): string; /** sets the value of a property */ setProperty(name: string, value: any): void; /** sets the output data */ setOutputData(slot: number, data: any): void; /** sets the output data */ setOutputDataType(slot: number, type: string): void; /** * Retrieves the input data (data traveling through the connection) from one slot * @param slot * @param 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 */ getInputData(slot: number, force_update?: boolean): T; /** * Retrieves the input data type (in case this supports multiple input types) * @param slot * @return datatype in string format */ getInputDataType(slot: number): string; /** * Retrieves the input data from one slot using its name instead of slot number * @param slot_name * @param 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 */ getInputDataByName(slot_name: string, force_update?: boolean): T; /** tells you if there is a connection in one input slot */ isInputConnected(slot: number): boolean; /** tells you info about an input connection (which node, type, etc) */ getInputInfo( slot: number ): { link: number; name: string; type: string | 0 } | null; /** Returns the link info in the connection of an input slot */ getInputLink(slot: number): LLink | null; /** returns the node connected in the input slot */ getInputNode(slot: number): LGraphNode | null; /** returns the value of an input with this name, otherwise checks if there is a property with that name */ getInputOrProperty(name: string): T; /** tells you the last output data that went in that slot */ getOutputData(slot: number): T | null; /** tells you info about an output connection (which node, type, etc) */ getOutputInfo( slot: number ): { name: string; type: string; links: number[] } | null; /** tells you if there is a connection in one output slot */ isOutputConnected(slot: number): boolean; /** tells you if there is any connection in the output slots */ isAnyOutputConnected(): boolean; /** retrieves all the nodes connected to this output slot */ getOutputNodes(slot: number): LGraphNode[]; /** Triggers an event in this node, this will trigger any output with the same name */ trigger(action: string, param: any): void; /** * Triggers an slot event in this node * @param slot the index of the output slot * @param param * @param link_id in case you want to trigger and specific output link in a slot */ triggerSlot(slot: number, param: any, link_id?: number): void; /** * clears the trigger slot animation * @param slot the index of the output slot * @param link_id in case you want to trigger and specific output link in a slot */ clearTriggeredSlot(slot: number, link_id?: number): void; /** changes node size and triggers callback */ setSize(size: Vector2): void; /** * add a new property to this node * @param name * @param default_value * @param type string defining the output type ("vec3","number",...) * @param extra_info this can be used to have special properties of the property (like values, etc) */ addProperty( name: string, default_value: any, type: string, extra_info?: object ): T; /** * add a new output slot to use in this node * @param name * @param type string defining the output type ("vec3","number",...) * @param extra_info this can be used to have special properties of an output (label, special color, position, etc) */ addOutput( name: string, type: string | -1, extra_info?: Partial ): INodeOutputSlot; /** * add a new output slot to use in this node * @param array of triplets like [[name,type,extra_info],[...]] */ addOutputs( array: [string, string | -1, Partial | undefined][] ): void; /** remove an existing output slot */ removeOutput(slot: number): void; /** * add a new input slot to use in this node * @param name * @param type string defining the input type ("vec3","number",...), it its a generic one use 0 * @param extra_info this can be used to have special properties of an input (label, color, position, etc) */ addInput( name: string, type: string | -1, extra_info?: Partial ): INodeInputSlot; /** * add several new input slots in this node * @param array of triplets like [[name,type,extra_info],[...]] */ addInputs( array: [string, string | -1, Partial | undefined][] ): void; /** remove an existing input slot */ removeInput(slot: number): void; /** * add an special connection to this node (used for special kinds of graphs) * @param name * @param type string defining the input type ("vec3","number",...) * @param pos position of the connection inside the node * @param direction if is input or output */ addConnection( name: string, type: string, pos: Vector2, direction: string ): { name: string; type: string; pos: Vector2; direction: string; links: null; }; /** computes the minimum size of a node according to its inputs and output slots */ computeSize(minHeight?: Vector2): Vector2; /** returns all the info available about a property of this node */ getPropertyInfo(property: string): object; /** * https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#node-widgets * @return created widget */ addWidget( type: T["type"], name: string, value: T["value"], callback?: WidgetCallback | string, options?: T["options"] ): T; addCustomWidget(customWidget: T): T; /** * returns the bounding of the object, used for rendering purposes * @method getBounding * @param out [optional] a place to store the output, to free garbage * @param compute_outer [optional] set to true to include the shadow and connection points in the bounding calculation * @return the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] */ getBounding(out?: Vector4, compute_outer?: boolean): Vector4; /** checks if a point is inside the shape of a node */ isPointInside( x: number, y: number, margin?: number, skipTitle?: boolean ): boolean; /** checks if a point is inside a node slot, and returns info about which slot */ getSlotInPosition( x: number, y: number ): { input?: INodeInputSlot; output?: INodeOutputSlot; slot: number; link_pos: Vector2; }; /** * returns the input slot with a given name (used for dynamic slots), -1 if not found * @param name the name of the slot * @return the slot (-1 if not found) */ findInputSlot(name: string): number; /** * returns the output slot with a given name (used for dynamic slots), -1 if not found * @param name the name of the slot * @return the slot (-1 if not found) */ findOutputSlot(name: string): number; /** * connect this node output to the input of another node * @param slot (could be the number of the slot or the string with the name of the slot) * @param targetNode the target node * @param targetSlot 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 */ connect( slot: number | string, targetNode: LGraphNode, targetSlot: number | string ): T | null; /** * disconnect one output to an specific node * @param slot (could be the number of the slot or the string with the name of the slot) * @param target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] * @return if it was disconnected successfully */ disconnectOutput(slot: number | string, targetNode?: LGraphNode): boolean; /** * disconnect one input * @param slot (could be the number of the slot or the string with the name of the slot) * @return if it was disconnected successfully */ disconnectInput(slot: number | string): boolean; /** * returns the center of a connection point in canvas coords * @param is_input true if if a input slot, false if it is an output * @param slot (could be the number of the slot or the string with the name of the slot) * @param out a place to store the output, to free garbage * @return the position **/ getConnectionPos( is_input: boolean, slot: number | string, out?: Vector2 ): Vector2; /** Force align to grid */ alignToGrid(): void; /** Console output */ trace(msg: string): void; /** Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ setDirtyCanvas(fg: boolean, bg: boolean): void; loadImage(url: string): void; /** Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ captureInput(v: any): void; /** Collapse the node to make it smaller on the canvas */ collapse(force: boolean): void; /** Forces the node to do not move or realign on Z */ pin(v?: boolean): void; localToScreen(x: number, y: number, graphCanvas: LGraphCanvas): Vector2; // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-appearance onDrawBackground?( ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement ): void; onDrawForeground?( ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement ): void; // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-behaviour onMouseDown?( event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas ): void; onMouseMove?( event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas ): void; onMouseUp?( event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas ): void; onMouseEnter?( event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas ): void; onMouseLeave?( event: MouseEvent, pos: Vector2, graphCanvas: LGraphCanvas ): void; onKey?(event: KeyboardEvent, pos: Vector2, graphCanvas: LGraphCanvas): void; /** Called by `LGraphCanvas.selectNodes` */ onSelected?(): void; /** Called by `LGraphCanvas.deselectNode` */ onDeselected?(): void; /** Called by `LGraph.runStep` `LGraphNode.getInputData` */ onExecute?(): void; /** Called by `LGraph.serialize` */ onSerialize?(o: SerializedLGraphNode): void; /** Called by `LGraph.configure` */ onConfigure?(o: SerializedLGraphNode): void; /** * when added to graph (warning: this is called BEFORE the node is configured when loading) * Called by `LGraph.add` */ onAdded?(graph: LGraph): void; /** * when removed from graph * Called by `LGraph.remove` `LGraph.clear` */ onRemoved?(): void; /** * if returns false the incoming connection will be canceled * Called by `LGraph.connect` * @param inputIndex target input slot number * @param outputType type of output slot * @param outputSlot output slot object * @param outputNode node containing the output * @param outputIndex index of output slot */ onConnectInput?( inputIndex: number, outputType: INodeOutputSlot["type"], outputSlot: INodeOutputSlot, outputNode: LGraphNode, outputIndex: number ): boolean; /** * if returns false the incoming connection will be canceled * Called by `LGraph.connect` * @param outputIndex target output slot number * @param inputType type of input slot * @param inputSlot input slot object * @param inputNode node containing the input * @param inputIndex index of input slot */ onConnectOutput?( outputIndex: number, inputType: INodeInputSlot["type"], inputSlot: INodeInputSlot, inputNode: LGraphNode, inputIndex: number ): boolean; /** * Called just before connection (or disconnect - if input is linked). * A convenient place to switch to another input, or create new one. * This allow for ability to automatically add slots if needed * @param inputIndex * @return selected input slot index, can differ from parameter value */ onBeforeConnectInput?( inputIndex: number ): number; /** a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info or output_info ) */ onConnectionsChange( type: number, slotIndex: number, isConnected: boolean, link: LLink, ioSlot: (INodeOutputSlot | INodeInputSlot) ): void; /** * if returns false, will abort the `LGraphNode.setProperty` * Called when a property is changed * @param property * @param value * @param prevValue */ onPropertyChanged?(property: string, value: any, prevValue: any): void | boolean; /** Called by `LGraphCanvas.processContextMenu` */ getMenuOptions?(graphCanvas: LGraphCanvas): ContextMenuItem[]; getSlotMenuOptions?(slot: INodeSlot): ContextMenuItem[]; } export type LGraphNodeConstructor = { new (): T; }; export type SerializedLGraphGroup = { title: LGraphGroup["title"]; bounding: LGraphGroup["_bounding"]; color: LGraphGroup["color"]; font: LGraphGroup["font"]; }; export declare class LGraphGroup { title: string; private _bounding: Vector4; color: string; font: string; configure(o: SerializedLGraphGroup): void; serialize(): SerializedLGraphGroup; move(deltaX: number, deltaY: number, ignoreNodes?: boolean): void; recomputeInsideNodes(): void; isPointInside: LGraphNode["isPointInside"]; setDirtyCanvas: LGraphNode["setDirtyCanvas"]; } export declare class DragAndScale { constructor(element?: HTMLElement, skipEvents?: boolean); offset: [number, number]; scale: number; max_scale: number; min_scale: number; onredraw: Function | null; enabled: boolean; last_mouse: Vector2; element: HTMLElement | null; visible_area: Vector4; bindEvents(element: HTMLElement): void; computeVisibleArea(): void; onMouse(e: MouseEvent): void; toCanvasContext(ctx: CanvasRenderingContext2D): void; convertOffsetToCanvas(pos: Vector2): Vector2; convertCanvasToOffset(pos: Vector2): Vector2; mouseDrag(x: number, y: number): void; changeScale(value: number, zooming_center?: Vector2): void; changeDeltaScale(value: number, zooming_center?: Vector2): void; reset(): void; } /** * 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 * * @param canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) * @param graph * @param options { skip_rendering, autoresize } */ export declare class LGraphCanvas { static node_colors: Record< string, { color: string; bgcolor: string; groupcolor: string; } >; static link_type_colors: Record; static gradients: object; static search_limit: number; static getFileExtension(url: string): string; static decodeHTML(str: string): string; static onMenuCollapseAll(): void; static onMenuNodeEdit(): void; static onShowPropertyEditor( item: any, options: any, e: any, menu: any, node: any ): void; /** Create menu for `Add Group` */ static onGroupAdd: ContextMenuEventListener; /** Create menu for `Add Node` */ static onMenuAdd: ContextMenuEventListener; static showMenuNodeOptionalInputs: ContextMenuEventListener; static showMenuNodeOptionalOutputs: ContextMenuEventListener; static onShowMenuNodeProperties: ContextMenuEventListener; static onResizeNode: ContextMenuEventListener; static onMenuNodeCollapse: ContextMenuEventListener; static onMenuNodePin: ContextMenuEventListener; static onMenuNodeMode: ContextMenuEventListener; static onMenuNodeColors: ContextMenuEventListener; static onMenuNodeShapes: ContextMenuEventListener; static onMenuNodeRemove: ContextMenuEventListener; static onMenuNodeClone: ContextMenuEventListener; constructor( canvas: HTMLCanvasElement | string, graph?: LGraph, options?: { skip_render?: boolean; autoresize?: boolean; } ); static active_canvas: HTMLCanvasElement; allow_dragcanvas: boolean; allow_dragnodes: boolean; /** allow to control widgets, buttons, collapse, etc */ allow_interaction: boolean; /** allows to change a connection with having to redo it again */ allow_reconnect_links: boolean; /** allow selecting multi nodes without pressing extra keys */ multi_select: boolean; /** No effect */ allow_searchbox: boolean; always_render_background: boolean; autoresize?: boolean; background_image: string; bgcanvas: HTMLCanvasElement; bgctx: CanvasRenderingContext2D; canvas: HTMLCanvasElement; canvas_mouse: Vector2; clear_background: boolean; connecting_node: LGraphNode | null; connections_width: number; ctx: CanvasRenderingContext2D; current_node: LGraphNode | null; default_connection_color: { input_off: string; input_on: string; output_off: string; output_on: string; }; default_link_color: string; dirty_area: Vector4 | null; dirty_bgcanvas?: boolean; dirty_canvas?: boolean; drag_mode: boolean; dragging_canvas: boolean; dragging_rectangle: Vector4 | null; ds: DragAndScale; /** used for transition */ editor_alpha: number; filter: any; fps: number; frame: number; graph: LGraph; highlighted_links: Record; highquality_render: boolean; inner_text_font: string; is_rendering: boolean; last_draw_time: number; last_mouse: Vector2; /** * Possible duplicated with `last_mouse` * https://github.com/jagenjo/litegraph.js/issues/70 */ last_mouse_position: Vector2; /** Timestamp of last mouse click, defaults to 0 */ last_mouseclick: number; links_render_mode: | typeof LiteGraph.STRAIGHT_LINK | typeof LiteGraph.LINEAR_LINK | typeof LiteGraph.SPLINE_LINK; live_mode: boolean; node_capturing_input: LGraphNode | null; node_dragged: LGraphNode | null; node_in_panel: LGraphNode | null; node_over: LGraphNode | null; node_title_color: string; node_widget: [LGraphNode, IWidget] | null; /** Called by `LGraphCanvas.drawBackCanvas` */ onDrawBackground: | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) | null; /** Called by `LGraphCanvas.drawFrontCanvas` */ onDrawForeground: | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) | null; onDrawOverlay: ((ctx: CanvasRenderingContext2D) => void) | null; /** Called by `LGraphCanvas.processMouseDown` */ onMouse: ((event: MouseEvent) => boolean) | null; /** Called by `LGraphCanvas.drawFrontCanvas` and `LGraphCanvas.drawLinkTooltip` */ onDrawLinkTooltip: ((ctx: CanvasRenderingContext2D, link: LLink, _this: this) => void) | null; /** Called by `LGraphCanvas.selectNodes` */ onNodeMoved: ((node: LGraphNode) => void) | null; /** Called by `LGraphCanvas.processNodeSelected` */ onNodeSelected: ((node: LGraphNode) => void) | null; /** Called by `LGraphCanvas.deselectNode` */ onNodeDeselected: ((node: LGraphNode) => void) | null; /** Called by `LGraphCanvas.processNodeDblClicked` */ onShowNodePanel: ((node: LGraphNode) => void) | null; /** Called by `LGraphCanvas.processNodeDblClicked` */ onNodeDblClicked: ((node: LGraphNode) => void) | null; /** Called by `LGraphCanvas.selectNodes` */ onSelectionChange: ((nodes: Record) => void) | null; /** Called by `LGraphCanvas.showSearchBox` */ onSearchBox: | (( helper: Element, value: string, graphCanvas: LGraphCanvas ) => string[]) | null; onSearchBoxSelection: | ((name: string, event: MouseEvent, graphCanvas: LGraphCanvas) => void) | null; pause_rendering: boolean; render_canvas_border: boolean; render_collapsed_slots: boolean; render_connection_arrows: boolean; render_connections_border: boolean; render_connections_shadows: boolean; render_curved_connections: boolean; render_execution_order: boolean; render_only_selected: boolean; render_shadows: boolean; render_title_colored: boolean; round_radius: number; selected_group: null | LGraphGroup; selected_group_resizing: boolean; selected_nodes: Record; show_info: boolean; title_text_font: string; /** set to true to render title bar with gradients */ use_gradients: boolean; visible_area: DragAndScale["visible_area"]; visible_links: LLink[]; visible_nodes: LGraphNode[]; zoom_modify_alpha: boolean; /** clears all the data inside */ clear(): void; /** assigns a graph, you can reassign graphs to the same canvas */ setGraph(graph: LGraph, skipClear?: boolean): void; /** opens a graph contained inside a node in the current graph */ openSubgraph(graph: LGraph): void; /** closes a subgraph contained inside a node */ closeSubgraph(): void; /** assigns a canvas */ setCanvas(canvas: HTMLCanvasElement, skipEvents?: boolean): void; /** binds mouse, keyboard, touch and drag events to the canvas */ bindEvents(): void; /** unbinds mouse events from the canvas */ unbindEvents(): void; /** * 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 **/ enableWebGL(): void; /** * marks as dirty the canvas, this way it will be rendered again * @param fg if the foreground canvas is dirty (the one containing the nodes) * @param bg if the background canvas is dirty (the one containing the wires) */ setDirty(fg: boolean, bg: boolean): void; /** * Used to attach the canvas in a popup * @return the window where the canvas is attached (the DOM root node) */ getCanvasWindow(): Window; /** starts rendering the content of the canvas when needed */ startRendering(): void; /** stops rendering the content of the canvas (to save resources) */ stopRendering(): void; processMouseDown(e: MouseEvent): boolean | undefined; processMouseMove(e: MouseEvent): boolean | undefined; processMouseUp(e: MouseEvent): boolean | undefined; processMouseWheel(e: MouseEvent): boolean | undefined; /** returns true if a position (in graph space) is on top of a node little corner box */ isOverNodeBox(node: LGraphNode, canvasX: number, canvasY: number): boolean; /** returns true if a position (in graph space) is on top of a node input slot */ isOverNodeInput( node: LGraphNode, canvasX: number, canvasY: number, slotPos: Vector2 ): boolean; /** process a key event */ processKey(e: KeyboardEvent): boolean | undefined; copyToClipboard(): void; pasteFromClipboard(): void; processDrop(e: DragEvent): void; checkDropItem(e: DragEvent): void; processNodeDblClicked(n: LGraphNode): void; processNodeSelected(n: LGraphNode, e: MouseEvent): void; processNodeDeselected(node: LGraphNode): void; /** selects a given node (or adds it to the current selection) */ selectNode(node: LGraphNode, add?: boolean): void; /** selects several nodes (or adds them to the current selection) */ selectNodes(nodes?: LGraphNode[], add?: boolean): void; /** removes a node from the current selection */ deselectNode(node: LGraphNode): void; /** removes all nodes from the current selection */ deselectAllNodes(): void; /** deletes all nodes in the current selection from the graph */ deleteSelectedNodes(): void; /** centers the camera on a given node */ centerOnNode(node: LGraphNode): void; /** changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom */ setZoom(value: number, center: Vector2): void; /** brings a node to front (above all other nodes) */ bringToFront(node: LGraphNode): void; /** sends a node to the back (below all other nodes) */ sendToBack(node: LGraphNode): void; /** checks which nodes are visible (inside the camera area) */ computeVisibleNodes(nodes: LGraphNode[]): LGraphNode[]; /** renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) */ draw(forceFG?: boolean, forceBG?: boolean): void; /** draws the front canvas (the one containing all the nodes) */ drawFrontCanvas(): void; /** draws some useful stats in the corner of the canvas */ renderInfo(ctx: CanvasRenderingContext2D, x: number, y: number): void; /** draws the back canvas (the one containing the background and the connections) */ drawBackCanvas(): void; /** draws the given node inside the canvas */ drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void; /** draws graphic for node's slot */ drawSlotGraphic(ctx: CanvasRenderingContext2D, pos: number[], shape: SlotShape, horizontal: boolean): void; /** draws the shape of the given node in the canvas */ drawNodeShape( node: LGraphNode, ctx: CanvasRenderingContext2D, size: [number, number], fgColor: string, bgColor: string, selected: boolean, mouseOver: boolean ): void; /** draws every connection visible in the canvas */ drawConnections(ctx: CanvasRenderingContext2D): void; /** * draws a link between two points * @param a start pos * @param b end pos * @param link the link object with all the link info * @param skipBorder ignore the shadow of the link * @param flow show flow animation (for events) * @param color the color for the link * @param startDir the direction enum * @param endDir the direction enum * @param numSublines number of sublines (useful to represent vec3 or rgb) **/ renderLink( a: Vector2, b: Vector2, link: object, skipBorder: boolean, flow: boolean, color?: string, startDir?: number, endDir?: number, numSublines?: number ): void; computeConnectionPoint( a: Vector2, b: Vector2, t: number, startDir?: number, endDir?: number ): void; drawExecutionOrder(ctx: CanvasRenderingContext2D): void; /** draws the widgets stored inside a node */ drawNodeWidgets( node: LGraphNode, posY: number, ctx: CanvasRenderingContext2D, activeWidget: object ): void; /** process an event on widgets */ processNodeWidgets( node: LGraphNode, pos: Vector2, event: Event, activeWidget: object ): void; /** draws every group area in the background */ drawGroups(canvas: any, ctx: CanvasRenderingContext2D): void; adjustNodesSize(): void; /** resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode */ resize(width?: number, height?: number): void; /** * switches to live mode (node shapes are not rendered, only the content) * this feature was designed when graphs where meant to create user interfaces **/ switchLiveMode(transition?: boolean): void; onNodeSelectionChange(): void; touchHandler(event: TouchEvent): void; showLinkMenu(link: LLink, e: any): false; prompt( title: string, value: any, callback: Function, event: any ): HTMLDivElement; showSearchBox(event?: MouseEvent): void; showEditPropertyValue(node: LGraphNode, property: any, options: any): void; createDialog( html: string, options?: { position?: Vector2; event?: MouseEvent } ): void; convertOffsetToCanvas: DragAndScale["convertOffsetToCanvas"]; convertCanvasToOffset: DragAndScale["convertCanvasToOffset"]; /** converts event coordinates from canvas2D to graph coordinates */ convertEventToCanvasOffset(e: MouseEvent): Vector2; /** adds some useful properties to a mouse event, like the position in graph coordinates */ adjustMouseEvent(e: MouseEvent): void; getCanvasMenuOptions(): ContextMenuItem[]; getNodeMenuOptions(node: LGraphNode): ContextMenuItem[]; getGroupMenuOptions(): ContextMenuItem[]; /** Called by `getCanvasMenuOptions`, replace default options */ getMenuOptions?(): ContextMenuItem[]; /** Called by `getCanvasMenuOptions`, append to default options */ getExtraMenuOptions?(): ContextMenuItem[]; /** Called when mouse right click */ processContextMenu(node: LGraphNode, event: Event): void; } declare class ContextMenu { static trigger( element: HTMLElement, event_name: string, params: any, origin: any ): void; static isCursorOverElement(event: MouseEvent, element: HTMLElement): void; static closeAllContextMenus(window: Window): void; constructor(values: ContextMenuItem[], options?: IContextMenuOptions, window?: Window); options: IContextMenuOptions; parentMenu?: ContextMenu; lock: boolean; current_submenu?: ContextMenu; addItem( name: string, value: ContextMenuItem, options?: IContextMenuOptions ): void; close(e?: MouseEvent, ignore_parent_menu?: boolean): void; getTopMenu(): void; getFirstEvent(): void; } declare function clamp(v: number, min: number, max: number): number; ================================================ FILE: src/litegraph.js ================================================ (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 use_deferred_actions: true, //executes actions during the graph execution flow 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 an object with some properties * like onCreate, inputs:Array, outputs:Array, properties, onExecute * @method buildNodeClassFromObject * @param {String} name node name with namespace (p.e.: 'math/sum') * @param {Object} object methods expected onCreate, inputs, outputs, properties, onExecute */ buildNodeClassFromObject: function( name, object ) { var ctor_code = ""; if(object.inputs) for(var i=0; i < object.inputs.length; ++i) { var _name = object.inputs[i][0]; var _type = object.inputs[i][1]; if(_type && _type.constructor === String) _type = '"'+_type+'"'; ctor_code += "this.addInput('"+_name+"',"+_type+");\n"; } if(object.outputs) for(var i=0; i < object.outputs.length; ++i) { var _name = object.outputs[i][0]; var _type = object.outputs[i][1]; if(_type && _type.constructor === String) _type = '"'+_type+'"'; ctor_code += "this.addOutput('"+_name+"',"+_type+");\n"; } if(object.properties) for(var i in object.properties) { var prop = object.properties[i]; if(prop && prop.constructor === String) prop = '"'+prop+'"'; ctor_code += "this.addProperty('"+i+"',"+prop+");\n"; } ctor_code += "if(this.onCreate)this.onCreate()"; var classobj = Function(ctor_code); for(var i in object) if(i!="inputs" && i!="outputs" && i!="properties") classobj.prototype[i] = object[i]; classobj.title = object.title || name.split("/").pop(); classobj.desc = object.desc || "Generated from object"; this.registerNodeType(name, classobj); return classobj; }, /** * 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 = ""; if(param_types !== null) //null means no inputs { var names = LiteGraph.getParameterNames(func); for (var i = 0; i < names.length; ++i) { var type = 0; if(param_types) { //type = param_types[i] != null ? "'" + param_types[i] + "'" : "0"; if( param_types[i] != null && param_types[i].constructor === String ) type = "'" + param_types[i] + "'" ; else if( param_types[i] != null ) type = param_types[i]; } code += "this.addInput('" + names[i] + "'," + type + ");\n"; } } if(return_type !== null) //null means no output code += "this.addOutput('out'," + (return_type != null ? (return_type.constructor === String ? "'" + 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); return 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); //not optimal: executes possible pending actions in node, problem is it is not optimized //it is done here as if it was done in the later loop it wont be called in the node missed the onExecute //from now on it will iterate only on executable nodes which is faster 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(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length) node.executePendingActions(); 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 { //catch errors try { //iterations for (var i = 0; i < num; i++) { for (var j = 0; j < limit; ++j) { var node = nodes[j]; if(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length) node.executePendingActions(); 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 ] != undefined)) 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 execution of actions that were deferred when the action was triggered * @method executePendingActions */ LGraphNode.prototype.executePendingActions = function() { if(!this._waiting_actions || !this._waiting_actions.length) return; for(var i = 0; i < this._waiting_actions.length;++i) { var p = this._waiting_actions[i]; this.onAction(p[0],p[1],p[2],p[3],p[4]); } this._waiting_actions.length = 0; } /** * Triggers the node code execution, place a boolean/counter to mark the node as being executed * @method doExecute * @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; } } else { } 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, action_slot ) { 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, action_slot); 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]; //instead of executing them now, it will be executed in the next graph loop, to ensure data flow if(LiteGraph.use_deferred_actions && node.onExecute) { if(!node._waiting_actions) node._waiting_actions = []; node._waiting_actions.push([target_connection.name, param, options, link_info.target_slot]); } else { // wrap node.onAction(target_connection.name, param); node.actionDo( target_connection.name, param, options, link_info.target_slot ); } } } }; /** * 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 * @method getBounding * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] */ LGraphNode.prototype.getBounding = function(out, compute_outer) { out = out || new Float32Array(4); const nodePos = this.pos; const isCollapsed = this.flags.collapsed; const nodeSize = this.size; let left_offset = 0; // 1 offset due to how nodes are rendered let right_offset = 1 ; let top_offset = 0; let bottom_offset = 0; if (compute_outer) { // 4 offset for collapsed node connection points left_offset = 4; // 6 offset for right shadow and collapsed node connection points right_offset = 6 + left_offset; // 4 offset for collapsed nodes top connection points top_offset = 4; // 5 offset for bottom shadow and collapsed node connection points bottom_offset = 5 + top_offset; } out[0] = nodePos[0] - left_offset; out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset; out[2] = isCollapsed ? (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset : nodeSize[0] + right_offset; out[3] = isCollapsed ? LiteGraph.NODE_TITLE_HEIGHT + bottom_offset : nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset; 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= 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 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.setAttribute("tabindex",1); //otherwise key events are ignored 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]; if(node.clonable === false) continue; 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, true))) { 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 deltaX = event.deltaX || event.deltax || 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(deltaX) w.value += 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: "" + (info.label ? info.label : i) + "" + "" + value + "", 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 = ""; 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 = " "; else dialog.innerHTML = " "; 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 = "Search "; if (options.do_type_filter){ dialog.innerHTML += ""; dialog.innerHTML += ""; } dialog.innerHTML += "
    "; 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) { if(that.search_box) 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) { refreshHelper(); 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 (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 = ""; } else if ( (type == "enum" || type == "combo") && info.values) { input_html = ""; } else if (type == "boolean" || type == "toggle") { input_html = ""; } else { console.warn("unknown type: " + type); return; } var dialog = this.createDialog( "" + (info.label ? info.label : property) + "" + input_html + "", 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 = "
    "; 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 = "✕"; 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 = ""; 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(""+node.type+""+(node.constructor.desc || "")+""); panel.addHTML("

    Properties

    "); 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 = "
    "; 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 = ""; 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 = " "; 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 = " + NameType"; 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 = " "; 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 = " + NameType"; 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: "No color" }); for (var i in LGraphCanvas.node_colors) { var color = LGraphCanvas.node_colors[i]; var value = { value: i, content: "" + i + "" }; 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( "Name", 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 = "
    " //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; } ================================================ FILE: src/litegraph.test.js ================================================ describe("register node types", () => { let lg; let Sum; beforeEach(() => { jest.resetModules(); lg = require("./litegraph"); Sum = function Sum() { this.addInput("a", "number"); this.addInput("b", "number"); this.addOutput("sum", "number"); }; Sum.prototype.onExecute = function (a, b) { this.setOutputData(0, a + b); }; }); afterEach(() => { jest.restoreAllMocks(); }); test("normal case", () => { lg.LiteGraph.registerNodeType("math/sum", Sum); let node = lg.LiteGraph.registered_node_types["math/sum"]; expect(node).toBeTruthy(); expect(node.type).toBe("math/sum"); expect(node.title).toBe("Sum"); expect(node.category).toBe("math"); expect(node.prototype.configure).toBe( lg.LGraphNode.prototype.configure ); }); test("callback triggers", () => { const consoleSpy = jest .spyOn(console, "log") .mockImplementation(() => {}); lg.LiteGraph.onNodeTypeRegistered = jest.fn(); lg.LiteGraph.onNodeTypeReplaced = jest.fn(); lg.LiteGraph.registerNodeType("math/sum", Sum); expect(lg.LiteGraph.onNodeTypeRegistered).toHaveBeenCalled(); expect(lg.LiteGraph.onNodeTypeReplaced).not.toHaveBeenCalled(); lg.LiteGraph.registerNodeType("math/sum", Sum); expect(lg.LiteGraph.onNodeTypeReplaced).toHaveBeenCalled(); expect(consoleSpy).toHaveBeenCalledWith( expect.stringMatching("replacing node type") ); expect(consoleSpy).toHaveBeenCalledWith( expect.stringMatching("math/sum") ); }); test("node with title", () => { Sum.title = "The sum title"; lg.LiteGraph.registerNodeType("math/sum", Sum); let node = lg.LiteGraph.registered_node_types["math/sum"]; expect(node.title).toBe("The sum title"); expect(node.title).not.toBe(node.name); }); test("handle error simple object", () => { expect(() => lg.LiteGraph.registerNodeType("math/sum", { simple: "type" }) ).toThrow("Cannot register a simple object"); }); test("check shape mapping", () => { lg.LiteGraph.registerNodeType("math/sum", Sum); const node_type = lg.LiteGraph.registered_node_types["math/sum"]; expect(new node_type().shape).toBe(undefined); node_type.prototype.shape = "default"; expect(new node_type().shape).toBe(undefined); node_type.prototype.shape = "box"; expect(new node_type().shape).toBe(lg.LiteGraph.BOX_SHAPE); node_type.prototype.shape = "round"; expect(new node_type().shape).toBe(lg.LiteGraph.ROUND_SHAPE); node_type.prototype.shape = "circle"; expect(new node_type().shape).toBe(lg.LiteGraph.CIRCLE_SHAPE); node_type.prototype.shape = "card"; expect(new node_type().shape).toBe(lg.LiteGraph.CARD_SHAPE); node_type.prototype.shape = "custom_shape"; expect(new node_type().shape).toBe("custom_shape"); // Check that also works for replaced node types jest.spyOn(console, "log").mockImplementation(() => {}); function NewCalcSum(a, b) { return a + b; } lg.LiteGraph.registerNodeType("math/sum", NewCalcSum); const new_node_type = lg.LiteGraph.registered_node_types["math/sum"]; new_node_type.prototype.shape = "box"; expect(new new_node_type().shape).toBe(lg.LiteGraph.BOX_SHAPE); }); test("onPropertyChanged warning", () => { const consoleSpy = jest .spyOn(console, "warn") .mockImplementation(() => {}); Sum.prototype.onPropertyChange = true; lg.LiteGraph.registerNodeType("math/sum", Sum); expect(consoleSpy).toBeCalledTimes(1); expect(consoleSpy).toBeCalledWith( expect.stringContaining("has onPropertyChange method") ); expect(consoleSpy).toBeCalledWith(expect.stringContaining("math/sum")); }); test("registering supported file extensions", () => { expect(lg.LiteGraph.node_types_by_file_extension).toEqual({}); // Create two node types with calc_times overriding .pdf Sum.supported_extensions = ["PDF", "exe", null]; function Times() { this.addInput("a", "number"); this.addInput("b", "number"); this.addOutput("times", "number"); } Times.prototype.onExecute = function (a, b) { this.setOutputData(0, a * b); }; Times.supported_extensions = ["pdf", "jpg"]; lg.LiteGraph.registerNodeType("math/sum", Sum); lg.LiteGraph.registerNodeType("math/times", Times); expect( Object.keys(lg.LiteGraph.node_types_by_file_extension).length ).toBe(3); expect(lg.LiteGraph.node_types_by_file_extension).toHaveProperty("pdf"); expect(lg.LiteGraph.node_types_by_file_extension).toHaveProperty("exe"); expect(lg.LiteGraph.node_types_by_file_extension).toHaveProperty("jpg"); expect(lg.LiteGraph.node_types_by_file_extension.exe).toBe(Sum); expect(lg.LiteGraph.node_types_by_file_extension.pdf).toBe(Times); expect(lg.LiteGraph.node_types_by_file_extension.jpg).toBe(Times); }); test("register in/out slot types", () => { expect(lg.LiteGraph.registered_slot_in_types).toEqual({}); expect(lg.LiteGraph.registered_slot_out_types).toEqual({}); // Test slot type registration with first type lg.LiteGraph.auto_load_slot_types = true; lg.LiteGraph.registerNodeType("math/sum", Sum); expect(lg.LiteGraph.registered_slot_in_types).toEqual({ number: { nodes: ["math/sum"] }, }); expect(lg.LiteGraph.registered_slot_out_types).toEqual({ number: { nodes: ["math/sum"] }, }); // Test slot type registration with second type function ToInt() { this.addInput("string", "string"); this.addOutput("number", "number"); }; ToInt.prototype.onExecute = function (str) { this.setOutputData(0, Number(str)); }; lg.LiteGraph.registerNodeType("basic/to_int", ToInt); expect(lg.LiteGraph.registered_slot_in_types).toEqual({ number: { nodes: ["math/sum"] }, string: { nodes: ["basic/to_int"] }, }); expect(lg.LiteGraph.registered_slot_out_types).toEqual({ number: { nodes: ["math/sum", "basic/to_int"] }, }); }); }); describe("unregister node types", () => { let lg; let Sum; beforeEach(() => { jest.resetModules(); lg = require("./litegraph"); Sum = function Sum() { this.addInput("a", "number"); this.addInput("b", "number"); this.addOutput("sum", "number"); }; Sum.prototype.onExecute = function (a, b) { this.setOutputData(0, a + b); }; }); afterEach(() => { jest.restoreAllMocks(); }); test("remove by name", () => { lg.LiteGraph.registerNodeType("math/sum", Sum); expect(lg.LiteGraph.registered_node_types["math/sum"]).toBeTruthy(); lg.LiteGraph.unregisterNodeType("math/sum"); expect(lg.LiteGraph.registered_node_types["math/sum"]).toBeFalsy(); }); test("remove by object", () => { lg.LiteGraph.registerNodeType("math/sum", Sum); expect(lg.LiteGraph.registered_node_types["math/sum"]).toBeTruthy(); lg.LiteGraph.unregisterNodeType(Sum); expect(lg.LiteGraph.registered_node_types["math/sum"]).toBeFalsy(); }); test("try removing with wrong name", () => { expect(() => lg.LiteGraph.unregisterNodeType("missing/type")).toThrow( "node type not found: missing/type" ); }); test("no constructor name", () => { function BlankNode() {} BlankNode.constructor = {} lg.LiteGraph.registerNodeType("blank/node", BlankNode); expect(lg.LiteGraph.registered_node_types["blank/node"]).toBeTruthy() lg.LiteGraph.unregisterNodeType("blank/node"); expect(lg.LiteGraph.registered_node_types["blank/node"]).toBeFalsy(); }) }); ================================================ FILE: src/nodes/audio.js ================================================ (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); ================================================ FILE: src/nodes/base.js ================================================ //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 JSONParse() { this.addInput("parse", LiteGraph.ACTION); this.addInput("json", "string"); this.addOutput("done", LiteGraph.EVENT); this.addOutput("object", "object"); this.widget = this.addWidget("button","parse","",this.parse.bind(this)); this._str = null; this._obj = null; } JSONParse.title = "JSON Parse"; JSONParse.desc = "Parses JSON String into object"; JSONParse.prototype.parse = function() { if(!this._str) return; try { this._str = this.getInputData(1); this._obj = JSON.parse(this._str); this.boxcolor = "#AEA"; this.triggerSlot(0); } catch (err) { this.boxcolor = "red"; } } JSONParse.prototype.onExecute = function() { this._str = this.getInputData(1); this.setOutputData(1, this._obj); }; JSONParse.prototype.onAction = function(name) { if(name == "parse") this.parse(); } LiteGraph.registerNodeType("basic/jsonparse", JSONParse); //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); ================================================ FILE: src/nodes/events.js ================================================ //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); //Sequence of events function WaitAll() { var that = this; this.addInput("", LiteGraph.ACTION); this.addInput("", LiteGraph.ACTION); this.addOutput("", LiteGraph.EVENT); this.addWidget("button","+",null,function(){ that.addInput("", LiteGraph.ACTION); that.size[0] = 90; }); this.size = [90, 70]; this.ready = []; } WaitAll.title = "WaitAll"; WaitAll.desc = "Wait until all input events arrive then triggers output"; WaitAll.prototype.getTitle = function() { return ""; }; WaitAll.prototype.onDrawBackground = function(ctx) { if (this.flags.collapsed) { return; } for(var i = 0; i < this.inputs.length; ++i) { var y = i * LiteGraph.NODE_SLOT_HEIGHT + 10; ctx.fillStyle = this.ready[i] ? "#AFB" : "#000"; ctx.fillRect(20, y, 10, 10); } } WaitAll.prototype.onAction = function(action, param, options, slot_index) { if(slot_index == null) return; //check all this.ready.length = this.outputs.length; this.ready[slot_index] = true; for(var i = 0; i < this.ready.length;++i) if(!this.ready[i]) return; //pass this.reset(); this.triggerSlot(0); }; WaitAll.prototype.reset = function() { this.ready.length = 0; } LiteGraph.registerNodeType("events/waitAll", WaitAll); //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); ================================================ FILE: src/nodes/geometry.js ================================================ (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); ================================================ FILE: src/nodes/glfx.js ================================================ (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); ================================================ FILE: src/nodes/glshaders.js ================================================ (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); ================================================ FILE: src/nodes/gltextures.js ================================================ (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 = "

    pixelcode must be vec3, uvcode must be vec2, is optional

    \

    uv: tex. coords

    color: texture colorB: textureB

    time: scene time value: input value

    For multiline you must type: result = ...

    "; 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 (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 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); ================================================ FILE: src/nodes/input.js ================================================ (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); ================================================ FILE: src/nodes/interface.js ================================================ //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); ================================================ FILE: src/nodes/logic.js ================================================ (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); ================================================ FILE: src/nodes/math.js ================================================ (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; } this.setOutputData(i, value); } }; MathCompare.prototype.onGetOutputs = function() { return [ ["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=", { 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); ================================================ FILE: src/nodes/math3d.js ================================================ (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); ================================================ FILE: src/nodes/midi.js ================================================ (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); ================================================ FILE: src/nodes/network.js ================================================ //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); //HTTP Request function HTTPRequestNode() { var that = this; this.addInput("request", LiteGraph.ACTION); this.addInput("url", "string"); this.addProperty("url", ""); this.addOutput("ready", LiteGraph.EVENT); this.addOutput("data", "string"); this.addWidget("button", "Fetch", null, this.fetch.bind(this)); this._data = null; this._fetching = null; } HTTPRequestNode.title = "HTTP Request"; HTTPRequestNode.desc = "Fetch data through HTTP"; HTTPRequestNode.prototype.fetch = function() { var url = this.properties.url; if(!url) return; this.boxcolor = "#FF0"; var that = this; this._fetching = fetch(url) .then(resp=>{ if(!resp.ok) { this.boxcolor = "#F00"; that.trigger("error"); } else { this.boxcolor = "#0F0"; return resp.text(); } }) .then(data=>{ that._data = data; that._fetching = null; that.trigger("ready"); }); } HTTPRequestNode.prototype.onAction = function(evt) { if(evt == "request") this.fetch(); } HTTPRequestNode.prototype.onExecute = function() { this.setOutputData(1, this._data); }; HTTPRequestNode.prototype.onGetOutputs = function() { return [["error",LiteGraph.EVENT]]; } LiteGraph.registerNodeType("network/httprequest", HTTPRequestNode); })(this); ================================================ FILE: src/nodes/others.js ================================================ (function(global) { var LiteGraph = global.LiteGraph; /* in types :: run in console :: var s=""; LiteGraph.slot_types_in.forEach(function(el){s+=el+"\n";}); console.log(s); */ if(typeof LiteGraph.slot_types_default_in == "undefined") LiteGraph.slot_types_default_in = {}; //[]; LiteGraph.slot_types_default_in["_event_"] = "widget/button"; LiteGraph.slot_types_default_in["array"] = "basic/array"; LiteGraph.slot_types_default_in["boolean"] = "basic/boolean"; LiteGraph.slot_types_default_in["number"] = "widget/number"; LiteGraph.slot_types_default_in["object"] = "basic/data"; LiteGraph.slot_types_default_in["string"] = ["basic/string","string/concatenate"]; LiteGraph.slot_types_default_in["vec2"] = "math3d/xy-to-vec2"; LiteGraph.slot_types_default_in["vec3"] = "math3d/xyz-to-vec3"; LiteGraph.slot_types_default_in["vec4"] = "math3d/xyzw-to-vec4"; /* out types :: run in console :: var s=""; LiteGraph.slot_types_out.forEach(function(el){s+=el+"\n";}); console.log(s); */ if(typeof LiteGraph.slot_types_default_out == "undefined") LiteGraph.slot_types_default_out = {}; LiteGraph.slot_types_default_out["_event_"] = ["logic/IF","events/sequencer","events/log","events/counter"]; LiteGraph.slot_types_default_out["array"] = ["basic/watch","basic/set_array","basic/array[]"]; LiteGraph.slot_types_default_out["boolean"] = ["logic/IF","basic/watch","math/branch","math/gate"]; LiteGraph.slot_types_default_out["number"] = ["basic/watch" ,{node:"math/operation",properties:{OP:"*"},title:"A*B"} ,{node:"math/operation",properties:{OP:"/"},title:"A/B"} ,{node:"math/operation",properties:{OP:"+"},title:"A+B"} ,{node:"math/operation",properties:{OP:"-"},title:"A-B"} ,{node:"math/compare",outputs:[["A==B", "boolean"]],title:"A==B"} ,{node:"math/compare",outputs:[["A>B", "boolean"]],title:"A>B"} ,{node:"math/compare",outputs:[["A console.log('Example app listening on http://127.0.0.1:8000')) ================================================ FILE: utils/temp.js ================================================ LiteGraph.registerNodeType("color/palette",{title:"Palette",desc:"Generates a color",inputs:[["f","number"]],outputs:[["Color","color"]],properties:{colorA:"#444444",colorB:"#44AAFF",colorC:"#44FFAA",colorD:"#FFFFFF"},onExecute:function(){var a=[];null!=this.properties.colorA&&a.push(hex2num(this.properties.colorA));null!=this.properties.colorB&&a.push(hex2num(this.properties.colorB));null!=this.properties.colorC&&a.push(hex2num(this.properties.colorC));null!=this.properties.colorD&&a.push(hex2num(this.properties.colorD)); var b=this.getInputData(0);null==b&&(b=0.5);1b&&(b=0);if(0!=a.length){var c=[0,0,0];if(0==b)c=a[0];else if(1==b)c=a[a.length-1];else{var d=(a.length-1)*b,b=a[Math.floor(d)],a=a[Math.floor(d)+1],d=d-Math.floor(d);c[0]=b[0]*(1-d)+a[0]*d;c[1]=b[1]*(1-d)+a[1]*d;c[2]=b[2]*(1-d)+a[2]*d}for(var e in c)c[e]/=255;this.boxcolor=colorToString(c);this.setOutputData(0,c)}}}); LiteGraph.registerNodeType("graphics/frame",{title:"Frame",desc:"Frame viewerew",inputs:[["","image"]],size:[200,200],widgets:[{name:"resize",text:"Resize box",type:"button"},{name:"view",text:"View Image",type:"button"}],onDrawBackground:function(a){this.frame&&a.drawImage(this.frame,0,0,this.size[0],this.size[1])},onExecute:function(){this.frame=this.getInputData(0);this.setDirtyCanvas(!0)},onWidget:function(a,b){if("resize"==b.name&&this.frame){var c=this.frame.width,d=this.frame.height;c||null== this.frame.videoWidth||(c=this.frame.videoWidth,d=this.frame.videoHeight);c&&d&&(this.size=[c,d]);this.setDirtyCanvas(!0,!0)}else"view"==b.name&&this.show()},show:function(){showElement&&this.frame&&showElement(this.frame)}}); LiteGraph.registerNodeType("visualization/graph",{desc:"Shows a graph of the inputs",inputs:[["",0],["",0],["",0],["",0]],size:[200,200],properties:{min:-1,max:1,bgColor:"#000"},onDrawBackground:function(a){var b=["#FFF","#FAA","#AFA","#AAF"];null!=this.properties.bgColor&&""!=this.properties.bgColor&&(a.fillStyle="#000",a.fillRect(2,2,this.size[0]-4,this.size[1]-4));if(this.data){var c=this.properties.min,d=this.properties.max,e;for(e in this.data){var h=this.data[e];if(h&&null!=this.getInputInfo(e)){a.strokeStyle= b[e];a.beginPath();for(var k=h.length/this.size[0],g=0;gf&&(f=0);0==g?a.moveTo(g/k,this.size[1]-5-(this.size[1]-10)*f):a.lineTo(g/k,this.size[1]-5-(this.size[1]-10)*f)}a.stroke()}}}},onExecute:function(){this.data||(this.data=[]);for(var a in this.inputs){var b=this.getInputData(a);"number"==typeof b?(b=b?b:0,this.data[a]||(this.data[a]=[]),this.data[a].push(b),this.data[a].length>this.size[1]-4&&(this.data[a]=this.data[a].slice(1,this.data[a].length))): this.data[a]=b}this.data.length&&this.setDirtyCanvas(!0)}}); LiteGraph.registerNodeType("graphics/supergraph",{title:"Supergraph",desc:"Shows a nice circular graph",inputs:[["x","number"],["y","number"],["c","color"]],outputs:[["","image"]],widgets:[{name:"clear_alpha",text:"Clear Alpha",type:"minibutton"},{name:"clear_color",text:"Clear color",type:"minibutton"}],properties:{size:256,bgcolor:"#000",lineWidth:1},bgcolor:"#000",flags:{allow_fastrender:!0},onLoad:function(){this.createCanvas()},createCanvas:function(){this.canvas=document.createElement("canvas"); this.canvas.width=this.properties.size;this.canvas.height=this.properties.size;this.oldpos=null;this.clearCanvas(!0)},onExecute:function(){var a=this.getInputData(0),b=this.getInputData(1),c=this.getInputData(2);if(null!=a||null!=b){a||(a=0);b||(b=0);var a=0.95*a,b=0.95*b,d=this.properties.size;d==this.canvas.width&&d==this.canvas.height||this.createCanvas();if(this.oldpos){var e=this.canvas.getContext("2d");null==c?c="rgba(255,255,255,0.5)":"object"==typeof c&&(c=colorToString(c));e.strokeStyle= c;e.beginPath();e.moveTo(this.oldpos[0],this.oldpos[1]);this.oldpos=[(0.5*a+0.5)*d,(0.5*b+0.5)*d];e.lineTo(this.oldpos[0],this.oldpos[1]);e.stroke();this.canvas.dirty=!0;this.setOutputData(0,this.canvas)}else this.oldpos=[(0.5*a+0.5)*d,(0.5*b+0.5)*d]}},clearCanvas:function(a){var b=this.canvas.getContext("2d");a?(b.clearRect(0,0,this.canvas.width,this.canvas.height),this.trace("Clearing alpha")):(b.fillStyle=this.properties.bgcolor,b.fillRect(0,0,this.canvas.width,this.canvas.height))},onWidget:function(a, b){"clear_color"==b.name?this.clearCanvas(!1):"clear_alpha"==b.name&&this.clearCanvas(!0)},onPropertyChange:function(a,b){if("size"==a)this.properties.size=parseInt(b),this.createCanvas();else if("bgcolor"==a)this.properties.bgcolor=b,this.createCanvas();else if("lineWidth"==a)this.properties.lineWidth=parseInt(b),this.canvas.getContext("2d").lineWidth=this.properties.lineWidth;else return!1;return!0}}); LiteGraph.registerNodeType("graphics/imagefade",{title:"Image fade",desc:"Fades between images",inputs:[["img1","image"],["img2","image"],["fade","number"]],outputs:[["","image"]],properties:{fade:0.5,width:512,height:512},widgets:[{name:"resizeA",text:"Resize to A",type:"button"},{name:"resizeB",text:"Resize to B",type:"button"}],onLoad:function(){this.createCanvas();var a=this.canvas.getContext("2d");a.fillStyle="#000";a.fillRect(0,0,this.properties.width,this.properties.height)},createCanvas:function(){this.canvas= document.createElement("canvas");this.canvas.width=this.properties.width;this.canvas.height=this.properties.height},onExecute:function(){var a=this.canvas.getContext("2d");this.canvas.width=this.canvas.width;var b=this.getInputData(0);null!=b&&a.drawImage(b,0,0,this.canvas.width,this.canvas.height);b=this.getInputData(2);null==b?b=this.properties.fade:this.properties.fade=b;a.globalAlpha=b;b=this.getInputData(1);null!=b&&a.drawImage(b,0,0,this.canvas.width,this.canvas.height);a.globalAlpha=1;this.setOutputData(0, this.canvas);this.setDirtyCanvas(!0)}}); LiteGraph.registerNodeType("graphics/image",{title:"Image",desc:"Image loader",inputs:[],outputs:[["frame","image"]],properties:{url:""},widgets:[{name:"load",text:"Load",type:"button"}],onLoad:function(){""!=this.properties.url&&null==this.img&&this.loadImage(this.properties.url)},onStart:function(){},onExecute:function(){this.img||(this.boxcolor="#000");this.img&&this.img.width?this.setOutputData(0,this.img):this.setOutputData(0,null);this.img.dirty&&(this.img.dirty=!1)},onPropertyChange:function(a, b){this.properties[a]=b;"url"==a&&""!=b&&this.loadImage(b);return!0},loadImage:function(a){if(""==a)this.img=null;else{this.trace("loading image...");this.img=document.createElement("img");this.img.src="miniproxy.php?url="+a;this.boxcolor="#F95";var b=this;this.img.onload=function(){b.trace("Image loaded, size: "+b.img.width+"x"+b.img.height);this.dirty=!0;b.boxcolor="#9F9";b.setDirtyCanvas(!0)}}},onWidget:function(a,b){"load"==b.name&&this.loadImage(this.properties.url)}}); LiteGraph.registerNodeType("graphics/cropImage",{title:"Crop",desc:"Crop Image",inputs:[["","image"]],outputs:[["","image"]],properties:{width:256,height:256,x:0,y:0,scale:1},size:[50,20],onLoad:function(){this.createCanvas()},createCanvas:function(){this.canvas=document.createElement("canvas");this.canvas.width=this.properties.width;this.canvas.height=this.properties.height},onExecute:function(){var a=this.getInputData(0);a&&(a.width?(this.canvas.getContext("2d").drawImage(a,-this.properties.x,-this.properties.y, a.width*this.properties.scale,a.height*this.properties.scale),this.setOutputData(0,this.canvas)):this.setOutputData(0,null))},onPropertyChange:function(a,b){this.properties[a]=b;"scale"==a?(this.properties[a]=parseFloat(b),0==this.properties[a]&&(this.trace("Error in scale"),this.properties[a]=1)):this.properties[a]=parseInt(b);this.createCanvas();return!0}}); LiteGraph.registerNodeType("graphics/video",{title:"Video",desc:"Video playback",inputs:[["t","number"]],outputs:[["frame","image"],["t","number"],["d","number"]],properties:{url:""},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"}],onClick:function(a){if(this.video&&20>distance([a.canvasX,a.canvasY],[this.pos[0]+55,this.pos[1]+40]))return this.play(),!0},onKeyDown:function(a){32== a.keyCode&&this.playPause()},onLoad:function(){""!=this.properties.url&&this.loadVideo(this.properties.url)},play:function(){this.video&&(this.trace("Video playing"),this.video.play())},playPause:function(){this.video&&(this.video.paused?this.play():this.pause())},stop:function(){this.video&&(this.trace("Video stopped"),this.video.pause(),this.video.currentTime=0)},pause:function(){this.video&&(this.trace("Video paused"),this.video.pause())},onExecute:function(){if(this.video){var a=this.getInputData(0); a&&0<=a&&1>=a&&(this.video.currentTime=a*this.video.duration,this.video.pause());this.video.dirty=!0;this.setOutputData(0,this.video);this.setOutputData(1,this.video.currentTime);this.setOutputData(2,this.video.duration);this.setDirtyCanvas(!0)}},onStart:function(){},onStop:function(){this.pause()},loadVideo:function(a){this.video=document.createElement("video");a?this.video.src=a:(this.video.src="modules/data/video.webm",this.properties.url=this.video.src);this.video.type="type=video/mp4";this.video.muted= !0;this.video.autoplay=!1;var b=this;this.video.addEventListener("loadedmetadata",function(a){b.trace("Duration: "+b.video.duration+" seconds");b.trace("Size: "+b.video.videoWidth+","+b.video.videoHeight);b.setDirtyCanvas(!0);this.width=this.videoWidth;this.height=this.videoHeight});this.video.addEventListener("progress",function(a){});this.video.addEventListener("error",function(a){b.trace("Error loading video: "+this.src);if(this.error)switch(this.error.code){case this.error.MEDIA_ERR_ABORTED:b.trace("You stopped the video."); break;case this.error.MEDIA_ERR_NETWORK:b.trace("Network error - please try again later.");break;case this.error.MEDIA_ERR_DECODE:b.trace("Video is broken..");break;case this.error.MEDIA_ERR_SRC_NOT_SUPPORTED:b.trace("Sorry, your browser can't play this video.")}});this.video.addEventListener("ended",function(a){b.trace("Ended.");this.play()})},onPropertyChange:function(a,b){this.properties[a]=b;"url"==a&&""!=b&&this.loadVideo(b);return!0},onWidget:function(a,b){"demo"==b.name?this.loadVideo():"play"== b.name&&this.video&&this.playPause();"stop"==b.name?this.stop():"mute"==b.name&&this.video&&(this.video.muted=!this.video.muted)}}); ================================================ FILE: utils/test.sh ================================================ #!/bin/bash set -eo pipefail cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" export NVM_DIR=$HOME/.nvm source "$NVM_DIR/nvm.sh" # This are versions 12, 14, 16, 18 NODE_VERSIONS=("lts/erbium" "lts/fermium" "lts/gallium" "lts/hydrogen") for NODE_VERSION in "${NODE_VERSIONS[@]}"; do nvm install "$NODE_VERSION" nvm exec "$NODE_VERSION" npm install nvm exec "$NODE_VERSION" npm test done