GithubHelp home page GithubHelp logo

yertleturtlegit / scanning-pipeline Goto Github PK

View Code? Open in Web Editor NEW
2.0 1.0 0.0 61.85 MB

wip basic node graph system for photometric stereo

Home Page: https://scanning-pipeline.netlify.app

License: GNU General Public License v3.0

HTML 0.27% JavaScript 99.52% CSS 0.21%
depth-estimation node-graph normal-mapping photometric-stereo webgl2

scanning-pipeline's Introduction

Scanning Pipeline

Live Demo in Web Browser (Chromium)

https://scanning-pipeline.netlify.app/

Run and Edit on Local Maschine

  1. Clone the repository:

    git clone https://github.com/YertleTurtleGit/scanning-pipeline
  2. Open your preferred web browser and open the 'index.html' file.

    Chromium based browsers work best. File access from files needs to be enabled:

    chromium --allow-file-access-from-files index.html
  3. You can now edit the files with your preferred Editor and refresh the web browser to see the results.

Recommendations

  1. Use Visual Studio Code and open the workspace file.

    Now you can run the application by using the configuration in launch.json.

    In Visual Studio Code: 'Run > Start Debugging (F5)'

  2. Use jsdoc.

    npm install jsdoc
  3. Install npm types for three.js and chart.js.

    npm install @types/three
    npm install @types/chart.js

scanning-pipeline's People

Contributors

yertleturtlegit avatar

Stargazers

 avatar  avatar

Watchers

 avatar

scanning-pipeline's Issues

Remove hard code.

Remove hard code.

// TODO Remove hard code.

         75,
         this.uiCanvas.clientWidth / this.uiCanvas.clientHeight
      );
      // TODO Remove hard code.
      this.uiCamera.position.set(10, 2, 10 / 2);
      this.uiCamera.lookAt(new THREE.Vector3(0, 0, this.cameraDistance / 3));
      this.uiCamera.rotateZ(180 * (Math.PI / 180));

91ede7817a75095b33a4e6d2e10d79e27bde5827

Handle multiple outputs.

Handle multiple outputs.

// TODO Handle multiple outputs.

/* exported NodeCallback */

class NodeCallback {
   /**
    * @param {GraphNodeUI} graphNodeUI
    */
   constructor(graphNodeUI) {
      this.graphNodeUI = graphNodeUI;
      /**
       * @public
       * @type {boolean}
       */
      this.abortFlag = false;
   }

   /**
    * @public
    * @param {string} info
    */
   setInfo(info) {}

   /**
    * @public
    * @param {number} progressPercent
    */
   setProgressPercent(progressPercent) {
      this.graphNodeUI.domProgressElement.hidden = false;
      if (progressPercent <= 0) {
         this.graphNodeUI.domProgressElement.removeAttribute("value");
      } else if (progressPercent >= 100) {
         this.graphNodeUI.domProgressElement.hidden = true;
      } else {
         this.graphNodeUI.domProgressElement.value = progressPercent;
      }
   }
}

class NodeGraph {
   /**
    * @param {HTMLElement} parentElement
    */
   constructor(parentElement) {
      /**
       * @protected
       * @type {GraphNode[]}
       */
      this.registeredNodes = [];

      /**
       * @protected
       * @type {GraphNodeUI[]}
       */
      this.placedNodes = [];

      this.parentElement = parentElement;

      this.domCanvas = document.createElement("canvas");
      this.domCanvas.style.backgroundColor = "transparent";
      this.domCanvas.style.position = "absolute";
      this.domCanvas.style.width = "100%";
      this.domCanvas.style.height = "100%";

      window.addEventListener("resize", this.resizeHandler.bind(this));
      this.parentElement.appendChild(this.domCanvas);
      this.domCanvasContext = this.domCanvas.getContext("2d");

      this.currentMousePosition = {
         x: this.parentElement.clientWidth / 2,
         y: this.parentElement.clientHeight / 2,
      };

      this.parentElement.addEventListener(
         "mousemove",
         this.mousemoveHandler.bind(this)
      );
      this.parentElement.addEventListener(
         "mouseup",
         this.mouseUpHandler.bind(this)
      );

      /**
       * @private
       * @type {GraphNodeUI}
       */
      this.grabbedNode = null;

      /**
       * @private
       * @type {GraphNodeInputUI | GraphNodeOutputUI}
       */
      this.linkedNodeIO = null;

      this.resizeHandler();
   }

   /**
    * @public
    * @param {Function} nodeExecuter
    * @returns {GraphNode}
    */
   registerNode(nodeExecuter) {
      const graphNode = new GraphNode(nodeExecuter, false);
      this.registeredNodes.push(graphNode);
      return graphNode;
   }

   /**
    * @public
    * @param {Function} nodeExecuter
    * @param {string[]} dependencies
    * @returns {GraphNode}
    */
   registerNodeAsWorker(nodeExecuter, ...dependencies) {
      const graphNode = new GraphNode(nodeExecuter, true, ...dependencies);
      this.registeredNodes.push(graphNode);
      return graphNode;
   }

   /**
    * @param {GraphNode} graphNode
    * @param {{x:number, y:number}} position
    * @returns {GraphNodeUI}
    */
   placeNode(graphNode, position = this.currentMousePosition) {
      const graphNodeUI = new GraphNodeUI(graphNode, this);
      this.placedNodes.push(graphNodeUI);
      graphNodeUI.setPosition(position);
      this.parentElement.appendChild(graphNodeUI.domElement);
      return graphNodeUI;
   }

   /**
    * @public
    * @param {InputGraphNode} inputGraphNode
    * @param {{x:number, y:number}} position
    */
   placeInputGraphNode(inputGraphNode, position) {
      this.placedNodes.push(inputGraphNode);
      if (position) inputGraphNode.setPosition(position);
      this.parentElement.appendChild(inputGraphNode.domElement);
   }

   /**
    * @public
    * @param {string} type
    * @param {{x:number, y:number}} position
    * @param {any} initValue
    * @returns {GraphNodeUI}
    */
   createInputNode(type, position, initValue = undefined) {
      const inputGraphNode = new InputGraphNode(this, type);
      if (initValue) {
         inputGraphNode.setValue(initValue);
      }
      this.placeInputGraphNode(inputGraphNode, position);
      return inputGraphNode;
   }

   /**
    * @public
    * @param {Promise<GraphNodeOutputUI>} output
    * @param {Promise<GraphNodeInputUI>} input
    */
   async connect(output, input) {
      const inputResolved = await input;
      const outputResolved = await output;
      inputResolved.setConnection(outputResolved);
      this.updateConnectionUI();
   }

   /**
    * @param {GraphNodeUI} graphNodeUI
    */
   displaceNode(graphNodeUI) {
      const graphNodeIndex = this.placedNodes.indexOf(graphNodeUI);
      this.placedNodes.splice(graphNodeIndex);
   }

   doubleClickHandler() {
      // TODO Add node selection menu.
      this.placeNode(this.registeredNodes[0]);
   }

   /**
    * @private
    */
   resizeHandler() {
      this.domCanvas.height = this.parentElement.clientHeight;
      this.domCanvas.width = this.parentElement.clientWidth;
      this.updateConnectionUI();
   }

   /**
    * @public
    * @param {GraphNodeInputUI | GraphNodeOutputUI} graphNodeIO
    */
   toggleConnection(graphNodeIO) {
      if (this.linkedNodeIO === null) {
         this.linkedNodeIO = graphNodeIO;
      } else if (
         graphNodeIO instanceof GraphNodeInputUI &&
         this.linkedNodeIO instanceof GraphNodeOutputUI
      ) {
         graphNodeIO.setConnection(this.linkedNodeIO);
         this.linkedNodeIO = null;
      } else if (
         graphNodeIO instanceof GraphNodeOutputUI &&
         this.linkedNodeIO instanceof GraphNodeInputUI
      ) {
         this.linkedNodeIO.setConnection(graphNodeIO);
         this.linkedNodeIO = null;
      }
      this.updateConnectionUI();
   }

   /**
    * @private
    * @returns {Promise<{input: GraphNodeInputUI, output: GraphNodeOutputUI}[]>}
    */
   async getConnections() {
      /** @type {{input: GraphNodeInputUI, output: GraphNodeOutputUI}[]} */
      const connections = [];
      this.placedNodes.forEach(async (node) => {
         connections.push(...(await node.getConnections()));
      });
      return connections;
   }

   /**
    * @public
    */
   async updateConnectionUI() {
      this.domCanvasContext.clearRect(
         0,
         0,
         this.domCanvasContext.canvas.width,
         this.domCanvasContext.canvas.height
      );
      this.domCanvasContext.beginPath();
      this.domCanvasContext.strokeStyle = "white";
      this.domCanvasContext.lineWidth = 2;

      const connections = await this.getConnections();
      connections.forEach((connection) => {
         if (connection.input && connection.output) {
            const startRect =
               connection.input.domElement.getBoundingClientRect();
            const start = {
               x: startRect.left,
               y: (startRect.top + startRect.bottom) / 2,
            };
            const endRect =
               connection.output.domElement.getBoundingClientRect();
            const end = {
               x: endRect.right,
               y: (endRect.top + endRect.bottom) / 2,
            };
            this.domCanvasContext.moveTo(start.x, start.y);
            this.domCanvasContext.lineTo(end.x, end.y);
         }
      });

      this.domCanvasContext.stroke();
   }

   mouseUpHandler() {
      this.grabbedNode = null;
      this.linkedNodeIO = null;
      this.updateConnectionUI();
   }

   /**
    * @param {GraphNodeUI} graphNode
    */
   setGrabbedNode(graphNode) {
      this.grabbedNode = graphNode;
   }

   /**
    * @param {GraphNodeInputUI | GraphNodeOutputUI} linkedNodeIO
    */
   setLinkedNodeIO(linkedNodeIO) {
      this.linkedNodeIO = linkedNodeIO;
      this.updateConnectionUI();
   }

   /**
    * @private
    * @param {MouseEvent} mouseEvent
    */
   mousemoveHandler(mouseEvent) {
      this.currentMousePosition = {
         x: mouseEvent.pageX - this.parentElement.offsetLeft,
         y: mouseEvent.pageY - this.parentElement.offsetTop,
      };

      if (this.grabbedNode) {
         this.grabbedNode.setPosition(this.currentMousePosition);
         this.updateConnectionUI();
      }

      if (this.linkedNodeIO) {
         this.domCanvasContext.clearRect(
            0,
            0,
            this.domCanvasContext.canvas.width,
            this.domCanvasContext.canvas.height
         );
         this.domCanvasContext.beginPath();
         this.domCanvasContext.strokeStyle = "white";
         this.domCanvasContext.lineWidth = 2;

         const startRect = this.linkedNodeIO.domElement.getBoundingClientRect();
         const start = {
            x: startRect.right,
            y: (startRect.top + startRect.bottom) / 2,
         };
         const end = this.currentMousePosition;
         this.domCanvasContext.moveTo(start.x, start.y);
         this.domCanvasContext.lineTo(end.x, end.y);

         this.domCanvasContext.stroke();
      }
   }
}

class GraphNode {
   /**
    * @param {Function} executer
    * @param {boolean} asWorker
    * @param {string[]} dependencies
    */
   constructor(executer, asWorker, ...dependencies) {
      /**
       * @public
       * @type {Function}
       */
      this.executer = executer;

      this.asWorker = asWorker;

      /**
       * @private
       * @type {string[]}
       */
      this.dependencies = dependencies;

      /**
       * @protected
       * @type {GraphNodeInput[]}
       */
      this.graphNodeInputs = [];

      /**
       * @protected
       * @type {GraphNodeOutput[]}
       */
      this.graphNodeOutputs = [];

      /**
       * @protected
       * @type {{output: GraphNodeOutput, input: GraphNodeInput}[]}
       */
      this.outputConnections = [];

      /**
       * @private
       * @type {boolean}
       */
      this.initialized = false;

      /**
       * @private
       * @type {boolean}
       */
      this.initializing = false;
   }

   /**
    * @public
    * @returns {Promise<string>}
    */
   async getDependenciesSource() {
      let dependenciesSource = "";
      for (let i = 0; i < this.dependencies.length; i++) {
         const dependencySource = await new Promise((resolve) => {
            window.fetch(this.dependencies[i]).then(async (response) => {
               resolve(await response.text());
            });
         });
         dependenciesSource += dependencySource;
      }
      return dependenciesSource;
   }

   /**
    * @private
    */
   async initialize() {
      while (this.initializing === true) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }
      if (this.initialized === false) {
         this.initializing = true;
         await this.readFunctionSourceWithDocs();
         this.initialized = true;
         this.initializing = false;
      }
   }

   /**
    * @public
    * @returns {Promise<GraphNodeInput[]>}
    */
   async getInputs() {
      await this.initialize();
      return this.graphNodeInputs;
   }

   /**
    * @public
    * @returns {Promise<GraphNodeOutput[]>}
    */
   async getOutputs() {
      await this.initialize();
      return this.graphNodeOutputs;
   }

   /**
    * @public
    * @returns {string}
    */
   getName() {
      return this.executer.name;
   }

   /**
    * @private
    */
   async readFunctionSourceWithDocs() {
      const scriptElements = Array.from(document.scripts);

      const functionSource = this.executer.toString();

      for (let i = 0, count = scriptElements.length; i < count; i++) {
         const scriptSource = await new Promise((resolve) => {
            window.fetch(scriptElements[i].src).then(async (response) => {
               resolve(await response.text());
            });
         });

         if (scriptSource.includes(functionSource)) {
            const jsDoc = new RegExp(
               "(\\/\\*\\*\\s*\\n([^\\*]|(\\*(?!\\/)))*\\*\\/)(\\s*\\n*\\s*)(.*)" +
                  this.getName() +
                  "\\s*\\(",
               "mg"
            )
               .exec(scriptSource)[0]
               .replaceAll("\n", "")
               .replaceAll("*", "");

            const jsDocArguments = jsDoc.split("@");
            jsDocArguments.shift();

            jsDocArguments.forEach((argument) => {
               const argumentType = argument.split(" ")[0];

               if (argumentType === "param") {
                  const argumentVarType = argument
                     .split("{")[1]
                     .split("}")[0]
                     .replace("Promise<", "")
                     .replace(">", "");
                  const argumentVarName = argument.split("} ")[1].split(" ")[0];
                  const argumentDescription = argument.split(
                     " " + argumentVarName + " ",
                     2
                  )[1];

                  this.graphNodeInputs.push(
                     new GraphNodeInput(
                        argumentVarName,
                        argumentVarType,
                        argumentDescription
                     )
                  );
               } else if (argumentType === "returns") {
                  const argumentVarType = argument
                     .split("{")[1]
                     .split("}")[0]
                     .replace("Promise<", "")
                     .replace(">", "");
                  const argumentDescription = argument.split("} ")[1];

                  this.graphNodeOutputs.push(
                     new GraphNodeOutput(argumentVarType, argumentDescription)
                  );
               }
            });
         }
      }
   }
}

class GraphNodeInput {
   /**
    * @param {string} name
    * @param {string} type
    * @param {string} description
    */
   constructor(name, type, description = undefined) {
      //this.name = name.replace(/([A-Z])/g, " $1");
      this.name = name;
      this.uiName = this.name.charAt(0).toUpperCase() + this.name.slice(1);
      this.type = type;
      this.description = description.replaceAll(/\s\s+/g, " ");
   }
}

class GraphNodeOutput {
   /**
    * @param {string} type
    * @param {string} description
    */
   constructor(type, description = undefined) {
      this.type = type;
      this.description = description;
   }
}

class GraphNodeInputUI extends GraphNodeInput {
   /**
    * @param {GraphNodeInput[]} graphNodeInputs
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNode
    * @returns {GraphNodeInputUI[]}
    */
   static getFromGraphNodeInputs(graphNodeInputs, nodeGraph, graphNode) {
      /** @type {GraphNodeInputUI[]} */
      const graphNodeInputsUI = [];

      graphNodeInputs.forEach((graphNodeInput) => {
         graphNodeInputsUI.push(
            new GraphNodeInputUI(
               graphNodeInput.name,
               graphNodeInput.type,
               graphNodeInput.description,
               nodeGraph,
               graphNode
            )
         );
      });

      return graphNodeInputsUI;
   }

   /**
    * @param {string} name
    * @param {string} type
    * @param {string} description
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNodeUI
    * @param {string} cssClass
    */
   constructor(
      name,
      type,
      description = undefined,
      nodeGraph,
      graphNodeUI,
      cssClass = "graphNodeInput"
   ) {
      super(name, type, description);
      /**
       * @private
       * @type {GraphNodeOutputUI}
       */
      this.connection = null;

      this.nodeGraph = nodeGraph;
      this.graphNodeUI = graphNodeUI;
      this.domElement = document.createElement("li");
      this.domElement.innerText = name;
      this.domElement.title = "[" + this.type + "]\n" + this.description;
      this.domElement.style.textAlign = "left";
      this.domElement.classList.add(cssClass);

      this.domElement.addEventListener("click", this.clickHandler.bind(this));
      this.domElement.addEventListener(
         "mousedown",
         this.mouseHandler.bind(this)
      );
      this.domElement.addEventListener("mouseup", this.mouseHandler.bind(this));
   }

   /**
    * @private
    * @param {MouseEvent} mouseEvent
    */
   clickHandler(mouseEvent) {
      if (mouseEvent.detail > 1) {
         this.doubleClickHandler();
      }
   }

   /**
    * @private
    */
   mouseHandler() {
      this.nodeGraph.toggleConnection(this);
   }

   /**
    * @private
    */
   doubleClickHandler() {
      /*const boundingRect = this.domElement.getBoundingClientRect();
      this.nodeGraph.placeInputGraphNode(
         new InputGraphNode(this.nodeGraph, this),
         { x: boundingRect.left - 200, y: boundingRect.top - 25 }
      );
      this.nodeGraph.setLinkedNodeIO(null);*/
   }

   /**
    * @public
    * @returns {GraphNodeOutputUI}
    */
   getConnection() {
      return this.connection;
   }

   /**
    * @public
    * @param {GraphNodeOutputUI} graphNodeOutput
    */
   setConnection(graphNodeOutput) {
      if (this.connection) {
         this.connection.removeConnection(this);
      }
      graphNodeOutput.addConnection(this);
      this.connection = graphNodeOutput;
      this.graphNodeUI.setRefreshFlag();
   }

   /**
    * @public
    */
   removeConnection() {
      this.connection = null;
   }
}

class GraphNodeOutputUI extends GraphNodeOutput {
   /**
    * @param {GraphNodeOutput[]} graphNodeOutputs
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNode
    * @returns {GraphNodeOutputUI[]}
    */
   static getFromGraphNodeOutputs(graphNodeOutputs, nodeGraph, graphNode) {
      /** @type {GraphNodeOutputUI[]} */
      const graphNodeOutputsUI = [];

      graphNodeOutputs.forEach((graphNodeOutput) => {
         graphNodeOutputsUI.push(
            new GraphNodeOutputUI(
               graphNodeOutput.type,
               graphNodeOutput.description,
               nodeGraph,
               graphNode
            )
         );
      });

      return graphNodeOutputsUI;
   }

   /**
    * @param {string} type
    * @param {string} description
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNode
    * @param {string} cssClass
    */
   constructor(
      type,
      description = undefined,
      nodeGraph,
      graphNode,
      cssClass = "graphNodeInput"
   ) {
      super(type, description);
      /**
       * @private
       * @type {any}
       */
      this.value = undefined;
      this.nodeGraph = nodeGraph;
      /**
       * @public
       * @type {GraphNodeUI}
       */
      this.graphNodeUI = graphNode;
      /**
       * @private
       * @type {GraphNodeInputUI[]}
       */
      this.connections = [];
      this.domElement = document.createElement("li");
      this.domElement.innerText = "โ–ถ";
      this.domElement.title = "[" + this.type + "]";
      this.domElement.style.textAlign = "right";
      this.domElement.classList.add(cssClass);

      this.domElement.addEventListener(
         "mousedown",
         this.mouseHandler.bind(this)
      );
   }

   /**
    * @public
    * @param {GraphNodeInputUI} graphNodeInputUI
    */
   addConnection(graphNodeInputUI) {
      this.connections.push(graphNodeInputUI);
   }

   /**
    * @public
    * @param {GraphNodeInputUI} graphNodeInputUI
    */
   removeConnection(graphNodeInputUI) {
      const id = this.connections.indexOf(graphNodeInputUI);
      this.connections.splice(id);
   }

   /**
    * @public
    * @returns {GraphNodeInputUI[]}
    */
   getConnections() {
      return this.connections;
   }

   /**
    * @public
    * @returns {any}
    */
   getValue() {
      return this.value;
   }

   /**
    * @public
    * @param {any} value
    */
   async setValue(value) {
      this.value = value;

      const outputNodes = this.graphNodeUI.getOutputNodes();

      outputNodes.forEach((outputNode) => {
         if (outputNode !== this.graphNodeUI) {
            outputNode.setRefreshFlag();
         }
      });
   }

   /**
    * @private
    */
   mouseHandler() {
      this.nodeGraph.toggleConnection(this);
   }
}

class GraphNodeUI {
   /**
    * @param {GraphNode} graphNode
    * @param {NodeGraph} nodeGraph
    * @param {string} cssClass
    */
   constructor(graphNode, nodeGraph, cssClass = "graphNode") {
      this.graphNode = graphNode;
      this.nodeGraph = nodeGraph;
      this.cssClass = cssClass;
      this.domElement = document.createElement("span");
      this.position = {
         x: this.nodeGraph.parentElement.clientWidth / 2,
         y: this.nodeGraph.parentElement.clientHeight / 2,
      };
      this.refreshFlag = true;

      if (this.graphNode) {
         this.initialize();
      }
   }

   /**
    * @public
    * @param {string} name
    * @returns {Promise<GraphNodeInputUI>}
    */
   async getInput(name) {
      await this.initialize();

      for (let i = 0; i < this.graphNodeInputs.length; i++) {
         if (this.graphNodeInputs[i].name === name)
            return this.graphNodeInputs[i];
      }
   }

   /**
    * @public
    * @param {string} description
    * @returns {Promise<GraphNodeOutputUI>}
    */
   async getOutput(description = undefined) {
      await this.initialize();

      if (this.graphNodeOutputs.length === 1) {
         return this.graphNodeOutputs[0];
      } else {
         for (let i = 0; i < this.graphNodeOutputs.length; i++) {
            if (this.graphNodeOutputs[i].description === description)
               return this.graphNodeOutputs[i];
         }
      }
   }

   /**
    * @public
    */
   setRefreshFlag() {
      this.refreshFlag = true;
      this.execute();
   }

   /**
    * @public
    */
   async execute() {
      if (this.graphNode.asWorker) {
         if (this.worker) {
            console.log("terminating " + this.graphNode.executer.name + ".");
            this.worker.terminate();
         }
      } else {
         if (this.executerCallback) {
            console.log("aborting " + this.graphNode.executer.name + ".");
            this.executerCallback.abortFlag = true;
         }
      }

      if (this.refreshFlag) {
         this.refreshFlag = false;

         const parameterValues = this.getParameterValues();
         if (parameterValues.includes(undefined)) return;

         console.log(
            "Calling function '" + this.graphNode.executer.name + "'."
         );

         this.executerCallback = new NodeCallback(this);
         this.executerCallback.setProgressPercent(0);

         if (this.graphNode.asWorker) {
            this.executeAsWorker(parameterValues);
         } else {
            this.executeAsPromise(parameterValues);
         }

         if (!parameterValues.includes(undefined)) {
            console.log("Executing " + this.graphNode.executer.name + ".");
         } else {
            console.log(
               "Function '" +
                  this.graphNode.executer.name +
                  "' did not pick up, because at least one parameter is undefined."
            );
         }
      }
   }

   /**
    * @private
    * @param {any[]} parameterValues
    */
   async executeAsPromise(parameterValues) {
      setTimeout(async () => {
         const result = await this.graphNode.executer(
            ...parameterValues,
            this.executerCallback
         );

         this.graphNodeOutputs[0].setValue(result);
         this.refreshValuePreview(result);
         this.executerCallback.setProgressPercent(100);
      });
   }

   /**
    * @private
    * @param {any[]} parameterValues
    */
   async executeAsWorker(parameterValues) {
      const toTransfer = [];
      const toCopy = [];

      let parameterValuesString = "(";
      let pointerCount = 0;
      let copyCount = 0;

      parameterValues.forEach((parameterValue) => {
         if (false && parameterValue instanceof ImageBitmap) {
            toTransfer.push(parameterValue);
            parameterValuesString +=
               "messageEvent.data.pointer[" + String(pointerCount) + "]";
            pointerCount++;
         } else {
            toCopy.push(parameterValue);
            parameterValuesString +=
               "messageEvent.data.copy[" + String(copyCount) + "]";
            copyCount++;
         }
         parameterValuesString += ",";
      });
      parameterValuesString += ")";

      this.worker = await this.createWorker(parameterValuesString);

      const cThis = this;
      this.worker.addEventListener(
         "message",
         async function handler(messageEvent) {
            cThis.worker.removeEventListener(messageEvent.type, handler);
            const resultValue = messageEvent.data;
            // TODO Handle multiple outputs.
            cThis.graphNodeOutputs[0].setValue(resultValue);
            cThis.refreshValuePreview(resultValue);

            cThis.worker.terminate();
            cThis.worker = undefined;
            cThis.executerCallback.setProgressPercent(100);
         }
      );

      this.worker.postMessage(
         { pointer: toTransfer, copy: toCopy },
         toTransfer
      );
   }

   /**
    * @private
    * @param {string} parameterValuesString
    * @returns {Promise<Worker>}
    */
   async createWorker(parameterValuesString) {
      const dependenciesSource = await this.graphNode.getDependenciesSource();

      const workerSource =
         dependenciesSource +
         "\n" +
         "const cSelf = self;\n" +
         "self.addEventListener('message', async (messageEvent) => {\n" +
         "cSelf.postMessage(await " +
         this.graphNode.executer.name +
         parameterValuesString +
         ");\n" +
         "});";

      const blob = new Blob([workerSource], {
         type: "text/javascript",
      });
      const workerSrc = window.URL.createObjectURL(blob);
      return new Worker(workerSrc);
   }

   /**
    * @protected
    * @param {any} value
    */
   refreshValuePreview(value) {
      this.outputUIElement.innerHTML = "";

      if (value instanceof ImageBitmap) {
         const imageCanvas = document.createElement("canvas");
         imageCanvas.width = value.width;
         imageCanvas.height = value.height;
         const context = imageCanvas.getContext("2d");
         context.drawImage(value, 0, 0, value.width, value.height);
         const imageElement = new Image();
         imageElement.style.maxWidth = "100%";
         imageCanvas.style.maxHeight = "5rem";
         this.outputUIElement.style.display = "flex";
         this.outputUIElement.style.justifyContent = "center";
         this.outputUIElement.appendChild(imageElement);
         imageElement.src = imageCanvas.toDataURL();
      } else if (Array.isArray(value) && value[0] instanceof ImageBitmap) {
         const imageCanvas = document.createElement("canvas");
         imageCanvas.width = value[0].width;
         imageCanvas.height = value[0].height;
         const context = imageCanvas.getContext("2d");
         context.drawImage(value[0], 0, 0, value[0].width, value[0].height);
         const imageElement = new Image();
         imageElement.style.maxWidth = "100%";
         imageCanvas.style.maxHeight = "5rem";
         this.outputUIElement.style.display = "flex";
         this.outputUIElement.style.justifyContent = "center";
         this.outputUIElement.appendChild(imageElement);
         imageElement.src = imageCanvas.toDataURL();
      } else if (typeof value === "number") {
         const numberElement = document.createElement("div");
         numberElement.innerText = String(value);
         numberElement.style.textAlign = "center";
         this.outputUIElement.appendChild(numberElement);
      } else if (typeof value === "string") {
         const valueImage = new Image();
         valueImage.src = value;
         valueImage.style.maxWidth = "100%";
         valueImage.style.maxHeight = "5rem";
         this.outputUIElement.style.display = "flex";
         this.outputUIElement.style.justifyContent = "center";
         this.outputUIElement.appendChild(valueImage);
      }

      this.nodeGraph.updateConnectionUI();
   }

   /**
    * @private
    * @returns {any[]}
    */
   getParameterValues() {
      const parameterValues = [];
      this.graphNodeInputs.forEach((input) => {
         const connection = input.getConnection();
         if (connection) {
            parameterValues.push(connection.getValue());
         } else {
            parameterValues.push(undefined);
         }
      });
      return parameterValues;
   }

   /**
    * @public
    */
   async initialize() {
      while (this.initializing) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }
      if (this.initialized) {
         return;
      }
      this.initialized = false;
      this.initializing = true;

      if (this.graphNode) {
         /**
          * @override
          * @protected
          * @type {GraphNodeInputUI[]}
          */
         this.graphNodeInputs = GraphNodeInputUI.getFromGraphNodeInputs(
            await this.graphNode.getInputs(),
            this.nodeGraph,
            this
         );

         /**
          * @override
          * @protected
          * @type {GraphNodeOutputUI[]}
          */
         this.graphNodeOutputs = GraphNodeOutputUI.getFromGraphNodeOutputs(
            await this.graphNode.getOutputs(),
            this.nodeGraph,
            this
         );
      }

      this.domElement.classList.add(this.cssClass);

      const domTitleElement = document.createElement("h1");
      domTitleElement.style.cursor = "grab";
      domTitleElement.addEventListener(
         "mousedown",
         this.mousedownGrabHandler.bind(this)
      );
      domTitleElement.innerText = this.graphNode.getName();
      domTitleElement.style.backgroundColor = "transparent";
      this.domElement.appendChild(domTitleElement);

      this.domProgressElement = document.createElement("progress");
      this.domProgressElement.style.width = "100%";
      this.domProgressElement.value = 0;
      this.domProgressElement.max = 100;
      this.domElement.appendChild(this.domProgressElement);
      this.domProgressElement.hidden = true;

      this.outputUIElement = document.createElement("div");
      this.domElement.appendChild(this.outputUIElement);

      const domIOElement = document.createElement("div");
      const domInputList = document.createElement("ul");
      const domOutputList = document.createElement("ul");

      domIOElement.style.display = "flex";
      domIOElement.style.justifyContent = "space-between";
      domIOElement.style.marginLeft = "-10%";
      domIOElement.style.width = "120%";

      this.domElement.appendChild(domIOElement);
      domIOElement.appendChild(domInputList);
      domIOElement.appendChild(domOutputList);

      this.graphNodeInputs.forEach((graphNodeInput) => {
         domInputList.appendChild(graphNodeInput.domElement);
      });
      this.graphNodeOutputs.forEach((graphNodeOutput) => {
         domOutputList.appendChild(graphNodeOutput.domElement);
      });

      this.execute();

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @public
    * @returns {Promise<{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]>}
    */
   async getConnections() {
      /** @type {{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]} */
      const connections = [];

      this.graphNodeInputs.forEach((graphNodeInput) => {
         if (graphNodeInput.graphNodeUI === this) {
            const output = graphNodeInput.getConnection();
            if (output) {
               connections.push({
                  input: graphNodeInput,
                  output: output,
               });
            }
         }
      });

      return connections;
   }

   /**
    * @public
    * @returns {GraphNodeUI[]}
    */
   getOutputNodes() {
      /** @type {GraphNodeUI[]} */
      const outputNodes = [];

      this.graphNodeOutputs.forEach((graphNodeOutput) => {
         graphNodeOutput.getConnections().forEach((connection) => {
            outputNodes.push(connection.graphNodeUI);
         });
      });
      return outputNodes;
   }

   /**
    * @protected
    */
   mousedownGrabHandler() {
      this.nodeGraph.setGrabbedNode(this);
   }

   /**
    * @public
    * @returns {{x:number, y:number}}
    */
   getPosition() {
      const boundingRect = this.domElement.getBoundingClientRect();
      return {
         x: boundingRect.left + boundingRect.width / 2,
         y: boundingRect.top + 5,
      };
   }

   /**
    * @param {{x:number, y:number}} position
    */
   setPosition(position) {
      this.position = position;
      this.domElement.style.transform =
         "translate(calc(" +
         this.position.x +
         "px - 50%), calc(" +
         this.position.y +
         "px - 0.25rem))";
   }
}

class InputGraphNode extends GraphNodeUI {
   /**
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeInputUI | string} inputNodeOrType
    * @param {string} cssClass
    */
   constructor(nodeGraph, inputNodeOrType, cssClass = "graphNode") {
      super(undefined, nodeGraph, cssClass);

      if (inputNodeOrType instanceof GraphNodeInputUI) {
         this.type = inputNodeOrType.type;
         this.inputNode = inputNodeOrType;
      } else {
         this.type = inputNodeOrType;
      }

      this.initialize();

      if (inputNodeOrType instanceof GraphNodeInputUI)
         this.setConnectionToInputNode();
   }

   /**
    * @private
    */
   async setConnectionToInputNode() {
      this.inputNode.setConnection(this.graphNodeOutputs);
      this.nodeGraph.updateConnectionUI();
   }

   /**
    * @override
    */
   async initialize() {
      while (this.initializing) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }
      if (this.initialized) {
         return;
      }

      this.domElement.classList.add(this.cssClass);

      const domTitleElement = document.createElement("h1");
      domTitleElement.style.cursor = "grab";
      domTitleElement.addEventListener(
         "mousedown",
         this.mousedownGrabHandler.bind(this)
      );
      domTitleElement.innerText = this.type;
      domTitleElement.style.backgroundColor = "transparent";
      this.domElement.appendChild(domTitleElement);

      this.domProgressElement = document.createElement("progress");
      this.domProgressElement.style.width = "100%";
      this.domProgressElement.value = 0;
      this.domProgressElement.max = 100;
      this.domElement.appendChild(this.domProgressElement);
      this.domProgressElement.hidden = true;

      this.outputUIElement = document.createElement("div");
      this.domElement.appendChild(this.outputUIElement);

      const domIOElement = document.createElement("div");
      const domInputList = document.createElement("ul");
      const domOutputList = document.createElement("ul");

      domIOElement.style.display = "flex";
      domIOElement.style.justifyContent = "space-between";
      domIOElement.style.marginLeft = "10%";
      domIOElement.style.width = "100%";

      this.domElement.appendChild(domIOElement);
      domIOElement.appendChild(domInputList);
      domIOElement.appendChild(domOutputList);

      this.inputElement = document.createElement("input");
      this.inputElement.style.width = "80%";
      this.inputElement.style.overflowWrap = "break-word";
      this.inputElement.style.hyphens = "auto";
      this.inputElement.style.whiteSpace = "normal";
      this.inputElement.multiple = false;

      if (this.type === "number") {
         this.inputElement.type = "number";
         domInputList.appendChild(this.inputElement);
      } else if (this.type === "ImageBitmap") {
         this.inputElement.type = "file";
         this.inputElement.accept = "image/*";
      } else if (this.type === "ImageBitmap[]") {
         this.inputElement.type = "file";
         this.inputElement.accept = "image/*";
         this.inputElement.multiple = true;
      } else {
         console.error("Input type '" + this.type + "' not supported.");
      }

      this.inputElement.addEventListener(
         "input",
         this.inputChangeHandler.bind(this)
      );

      domInputList.appendChild(this.inputElement);

      this.graphNodeOutputs = [
         new GraphNodeOutputUI(
            this.type,
            "[" + this.type + "]",
            this.nodeGraph,
            this
         ),
      ];

      domOutputList.appendChild(this.graphNodeOutputs[0].domElement);

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @public
    * @param {any} value
    */
   async setValue(value) {
      this.inputElement.value = value;
      this.inputElement.dispatchEvent(new Event("input"));
   }

   /**
    * @private
    * @param {InputEvent} inputEvent
    */
   inputChangeHandler(inputEvent) {
      if (this.type === "number") {
         let value = Number(
            /** @type {HTMLInputElement} */ (inputEvent.target).value
         );
         if (!value) {
            value = 0;
         }
         this.graphNodeOutputs[0].setValue(value);
         this.refreshValuePreview(value);
      } else if (this.type === "ImageBitmap") {
         const nodeCallback = new NodeCallback(this);
         nodeCallback.setProgressPercent(0);

         const imageLoaderWorker = new Worker("./src/image-loader-worker.js");

         imageLoaderWorker.addEventListener("message", async (messageEvent) => {
            const imageBitmap = messageEvent.data;
            this.graphNodeOutputs[0].setValue(imageBitmap);
            this.refreshValuePreview(imageBitmap);
            nodeCallback.setProgressPercent(100);
         });
         imageLoaderWorker.postMessage(inputEvent.target.files[0]);
      } else if (this.type === "ImageBitmap[]") {
         const nodeCallback = new NodeCallback(this);
         nodeCallback.setProgressPercent(0);

         const files = Array.from(inputEvent.target.files);
         const imageCount = files.length;
         const imageBitmapArray = [];

         files.forEach((file) => {
            const imageLoaderWorker = new Worker(
               "./src/image-loader-worker.js"
            );

            imageLoaderWorker.addEventListener(
               "message",
               async (messageEvent) => {
                  const imageBitmap = messageEvent.data;
                  imageBitmapArray.push(imageBitmap);
                  if (imageBitmapArray.length === imageCount) {
                     this.graphNodeOutputs[0].setValue(imageBitmapArray);
                     this.refreshValuePreview(imageBitmap);
                  }
                  nodeCallback.setProgressPercent(
                     (imageBitmapArray.length / imageCount) * 100
                  );
               }
            );
            imageLoaderWorker.postMessage(file);
         });
      }
   }

   /**
    * @override
    * @public
    * @returns {Promise<{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]>}
    */
   async getConnections() {
      // TODO Handle multiple outputs.
      return [{ input: this.inputNode, output: this.graphNodeOutputs[0] }];
   }

   /**
    * @override
    * @public
    */
   async execute() {
      this.refreshFlag = false;
   }
}

d08de5eadcf45762c694dd19aa8ede220856fc60

Handle multiple outputs.

Handle multiple outputs.

// TODO Handle multiple outputs.

/* exported NodeCallback */

class NodeCallback {
   /**
    * @param {GraphNodeUI} graphNodeUI
    */
   constructor(graphNodeUI) {
      this.graphNodeUI = graphNodeUI;
      /**
       * @public
       * @type {boolean}
       */
      this.abortFlag = false;
   }

   /**
    * @public
    * @param {string} info
    */
   setInfo(info) {}

   /**
    * @public
    * @param {number} progressPercent
    */
   setProgressPercent(progressPercent) {
      this.graphNodeUI.domProgressElement.hidden = false;
      if (progressPercent <= 0) {
         this.graphNodeUI.domProgressElement.removeAttribute("value");
      } else if (progressPercent >= 100) {
         this.graphNodeUI.domProgressElement.hidden = true;
      } else {
         this.graphNodeUI.domProgressElement.value = progressPercent;
      }
   }
}

class NodeGraph {
   /**
    * @param {HTMLElement} parentElement
    */
   constructor(parentElement) {
      /**
       * @protected
       * @type {GraphNode[]}
       */
      this.registeredNodes = [];

      /**
       * @protected
       * @type {GraphNodeUI[]}
       */
      this.placedNodes = [];

      this.parentElement = parentElement;

      this.domCanvas = document.createElement("canvas");
      this.domCanvas.style.backgroundColor = "transparent";
      this.domCanvas.style.position = "absolute";
      this.domCanvas.style.width = "100%";
      this.domCanvas.style.height = "100%";

      window.addEventListener("resize", this.resizeHandler.bind(this));
      this.parentElement.appendChild(this.domCanvas);
      this.domCanvasContext = this.domCanvas.getContext("2d");

      this.currentMousePosition = {
         x: this.parentElement.clientWidth / 2,
         y: this.parentElement.clientHeight / 2,
      };

      this.parentElement.addEventListener(
         "mousemove",
         this.mousemoveHandler.bind(this)
      );
      this.parentElement.addEventListener(
         "mouseup",
         this.mouseUpHandler.bind(this)
      );

      /**
       * @private
       * @type {GraphNodeUI}
       */
      this.grabbedNode = null;

      /**
       * @private
       * @type {GraphNodeInputUI | GraphNodeOutputUI}
       */
      this.linkedNodeIO = null;

      this.resizeHandler();
   }

   /**
    * @public
    * @param {Function} nodeExecuter
    * @returns {GraphNode}
    */
   registerNode(nodeExecuter) {
      const graphNode = new GraphNode(nodeExecuter, false);
      this.registeredNodes.push(graphNode);
      return graphNode;
   }

   /**
    * @public
    * @param {Function} nodeExecuter
    * @param {string[]} dependencies
    * @returns {GraphNode}
    */
   registerNodeAsWorker(nodeExecuter, ...dependencies) {
      const graphNode = new GraphNode(nodeExecuter, true, ...dependencies);
      this.registeredNodes.push(graphNode);
      return graphNode;
   }

   /**
    * @param {GraphNode} graphNode
    * @param {{x:number, y:number}} position
    * @returns {GraphNodeUI}
    */
   placeNode(graphNode, position = this.currentMousePosition) {
      const graphNodeUI = new GraphNodeUI(graphNode, this);
      this.placedNodes.push(graphNodeUI);
      graphNodeUI.setPosition(position);
      this.parentElement.appendChild(graphNodeUI.domElement);
      return graphNodeUI;
   }

   /**
    * @public
    * @param {InputGraphNode} inputGraphNode
    * @param {{x:number, y:number}} position
    */
   placeInputGraphNode(inputGraphNode, position) {
      this.placedNodes.push(inputGraphNode);
      if (position) inputGraphNode.setPosition(position);
      this.parentElement.appendChild(inputGraphNode.domElement);
   }

   /**
    * @public
    * @param {string} type
    * @param {{x:number, y:number}} position
    * @param {any} initValue
    * @returns {GraphNodeUI}
    */
   createInputNode(type, position, initValue = undefined) {
      const inputGraphNode = new InputGraphNode(this, type);
      if (initValue) {
         inputGraphNode.setValue(initValue);
      }
      this.placeInputGraphNode(inputGraphNode, position);
      return inputGraphNode;
   }

   /**
    * @public
    * @param {Promise<GraphNodeOutputUI>} output
    * @param {Promise<GraphNodeInputUI>} input
    */
   async connect(output, input) {
      const inputResolved = await input;
      const outputResolved = await output;
      inputResolved.setConnection(outputResolved);
      this.updateConnectionUI();
   }

   /**
    * @param {GraphNodeUI} graphNodeUI
    */
   displaceNode(graphNodeUI) {
      const graphNodeIndex = this.placedNodes.indexOf(graphNodeUI);
      this.placedNodes.splice(graphNodeIndex);
   }

   doubleClickHandler() {
      // TODO Add node selection menu.
      this.placeNode(this.registeredNodes[0]);
   }

   /**
    * @private
    */
   resizeHandler() {
      this.domCanvas.height = this.parentElement.clientHeight;
      this.domCanvas.width = this.parentElement.clientWidth;
      this.updateConnectionUI();
   }

   /**
    * @public
    * @param {GraphNodeInputUI | GraphNodeOutputUI} graphNodeIO
    */
   toggleConnection(graphNodeIO) {
      if (this.linkedNodeIO === null) {
         this.linkedNodeIO = graphNodeIO;
      } else if (
         graphNodeIO instanceof GraphNodeInputUI &&
         this.linkedNodeIO instanceof GraphNodeOutputUI
      ) {
         graphNodeIO.setConnection(this.linkedNodeIO);
         this.linkedNodeIO = null;
      } else if (
         graphNodeIO instanceof GraphNodeOutputUI &&
         this.linkedNodeIO instanceof GraphNodeInputUI
      ) {
         this.linkedNodeIO.setConnection(graphNodeIO);
         this.linkedNodeIO = null;
      }
      this.updateConnectionUI();
   }

   /**
    * @private
    * @returns {Promise<{input: GraphNodeInputUI, output: GraphNodeOutputUI}[]>}
    */
   async getConnections() {
      /** @type {{input: GraphNodeInputUI, output: GraphNodeOutputUI}[]} */
      const connections = [];
      this.placedNodes.forEach(async (node) => {
         connections.push(...(await node.getConnections()));
      });
      return connections;
   }

   /**
    * @public
    */
   async updateConnectionUI() {
      this.domCanvasContext.clearRect(
         0,
         0,
         this.domCanvasContext.canvas.width,
         this.domCanvasContext.canvas.height
      );
      this.domCanvasContext.beginPath();
      this.domCanvasContext.strokeStyle = "white";
      this.domCanvasContext.lineWidth = 2;

      const connections = await this.getConnections();
      connections.forEach((connection) => {
         if (connection.input && connection.output) {
            const startRect =
               connection.input.domElement.getBoundingClientRect();
            const start = {
               x: startRect.left,
               y: (startRect.top + startRect.bottom) / 2,
            };
            const endRect =
               connection.output.domElement.getBoundingClientRect();
            const end = {
               x: endRect.right,
               y: (endRect.top + endRect.bottom) / 2,
            };
            this.domCanvasContext.moveTo(start.x, start.y);
            this.domCanvasContext.lineTo(end.x, end.y);
         }
      });

      this.domCanvasContext.stroke();
   }

   mouseUpHandler() {
      this.grabbedNode = null;
      this.linkedNodeIO = null;
      this.updateConnectionUI();
   }

   /**
    * @param {GraphNodeUI} graphNode
    */
   setGrabbedNode(graphNode) {
      this.grabbedNode = graphNode;
   }

   /**
    * @param {GraphNodeInputUI | GraphNodeOutputUI} linkedNodeIO
    */
   setLinkedNodeIO(linkedNodeIO) {
      this.linkedNodeIO = linkedNodeIO;
      this.updateConnectionUI();
   }

   /**
    * @private
    * @param {MouseEvent} mouseEvent
    */
   mousemoveHandler(mouseEvent) {
      this.currentMousePosition = {
         x: mouseEvent.pageX - this.parentElement.offsetLeft,
         y: mouseEvent.pageY - this.parentElement.offsetTop,
      };

      if (this.grabbedNode) {
         this.grabbedNode.setPosition(this.currentMousePosition);
         this.updateConnectionUI();
      }

      if (this.linkedNodeIO) {
         this.domCanvasContext.clearRect(
            0,
            0,
            this.domCanvasContext.canvas.width,
            this.domCanvasContext.canvas.height
         );
         this.domCanvasContext.beginPath();
         this.domCanvasContext.strokeStyle = "white";
         this.domCanvasContext.lineWidth = 2;

         const startRect = this.linkedNodeIO.domElement.getBoundingClientRect();
         const start = {
            x: startRect.right,
            y: (startRect.top + startRect.bottom) / 2,
         };
         const end = this.currentMousePosition;
         this.domCanvasContext.moveTo(start.x, start.y);
         this.domCanvasContext.lineTo(end.x, end.y);

         this.domCanvasContext.stroke();
      }
   }
}

class GraphNode {
   /**
    * @param {Function} executer
    * @param {boolean} asWorker
    * @param {string[]} dependencies
    */
   constructor(executer, asWorker, ...dependencies) {
      /**
       * @public
       * @type {Function}
       */
      this.executer = executer;

      this.asWorker = asWorker;

      /**
       * @private
       * @type {string[]}
       */
      this.dependencies = dependencies;

      /**
       * @protected
       * @type {GraphNodeInput[]}
       */
      this.graphNodeInputs = [];

      /**
       * @protected
       * @type {GraphNodeOutput[]}
       */
      this.graphNodeOutputs = [];

      /**
       * @protected
       * @type {{output: GraphNodeOutput, input: GraphNodeInput}[]}
       */
      this.outputConnections = [];

      /**
       * @private
       * @type {boolean}
       */
      this.initialized = false;

      /**
       * @private
       * @type {boolean}
       */
      this.initializing = false;
   }

   /**
    * @public
    * @returns {Promise<string>}
    */
   async getDependenciesSource() {
      let dependenciesSource = "";
      for (let i = 0; i < this.dependencies.length; i++) {
         const dependencySource = await new Promise((resolve) => {
            window.fetch(this.dependencies[i]).then(async (response) => {
               resolve(await response.text());
            });
         });
         dependenciesSource += dependencySource;
      }
      return dependenciesSource;
   }

   /**
    * @private
    */
   async initialize() {
      while (this.initializing === true) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }
      if (this.initialized === false) {
         this.initializing = true;
         await this.readFunctionSourceWithDocs();
         this.initialized = true;
         this.initializing = false;
      }
   }

   /**
    * @public
    * @returns {Promise<GraphNodeInput[]>}
    */
   async getInputs() {
      await this.initialize();
      return this.graphNodeInputs;
   }

   /**
    * @public
    * @returns {Promise<GraphNodeOutput[]>}
    */
   async getOutputs() {
      await this.initialize();
      return this.graphNodeOutputs;
   }

   /**
    * @public
    * @returns {string}
    */
   getName() {
      return this.executer.name;
   }

   /**
    * @private
    */
   async readFunctionSourceWithDocs() {
      const scriptElements = Array.from(document.scripts);

      const functionSource = this.executer.toString();

      for (let i = 0, count = scriptElements.length; i < count; i++) {
         const scriptSource = await new Promise((resolve) => {
            window.fetch(scriptElements[i].src).then(async (response) => {
               resolve(await response.text());
            });
         });

         if (scriptSource.includes(functionSource)) {
            const jsDoc = new RegExp(
               "(\\/\\*\\*\\s*\\n([^\\*]|(\\*(?!\\/)))*\\*\\/)(\\s*\\n*\\s*)(.*)" +
                  this.getName() +
                  "\\s*\\(",
               "mg"
            )
               .exec(scriptSource)[0]
               .replaceAll("\n", "")
               .replaceAll("*", "");

            const jsDocArguments = jsDoc.split("@");
            jsDocArguments.shift();

            jsDocArguments.forEach((argument) => {
               const argumentType = argument.split(" ")[0];

               if (argumentType === "param") {
                  const argumentVarType = argument
                     .split("{")[1]
                     .split("}")[0]
                     .replace("Promise<", "")
                     .replace(">", "");
                  const argumentVarName = argument.split("} ")[1].split(" ")[0];
                  const argumentDescription = argument.split(
                     " " + argumentVarName + " ",
                     2
                  )[1];

                  this.graphNodeInputs.push(
                     new GraphNodeInput(
                        argumentVarName,
                        argumentVarType,
                        argumentDescription
                     )
                  );
               } else if (argumentType === "returns") {
                  const argumentVarType = argument
                     .split("{")[1]
                     .split("}")[0]
                     .replace("Promise<", "")
                     .replace(">", "");
                  const argumentDescription = argument.split("} ")[1];

                  this.graphNodeOutputs.push(
                     new GraphNodeOutput(argumentVarType, argumentDescription)
                  );
               }
            });
         }
      }
   }
}

class GraphNodeInput {
   /**
    * @param {string} name
    * @param {string} type
    * @param {string} description
    */
   constructor(name, type, description = undefined) {
      //this.name = name.replace(/([A-Z])/g, " $1");
      this.name = name;
      this.uiName = this.name.charAt(0).toUpperCase() + this.name.slice(1);
      this.type = type;
      this.description = description.replaceAll(/\s\s+/g, " ");
   }
}

class GraphNodeOutput {
   /**
    * @param {string} type
    * @param {string} description
    */
   constructor(type, description = undefined) {
      this.type = type;
      this.description = description;
   }
}

class GraphNodeInputUI extends GraphNodeInput {
   /**
    * @param {GraphNodeInput[]} graphNodeInputs
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNode
    * @returns {GraphNodeInputUI[]}
    */
   static getFromGraphNodeInputs(graphNodeInputs, nodeGraph, graphNode) {
      /** @type {GraphNodeInputUI[]} */
      const graphNodeInputsUI = [];

      graphNodeInputs.forEach((graphNodeInput) => {
         graphNodeInputsUI.push(
            new GraphNodeInputUI(
               graphNodeInput.name,
               graphNodeInput.type,
               graphNodeInput.description,
               nodeGraph,
               graphNode
            )
         );
      });

      return graphNodeInputsUI;
   }

   /**
    * @param {string} name
    * @param {string} type
    * @param {string} description
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNodeUI
    * @param {string} cssClass
    */
   constructor(
      name,
      type,
      description = undefined,
      nodeGraph,
      graphNodeUI,
      cssClass = "graphNodeInput"
   ) {
      super(name, type, description);
      /**
       * @private
       * @type {GraphNodeOutputUI}
       */
      this.connection = null;

      this.nodeGraph = nodeGraph;
      this.graphNodeUI = graphNodeUI;
      this.domElement = document.createElement("li");
      this.domElement.innerText = name;
      this.domElement.title = "[" + this.type + "]\n" + this.description;
      this.domElement.style.textAlign = "left";
      this.domElement.classList.add(cssClass);

      this.domElement.addEventListener("click", this.clickHandler.bind(this));
      this.domElement.addEventListener(
         "mousedown",
         this.mouseHandler.bind(this)
      );
      this.domElement.addEventListener("mouseup", this.mouseHandler.bind(this));
   }

   /**
    * @private
    * @param {MouseEvent} mouseEvent
    */
   clickHandler(mouseEvent) {
      if (mouseEvent.detail > 1) {
         this.doubleClickHandler();
      }
   }

   /**
    * @private
    */
   mouseHandler() {
      this.nodeGraph.toggleConnection(this);
   }

   /**
    * @private
    */
   doubleClickHandler() {
      /*const boundingRect = this.domElement.getBoundingClientRect();
      this.nodeGraph.placeInputGraphNode(
         new InputGraphNode(this.nodeGraph, this),
         { x: boundingRect.left - 200, y: boundingRect.top - 25 }
      );
      this.nodeGraph.setLinkedNodeIO(null);*/
   }

   /**
    * @public
    * @returns {GraphNodeOutputUI}
    */
   getConnection() {
      return this.connection;
   }

   /**
    * @public
    * @param {GraphNodeOutputUI} graphNodeOutput
    */
   setConnection(graphNodeOutput) {
      if (this.connection) {
         this.connection.removeConnection(this);
      }
      graphNodeOutput.addConnection(this);
      this.connection = graphNodeOutput;
      this.graphNodeUI.setRefreshFlag();
   }

   /**
    * @public
    */
   removeConnection() {
      this.connection = null;
   }
}

class GraphNodeOutputUI extends GraphNodeOutput {
   /**
    * @param {GraphNodeOutput[]} graphNodeOutputs
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNode
    * @returns {GraphNodeOutputUI[]}
    */
   static getFromGraphNodeOutputs(graphNodeOutputs, nodeGraph, graphNode) {
      /** @type {GraphNodeOutputUI[]} */
      const graphNodeOutputsUI = [];

      graphNodeOutputs.forEach((graphNodeOutput) => {
         graphNodeOutputsUI.push(
            new GraphNodeOutputUI(
               graphNodeOutput.type,
               graphNodeOutput.description,
               nodeGraph,
               graphNode
            )
         );
      });

      return graphNodeOutputsUI;
   }

   /**
    * @param {string} type
    * @param {string} description
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNode
    * @param {string} cssClass
    */
   constructor(
      type,
      description = undefined,
      nodeGraph,
      graphNode,
      cssClass = "graphNodeInput"
   ) {
      super(type, description);
      /**
       * @private
       * @type {any}
       */
      this.value = undefined;
      this.nodeGraph = nodeGraph;
      /**
       * @public
       * @type {GraphNodeUI}
       */
      this.graphNodeUI = graphNode;
      /**
       * @private
       * @type {GraphNodeInputUI[]}
       */
      this.connections = [];
      this.domElement = document.createElement("li");
      this.domElement.innerText = "โ–ถ";
      this.domElement.title = "[" + this.type + "]";
      this.domElement.style.textAlign = "right";
      this.domElement.classList.add(cssClass);

      this.domElement.addEventListener(
         "mousedown",
         this.mouseHandler.bind(this)
      );
   }

   /**
    * @public
    * @param {GraphNodeInputUI} graphNodeInputUI
    */
   addConnection(graphNodeInputUI) {
      this.connections.push(graphNodeInputUI);
   }

   /**
    * @public
    * @param {GraphNodeInputUI} graphNodeInputUI
    */
   removeConnection(graphNodeInputUI) {
      const id = this.connections.indexOf(graphNodeInputUI);
      this.connections.splice(id);
   }

   /**
    * @public
    * @returns {GraphNodeInputUI[]}
    */
   getConnections() {
      return this.connections;
   }

   /**
    * @public
    * @returns {any}
    */
   getValue() {
      return this.value;
   }

   /**
    * @public
    * @param {any} value
    */
   async setValue(value) {
      this.value = value;

      const outputNodes = this.graphNodeUI.getOutputNodes();

      outputNodes.forEach((outputNode) => {
         if (outputNode !== this.graphNodeUI) {
            outputNode.setRefreshFlag();
         }
      });
   }

   /**
    * @private
    */
   mouseHandler() {
      this.nodeGraph.toggleConnection(this);
   }
}

class GraphNodeUI {
   /**
    * @param {GraphNode} graphNode
    * @param {NodeGraph} nodeGraph
    * @param {string} cssClass
    */
   constructor(graphNode, nodeGraph, cssClass = "graphNode") {
      this.graphNode = graphNode;
      this.nodeGraph = nodeGraph;
      this.cssClass = cssClass;
      this.domElement = document.createElement("span");
      this.position = {
         x: this.nodeGraph.parentElement.clientWidth / 2,
         y: this.nodeGraph.parentElement.clientHeight / 2,
      };
      this.refreshFlag = true;

      if (this.graphNode) {
         this.initialize();
      }
   }

   /**
    * @public
    * @param {string} name
    * @returns {Promise<GraphNodeInputUI>}
    */
   async getInput(name) {
      await this.initialize();

      for (let i = 0; i < this.graphNodeInputs.length; i++) {
         if (this.graphNodeInputs[i].name === name)
            return this.graphNodeInputs[i];
      }
   }

   /**
    * @public
    * @param {string} description
    * @returns {Promise<GraphNodeOutputUI>}
    */
   async getOutput(description = undefined) {
      await this.initialize();

      if (this.graphNodeOutputs.length === 1) {
         return this.graphNodeOutputs[0];
      } else {
         for (let i = 0; i < this.graphNodeOutputs.length; i++) {
            if (this.graphNodeOutputs[i].description === description)
               return this.graphNodeOutputs[i];
         }
      }
   }

   /**
    * @public
    */
   setRefreshFlag() {
      this.refreshFlag = true;
      this.execute();
   }

   /**
    * @public
    */
   async execute() {
      if (this.graphNode.asWorker) {
         if (this.worker) {
            console.log("terminating " + this.graphNode.executer.name + ".");
            this.worker.terminate();
         }
      } else {
         if (this.executerCallback) {
            console.log("aborting " + this.graphNode.executer.name + ".");
            this.executerCallback.abortFlag = true;
         }
      }

      if (this.refreshFlag) {
         this.refreshFlag = false;

         const parameterValues = this.getParameterValues();
         if (parameterValues.includes(undefined)) return;

         console.log(
            "Calling function '" + this.graphNode.executer.name + "'."
         );

         this.executerCallback = new NodeCallback(this);
         this.executerCallback.setProgressPercent(0);

         if (this.graphNode.asWorker) {
            this.executeAsWorker(parameterValues);
         } else {
            this.executeAsPromise(parameterValues);
         }

         if (!parameterValues.includes(undefined)) {
            console.log("Executing " + this.graphNode.executer.name + ".");
         } else {
            console.log(
               "Function '" +
                  this.graphNode.executer.name +
                  "' did not pick up, because at least one parameter is undefined."
            );
         }
      }
   }

   /**
    * @private
    * @param {any[]} parameterValues
    */
   async executeAsPromise(parameterValues) {
      setTimeout(async () => {
         const result = await this.graphNode.executer(
            ...parameterValues,
            this.executerCallback
         );

         this.graphNodeOutputs[0].setValue(result);
         this.refreshValuePreview(result);
         this.executerCallback.setProgressPercent(100);
      });
   }

   /**
    * @private
    * @param {any[]} parameterValues
    */
   async executeAsWorker(parameterValues) {
      const toTransfer = [];
      const toCopy = [];

      let parameterValuesString = "(";
      let pointerCount = 0;
      let copyCount = 0;

      parameterValues.forEach((parameterValue) => {
         if (false && parameterValue instanceof ImageBitmap) {
            toTransfer.push(parameterValue);
            parameterValuesString +=
               "messageEvent.data.pointer[" + String(pointerCount) + "]";
            pointerCount++;
         } else {
            toCopy.push(parameterValue);
            parameterValuesString +=
               "messageEvent.data.copy[" + String(copyCount) + "]";
            copyCount++;
         }
         parameterValuesString += ",";
      });
      parameterValuesString += ")";

      this.worker = await this.createWorker(parameterValuesString);

      const cThis = this;
      this.worker.addEventListener(
         "message",
         async function handler(messageEvent) {
            cThis.worker.removeEventListener(messageEvent.type, handler);
            const resultValue = messageEvent.data;
            // TODO Handle multiple outputs.
            cThis.graphNodeOutputs[0].setValue(resultValue);
            cThis.refreshValuePreview(resultValue);

            cThis.worker.terminate();
            cThis.worker = undefined;
            cThis.executerCallback.setProgressPercent(100);
         }
      );

      this.worker.postMessage(
         { pointer: toTransfer, copy: toCopy },
         toTransfer
      );
   }

   /**
    * @private
    * @param {string} parameterValuesString
    * @returns {Promise<Worker>}
    */
   async createWorker(parameterValuesString) {
      const dependenciesSource = await this.graphNode.getDependenciesSource();

      const workerSource =
         dependenciesSource +
         "\n" +
         "const cSelf = self;\n" +
         "self.addEventListener('message', async (messageEvent) => {\n" +
         "cSelf.postMessage(await " +
         this.graphNode.executer.name +
         parameterValuesString +
         ");\n" +
         "});";

      const blob = new Blob([workerSource], {
         type: "text/javascript",
      });
      const workerSrc = window.URL.createObjectURL(blob);
      return new Worker(workerSrc);
   }

   /**
    * @protected
    * @param {any} value
    */
   refreshValuePreview(value) {
      this.outputUIElement.innerHTML = "";

      if (value instanceof ImageBitmap) {
         const imageCanvas = document.createElement("canvas");
         imageCanvas.width = value.width;
         imageCanvas.height = value.height;
         const context = imageCanvas.getContext("2d");
         context.drawImage(value, 0, 0, value.width, value.height);
         const imageElement = new Image();
         imageElement.style.maxWidth = "100%";
         imageCanvas.style.maxHeight = "5rem";
         this.outputUIElement.style.display = "flex";
         this.outputUIElement.style.justifyContent = "center";
         this.outputUIElement.appendChild(imageElement);
         imageElement.src = imageCanvas.toDataURL();
      } else if (Array.isArray(value) && value[0] instanceof ImageBitmap) {
         const imageCanvas = document.createElement("canvas");
         imageCanvas.width = value[0].width;
         imageCanvas.height = value[0].height;
         const context = imageCanvas.getContext("2d");
         context.drawImage(value[0], 0, 0, value[0].width, value[0].height);
         const imageElement = new Image();
         imageElement.style.maxWidth = "100%";
         imageCanvas.style.maxHeight = "5rem";
         this.outputUIElement.style.display = "flex";
         this.outputUIElement.style.justifyContent = "center";
         this.outputUIElement.appendChild(imageElement);
         imageElement.src = imageCanvas.toDataURL();
      } else if (typeof value === "number") {
         const numberElement = document.createElement("div");
         numberElement.innerText = String(value);
         numberElement.style.textAlign = "center";
         this.outputUIElement.appendChild(numberElement);
      } else if (typeof value === "string") {
         const valueImage = new Image();
         valueImage.src = value;
         valueImage.style.maxWidth = "100%";
         valueImage.style.maxHeight = "5rem";
         this.outputUIElement.style.display = "flex";
         this.outputUIElement.style.justifyContent = "center";
         this.outputUIElement.appendChild(valueImage);
      }

      this.nodeGraph.updateConnectionUI();
   }

   /**
    * @private
    * @returns {any[]}
    */
   getParameterValues() {
      const parameterValues = [];
      this.graphNodeInputs.forEach((input) => {
         const connection = input.getConnection();
         if (connection) {
            parameterValues.push(connection.getValue());
         } else {
            parameterValues.push(undefined);
         }
      });
      return parameterValues;
   }

   /**
    * @public
    */
   async initialize() {
      while (this.initializing) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }
      if (this.initialized) {
         return;
      }
      this.initialized = false;
      this.initializing = true;

      if (this.graphNode) {
         /**
          * @override
          * @protected
          * @type {GraphNodeInputUI[]}
          */
         this.graphNodeInputs = GraphNodeInputUI.getFromGraphNodeInputs(
            await this.graphNode.getInputs(),
            this.nodeGraph,
            this
         );

         /**
          * @override
          * @protected
          * @type {GraphNodeOutputUI[]}
          */
         this.graphNodeOutputs = GraphNodeOutputUI.getFromGraphNodeOutputs(
            await this.graphNode.getOutputs(),
            this.nodeGraph,
            this
         );
      }

      this.domElement.classList.add(this.cssClass);

      const domTitleElement = document.createElement("h1");
      domTitleElement.style.cursor = "grab";
      domTitleElement.addEventListener(
         "mousedown",
         this.mousedownGrabHandler.bind(this)
      );
      domTitleElement.innerText = this.graphNode.getName();
      domTitleElement.style.backgroundColor = "transparent";
      this.domElement.appendChild(domTitleElement);

      this.domProgressElement = document.createElement("progress");
      this.domProgressElement.style.width = "100%";
      this.domProgressElement.value = 0;
      this.domProgressElement.max = 100;
      this.domElement.appendChild(this.domProgressElement);
      this.domProgressElement.hidden = true;

      this.outputUIElement = document.createElement("div");
      this.domElement.appendChild(this.outputUIElement);

      const domIOElement = document.createElement("div");
      const domInputList = document.createElement("ul");
      const domOutputList = document.createElement("ul");

      domIOElement.style.display = "flex";
      domIOElement.style.justifyContent = "space-between";
      domIOElement.style.marginLeft = "-10%";
      domIOElement.style.width = "120%";

      this.domElement.appendChild(domIOElement);
      domIOElement.appendChild(domInputList);
      domIOElement.appendChild(domOutputList);

      this.graphNodeInputs.forEach((graphNodeInput) => {
         domInputList.appendChild(graphNodeInput.domElement);
      });
      this.graphNodeOutputs.forEach((graphNodeOutput) => {
         domOutputList.appendChild(graphNodeOutput.domElement);
      });

      this.execute();

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @public
    * @returns {Promise<{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]>}
    */
   async getConnections() {
      /** @type {{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]} */
      const connections = [];

      this.graphNodeInputs.forEach((graphNodeInput) => {
         if (graphNodeInput.graphNodeUI === this) {
            const output = graphNodeInput.getConnection();
            if (output) {
               connections.push({
                  input: graphNodeInput,
                  output: output,
               });
            }
         }
      });

      return connections;
   }

   /**
    * @public
    * @returns {GraphNodeUI[]}
    */
   getOutputNodes() {
      /** @type {GraphNodeUI[]} */
      const outputNodes = [];

      this.graphNodeOutputs.forEach((graphNodeOutput) => {
         graphNodeOutput.getConnections().forEach((connection) => {
            outputNodes.push(connection.graphNodeUI);
         });
      });
      return outputNodes;
   }

   /**
    * @protected
    */
   mousedownGrabHandler() {
      this.nodeGraph.setGrabbedNode(this);
   }

   /**
    * @public
    * @returns {{x:number, y:number}}
    */
   getPosition() {
      const boundingRect = this.domElement.getBoundingClientRect();
      return {
         x: boundingRect.left + boundingRect.width / 2,
         y: boundingRect.top + 5,
      };
   }

   /**
    * @param {{x:number, y:number}} position
    */
   setPosition(position) {
      this.position = position;
      this.domElement.style.transform =
         "translate(calc(" +
         this.position.x +
         "px - 50%), calc(" +
         this.position.y +
         "px - 0.25rem))";
   }
}

class InputGraphNode extends GraphNodeUI {
   /**
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeInputUI | string} inputNodeOrType
    * @param {string} cssClass
    */
   constructor(nodeGraph, inputNodeOrType, cssClass = "graphNode") {
      super(undefined, nodeGraph, cssClass);

      if (inputNodeOrType instanceof GraphNodeInputUI) {
         this.type = inputNodeOrType.type;
         this.inputNode = inputNodeOrType;
      } else {
         this.type = inputNodeOrType;
      }

      this.initialize();

      if (inputNodeOrType instanceof GraphNodeInputUI)
         this.setConnectionToInputNode();
   }

   /**
    * @private
    */
   async setConnectionToInputNode() {
      this.inputNode.setConnection(this.graphNodeOutputs);
      this.nodeGraph.updateConnectionUI();
   }

   /**
    * @override
    */
   async initialize() {
      while (this.initializing) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }
      if (this.initialized) {
         return;
      }

      this.domElement.classList.add(this.cssClass);

      const domTitleElement = document.createElement("h1");
      domTitleElement.style.cursor = "grab";
      domTitleElement.addEventListener(
         "mousedown",
         this.mousedownGrabHandler.bind(this)
      );
      domTitleElement.innerText = this.type;
      domTitleElement.style.backgroundColor = "transparent";
      this.domElement.appendChild(domTitleElement);

      this.domProgressElement = document.createElement("progress");
      this.domProgressElement.style.width = "100%";
      this.domProgressElement.value = 0;
      this.domProgressElement.max = 100;
      this.domElement.appendChild(this.domProgressElement);
      this.domProgressElement.hidden = true;

      this.outputUIElement = document.createElement("div");
      this.domElement.appendChild(this.outputUIElement);

      const domIOElement = document.createElement("div");
      const domInputList = document.createElement("ul");
      const domOutputList = document.createElement("ul");

      domIOElement.style.display = "flex";
      domIOElement.style.justifyContent = "space-between";
      domIOElement.style.marginLeft = "10%";
      domIOElement.style.width = "100%";

      this.domElement.appendChild(domIOElement);
      domIOElement.appendChild(domInputList);
      domIOElement.appendChild(domOutputList);

      this.inputElement = document.createElement("input");
      this.inputElement.style.width = "80%";
      this.inputElement.style.overflowWrap = "break-word";
      this.inputElement.style.hyphens = "auto";
      this.inputElement.style.whiteSpace = "normal";
      this.inputElement.multiple = false;

      if (this.type === "number") {
         this.inputElement.type = "number";
         domInputList.appendChild(this.inputElement);
      } else if (this.type === "ImageBitmap") {
         this.inputElement.type = "file";
         this.inputElement.accept = "image/*";
      } else if (this.type === "ImageBitmap[]") {
         this.inputElement.type = "file";
         this.inputElement.accept = "image/*";
         this.inputElement.multiple = true;
      } else {
         console.error("Input type '" + this.type + "' not supported.");
      }

      this.inputElement.addEventListener(
         "input",
         this.inputChangeHandler.bind(this)
      );

      domInputList.appendChild(this.inputElement);

      this.graphNodeOutputs = [
         new GraphNodeOutputUI(
            this.type,
            "[" + this.type + "]",
            this.nodeGraph,
            this
         ),
      ];

      domOutputList.appendChild(this.graphNodeOutputs[0].domElement);

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @public
    * @param {any} value
    */
   async setValue(value) {
      this.inputElement.value = value;
      this.inputElement.dispatchEvent(new Event("input"));
   }

   /**
    * @private
    * @param {InputEvent} inputEvent
    */
   inputChangeHandler(inputEvent) {
      if (this.type === "number") {
         let value = Number(
            /** @type {HTMLInputElement} */ (inputEvent.target).value
         );
         if (!value) {
            value = 0;
         }
         this.graphNodeOutputs[0].setValue(value);
         this.refreshValuePreview(value);
      } else if (this.type === "ImageBitmap") {
         const nodeCallback = new NodeCallback(this);
         nodeCallback.setProgressPercent(0);

         const imageLoaderWorker = new Worker("./src/image-loader-worker.js");

         imageLoaderWorker.addEventListener("message", async (messageEvent) => {
            const imageBitmap = messageEvent.data;
            this.graphNodeOutputs[0].setValue(imageBitmap);
            this.refreshValuePreview(imageBitmap);
            nodeCallback.setProgressPercent(100);
         });
         imageLoaderWorker.postMessage(inputEvent.target.files[0]);
      } else if (this.type === "ImageBitmap[]") {
         const nodeCallback = new NodeCallback(this);
         nodeCallback.setProgressPercent(0);

         const files = Array.from(inputEvent.target.files);
         const imageCount = files.length;
         const imageBitmapArray = [];

         files.forEach((file) => {
            const imageLoaderWorker = new Worker(
               "./src/image-loader-worker.js"
            );

            imageLoaderWorker.addEventListener(
               "message",
               async (messageEvent) => {
                  const imageBitmap = messageEvent.data;
                  imageBitmapArray.push(imageBitmap);
                  if (imageBitmapArray.length === imageCount) {
                     this.graphNodeOutputs[0].setValue(imageBitmapArray);
                     this.refreshValuePreview(imageBitmap);
                  }
                  nodeCallback.setProgressPercent(
                     (imageBitmapArray.length / imageCount) * 100
                  );
               }
            );
            imageLoaderWorker.postMessage(file);
         });
      }
   }

   /**
    * @override
    * @public
    * @returns {Promise<{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]>}
    */
   async getConnections() {
      // TODO Handle multiple outputs.
      return [{ input: this.inputNode, output: this.graphNodeOutputs[0] }];
   }

   /**
    * @override
    * @public
    */
   async execute() {
      this.refreshFlag = false;
   }
}

97d7816138853660564477f9f208240c6a115908

Remove hard code.

Remove hard code.

// TODO Remove hard code.

      this.handleResize();
      this.updateCameraPlanes();
   }
}
/** @constant */
VirtualInputRenderer.MIN_CAMERA_PLANES_DISTANCE = 0.5;
/** @type {VirtualInputRenderer[]} */
VirtualInputRenderer.instances = [];

class PhotometricStereoRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }

   /**
    * @override
    * @protected
    */
   async initialize() {
      if (this.initialized || (!this.initialized && this.initializing)) {
         return;
      }
      this.initializing = true;

      super.initialize();

      this.lights = new Array(8);
      this.lightHelpers = new Array(8);

      for (let i = 0; i < 8; i++) {
         // TODO Remove hard code.
         this.lights[i] = new THREE.PointLight("white", 0.25);
         this.lights[i].castShadow = true;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.width = 512 * 2;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.height = 512 * 2;
         // TODO Remove hard code.
         this.lightHelpers[i] = new THREE.PointLightHelper(this.lights[i], 0.2);
         this.scene.add(this.lights[i]);
         this.scene.add(this.lightHelpers[i]);
      }

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @override
    * @public
    * @returns {Promise<HTMLImageElement[]>}
    */
   async render() {
      this.renderId++;
      const renderId = this.renderId;

      await this.initialize();

      if (this.isRenderObsolete(renderId)) return;

      const lightCount = this.lights.length;
      const renderPromises = [];

      for (let i = 0; i < lightCount; i++) {
         this.lightHelpers[i].visible = false;
      }
      this.cameraHelper.visible = false;

      for (let i = 0; i < lightCount; i++) {
         for (let i = 0; i < lightCount; i++) {
            this.lights[i].visible = false;
         }
         this.lights[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 25 * (this.cameraDistance / 8);

         if (this.isRenderObsolete(renderId)) return;

         this.renderer.render(this.scene, this.camera);
         const renderImageDataUrl = this.renderer.domElement.toDataURL();

         renderPromises.push(
            new Promise((resolve) => {
               setTimeout(() => {
                  const image = new Image();
                  image.addEventListener("load", () => {
                     resolve(image);
                  });
                  image.src = renderImageDataUrl;
               });
            })
         );
      }

      for (let i = 0; i < lightCount; i++) {
         this.lights[i].visible = true;
         this.lightHelpers[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 0.25;
      }
      this.cameraHelper.visible = true;

      this.updateCameraPlanes();
      this.uiRenderer.render(this.scene, this.uiCamera);

      return Promise.all(renderPromises);
   }

   /**
    * @public
    * @param {number} lightPolarAngleDeg
    */
   async setLightPolarAngleDeg(lightPolarAngleDeg) {
      await this.initialize();
      this.lightPolarAngleDeg = lightPolarAngleDeg;
      this.updateLightPositions();
   }

   /**
    * @override
    * @public
    * @param {number} lightDistance
    */
   async setLightDistance(lightDistance) {
      await this.initialize();

      this.lightDistance = lightDistance;
      for (let i = 0; i < this.lights.length; i++) {
         this.lights[i].distance = this.lightDistance * 2;
      }
      this.updateLightPositions();
   }

   async updateLightPositions() {
      await this.initialize();

      const correctedLightPolarDegree = 360 - this.lightPolarAngleDeg;

      /**
       * @param {THREE.Light} light
       * @param {number} lightAzimuthalDegree
       */
      const setSingleLightAzimuthalAngle = (light, lightAzimuthalDegree) => {
         let lightVector = new THREE.Vector3(this.lightDistance, 0, 0);

         let lightPolarRotationAxis = new THREE.Vector3(0, 1, 0).normalize();
         lightVector.applyAxisAngle(
            lightPolarRotationAxis,
            correctedLightPolarDegree * (Math.PI / 180)
         );

         const lightRotation = lightAzimuthalDegree * (Math.PI / 180);
         const lightRotationAxis = new THREE.Vector3(0, 0, 1).normalize();
         lightVector.applyAxisAngle(lightRotationAxis, lightRotation);

         light.position.set(lightVector.x, lightVector.y, lightVector.z);
      };

      const lightCount = this.lights.length;
      for (let i = 0; i < lightCount; i++) {
         setSingleLightAzimuthalAngle(this.lights[i], i * (360 / lightCount));
      }
   }
}

class SphericalGradientRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }
}

f39176ee8b4b393b919c0a86b9e008ac8a522b6e

Disable preserved drawing buffers to enable swapping for better performance.

Disable preserved drawing buffers to enable swapping for better performance.

// TODO Disable preserved drawing buffers to enable swapping for better performance.

      this.cameraHelper = new THREE.CameraHelper(this.camera);
      this.scene.add(this.cameraHelper);

      // TODO Disable preserved drawing buffers to enable swapping for better performance.
      this.renderer = new THREE.WebGLRenderer({
         preserveDrawingBuffer: true,
      });

e9898385a0522ad3d02b1e3920ffd8c125498a6c

Remove hard code.

Remove hard code.

// TODO Remove hard code.

      this.handleResize();
      this.updateCameraPlanes();
   }
}
/** @constant */
VirtualInputRenderer.MIN_CAMERA_PLANES_DISTANCE = 0.5;
/** @type {VirtualInputRenderer[]} */
VirtualInputRenderer.instances = [];

class PhotometricStereoRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }

   /**
    * @override
    * @protected
    */
   async initialize() {
      if (this.initialized || (!this.initialized && this.initializing)) {
         return;
      }
      this.initializing = true;

      super.initialize();

      this.lights = new Array(8);
      this.lightHelpers = new Array(8);

      for (let i = 0; i < 8; i++) {
         // TODO Remove hard code.
         this.lights[i] = new THREE.PointLight("white", 0.25);
         this.lights[i].castShadow = true;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.width = 512 * 2;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.height = 512 * 2;
         // TODO Remove hard code.
         this.lightHelpers[i] = new THREE.PointLightHelper(this.lights[i], 0.2);
         this.scene.add(this.lights[i]);
         this.scene.add(this.lightHelpers[i]);
      }

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @override
    * @public
    * @returns {Promise<HTMLImageElement[]>}
    */
   async render() {
      this.renderId++;
      const renderId = this.renderId;

      await this.initialize();

      if (this.isRenderObsolete(renderId)) return;

      const lightCount = this.lights.length;
      const renderPromises = [];

      for (let i = 0; i < lightCount; i++) {
         this.lightHelpers[i].visible = false;
      }
      this.cameraHelper.visible = false;

      for (let i = 0; i < lightCount; i++) {
         for (let i = 0; i < lightCount; i++) {
            this.lights[i].visible = false;
         }
         this.lights[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 25 * (this.cameraDistance / 8);

         if (this.isRenderObsolete(renderId)) return;

         this.renderer.render(this.scene, this.camera);
         const renderImageDataUrl = this.renderer.domElement.toDataURL();

         renderPromises.push(
            new Promise((resolve) => {
               setTimeout(() => {
                  const image = new Image();
                  image.addEventListener("load", () => {
                     resolve(image);
                  });
                  image.src = renderImageDataUrl;
               });
            })
         );
      }

      for (let i = 0; i < lightCount; i++) {
         this.lights[i].visible = true;
         this.lightHelpers[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 0.25;
      }
      this.cameraHelper.visible = true;

      this.updateCameraPlanes();
      this.uiRenderer.render(this.scene, this.uiCamera);

      return Promise.all(renderPromises);
   }

   /**
    * @public
    * @param {number} lightPolarAngleDeg
    */
   async setLightPolarAngleDeg(lightPolarAngleDeg) {
      await this.initialize();
      this.lightPolarAngleDeg = lightPolarAngleDeg;
      this.updateLightPositions();
   }

   /**
    * @override
    * @public
    * @param {number} lightDistance
    */
   async setLightDistance(lightDistance) {
      await this.initialize();

      this.lightDistance = lightDistance;
      for (let i = 0; i < this.lights.length; i++) {
         this.lights[i].distance = this.lightDistance * 2;
      }
      this.updateLightPositions();
   }

   async updateLightPositions() {
      await this.initialize();

      const correctedLightPolarDegree = 360 - this.lightPolarAngleDeg;

      /**
       * @param {THREE.Light} light
       * @param {number} lightAzimuthalDegree
       */
      const setSingleLightAzimuthalAngle = (light, lightAzimuthalDegree) => {
         let lightVector = new THREE.Vector3(this.lightDistance, 0, 0);

         let lightPolarRotationAxis = new THREE.Vector3(0, 1, 0).normalize();
         lightVector.applyAxisAngle(
            lightPolarRotationAxis,
            correctedLightPolarDegree * (Math.PI / 180)
         );

         const lightRotation = lightAzimuthalDegree * (Math.PI / 180);
         const lightRotationAxis = new THREE.Vector3(0, 0, 1).normalize();
         lightVector.applyAxisAngle(lightRotationAxis, lightRotation);

         light.position.set(lightVector.x, lightVector.y, lightVector.z);
      };

      const lightCount = this.lights.length;
      for (let i = 0; i < lightCount; i++) {
         setSingleLightAzimuthalAngle(this.lights[i], i * (360 / lightCount));
      }
   }
}

class SphericalGradientRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }
}

f39176ee8b4b393b919c0a86b9e008ac8a522b6e

Remove hard code.

Remove hard code.

// TODO Remove hard code.

      this.handleResize();
      this.updateCameraPlanes();
   }
}
/** @constant */
VirtualInputRenderer.MIN_CAMERA_PLANES_DISTANCE = 0.5;
/** @type {VirtualInputRenderer[]} */
VirtualInputRenderer.instances = [];

class PhotometricStereoRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }

   /**
    * @override
    * @protected
    */
   async initialize() {
      if (this.initialized || (!this.initialized && this.initializing)) {
         return;
      }
      this.initializing = true;

      super.initialize();

      this.lights = new Array(8);
      this.lightHelpers = new Array(8);

      for (let i = 0; i < 8; i++) {
         // TODO Remove hard code.
         this.lights[i] = new THREE.PointLight("white", 0.25);
         this.lights[i].castShadow = true;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.width = 512 * 2;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.height = 512 * 2;
         // TODO Remove hard code.
         this.lightHelpers[i] = new THREE.PointLightHelper(this.lights[i], 0.2);
         this.scene.add(this.lights[i]);
         this.scene.add(this.lightHelpers[i]);
      }

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @override
    * @public
    * @returns {Promise<HTMLImageElement[]>}
    */
   async render() {
      this.renderId++;
      const renderId = this.renderId;

      await this.initialize();

      if (this.isRenderObsolete(renderId)) return;

      const lightCount = this.lights.length;
      const renderPromises = [];

      for (let i = 0; i < lightCount; i++) {
         this.lightHelpers[i].visible = false;
      }
      this.cameraHelper.visible = false;

      for (let i = 0; i < lightCount; i++) {
         for (let i = 0; i < lightCount; i++) {
            this.lights[i].visible = false;
         }
         this.lights[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 25 * (this.cameraDistance / 8);

         if (this.isRenderObsolete(renderId)) return;

         this.renderer.render(this.scene, this.camera);
         const renderImageDataUrl = this.renderer.domElement.toDataURL();

         renderPromises.push(
            new Promise((resolve) => {
               setTimeout(() => {
                  const image = new Image();
                  image.addEventListener("load", () => {
                     resolve(image);
                  });
                  image.src = renderImageDataUrl;
               });
            })
         );
      }

      for (let i = 0; i < lightCount; i++) {
         this.lights[i].visible = true;
         this.lightHelpers[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 0.25;
      }
      this.cameraHelper.visible = true;

      this.updateCameraPlanes();
      this.uiRenderer.render(this.scene, this.uiCamera);

      return Promise.all(renderPromises);
   }

   /**
    * @public
    * @param {number} lightPolarAngleDeg
    */
   async setLightPolarAngleDeg(lightPolarAngleDeg) {
      await this.initialize();
      this.lightPolarAngleDeg = lightPolarAngleDeg;
      this.updateLightPositions();
   }

   /**
    * @override
    * @public
    * @param {number} lightDistance
    */
   async setLightDistance(lightDistance) {
      await this.initialize();

      this.lightDistance = lightDistance;
      for (let i = 0; i < this.lights.length; i++) {
         this.lights[i].distance = this.lightDistance * 2;
      }
      this.updateLightPositions();
   }

   async updateLightPositions() {
      await this.initialize();

      const correctedLightPolarDegree = 360 - this.lightPolarAngleDeg;

      /**
       * @param {THREE.Light} light
       * @param {number} lightAzimuthalDegree
       */
      const setSingleLightAzimuthalAngle = (light, lightAzimuthalDegree) => {
         let lightVector = new THREE.Vector3(this.lightDistance, 0, 0);

         let lightPolarRotationAxis = new THREE.Vector3(0, 1, 0).normalize();
         lightVector.applyAxisAngle(
            lightPolarRotationAxis,
            correctedLightPolarDegree * (Math.PI / 180)
         );

         const lightRotation = lightAzimuthalDegree * (Math.PI / 180);
         const lightRotationAxis = new THREE.Vector3(0, 0, 1).normalize();
         lightVector.applyAxisAngle(lightRotationAxis, lightRotation);

         light.position.set(lightVector.x, lightVector.y, lightVector.z);
      };

      const lightCount = this.lights.length;
      for (let i = 0; i < lightCount; i++) {
         setSingleLightAzimuthalAngle(this.lights[i], i * (360 / lightCount));
      }
   }
}

class SphericalGradientRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }
}

f39176ee8b4b393b919c0a86b9e008ac8a522b6e

fix alpha

fix alpha

// TODO: fix alpha

/* global GLSL */
/* exported photometricStereoNormalMap */

/**
 * @public
 * @param {number} lightPolarAngleDeg
 * @param {ImageBitmap[]} lightImages
 * @returns {Promise<ImageBitmap>}
 */
async function photometricStereoNormalMap(lightPolarAngleDeg, lightImages) {
   const lightImage_000 = lightImages[0];
   const lightImage_045 = lightImages[1];
   const lightImage_090 = lightImages[2];
   const lightImage_135 = lightImages[3];
   const lightImage_180 = lightImages[4];
   const lightImage_225 = lightImages[5];
   const lightImage_270 = lightImages[6];
   const lightImage_315 = lightImages[7];

   const normalMapShader = new GLSL.Shader({
      width: lightImage_000.width,
      height: lightImage_000.height,
   });

   normalMapShader.bind();

   const lightLuminances = [
      GLSL.Image.load(lightImage_000).getLuminance(),
      GLSL.Image.load(lightImage_045).getLuminance(),
      GLSL.Image.load(lightImage_090).getLuminance(),
      GLSL.Image.load(lightImage_135).getLuminance(),
      GLSL.Image.load(lightImage_180).getLuminance(),
      GLSL.Image.load(lightImage_225).getLuminance(),
      GLSL.Image.load(lightImage_270).getLuminance(),
      GLSL.Image.load(lightImage_315).getLuminance(),
   ];

   const all = new GLSL.Float(0).maximum(...lightLuminances);

   let mask = new GLSL.Float(1);

   /*if (lightImage_NONE) {
      const lightLuminance_NONE =
         GLSL.Image.load(lightImage_NONE).getLuminance();

      for (let i = 0; i < lightLuminances.length; i++) {
         lightLuminances[i] =
            lightLuminances[i].subtractFloat(lightLuminance_NONE);
      }

      mask = all
         .subtractFloat(lightLuminance_NONE)
         .step(new GLSL.Float(maskThreshold));
   }*/

   for (let i = 0; i < lightLuminances.length; i++) {
      lightLuminances[i] = lightLuminances[i].divideFloat(all);
   }

   /**
    * @param {GLSL.Float} originLuminance
    * @param {GLSL.Float} orthogonalLuminance
    * @param {GLSL.Float} oppositeLuminance
    * @param {number} originAzimuthalAngleDeg
    * @param {number} orthogonalAzimuthalAngleDeg
    * @param {number} oppositeAzimuthalAngleDeg
    * @returns {GLSL.Vector3}
    */
   function getAnisotropicNormalVector(
      originLuminance,
      orthogonalLuminance,
      oppositeLuminance,

      originAzimuthalAngleDeg,
      orthogonalAzimuthalAngleDeg,
      oppositeAzimuthalAngleDeg
   ) {
      /**
       * @param {number} azimuthalAngleDeg
       * @param {number} polarAngleDeg
       * @returns {GLSL.Vector3}
       */
      function getLightDirectionVector(azimuthalAngleDeg, polarAngleDeg) {
         const polar = new GLSL.Float(polarAngleDeg).radians();
         const azimuthal = new GLSL.Float(azimuthalAngleDeg).radians();

         return new GLSL.Vector3([
            polar.sin().multiplyFloat(azimuthal.cos()),
            polar.sin().multiplyFloat(azimuthal.sin()),
            polar.cos(),
         ]).normalize();
      }

      const originLightDirection = getLightDirectionVector(
         originAzimuthalAngleDeg,
         lightPolarAngleDeg
      );
      const orthogonalLightDirection = getLightDirectionVector(
         orthogonalAzimuthalAngleDeg,
         lightPolarAngleDeg
      );
      const oppositeLightDirection = getLightDirectionVector(
         oppositeAzimuthalAngleDeg,
         lightPolarAngleDeg
      );

      const lightMatrix = new GLSL.Matrix3([
         [
            originLightDirection.channel(0),
            originLightDirection.channel(1),
            originLightDirection.channel(2),
         ],
         [
            orthogonalLightDirection.channel(0),
            orthogonalLightDirection.channel(1),
            orthogonalLightDirection.channel(2),
         ],
         [
            oppositeLightDirection.channel(0),
            oppositeLightDirection.channel(1),
            oppositeLightDirection.channel(2),
         ],
      ]).inverse();

      const reflection = new GLSL.Vector3([
         originLuminance,
         orthogonalLuminance,
         oppositeLuminance,
      ]);

      return lightMatrix
         .multiplyVector3(reflection)
         .normalize()
         .addFloat(new GLSL.Float(1))
         .divideFloat(new GLSL.Float(2));
   }

   /** @type {number[][]} */
   const anisotropicCombinations = [
      [180, 270, 0],
      [180, 90, 0],
      [90, 180, 270],
      [90, 0, 270],
      [225, 315, 45],
      [225, 135, 45],
      [315, 45, 135],
      [315, 225, 135],
   ];

   /** @type {GLSL.Vector3[]} */
   const normalVectors = [];

   anisotropicCombinations.forEach((combination) => {
      /**
       * @param {number} azimuthalAngle
       * @returns {GLSL.Float}
       */
      function getLightLuminance(azimuthalAngle) {
         const lightAzimuthalAngles = [0, 45, 90, 135, 180, 225, 270, 315];
         const id = lightAzimuthalAngles.findIndex((value) => {
            return value === azimuthalAngle;
         });

         return lightLuminances[id];
      }

      normalVectors.push(
         getAnisotropicNormalVector(
            getLightLuminance(combination[0]),
            getLightLuminance(combination[1]),
            getLightLuminance(combination[2]),
            combination[0],
            combination[1],
            combination[2]
         )
      );
   });

   let normalVector = new GLSL.Vector3([
      new GLSL.Float(0),
      new GLSL.Float(0),
      new GLSL.Float(0),
   ])
      .addVector3(...normalVectors)
      .divideFloat(new GLSL.Float(normalVectors.length))
      .normalize()
      .multiplyFloat(mask);

   /*if (cameraVerticalShift) {
      // TODO: use cameraVerticalShift
      const cameraAngle = Math.atan(
                  1 / Math.tan(lightPolarAngleDeg * (Math.PI / 180))
               );

               const zero = new GLSL.Float(0);
               const one = new GLSL.Float(1);
               const sine = new GLSL.Float(Math.sin(cameraAngle));
               const cosine = new GLSL.Float(Math.cos(cameraAngle));

               const rotationMatrix = new GLSL.Matrix3([
                  [one, zero, zero],
                  [zero, cosine, sine],
                  [zero, sine.multiplyFloat(new GLSL.Float(-1)), cosine],
               ]);

               normalVector = rotationMatrix.multiplyVector3(normalVector);
   }*/

   // TODO: fix alpha
   const alpha = normalVector
      .channel(0)
      .minimum(normalVector.channel(1), normalVector.channel(2))
      .multiplyFloat(new GLSL.Float(99999))
      .minimum(new GLSL.Float(1));

   const normalMapRendering = GLSL.render(normalVector.getVector4(alpha));
   const normalMap = await normalMapRendering.getImageBitmap();
   normalMapShader.purge();
   return normalMap;
}

/**
 * @public
 * @param {ImageBitmap} lightImage_000
 * @param {ImageBitmap} lightImage_090
 * @param {ImageBitmap} lightImage_180
 * @param {ImageBitmap} lightImage_270
 * @param {ImageBitmap} lightImage_ALL
 * @param {ImageBitmap} lightImage_FRONT
 * @param {ImageBitmap} lightImage_NONE
 * @param {ImageBitmap} uiImageElement
 * @param {number} resolutionPercent
 * @returns {Promise<ImageBitmap>}
 */
/*async function sphericalGradientNormalMap(
   lightImage_000,
   lightImage_090,
   lightImage_180,
   lightImage_270,
   lightImage_ALL,
   lightImage_FRONT,
   lightImage_NONE = undefined,
   uiImageElement = undefined,
   resolutionPercent = 100
) {
   return new Promise((resolve) => {
      setTimeout(async () => {
         const normalMapShader = new GLSL.Shader({
            width: lightImage_000.width * (resolutionPercent / 100),
            height: lightImage_000.naturalHeight * (resolutionPercent / 100),
         });
         normalMapShader.bind();

         let lightLuminance_ALL =
            GLSL.Image.load(lightImage_ALL).getLuminance();

         const lightLuminances = [
            GLSL.Image.load(lightImage_000).getLuminance(),
            GLSL.Image.load(lightImage_090).getLuminance(),
            GLSL.Image.load(lightImage_180).getLuminance(),
            GLSL.Image.load(lightImage_270).getLuminance(),
            GLSL.Image.load(lightImage_FRONT).getLuminance(),
         ];

         if (lightImage_NONE) {
            const lightLuminance_NONE =
               GLSL.Image.load(lightImage_NONE).getLuminance();

            for (let i = 0; i < lightLuminances.length; i++) {
               lightLuminances[i] =
                  lightLuminances[i].subtractFloat(lightLuminance_NONE);
            }
            lightLuminance_ALL =
               lightLuminance_ALL.subtractFloat(lightLuminance_NONE);
         }

         for (let i = 0; i < lightLuminances.length; i++) {
            lightLuminances[i] =
               lightLuminances[i].divideFloat(lightLuminance_ALL);
         }

         const horizontal = lightLuminances[0]
            .subtractFloat(lightLuminances[2])
            .addFloat(new GLSL.Float(1))
            .divideFloat(new GLSL.Float(2));

         const vertical = lightLuminances[3]
            .subtractFloat(lightLuminances[1])
            .addFloat(new GLSL.Float(1))
            .divideFloat(new GLSL.Float(2));

         const normalVector = new GLSL.Vector3([
            horizontal,
            vertical,
            lightLuminances[4],
         ]);

         const normalMapRendering = GLSL.render(
            normalVector.normalize().getVector4()
         );

         normalMapShader.purge();

         const normalMap = await normalMapRendering.getJsImage();

         if (uiImageElement && normalMap) {
            uiImageElement.src = normalMap.src;
         }

         resolve(normalMap);
      });
   });
}*/

5789cb7f47e2247c2618693ed1451ef6c660709d

use cameraVerticalShift

use cameraVerticalShift

const cameraAngle = Math.atan(

1 / Math.tan(lightPolarAngleDeg * (Math.PI / 180))

);

const zero = new GLSL.Float(0);

const one = new GLSL.Float(1);

const sine = new GLSL.Float(Math.sin(cameraAngle));

const cosine = new GLSL.Float(Math.cos(cameraAngle));

const rotationMatrix = new GLSL.Matrix3([

[one, zero, zero],

[zero, cosine, sine],

[zero, sine.multiplyFloat(new GLSL.Float(-1)), cosine],

]);

normalVector = rotationMatrix.multiplyVector3(normalVector);

}

// TODO: use cameraVerticalShift

/* global GLSL */
/* exported photometricStereoNormalMap */

/**
 * @public
 * @param {number} lightPolarAngleDeg
 * @param {ImageBitmap[]} lightImages
 * @returns {Promise<ImageBitmap>}
 */
async function photometricStereoNormalMap(lightPolarAngleDeg, lightImages) {
   const lightImage_000 = lightImages[0];
   const lightImage_045 = lightImages[1];
   const lightImage_090 = lightImages[2];
   const lightImage_135 = lightImages[3];
   const lightImage_180 = lightImages[4];
   const lightImage_225 = lightImages[5];
   const lightImage_270 = lightImages[6];
   const lightImage_315 = lightImages[7];

   const normalMapShader = new GLSL.Shader({
      width: lightImage_000.width,
      height: lightImage_000.height,
   });

   normalMapShader.bind();

   const lightLuminances = [
      GLSL.Image.load(lightImage_000).getLuminance(),
      GLSL.Image.load(lightImage_045).getLuminance(),
      GLSL.Image.load(lightImage_090).getLuminance(),
      GLSL.Image.load(lightImage_135).getLuminance(),
      GLSL.Image.load(lightImage_180).getLuminance(),
      GLSL.Image.load(lightImage_225).getLuminance(),
      GLSL.Image.load(lightImage_270).getLuminance(),
      GLSL.Image.load(lightImage_315).getLuminance(),
   ];

   const all = new GLSL.Float(0).maximum(...lightLuminances);

   let mask = new GLSL.Float(1);

   /*if (lightImage_NONE) {
      const lightLuminance_NONE =
         GLSL.Image.load(lightImage_NONE).getLuminance();

      for (let i = 0; i < lightLuminances.length; i++) {
         lightLuminances[i] =
            lightLuminances[i].subtractFloat(lightLuminance_NONE);
      }

      mask = all
         .subtractFloat(lightLuminance_NONE)
         .step(new GLSL.Float(maskThreshold));
   }*/

   for (let i = 0; i < lightLuminances.length; i++) {
      lightLuminances[i] = lightLuminances[i].divideFloat(all);
   }

   /**
    * @param {GLSL.Float} originLuminance
    * @param {GLSL.Float} orthogonalLuminance
    * @param {GLSL.Float} oppositeLuminance
    * @param {number} originAzimuthalAngleDeg
    * @param {number} orthogonalAzimuthalAngleDeg
    * @param {number} oppositeAzimuthalAngleDeg
    * @returns {GLSL.Vector3}
    */
   function getAnisotropicNormalVector(
      originLuminance,
      orthogonalLuminance,
      oppositeLuminance,

      originAzimuthalAngleDeg,
      orthogonalAzimuthalAngleDeg,
      oppositeAzimuthalAngleDeg
   ) {
      /**
       * @param {number} azimuthalAngleDeg
       * @param {number} polarAngleDeg
       * @returns {GLSL.Vector3}
       */
      function getLightDirectionVector(azimuthalAngleDeg, polarAngleDeg) {
         const polar = new GLSL.Float(polarAngleDeg).radians();
         const azimuthal = new GLSL.Float(azimuthalAngleDeg).radians();

         return new GLSL.Vector3([
            polar.sin().multiplyFloat(azimuthal.cos()),
            polar.sin().multiplyFloat(azimuthal.sin()),
            polar.cos(),
         ]).normalize();
      }

      const originLightDirection = getLightDirectionVector(
         originAzimuthalAngleDeg,
         lightPolarAngleDeg
      );
      const orthogonalLightDirection = getLightDirectionVector(
         orthogonalAzimuthalAngleDeg,
         lightPolarAngleDeg
      );
      const oppositeLightDirection = getLightDirectionVector(
         oppositeAzimuthalAngleDeg,
         lightPolarAngleDeg
      );

      const lightMatrix = new GLSL.Matrix3([
         [
            originLightDirection.channel(0),
            originLightDirection.channel(1),
            originLightDirection.channel(2),
         ],
         [
            orthogonalLightDirection.channel(0),
            orthogonalLightDirection.channel(1),
            orthogonalLightDirection.channel(2),
         ],
         [
            oppositeLightDirection.channel(0),
            oppositeLightDirection.channel(1),
            oppositeLightDirection.channel(2),
         ],
      ]).inverse();

      const reflection = new GLSL.Vector3([
         originLuminance,
         orthogonalLuminance,
         oppositeLuminance,
      ]);

      return lightMatrix
         .multiplyVector3(reflection)
         .normalize()
         .addFloat(new GLSL.Float(1))
         .divideFloat(new GLSL.Float(2));
   }

   /** @type {number[][]} */
   const anisotropicCombinations = [
      [180, 270, 0],
      [180, 90, 0],
      [90, 180, 270],
      [90, 0, 270],
      [225, 315, 45],
      [225, 135, 45],
      [315, 45, 135],
      [315, 225, 135],
   ];

   /** @type {GLSL.Vector3[]} */
   const normalVectors = [];

   anisotropicCombinations.forEach((combination) => {
      /**
       * @param {number} azimuthalAngle
       * @returns {GLSL.Float}
       */
      function getLightLuminance(azimuthalAngle) {
         const lightAzimuthalAngles = [0, 45, 90, 135, 180, 225, 270, 315];
         const id = lightAzimuthalAngles.findIndex((value) => {
            return value === azimuthalAngle;
         });

         return lightLuminances[id];
      }

      normalVectors.push(
         getAnisotropicNormalVector(
            getLightLuminance(combination[0]),
            getLightLuminance(combination[1]),
            getLightLuminance(combination[2]),
            combination[0],
            combination[1],
            combination[2]
         )
      );
   });

   let normalVector = new GLSL.Vector3([
      new GLSL.Float(0),
      new GLSL.Float(0),
      new GLSL.Float(0),
   ])
      .addVector3(...normalVectors)
      .divideFloat(new GLSL.Float(normalVectors.length))
      .normalize()
      .multiplyFloat(mask);

   /*if (cameraVerticalShift) {
      // TODO: use cameraVerticalShift
      const cameraAngle = Math.atan(
                  1 / Math.tan(lightPolarAngleDeg * (Math.PI / 180))
               );

               const zero = new GLSL.Float(0);
               const one = new GLSL.Float(1);
               const sine = new GLSL.Float(Math.sin(cameraAngle));
               const cosine = new GLSL.Float(Math.cos(cameraAngle));

               const rotationMatrix = new GLSL.Matrix3([
                  [one, zero, zero],
                  [zero, cosine, sine],
                  [zero, sine.multiplyFloat(new GLSL.Float(-1)), cosine],
               ]);

               normalVector = rotationMatrix.multiplyVector3(normalVector);
   }*/

   // TODO: fix alpha
   const alpha = normalVector
      .channel(0)
      .minimum(normalVector.channel(1), normalVector.channel(2))
      .multiplyFloat(new GLSL.Float(99999))
      .minimum(new GLSL.Float(1));

   const normalMapRendering = GLSL.render(normalVector.getVector4(alpha));
   const normalMap = await normalMapRendering.getImageBitmap();
   normalMapShader.purge();
   return normalMap;
}

/**
 * @public
 * @param {ImageBitmap} lightImage_000
 * @param {ImageBitmap} lightImage_090
 * @param {ImageBitmap} lightImage_180
 * @param {ImageBitmap} lightImage_270
 * @param {ImageBitmap} lightImage_ALL
 * @param {ImageBitmap} lightImage_FRONT
 * @param {ImageBitmap} lightImage_NONE
 * @param {ImageBitmap} uiImageElement
 * @param {number} resolutionPercent
 * @returns {Promise<ImageBitmap>}
 */
/*async function sphericalGradientNormalMap(
   lightImage_000,
   lightImage_090,
   lightImage_180,
   lightImage_270,
   lightImage_ALL,
   lightImage_FRONT,
   lightImage_NONE = undefined,
   uiImageElement = undefined,
   resolutionPercent = 100
) {
   return new Promise((resolve) => {
      setTimeout(async () => {
         const normalMapShader = new GLSL.Shader({
            width: lightImage_000.width * (resolutionPercent / 100),
            height: lightImage_000.naturalHeight * (resolutionPercent / 100),
         });
         normalMapShader.bind();

         let lightLuminance_ALL =
            GLSL.Image.load(lightImage_ALL).getLuminance();

         const lightLuminances = [
            GLSL.Image.load(lightImage_000).getLuminance(),
            GLSL.Image.load(lightImage_090).getLuminance(),
            GLSL.Image.load(lightImage_180).getLuminance(),
            GLSL.Image.load(lightImage_270).getLuminance(),
            GLSL.Image.load(lightImage_FRONT).getLuminance(),
         ];

         if (lightImage_NONE) {
            const lightLuminance_NONE =
               GLSL.Image.load(lightImage_NONE).getLuminance();

            for (let i = 0; i < lightLuminances.length; i++) {
               lightLuminances[i] =
                  lightLuminances[i].subtractFloat(lightLuminance_NONE);
            }
            lightLuminance_ALL =
               lightLuminance_ALL.subtractFloat(lightLuminance_NONE);
         }

         for (let i = 0; i < lightLuminances.length; i++) {
            lightLuminances[i] =
               lightLuminances[i].divideFloat(lightLuminance_ALL);
         }

         const horizontal = lightLuminances[0]
            .subtractFloat(lightLuminances[2])
            .addFloat(new GLSL.Float(1))
            .divideFloat(new GLSL.Float(2));

         const vertical = lightLuminances[3]
            .subtractFloat(lightLuminances[1])
            .addFloat(new GLSL.Float(1))
            .divideFloat(new GLSL.Float(2));

         const normalVector = new GLSL.Vector3([
            horizontal,
            vertical,
            lightLuminances[4],
         ]);

         const normalMapRendering = GLSL.render(
            normalVector.normalize().getVector4()
         );

         normalMapShader.purge();

         const normalMap = await normalMapRendering.getJsImage();

         if (uiImageElement && normalMap) {
            uiImageElement.src = normalMap.src;
         }

         resolve(normalMap);
      });
   });
}*/

6b2186ebd48a2f90bfff69ac4286dda7172b65d5

Better design

Better design

// TODO Better design

/* global THREE */
/* exported PointCloudHelper */

/**
 * @global
 */
class PointCloudHelper {
   /**
    * This functions calculates a point cloud by a given
    * depth mapping.
    *
    * @public
    * @param {HTMLImageElement} depthMapImage The depth
    * mapping that is used to calculate the point cloud.
    * @param {HTMLCanvasElement} renderCanvas The
    * UI-canvas to display the point cloud.
    * @param {number} depthFactor The factor that is
    * multiplied with the z-coordinate (depth-coordinate).
    * @param {HTMLImageElement} textureImage The texture
    * that is used for the point cloud vertex color.
    * @returns {Promise<number[]>} The vertices of the
    * calculated point cloud in an array. [x1, y1, z1, x2,
    * y2, z2, ...]
    */
   static async calculatePointCloud(
      depthMapImage,
      renderCanvas,
      depthFactor = 0.15,
      textureImage = depthMapImage
   ) {
      const pointCloudHelper = new PointCloudHelper(renderCanvas);

      return new Promise((resolve) => {
         setTimeout(async () => {
            if (depthMapImage.naturalWidth === 0) return;

            if (pointCloudHelper.isRenderObsolete()) return;
            await pointCloudHelper.renderingContext.initialize();
            if (pointCloudHelper.isRenderObsolete()) return;

            const dataCanvas = document.createElement("canvas");
            dataCanvas.width = depthMapImage.naturalWidth;
            dataCanvas.height = depthMapImage.naturalHeight;
            const dataContext = dataCanvas.getContext("2d");

            dataContext.drawImage(depthMapImage, 0, 0);

            if (pointCloudHelper.isRenderObsolete()) return;

            const imageData = dataContext.getImageData(
               0,
               0,
               dataCanvas.width,
               dataCanvas.height
            ).data;

            dataContext.drawImage(textureImage, 0, 0);

            if (pointCloudHelper.isRenderObsolete()) return;

            const textureData = dataContext.getImageData(
               0,
               0,
               dataCanvas.width,
               dataCanvas.height
            ).data;

            const vertices = [];
            const vertexColors = [];

            const maxDimension = Math.max(dataCanvas.width, dataCanvas.height);
            /*
            const aspectWidth = dataCanvas.width / dataCanvas.height;
            const aspectHeight = dataCanvas.height / dataCanvas.width;
            */

            for (let x = 0; x < dataCanvas.width; x++) {
               for (let y = 0; y < dataCanvas.height; y++) {
                  const index = (y * dataCanvas.width + x) * 4;

                  const r = textureData[index + 0];
                  const g = textureData[index + 1];
                  const b = textureData[index + 2];

                  if (r !== 0 || g !== 0 || b !== 0) {
                     const xC = (x / maxDimension - 0.5) * 100;
                     const yC = (1 - y / maxDimension - 0.75) * 100;
                     const zC = (imageData[index] / 255) * 100 * depthFactor;

                     vertices.push(xC, yC, zC);
                     vertexColors.push(r / 255, g / 255, b / 255);
                  }
               }
               if (pointCloudHelper.isRenderObsolete()) return;
            }

            PointCloudHelper.vertices = vertices;
            // TODO Better design
            resolve(vertices);

            pointCloudHelper.renderingContext.geometry.setAttribute(
               "position",
               new THREE.Float32BufferAttribute(vertices, 3)
            );
            pointCloudHelper.renderingContext.geometry.setAttribute(
               "color",
               new THREE.Float32BufferAttribute(vertexColors, 3)
            );

            if (pointCloudHelper.isRenderObsolete()) return;

            pointCloudHelper.renderingContext.geometry.attributes.position.needsUpdate = true;
            pointCloudHelper.renderingContext.geometry.attributes.color.needsUpdate = true;

            pointCloudHelper.renderingContext.render();

            pointCloudHelper.renderingContext.handleResize();
         }, 100);
      });
   }

   static async downloadOBJ() {
      // TODO Better design.
      const vertices = PointCloudHelper.vertices;

      if (vertices.length > 3) {
         await new Promise((resolve) => {
            setTimeout(() => {
               const filename = "point_cloud.obj";
               let objString = "";

               for (
                  let i = 0, vertexCount = vertices.length;
                  i < vertexCount;
                  i += 3
               ) {
                  const x = vertices[i];
                  const y = vertices[i + 1];
                  const z = vertices[i + 2];
                  objString += "v " + x + " " + y + " " + z + "\n";
               }

               let element = document.createElement("a");
               element.style.display = "none";

               let blob = new Blob([objString], {
                  type: "text/plain; charset = utf-8",
               });

               let url = window.URL.createObjectURL(blob);
               element.setAttribute("href", window.URL.createObjectURL(blob));
               element.setAttribute("download", filename);

               document.body.appendChild(element);

               element.click();

               window.URL.revokeObjectURL(url);
               element.remove();

               resolve();
            });
         });
      }
   }

   /**
    * @public
    */
   static cancelRenderJobs() {
      PointCloudHelper.renderId++;
   }

   /**
    * @public
    * @param {HTMLCanvasElement} canvas
    */
   static clearCanvas(canvas) {
      const pointCloudHelperRenderingContext =
         PointCloudHelperRenderingContext.getInstance(canvas);

      pointCloudHelperRenderingContext.geometry.setAttribute(
         "position",
         new THREE.Float32BufferAttribute([], 3)
      );
      pointCloudHelperRenderingContext.geometry.setAttribute(
         "color",
         new THREE.Float32BufferAttribute([], 3)
      );

      pointCloudHelperRenderingContext.geometry.attributes.position.needsUpdate = true;
      pointCloudHelperRenderingContext.geometry.attributes.color.needsUpdate = true;

      pointCloudHelperRenderingContext.render();
   }

   /**
    * @private
    * @param {HTMLCanvasElement} renderCanvas
    */
   constructor(renderCanvas) {
      this.renderId = PointCloudHelper.renderId;

      this.renderingContext =
         PointCloudHelperRenderingContext.getInstance(renderCanvas);
   }

   /**
    * @private
    * @returns {boolean}
    */
   isRenderObsolete() {
      return this.renderId < PointCloudHelper.renderId;
   }
}
PointCloudHelper.renderId = 0;

/** @type {PointCloudHelperRenderingContext[]} */
const PointCloudHelperRenderingContext_instances = [];

class PointCloudHelperRenderingContext {
   /**
    * @public
    * @param {HTMLCanvasElement} renderCanvas
    * @returns {PointCloudHelperRenderingContext}
    */
   static getInstance(renderCanvas) {
      for (
         let i = 0;
         i < PointCloudHelperRenderingContext_instances.length;
         i++
      ) {
         const testInstance = PointCloudHelperRenderingContext_instances[i];
         if (testInstance.renderCanvas === renderCanvas) {
            const instance = testInstance;
            return instance;
         }
      }

      const instance = new PointCloudHelperRenderingContext(renderCanvas);
      return instance;
   }

   /**
    * @private
    * @param {HTMLCanvasElement} renderCanvas
    */
   constructor(renderCanvas) {
      this.initialized = false;
      this.renderCanvas = renderCanvas;

      this.renderer = new THREE.WebGLRenderer({
         canvas: renderCanvas,
         alpha: true,
         antialias: true,
      });
      this.camera = new THREE.PerspectiveCamera(
         50,
         renderCanvas.width / renderCanvas.height,
         0.01,
         1000
      );

      // @ts-ignore
      this.controls = new THREE.OrbitControls(this.camera, this.renderCanvas);
      this.scene = new THREE.Scene();
      this.geometry = new THREE.BufferGeometry();
      this.material = new THREE.PointsMaterial({
         size: 2,
         vertexColors: true,
      });
      this.pointCloud = new THREE.Points(this.geometry, this.material);

      this.pointCloud.rotateX(35 * (Math.PI / 180));
      this.pointCloud.translateY(15);

      PointCloudHelperRenderingContext_instances.push(this);

      if (
         PointCloudHelperRenderingContext_instances.length >
         PointCloudHelperRenderingContext.MAX_INSTANCES
      ) {
         console.warn(
            "PointCloudHelperRenderingContext exceeded maximum render canvas instance count. The last instance gets deleted."
         );
         PointCloudHelperRenderingContext_instances.shift();
      }
   }

   async render() {
      await this.initialize();
      this.renderer.render(this.scene, this.camera);
   }

   /**
    * @public
    */
   async initialize() {
      if (this.initialized) {
         return;
      }
      await new Promise((resolve) => {
         setTimeout(() => {
            this.scene.add(this.pointCloud);

            this.camera.position.z = 75;
            this.camera.position.y = -100;

            this.controls.target = new THREE.Vector3(0, 0, 0);

            this.initialized = true;
            resolve();

            this.controls.addEventListener("change", () => {
               this.render();
            });
            window.addEventListener("resize", this.handleResize.bind(this));

            this.controls.update();
            this.handleResize();
         });
      });
   }

   /**
    * @public
    */
   handleResize() {
      const width = this.renderCanvas.clientWidth;
      const height = this.renderCanvas.clientHeight;
      const needResize =
         this.renderCanvas.width !== width ||
         this.renderCanvas.height !== height;
      if (needResize) {
         this.renderer.setSize(width, height);

         this.camera.aspect =
            this.renderCanvas.width / this.renderCanvas.height;
         this.camera.updateProjectionMatrix();
         this.render();
      }
   }
}

/** @type {number[]} */
PointCloudHelper.vertices = [];

/** @constant */
PointCloudHelperRenderingContext.MAX_INSTANCES = 8;

3649aad5ee8c84f7167d6233103200dcb9c2d243

Add node selection menu.

Add node selection menu.

// TODO Add node selection menu.

/* exported NodeCallback */

class NodeCallback {
   /**
    * @param {GraphNodeUI} graphNodeUI
    */
   constructor(graphNodeUI) {
      this.graphNodeUI = graphNodeUI;
      /**
       * @public
       * @type {boolean}
       */
      this.abortFlag = false;
   }

   /**
    * @public
    * @param {string} info
    */
   setInfo(info) {}

   /**
    * @public
    * @param {number} progressPercent
    */
   setProgressPercent(progressPercent) {
      this.graphNodeUI.domProgressElement.hidden = false;
      if (progressPercent <= 0) {
         this.graphNodeUI.domProgressElement.removeAttribute("value");
      } else if (progressPercent >= 100) {
         this.graphNodeUI.domProgressElement.hidden = true;
      } else {
         this.graphNodeUI.domProgressElement.value = progressPercent;
      }
   }
}

class NodeGraph {
   /**
    * @param {HTMLElement} parentElement
    */
   constructor(parentElement) {
      /**
       * @protected
       * @type {GraphNode[]}
       */
      this.registeredNodes = [];

      /**
       * @protected
       * @type {GraphNodeUI[]}
       */
      this.placedNodes = [];

      this.parentElement = parentElement;

      this.domCanvas = document.createElement("canvas");
      this.domCanvas.style.backgroundColor = "transparent";
      this.domCanvas.style.position = "absolute";
      this.domCanvas.style.width = "100%";
      this.domCanvas.style.height = "100%";

      window.addEventListener("resize", this.resizeHandler.bind(this));
      this.parentElement.appendChild(this.domCanvas);
      this.domCanvasContext = this.domCanvas.getContext("2d");

      this.currentMousePosition = {
         x: this.parentElement.clientWidth / 2,
         y: this.parentElement.clientHeight / 2,
      };

      this.parentElement.addEventListener(
         "mousemove",
         this.mousemoveHandler.bind(this)
      );
      this.parentElement.addEventListener(
         "mouseup",
         this.mouseUpHandler.bind(this)
      );

      /**
       * @private
       * @type {GraphNodeUI}
       */
      this.grabbedNode = null;

      /**
       * @private
       * @type {GraphNodeInputUI | GraphNodeOutputUI}
       */
      this.linkedNodeIO = null;

      this.resizeHandler();
   }

   /**
    * @public
    * @param {Function} nodeExecuter
    * @returns {GraphNode}
    */
   registerNode(nodeExecuter) {
      const graphNode = new GraphNode(nodeExecuter, false);
      this.registeredNodes.push(graphNode);
      return graphNode;
   }

   /**
    * @public
    * @param {Function} nodeExecuter
    * @param {string[]} dependencies
    * @returns {GraphNode}
    */
   registerNodeAsWorker(nodeExecuter, ...dependencies) {
      const graphNode = new GraphNode(nodeExecuter, true, ...dependencies);
      this.registeredNodes.push(graphNode);
      return graphNode;
   }

   /**
    * @param {GraphNode} graphNode
    * @param {{x:number, y:number}} position
    * @returns {GraphNodeUI}
    */
   placeNode(graphNode, position = this.currentMousePosition) {
      const graphNodeUI = new GraphNodeUI(graphNode, this);
      this.placedNodes.push(graphNodeUI);
      graphNodeUI.setPosition(position);
      this.parentElement.appendChild(graphNodeUI.domElement);
      return graphNodeUI;
   }

   /**
    * @public
    * @param {InputGraphNode} inputGraphNode
    * @param {{x:number, y:number}} position
    */
   placeInputGraphNode(inputGraphNode, position) {
      this.placedNodes.push(inputGraphNode);
      if (position) inputGraphNode.setPosition(position);
      this.parentElement.appendChild(inputGraphNode.domElement);
   }

   /**
    * @public
    * @param {string} type
    * @param {{x:number, y:number}} position
    * @param {any} initValue
    * @returns {GraphNodeUI}
    */
   createInputNode(type, position, initValue = undefined) {
      const inputGraphNode = new InputGraphNode(this, type);
      if (initValue) {
         inputGraphNode.setValue(initValue);
      }
      this.placeInputGraphNode(inputGraphNode, position);
      return inputGraphNode;
   }

   /**
    * @public
    * @param {Promise<GraphNodeOutputUI>} output
    * @param {Promise<GraphNodeInputUI>} input
    */
   async connect(output, input) {
      const inputResolved = await input;
      const outputResolved = await output;
      inputResolved.setConnection(outputResolved);
      this.updateConnectionUI();
   }

   /**
    * @param {GraphNodeUI} graphNodeUI
    */
   displaceNode(graphNodeUI) {
      const graphNodeIndex = this.placedNodes.indexOf(graphNodeUI);
      this.placedNodes.splice(graphNodeIndex);
   }

   doubleClickHandler() {
      // TODO Add node selection menu.
      this.placeNode(this.registeredNodes[0]);
   }

   /**
    * @private
    */
   resizeHandler() {
      this.domCanvas.height = this.parentElement.clientHeight;
      this.domCanvas.width = this.parentElement.clientWidth;
      this.updateConnectionUI();
   }

   /**
    * @public
    * @param {GraphNodeInputUI | GraphNodeOutputUI} graphNodeIO
    */
   toggleConnection(graphNodeIO) {
      if (this.linkedNodeIO === null) {
         this.linkedNodeIO = graphNodeIO;
      } else if (
         graphNodeIO instanceof GraphNodeInputUI &&
         this.linkedNodeIO instanceof GraphNodeOutputUI
      ) {
         graphNodeIO.setConnection(this.linkedNodeIO);
         this.linkedNodeIO = null;
      } else if (
         graphNodeIO instanceof GraphNodeOutputUI &&
         this.linkedNodeIO instanceof GraphNodeInputUI
      ) {
         this.linkedNodeIO.setConnection(graphNodeIO);
         this.linkedNodeIO = null;
      }
      this.updateConnectionUI();
   }

   /**
    * @private
    * @returns {Promise<{input: GraphNodeInputUI, output: GraphNodeOutputUI}[]>}
    */
   async getConnections() {
      /** @type {{input: GraphNodeInputUI, output: GraphNodeOutputUI}[]} */
      const connections = [];
      this.placedNodes.forEach(async (node) => {
         connections.push(...(await node.getConnections()));
      });
      return connections;
   }

   /**
    * @public
    */
   async updateConnectionUI() {
      this.domCanvasContext.clearRect(
         0,
         0,
         this.domCanvasContext.canvas.width,
         this.domCanvasContext.canvas.height
      );
      this.domCanvasContext.beginPath();
      this.domCanvasContext.strokeStyle = "white";
      this.domCanvasContext.lineWidth = 2;

      const connections = await this.getConnections();
      connections.forEach((connection) => {
         if (connection.input && connection.output) {
            const startRect =
               connection.input.domElement.getBoundingClientRect();
            const start = {
               x: startRect.left,
               y: (startRect.top + startRect.bottom) / 2,
            };
            const endRect =
               connection.output.domElement.getBoundingClientRect();
            const end = {
               x: endRect.right,
               y: (endRect.top + endRect.bottom) / 2,
            };
            this.domCanvasContext.moveTo(start.x, start.y);
            this.domCanvasContext.lineTo(end.x, end.y);
         }
      });

      this.domCanvasContext.stroke();
   }

   mouseUpHandler() {
      this.grabbedNode = null;
      this.linkedNodeIO = null;
      this.updateConnectionUI();
   }

   /**
    * @param {GraphNodeUI} graphNode
    */
   setGrabbedNode(graphNode) {
      this.grabbedNode = graphNode;
   }

   /**
    * @param {GraphNodeInputUI | GraphNodeOutputUI} linkedNodeIO
    */
   setLinkedNodeIO(linkedNodeIO) {
      this.linkedNodeIO = linkedNodeIO;
      this.updateConnectionUI();
   }

   /**
    * @private
    * @param {MouseEvent} mouseEvent
    */
   mousemoveHandler(mouseEvent) {
      this.currentMousePosition = {
         x: mouseEvent.pageX - this.parentElement.offsetLeft,
         y: mouseEvent.pageY - this.parentElement.offsetTop,
      };

      if (this.grabbedNode) {
         this.grabbedNode.setPosition(this.currentMousePosition);
         this.updateConnectionUI();
      }

      if (this.linkedNodeIO) {
         this.domCanvasContext.clearRect(
            0,
            0,
            this.domCanvasContext.canvas.width,
            this.domCanvasContext.canvas.height
         );
         this.domCanvasContext.beginPath();
         this.domCanvasContext.strokeStyle = "white";
         this.domCanvasContext.lineWidth = 2;

         const startRect = this.linkedNodeIO.domElement.getBoundingClientRect();
         const start = {
            x: startRect.right,
            y: (startRect.top + startRect.bottom) / 2,
         };
         const end = this.currentMousePosition;
         this.domCanvasContext.moveTo(start.x, start.y);
         this.domCanvasContext.lineTo(end.x, end.y);

         this.domCanvasContext.stroke();
      }
   }
}

class GraphNode {
   /**
    * @param {Function} executer
    * @param {boolean} asWorker
    * @param {string[]} dependencies
    */
   constructor(executer, asWorker, ...dependencies) {
      /**
       * @public
       * @type {Function}
       */
      this.executer = executer;

      this.asWorker = asWorker;

      /**
       * @private
       * @type {string[]}
       */
      this.dependencies = dependencies;

      /**
       * @protected
       * @type {GraphNodeInput[]}
       */
      this.graphNodeInputs = [];

      /**
       * @protected
       * @type {GraphNodeOutput[]}
       */
      this.graphNodeOutputs = [];

      /**
       * @protected
       * @type {{output: GraphNodeOutput, input: GraphNodeInput}[]}
       */
      this.outputConnections = [];

      /**
       * @private
       * @type {boolean}
       */
      this.initialized = false;

      /**
       * @private
       * @type {boolean}
       */
      this.initializing = false;
   }

   /**
    * @public
    * @returns {Promise<string>}
    */
   async getDependenciesSource() {
      let dependenciesSource = "";
      for (let i = 0; i < this.dependencies.length; i++) {
         const dependencySource = await new Promise((resolve) => {
            window.fetch(this.dependencies[i]).then(async (response) => {
               resolve(await response.text());
            });
         });
         dependenciesSource += dependencySource;
      }
      return dependenciesSource;
   }

   /**
    * @private
    */
   async initialize() {
      while (this.initializing === true) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }
      if (this.initialized === false) {
         this.initializing = true;
         await this.readFunctionSourceWithDocs();
         this.initialized = true;
         this.initializing = false;
      }
   }

   /**
    * @public
    * @returns {Promise<GraphNodeInput[]>}
    */
   async getInputs() {
      await this.initialize();
      return this.graphNodeInputs;
   }

   /**
    * @public
    * @returns {Promise<GraphNodeOutput[]>}
    */
   async getOutputs() {
      await this.initialize();
      return this.graphNodeOutputs;
   }

   /**
    * @public
    * @returns {string}
    */
   getName() {
      return this.executer.name;
   }

   /**
    * @private
    */
   async readFunctionSourceWithDocs() {
      const scriptElements = Array.from(document.scripts);

      const functionSource = this.executer.toString();

      for (let i = 0, count = scriptElements.length; i < count; i++) {
         const scriptSource = await new Promise((resolve) => {
            window.fetch(scriptElements[i].src).then(async (response) => {
               resolve(await response.text());
            });
         });

         if (scriptSource.includes(functionSource)) {
            const jsDoc = new RegExp(
               "(\\/\\*\\*\\s*\\n([^\\*]|(\\*(?!\\/)))*\\*\\/)(\\s*\\n*\\s*)(.*)" +
                  this.getName() +
                  "\\s*\\(",
               "mg"
            )
               .exec(scriptSource)[0]
               .replaceAll("\n", "")
               .replaceAll("*", "");

            const jsDocArguments = jsDoc.split("@");
            jsDocArguments.shift();

            jsDocArguments.forEach((argument) => {
               const argumentType = argument.split(" ")[0];

               if (argumentType === "param") {
                  const argumentVarType = argument
                     .split("{")[1]
                     .split("}")[0]
                     .replace("Promise<", "")
                     .replace(">", "");
                  const argumentVarName = argument.split("} ")[1].split(" ")[0];
                  const argumentDescription = argument.split(
                     " " + argumentVarName + " ",
                     2
                  )[1];

                  this.graphNodeInputs.push(
                     new GraphNodeInput(
                        argumentVarName,
                        argumentVarType,
                        argumentDescription
                     )
                  );
               } else if (argumentType === "returns") {
                  const argumentVarType = argument
                     .split("{")[1]
                     .split("}")[0]
                     .replace("Promise<", "")
                     .replace(">", "");
                  const argumentDescription = argument.split("} ")[1];

                  this.graphNodeOutputs.push(
                     new GraphNodeOutput(argumentVarType, argumentDescription)
                  );
               }
            });
         }
      }
   }
}

class GraphNodeInput {
   /**
    * @param {string} name
    * @param {string} type
    * @param {string} description
    */
   constructor(name, type, description = undefined) {
      //this.name = name.replace(/([A-Z])/g, " $1");
      this.name = name;
      this.uiName = this.name.charAt(0).toUpperCase() + this.name.slice(1);
      this.type = type;
      this.description = description.replaceAll(/\s\s+/g, " ");
   }
}

class GraphNodeOutput {
   /**
    * @param {string} type
    * @param {string} description
    */
   constructor(type, description = undefined) {
      this.type = type;
      this.description = description;
   }
}

class GraphNodeInputUI extends GraphNodeInput {
   /**
    * @param {GraphNodeInput[]} graphNodeInputs
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNode
    * @returns {GraphNodeInputUI[]}
    */
   static getFromGraphNodeInputs(graphNodeInputs, nodeGraph, graphNode) {
      /** @type {GraphNodeInputUI[]} */
      const graphNodeInputsUI = [];

      graphNodeInputs.forEach((graphNodeInput) => {
         graphNodeInputsUI.push(
            new GraphNodeInputUI(
               graphNodeInput.name,
               graphNodeInput.type,
               graphNodeInput.description,
               nodeGraph,
               graphNode
            )
         );
      });

      return graphNodeInputsUI;
   }

   /**
    * @param {string} name
    * @param {string} type
    * @param {string} description
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNodeUI
    * @param {string} cssClass
    */
   constructor(
      name,
      type,
      description = undefined,
      nodeGraph,
      graphNodeUI,
      cssClass = "graphNodeInput"
   ) {
      super(name, type, description);
      /**
       * @private
       * @type {GraphNodeOutputUI}
       */
      this.connection = null;

      this.nodeGraph = nodeGraph;
      this.graphNodeUI = graphNodeUI;
      this.domElement = document.createElement("li");
      this.domElement.innerText = name;
      this.domElement.title = "[" + this.type + "]\n" + this.description;
      this.domElement.style.textAlign = "left";
      this.domElement.classList.add(cssClass);

      this.domElement.addEventListener("click", this.clickHandler.bind(this));
      this.domElement.addEventListener(
         "mousedown",
         this.mouseHandler.bind(this)
      );
      this.domElement.addEventListener("mouseup", this.mouseHandler.bind(this));
   }

   /**
    * @private
    * @param {MouseEvent} mouseEvent
    */
   clickHandler(mouseEvent) {
      if (mouseEvent.detail > 1) {
         this.doubleClickHandler();
      }
   }

   /**
    * @private
    */
   mouseHandler() {
      this.nodeGraph.toggleConnection(this);
   }

   /**
    * @private
    */
   doubleClickHandler() {
      /*const boundingRect = this.domElement.getBoundingClientRect();
      this.nodeGraph.placeInputGraphNode(
         new InputGraphNode(this.nodeGraph, this),
         { x: boundingRect.left - 200, y: boundingRect.top - 25 }
      );
      this.nodeGraph.setLinkedNodeIO(null);*/
   }

   /**
    * @public
    * @returns {GraphNodeOutputUI}
    */
   getConnection() {
      return this.connection;
   }

   /**
    * @public
    * @param {GraphNodeOutputUI} graphNodeOutput
    */
   setConnection(graphNodeOutput) {
      if (this.connection) {
         this.connection.removeConnection(this);
      }
      graphNodeOutput.addConnection(this);
      this.connection = graphNodeOutput;
      this.graphNodeUI.setRefreshFlag();
   }

   /**
    * @public
    */
   removeConnection() {
      this.connection = null;
   }
}

class GraphNodeOutputUI extends GraphNodeOutput {
   /**
    * @param {GraphNodeOutput[]} graphNodeOutputs
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNode
    * @returns {GraphNodeOutputUI[]}
    */
   static getFromGraphNodeOutputs(graphNodeOutputs, nodeGraph, graphNode) {
      /** @type {GraphNodeOutputUI[]} */
      const graphNodeOutputsUI = [];

      graphNodeOutputs.forEach((graphNodeOutput) => {
         graphNodeOutputsUI.push(
            new GraphNodeOutputUI(
               graphNodeOutput.type,
               graphNodeOutput.description,
               nodeGraph,
               graphNode
            )
         );
      });

      return graphNodeOutputsUI;
   }

   /**
    * @param {string} type
    * @param {string} description
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeUI} graphNode
    * @param {string} cssClass
    */
   constructor(
      type,
      description = undefined,
      nodeGraph,
      graphNode,
      cssClass = "graphNodeInput"
   ) {
      super(type, description);
      /**
       * @private
       * @type {any}
       */
      this.value = undefined;
      this.nodeGraph = nodeGraph;
      /**
       * @public
       * @type {GraphNodeUI}
       */
      this.graphNodeUI = graphNode;
      /**
       * @private
       * @type {GraphNodeInputUI[]}
       */
      this.connections = [];
      this.domElement = document.createElement("li");
      this.domElement.innerText = "โ–ถ";
      this.domElement.title = "[" + this.type + "]";
      this.domElement.style.textAlign = "right";
      this.domElement.classList.add(cssClass);

      this.domElement.addEventListener(
         "mousedown",
         this.mouseHandler.bind(this)
      );
   }

   /**
    * @public
    * @param {GraphNodeInputUI} graphNodeInputUI
    */
   addConnection(graphNodeInputUI) {
      this.connections.push(graphNodeInputUI);
   }

   /**
    * @public
    * @param {GraphNodeInputUI} graphNodeInputUI
    */
   removeConnection(graphNodeInputUI) {
      const id = this.connections.indexOf(graphNodeInputUI);
      this.connections.splice(id);
   }

   /**
    * @public
    * @returns {GraphNodeInputUI[]}
    */
   getConnections() {
      return this.connections;
   }

   /**
    * @public
    * @returns {any}
    */
   getValue() {
      return this.value;
   }

   /**
    * @public
    * @param {any} value
    */
   async setValue(value) {
      this.value = value;

      const outputNodes = this.graphNodeUI.getOutputNodes();

      outputNodes.forEach((outputNode) => {
         if (outputNode !== this.graphNodeUI) {
            outputNode.setRefreshFlag();
         }
      });
   }

   /**
    * @private
    */
   mouseHandler() {
      this.nodeGraph.toggleConnection(this);
   }
}

class GraphNodeUI {
   /**
    * @param {GraphNode} graphNode
    * @param {NodeGraph} nodeGraph
    * @param {string} cssClass
    */
   constructor(graphNode, nodeGraph, cssClass = "graphNode") {
      this.graphNode = graphNode;
      this.nodeGraph = nodeGraph;
      this.cssClass = cssClass;
      this.domElement = document.createElement("span");
      this.position = {
         x: this.nodeGraph.parentElement.clientWidth / 2,
         y: this.nodeGraph.parentElement.clientHeight / 2,
      };
      this.refreshFlag = true;

      if (this.graphNode) {
         this.initialize();
      }
   }

   /**
    * @public
    * @param {string} name
    * @returns {Promise<GraphNodeInputUI>}
    */
   async getInput(name) {
      await this.initialize();

      for (let i = 0; i < this.graphNodeInputs.length; i++) {
         if (this.graphNodeInputs[i].name === name)
            return this.graphNodeInputs[i];
      }
   }

   /**
    * @public
    * @param {string} description
    * @returns {Promise<GraphNodeOutputUI>}
    */
   async getOutput(description = undefined) {
      await this.initialize();

      if (this.graphNodeOutputs.length === 1) {
         return this.graphNodeOutputs[0];
      } else {
         for (let i = 0; i < this.graphNodeOutputs.length; i++) {
            if (this.graphNodeOutputs[i].description === description)
               return this.graphNodeOutputs[i];
         }
      }
   }

   /**
    * @public
    */
   setRefreshFlag() {
      this.refreshFlag = true;
      this.execute();
   }

   /**
    * @public
    */
   async execute() {
      if (this.graphNode.asWorker) {
         if (this.worker) {
            console.log("terminating " + this.graphNode.executer.name + ".");
            this.worker.terminate();
         }
      } else {
         if (this.executerCallback) {
            console.log("aborting " + this.graphNode.executer.name + ".");
            this.executerCallback.abortFlag = true;
         }
      }

      if (this.refreshFlag) {
         this.refreshFlag = false;

         const parameterValues = this.getParameterValues();
         if (parameterValues.includes(undefined)) return;

         console.log(
            "Calling function '" + this.graphNode.executer.name + "'."
         );

         this.executerCallback = new NodeCallback(this);
         this.executerCallback.setProgressPercent(0);

         if (this.graphNode.asWorker) {
            this.executeAsWorker(parameterValues);
         } else {
            this.executeAsPromise(parameterValues);
         }

         if (!parameterValues.includes(undefined)) {
            console.log("Executing " + this.graphNode.executer.name + ".");
         } else {
            console.log(
               "Function '" +
                  this.graphNode.executer.name +
                  "' did not pick up, because at least one parameter is undefined."
            );
         }
      }
   }

   /**
    * @private
    * @param {any[]} parameterValues
    */
   async executeAsPromise(parameterValues) {
      setTimeout(async () => {
         const result = await this.graphNode.executer(
            ...parameterValues,
            this.executerCallback
         );

         this.graphNodeOutputs[0].setValue(result);
         this.refreshValuePreview(result);
         this.executerCallback.setProgressPercent(100);
      });
   }

   /**
    * @private
    * @param {any[]} parameterValues
    */
   async executeAsWorker(parameterValues) {
      const toTransfer = [];
      const toCopy = [];

      let parameterValuesString = "(";
      let pointerCount = 0;
      let copyCount = 0;

      parameterValues.forEach((parameterValue) => {
         if (false && parameterValue instanceof ImageBitmap) {
            toTransfer.push(parameterValue);
            parameterValuesString +=
               "messageEvent.data.pointer[" + String(pointerCount) + "]";
            pointerCount++;
         } else {
            toCopy.push(parameterValue);
            parameterValuesString +=
               "messageEvent.data.copy[" + String(copyCount) + "]";
            copyCount++;
         }
         parameterValuesString += ",";
      });
      parameterValuesString += ")";

      this.worker = await this.createWorker(parameterValuesString);

      const cThis = this;
      this.worker.addEventListener(
         "message",
         async function handler(messageEvent) {
            cThis.worker.removeEventListener(messageEvent.type, handler);
            const resultValue = messageEvent.data;
            // TODO Handle multiple outputs.
            cThis.graphNodeOutputs[0].setValue(resultValue);
            cThis.refreshValuePreview(resultValue);

            cThis.worker.terminate();
            cThis.worker = undefined;
            cThis.executerCallback.setProgressPercent(100);
         }
      );

      this.worker.postMessage(
         { pointer: toTransfer, copy: toCopy },
         toTransfer
      );
   }

   /**
    * @private
    * @param {string} parameterValuesString
    * @returns {Promise<Worker>}
    */
   async createWorker(parameterValuesString) {
      const dependenciesSource = await this.graphNode.getDependenciesSource();

      const workerSource =
         dependenciesSource +
         "\n" +
         "const cSelf = self;\n" +
         "self.addEventListener('message', async (messageEvent) => {\n" +
         "cSelf.postMessage(await " +
         this.graphNode.executer.name +
         parameterValuesString +
         ");\n" +
         "});";

      const blob = new Blob([workerSource], {
         type: "text/javascript",
      });
      const workerSrc = window.URL.createObjectURL(blob);
      return new Worker(workerSrc);
   }

   /**
    * @protected
    * @param {any} value
    */
   refreshValuePreview(value) {
      this.outputUIElement.innerHTML = "";

      if (value instanceof ImageBitmap) {
         const imageCanvas = document.createElement("canvas");
         imageCanvas.width = value.width;
         imageCanvas.height = value.height;
         const context = imageCanvas.getContext("2d");
         context.drawImage(value, 0, 0, value.width, value.height);
         const imageElement = new Image();
         imageElement.style.maxWidth = "100%";
         imageCanvas.style.maxHeight = "5rem";
         this.outputUIElement.style.display = "flex";
         this.outputUIElement.style.justifyContent = "center";
         this.outputUIElement.appendChild(imageElement);
         imageElement.src = imageCanvas.toDataURL();
      } else if (Array.isArray(value) && value[0] instanceof ImageBitmap) {
         const imageCanvas = document.createElement("canvas");
         imageCanvas.width = value[0].width;
         imageCanvas.height = value[0].height;
         const context = imageCanvas.getContext("2d");
         context.drawImage(value[0], 0, 0, value[0].width, value[0].height);
         const imageElement = new Image();
         imageElement.style.maxWidth = "100%";
         imageCanvas.style.maxHeight = "5rem";
         this.outputUIElement.style.display = "flex";
         this.outputUIElement.style.justifyContent = "center";
         this.outputUIElement.appendChild(imageElement);
         imageElement.src = imageCanvas.toDataURL();
      } else if (typeof value === "number") {
         const numberElement = document.createElement("div");
         numberElement.innerText = String(value);
         numberElement.style.textAlign = "center";
         this.outputUIElement.appendChild(numberElement);
      } else if (typeof value === "string") {
         const valueImage = new Image();
         valueImage.src = value;
         valueImage.style.maxWidth = "100%";
         valueImage.style.maxHeight = "5rem";
         this.outputUIElement.style.display = "flex";
         this.outputUIElement.style.justifyContent = "center";
         this.outputUIElement.appendChild(valueImage);
      }

      this.nodeGraph.updateConnectionUI();
   }

   /**
    * @private
    * @returns {any[]}
    */
   getParameterValues() {
      const parameterValues = [];
      this.graphNodeInputs.forEach((input) => {
         const connection = input.getConnection();
         if (connection) {
            parameterValues.push(connection.getValue());
         } else {
            parameterValues.push(undefined);
         }
      });
      return parameterValues;
   }

   /**
    * @public
    */
   async initialize() {
      while (this.initializing) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }
      if (this.initialized) {
         return;
      }
      this.initialized = false;
      this.initializing = true;

      if (this.graphNode) {
         /**
          * @override
          * @protected
          * @type {GraphNodeInputUI[]}
          */
         this.graphNodeInputs = GraphNodeInputUI.getFromGraphNodeInputs(
            await this.graphNode.getInputs(),
            this.nodeGraph,
            this
         );

         /**
          * @override
          * @protected
          * @type {GraphNodeOutputUI[]}
          */
         this.graphNodeOutputs = GraphNodeOutputUI.getFromGraphNodeOutputs(
            await this.graphNode.getOutputs(),
            this.nodeGraph,
            this
         );
      }

      this.domElement.classList.add(this.cssClass);

      const domTitleElement = document.createElement("h1");
      domTitleElement.style.cursor = "grab";
      domTitleElement.addEventListener(
         "mousedown",
         this.mousedownGrabHandler.bind(this)
      );
      domTitleElement.innerText = this.graphNode.getName();
      domTitleElement.style.backgroundColor = "transparent";
      this.domElement.appendChild(domTitleElement);

      this.domProgressElement = document.createElement("progress");
      this.domProgressElement.style.width = "100%";
      this.domProgressElement.value = 0;
      this.domProgressElement.max = 100;
      this.domElement.appendChild(this.domProgressElement);
      this.domProgressElement.hidden = true;

      this.outputUIElement = document.createElement("div");
      this.domElement.appendChild(this.outputUIElement);

      const domIOElement = document.createElement("div");
      const domInputList = document.createElement("ul");
      const domOutputList = document.createElement("ul");

      domIOElement.style.display = "flex";
      domIOElement.style.justifyContent = "space-between";
      domIOElement.style.marginLeft = "-10%";
      domIOElement.style.width = "120%";

      this.domElement.appendChild(domIOElement);
      domIOElement.appendChild(domInputList);
      domIOElement.appendChild(domOutputList);

      this.graphNodeInputs.forEach((graphNodeInput) => {
         domInputList.appendChild(graphNodeInput.domElement);
      });
      this.graphNodeOutputs.forEach((graphNodeOutput) => {
         domOutputList.appendChild(graphNodeOutput.domElement);
      });

      this.execute();

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @public
    * @returns {Promise<{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]>}
    */
   async getConnections() {
      /** @type {{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]} */
      const connections = [];

      this.graphNodeInputs.forEach((graphNodeInput) => {
         if (graphNodeInput.graphNodeUI === this) {
            const output = graphNodeInput.getConnection();
            if (output) {
               connections.push({
                  input: graphNodeInput,
                  output: output,
               });
            }
         }
      });

      return connections;
   }

   /**
    * @public
    * @returns {GraphNodeUI[]}
    */
   getOutputNodes() {
      /** @type {GraphNodeUI[]} */
      const outputNodes = [];

      this.graphNodeOutputs.forEach((graphNodeOutput) => {
         graphNodeOutput.getConnections().forEach((connection) => {
            outputNodes.push(connection.graphNodeUI);
         });
      });
      return outputNodes;
   }

   /**
    * @protected
    */
   mousedownGrabHandler() {
      this.nodeGraph.setGrabbedNode(this);
   }

   /**
    * @public
    * @returns {{x:number, y:number}}
    */
   getPosition() {
      const boundingRect = this.domElement.getBoundingClientRect();
      return {
         x: boundingRect.left + boundingRect.width / 2,
         y: boundingRect.top + 5,
      };
   }

   /**
    * @param {{x:number, y:number}} position
    */
   setPosition(position) {
      this.position = position;
      this.domElement.style.transform =
         "translate(calc(" +
         this.position.x +
         "px - 50%), calc(" +
         this.position.y +
         "px - 0.25rem))";
   }
}

class InputGraphNode extends GraphNodeUI {
   /**
    * @param {NodeGraph} nodeGraph
    * @param {GraphNodeInputUI | string} inputNodeOrType
    * @param {string} cssClass
    */
   constructor(nodeGraph, inputNodeOrType, cssClass = "graphNode") {
      super(undefined, nodeGraph, cssClass);

      if (inputNodeOrType instanceof GraphNodeInputUI) {
         this.type = inputNodeOrType.type;
         this.inputNode = inputNodeOrType;
      } else {
         this.type = inputNodeOrType;
      }

      this.initialize();

      if (inputNodeOrType instanceof GraphNodeInputUI)
         this.setConnectionToInputNode();
   }

   /**
    * @private
    */
   async setConnectionToInputNode() {
      this.inputNode.setConnection(this.graphNodeOutputs);
      this.nodeGraph.updateConnectionUI();
   }

   /**
    * @override
    */
   async initialize() {
      while (this.initializing) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }
      if (this.initialized) {
         return;
      }

      this.domElement.classList.add(this.cssClass);

      const domTitleElement = document.createElement("h1");
      domTitleElement.style.cursor = "grab";
      domTitleElement.addEventListener(
         "mousedown",
         this.mousedownGrabHandler.bind(this)
      );
      domTitleElement.innerText = this.type;
      domTitleElement.style.backgroundColor = "transparent";
      this.domElement.appendChild(domTitleElement);

      this.domProgressElement = document.createElement("progress");
      this.domProgressElement.style.width = "100%";
      this.domProgressElement.value = 0;
      this.domProgressElement.max = 100;
      this.domElement.appendChild(this.domProgressElement);
      this.domProgressElement.hidden = true;

      this.outputUIElement = document.createElement("div");
      this.domElement.appendChild(this.outputUIElement);

      const domIOElement = document.createElement("div");
      const domInputList = document.createElement("ul");
      const domOutputList = document.createElement("ul");

      domIOElement.style.display = "flex";
      domIOElement.style.justifyContent = "space-between";
      domIOElement.style.marginLeft = "10%";
      domIOElement.style.width = "100%";

      this.domElement.appendChild(domIOElement);
      domIOElement.appendChild(domInputList);
      domIOElement.appendChild(domOutputList);

      this.inputElement = document.createElement("input");
      this.inputElement.style.width = "80%";
      this.inputElement.style.overflowWrap = "break-word";
      this.inputElement.style.hyphens = "auto";
      this.inputElement.style.whiteSpace = "normal";
      this.inputElement.multiple = false;

      if (this.type === "number") {
         this.inputElement.type = "number";
         domInputList.appendChild(this.inputElement);
      } else if (this.type === "ImageBitmap") {
         this.inputElement.type = "file";
         this.inputElement.accept = "image/*";
      } else if (this.type === "ImageBitmap[]") {
         this.inputElement.type = "file";
         this.inputElement.accept = "image/*";
         this.inputElement.multiple = true;
      } else {
         console.error("Input type '" + this.type + "' not supported.");
      }

      this.inputElement.addEventListener(
         "input",
         this.inputChangeHandler.bind(this)
      );

      domInputList.appendChild(this.inputElement);

      this.graphNodeOutputs = [
         new GraphNodeOutputUI(
            this.type,
            "[" + this.type + "]",
            this.nodeGraph,
            this
         ),
      ];

      domOutputList.appendChild(this.graphNodeOutputs[0].domElement);

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @public
    * @param {any} value
    */
   async setValue(value) {
      this.inputElement.value = value;
      this.inputElement.dispatchEvent(new Event("input"));
   }

   /**
    * @private
    * @param {InputEvent} inputEvent
    */
   inputChangeHandler(inputEvent) {
      if (this.type === "number") {
         let value = Number(
            /** @type {HTMLInputElement} */ (inputEvent.target).value
         );
         if (!value) {
            value = 0;
         }
         this.graphNodeOutputs[0].setValue(value);
         this.refreshValuePreview(value);
      } else if (this.type === "ImageBitmap") {
         const nodeCallback = new NodeCallback(this);
         nodeCallback.setProgressPercent(0);

         const imageLoaderWorker = new Worker("./src/image-loader-worker.js");

         imageLoaderWorker.addEventListener("message", async (messageEvent) => {
            const imageBitmap = messageEvent.data;
            this.graphNodeOutputs[0].setValue(imageBitmap);
            this.refreshValuePreview(imageBitmap);
            nodeCallback.setProgressPercent(100);
         });
         imageLoaderWorker.postMessage(inputEvent.target.files[0]);
      } else if (this.type === "ImageBitmap[]") {
         const nodeCallback = new NodeCallback(this);
         nodeCallback.setProgressPercent(0);

         const files = Array.from(inputEvent.target.files);
         const imageCount = files.length;
         const imageBitmapArray = [];

         files.forEach((file) => {
            const imageLoaderWorker = new Worker(
               "./src/image-loader-worker.js"
            );

            imageLoaderWorker.addEventListener(
               "message",
               async (messageEvent) => {
                  const imageBitmap = messageEvent.data;
                  imageBitmapArray.push(imageBitmap);
                  if (imageBitmapArray.length === imageCount) {
                     this.graphNodeOutputs[0].setValue(imageBitmapArray);
                     this.refreshValuePreview(imageBitmap);
                  }
                  nodeCallback.setProgressPercent(
                     (imageBitmapArray.length / imageCount) * 100
                  );
               }
            );
            imageLoaderWorker.postMessage(file);
         });
      }
   }

   /**
    * @override
    * @public
    * @returns {Promise<{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]>}
    */
   async getConnections() {
      // TODO Handle multiple outputs.
      return [{ input: this.inputNode, output: this.graphNodeOutputs[0] }];
   }

   /**
    * @override
    * @public
    */
   async execute() {
      this.refreshFlag = false;
   }
}

c245f34978705a63aacff73b2449b76e745b0507

use cameraVerticalShift

use cameraVerticalShift

// TODO: use cameraVerticalShift

/* global GLSL */
/* exported photometricStereoNormalMap */

/**
 * @public
 * @param {number} lightPolarAngleDeg
 * @param {ImageBitmap[]} lightImages
 * @returns {Promise<ImageBitmap>}
 */
async function photometricStereoNormalMap(lightPolarAngleDeg, lightImages) {
   const lightImage_000 = lightImages[0];
   const lightImage_045 = lightImages[1];
   const lightImage_090 = lightImages[2];
   const lightImage_135 = lightImages[3];
   const lightImage_180 = lightImages[4];
   const lightImage_225 = lightImages[5];
   const lightImage_270 = lightImages[6];
   const lightImage_315 = lightImages[7];

   const normalMapShader = new GLSL.Shader({
      width: lightImage_000.width,
      height: lightImage_000.height,
   });

   normalMapShader.bind();

   const lightLuminances = [
      GLSL.Image.load(lightImage_000).getLuminance(),
      GLSL.Image.load(lightImage_045).getLuminance(),
      GLSL.Image.load(lightImage_090).getLuminance(),
      GLSL.Image.load(lightImage_135).getLuminance(),
      GLSL.Image.load(lightImage_180).getLuminance(),
      GLSL.Image.load(lightImage_225).getLuminance(),
      GLSL.Image.load(lightImage_270).getLuminance(),
      GLSL.Image.load(lightImage_315).getLuminance(),
   ];

   const all = new GLSL.Float(0).maximum(...lightLuminances);

   let mask = new GLSL.Float(1);

   /*if (lightImage_NONE) {
      const lightLuminance_NONE =
         GLSL.Image.load(lightImage_NONE).getLuminance();

      for (let i = 0; i < lightLuminances.length; i++) {
         lightLuminances[i] =
            lightLuminances[i].subtractFloat(lightLuminance_NONE);
      }

      mask = all
         .subtractFloat(lightLuminance_NONE)
         .step(new GLSL.Float(maskThreshold));
   }*/

   for (let i = 0; i < lightLuminances.length; i++) {
      lightLuminances[i] = lightLuminances[i].divideFloat(all);
   }

   /**
    * @param {GLSL.Float} originLuminance
    * @param {GLSL.Float} orthogonalLuminance
    * @param {GLSL.Float} oppositeLuminance
    * @param {number} originAzimuthalAngleDeg
    * @param {number} orthogonalAzimuthalAngleDeg
    * @param {number} oppositeAzimuthalAngleDeg
    * @returns {GLSL.Vector3}
    */
   function getAnisotropicNormalVector(
      originLuminance,
      orthogonalLuminance,
      oppositeLuminance,

      originAzimuthalAngleDeg,
      orthogonalAzimuthalAngleDeg,
      oppositeAzimuthalAngleDeg
   ) {
      /**
       * @param {number} azimuthalAngleDeg
       * @param {number} polarAngleDeg
       * @returns {GLSL.Vector3}
       */
      function getLightDirectionVector(azimuthalAngleDeg, polarAngleDeg) {
         const polar = new GLSL.Float(polarAngleDeg).radians();
         const azimuthal = new GLSL.Float(azimuthalAngleDeg).radians();

         return new GLSL.Vector3([
            polar.sin().multiplyFloat(azimuthal.cos()),
            polar.sin().multiplyFloat(azimuthal.sin()),
            polar.cos(),
         ]).normalize();
      }

      const originLightDirection = getLightDirectionVector(
         originAzimuthalAngleDeg,
         lightPolarAngleDeg
      );
      const orthogonalLightDirection = getLightDirectionVector(
         orthogonalAzimuthalAngleDeg,
         lightPolarAngleDeg
      );
      const oppositeLightDirection = getLightDirectionVector(
         oppositeAzimuthalAngleDeg,
         lightPolarAngleDeg
      );

      const lightMatrix = new GLSL.Matrix3([
         [
            originLightDirection.channel(0),
            originLightDirection.channel(1),
            originLightDirection.channel(2),
         ],
         [
            orthogonalLightDirection.channel(0),
            orthogonalLightDirection.channel(1),
            orthogonalLightDirection.channel(2),
         ],
         [
            oppositeLightDirection.channel(0),
            oppositeLightDirection.channel(1),
            oppositeLightDirection.channel(2),
         ],
      ]).inverse();

      const reflection = new GLSL.Vector3([
         originLuminance,
         orthogonalLuminance,
         oppositeLuminance,
      ]);

      return lightMatrix
         .multiplyVector3(reflection)
         .normalize()
         .addFloat(new GLSL.Float(1))
         .divideFloat(new GLSL.Float(2));
   }

   /** @type {number[][]} */
   const anisotropicCombinations = [
      [180, 270, 0],
      [180, 90, 0],
      [90, 180, 270],
      [90, 0, 270],
      [225, 315, 45],
      [225, 135, 45],
      [315, 45, 135],
      [315, 225, 135],
   ];

   /** @type {GLSL.Vector3[]} */
   const normalVectors = [];

   anisotropicCombinations.forEach((combination) => {
      /**
       * @param {number} azimuthalAngle
       * @returns {GLSL.Float}
       */
      function getLightLuminance(azimuthalAngle) {
         const lightAzimuthalAngles = [0, 45, 90, 135, 180, 225, 270, 315];
         const id = lightAzimuthalAngles.findIndex((value) => {
            return value === azimuthalAngle;
         });

         return lightLuminances[id];
      }

      normalVectors.push(
         getAnisotropicNormalVector(
            getLightLuminance(combination[0]),
            getLightLuminance(combination[1]),
            getLightLuminance(combination[2]),
            combination[0],
            combination[1],
            combination[2]
         )
      );
   });

   let normalVector = new GLSL.Vector3([
      new GLSL.Float(0),
      new GLSL.Float(0),
      new GLSL.Float(0),
   ])
      .addVector3(...normalVectors)
      .divideFloat(new GLSL.Float(normalVectors.length))
      .normalize()
      .multiplyFloat(mask);

   /*if (cameraVerticalShift) {
      // TODO: use cameraVerticalShift
      const cameraAngle = Math.atan(
                  1 / Math.tan(lightPolarAngleDeg * (Math.PI / 180))
               );

               const zero = new GLSL.Float(0);
               const one = new GLSL.Float(1);
               const sine = new GLSL.Float(Math.sin(cameraAngle));
               const cosine = new GLSL.Float(Math.cos(cameraAngle));

               const rotationMatrix = new GLSL.Matrix3([
                  [one, zero, zero],
                  [zero, cosine, sine],
                  [zero, sine.multiplyFloat(new GLSL.Float(-1)), cosine],
               ]);

               normalVector = rotationMatrix.multiplyVector3(normalVector);
   }*/

   // TODO: fix alpha
   const alpha = normalVector
      .channel(0)
      .minimum(normalVector.channel(1), normalVector.channel(2))
      .multiplyFloat(new GLSL.Float(99999))
      .minimum(new GLSL.Float(1));

   const normalMapRendering = GLSL.render(normalVector.getVector4(alpha));
   const normalMap = await normalMapRendering.getImageBitmap();
   normalMapShader.purge();
   return normalMap;
}

/**
 * @public
 * @param {ImageBitmap} lightImage_000
 * @param {ImageBitmap} lightImage_090
 * @param {ImageBitmap} lightImage_180
 * @param {ImageBitmap} lightImage_270
 * @param {ImageBitmap} lightImage_ALL
 * @param {ImageBitmap} lightImage_FRONT
 * @param {ImageBitmap} lightImage_NONE
 * @param {ImageBitmap} uiImageElement
 * @param {number} resolutionPercent
 * @returns {Promise<ImageBitmap>}
 */
/*async function sphericalGradientNormalMap(
   lightImage_000,
   lightImage_090,
   lightImage_180,
   lightImage_270,
   lightImage_ALL,
   lightImage_FRONT,
   lightImage_NONE = undefined,
   uiImageElement = undefined,
   resolutionPercent = 100
) {
   return new Promise((resolve) => {
      setTimeout(async () => {
         const normalMapShader = new GLSL.Shader({
            width: lightImage_000.width * (resolutionPercent / 100),
            height: lightImage_000.naturalHeight * (resolutionPercent / 100),
         });
         normalMapShader.bind();

         let lightLuminance_ALL =
            GLSL.Image.load(lightImage_ALL).getLuminance();

         const lightLuminances = [
            GLSL.Image.load(lightImage_000).getLuminance(),
            GLSL.Image.load(lightImage_090).getLuminance(),
            GLSL.Image.load(lightImage_180).getLuminance(),
            GLSL.Image.load(lightImage_270).getLuminance(),
            GLSL.Image.load(lightImage_FRONT).getLuminance(),
         ];

         if (lightImage_NONE) {
            const lightLuminance_NONE =
               GLSL.Image.load(lightImage_NONE).getLuminance();

            for (let i = 0; i < lightLuminances.length; i++) {
               lightLuminances[i] =
                  lightLuminances[i].subtractFloat(lightLuminance_NONE);
            }
            lightLuminance_ALL =
               lightLuminance_ALL.subtractFloat(lightLuminance_NONE);
         }

         for (let i = 0; i < lightLuminances.length; i++) {
            lightLuminances[i] =
               lightLuminances[i].divideFloat(lightLuminance_ALL);
         }

         const horizontal = lightLuminances[0]
            .subtractFloat(lightLuminances[2])
            .addFloat(new GLSL.Float(1))
            .divideFloat(new GLSL.Float(2));

         const vertical = lightLuminances[3]
            .subtractFloat(lightLuminances[1])
            .addFloat(new GLSL.Float(1))
            .divideFloat(new GLSL.Float(2));

         const normalVector = new GLSL.Vector3([
            horizontal,
            vertical,
            lightLuminances[4],
         ]);

         const normalMapRendering = GLSL.render(
            normalVector.normalize().getVector4()
         );

         normalMapShader.purge();

         const normalMap = await normalMapRendering.getJsImage();

         if (uiImageElement && normalMap) {
            uiImageElement.src = normalMap.src;
         }

         resolve(normalMap);
      });
   });
}*/

c73f9ee04347b85ecd27e399950266c236e50a30

Remove hard code.

Remove hard code.

// TODO Remove hard code.

      this.handleResize();
      this.updateCameraPlanes();
   }
}
/** @constant */
VirtualInputRenderer.MIN_CAMERA_PLANES_DISTANCE = 0.5;
/** @type {VirtualInputRenderer[]} */
VirtualInputRenderer.instances = [];

class PhotometricStereoRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }

   /**
    * @override
    * @protected
    */
   async initialize() {
      if (this.initialized || (!this.initialized && this.initializing)) {
         return;
      }
      this.initializing = true;

      super.initialize();

      this.lights = new Array(8);
      this.lightHelpers = new Array(8);

      for (let i = 0; i < 8; i++) {
         // TODO Remove hard code.
         this.lights[i] = new THREE.PointLight("white", 0.25);
         this.lights[i].castShadow = true;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.width = 512 * 2;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.height = 512 * 2;
         // TODO Remove hard code.
         this.lightHelpers[i] = new THREE.PointLightHelper(this.lights[i], 0.2);
         this.scene.add(this.lights[i]);
         this.scene.add(this.lightHelpers[i]);
      }

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @override
    * @public
    * @returns {Promise<HTMLImageElement[]>}
    */
   async render() {
      this.renderId++;
      const renderId = this.renderId;

      await this.initialize();

      if (this.isRenderObsolete(renderId)) return;

      const lightCount = this.lights.length;
      const renderPromises = [];

      for (let i = 0; i < lightCount; i++) {
         this.lightHelpers[i].visible = false;
      }
      this.cameraHelper.visible = false;

      for (let i = 0; i < lightCount; i++) {
         for (let i = 0; i < lightCount; i++) {
            this.lights[i].visible = false;
         }
         this.lights[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 25 * (this.cameraDistance / 8);

         if (this.isRenderObsolete(renderId)) return;

         this.renderer.render(this.scene, this.camera);
         const renderImageDataUrl = this.renderer.domElement.toDataURL();

         renderPromises.push(
            new Promise((resolve) => {
               setTimeout(() => {
                  const image = new Image();
                  image.addEventListener("load", () => {
                     resolve(image);
                  });
                  image.src = renderImageDataUrl;
               });
            })
         );
      }

      for (let i = 0; i < lightCount; i++) {
         this.lights[i].visible = true;
         this.lightHelpers[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 0.25;
      }
      this.cameraHelper.visible = true;

      this.updateCameraPlanes();
      this.uiRenderer.render(this.scene, this.uiCamera);

      return Promise.all(renderPromises);
   }

   /**
    * @public
    * @param {number} lightPolarAngleDeg
    */
   async setLightPolarAngleDeg(lightPolarAngleDeg) {
      await this.initialize();
      this.lightPolarAngleDeg = lightPolarAngleDeg;
      this.updateLightPositions();
   }

   /**
    * @override
    * @public
    * @param {number} lightDistance
    */
   async setLightDistance(lightDistance) {
      await this.initialize();

      this.lightDistance = lightDistance;
      for (let i = 0; i < this.lights.length; i++) {
         this.lights[i].distance = this.lightDistance * 2;
      }
      this.updateLightPositions();
   }

   async updateLightPositions() {
      await this.initialize();

      const correctedLightPolarDegree = 360 - this.lightPolarAngleDeg;

      /**
       * @param {THREE.Light} light
       * @param {number} lightAzimuthalDegree
       */
      const setSingleLightAzimuthalAngle = (light, lightAzimuthalDegree) => {
         let lightVector = new THREE.Vector3(this.lightDistance, 0, 0);

         let lightPolarRotationAxis = new THREE.Vector3(0, 1, 0).normalize();
         lightVector.applyAxisAngle(
            lightPolarRotationAxis,
            correctedLightPolarDegree * (Math.PI / 180)
         );

         const lightRotation = lightAzimuthalDegree * (Math.PI / 180);
         const lightRotationAxis = new THREE.Vector3(0, 0, 1).normalize();
         lightVector.applyAxisAngle(lightRotationAxis, lightRotation);

         light.position.set(lightVector.x, lightVector.y, lightVector.z);
      };

      const lightCount = this.lights.length;
      for (let i = 0; i < lightCount; i++) {
         setSingleLightAzimuthalAngle(this.lights[i], i * (360 / lightCount));
      }
   }
}

class SphericalGradientRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }
}

f39176ee8b4b393b919c0a86b9e008ac8a522b6e

use cameraVerticalShift

use cameraVerticalShift

// TODO: use cameraVerticalShift

/* global GLSL */
/* exported NormalMapHelper */

/**
 * @global
 */
class NormalMapHelper {
   /**
    * @public
    * @param {number} lightPolarAngleDeg
    * @param {HTMLImageElement} lightImage_000
    * @param {HTMLImageElement} lightImage_045
    * @param {HTMLImageElement} lightImage_090
    * @param {HTMLImageElement} lightImage_135
    * @param {HTMLImageElement} lightImage_180
    * @param {HTMLImageElement} lightImage_225
    * @param {HTMLImageElement} lightImage_270
    * @param {HTMLImageElement} lightImage_315
    * @param {HTMLImageElement} lightImage_NONE
    * @param {HTMLImageElement} uiImageElement
    * @param {number} resolutionPercent
    * @param {boolean} cameraVerticalShift
    * @param {number} maskThresholdPercent
    * @returns {Promise<HTMLImageElement>}
    */
   static async calculatePhotometricStereoNormalMap(
      lightPolarAngleDeg,
      lightImage_000,
      lightImage_045,
      lightImage_090,
      lightImage_135,
      lightImage_180,
      lightImage_225,
      lightImage_270,
      lightImage_315,
      lightImage_NONE = undefined,
      uiImageElement = undefined,
      resolutionPercent = 100,
      cameraVerticalShift = false,
      maskThresholdPercent = 5
   ) {
      const maskThreshold = maskThresholdPercent / 100;

      const normalMapHelper = new NormalMapHelper();

      return new Promise((resolve) => {
         setTimeout(async () => {
            if (
               lightImage_000.naturalWidth < 1 ||
               lightImage_045.naturalWidth < 1 ||
               lightImage_090.naturalWidth < 1 ||
               lightImage_180.naturalWidth < 1 ||
               lightImage_225.naturalWidth < 1 ||
               lightImage_270.naturalWidth < 1 ||
               lightImage_315.naturalWidth < 1
            )
               return;

            if (normalMapHelper.isRenderObsolete()) return;

            const normalMapShader = new GLSL.Shader({
               width: lightImage_000.naturalWidth * (resolutionPercent / 100),
               height: lightImage_000.naturalHeight * (resolutionPercent / 100),
            });

            if (normalMapHelper.isRenderObsolete()) return;

            normalMapShader.bind();

            const lightLuminances = [
               GLSL.Image.load(lightImage_000).getLuminance(),
               GLSL.Image.load(lightImage_045).getLuminance(),
               GLSL.Image.load(lightImage_090).getLuminance(),
               GLSL.Image.load(lightImage_135).getLuminance(),
               GLSL.Image.load(lightImage_180).getLuminance(),
               GLSL.Image.load(lightImage_225).getLuminance(),
               GLSL.Image.load(lightImage_270).getLuminance(),
               GLSL.Image.load(lightImage_315).getLuminance(),
            ];

            const all = new GLSL.Float(0).maximum(...lightLuminances);

            let mask = new GLSL.Float(1);

            if (
               lightImage_NONE &&
               Math.min(
                  lightImage_NONE.naturalWidth,
                  lightImage_NONE.naturalHeight
               ) > 0
            ) {
               const lightLuminance_NONE =
                  GLSL.Image.load(lightImage_NONE).getLuminance();

               for (let i = 0; i < lightLuminances.length; i++) {
                  lightLuminances[i] =
                     lightLuminances[i].subtractFloat(lightLuminance_NONE);
               }

               mask = all
                  .subtractFloat(lightLuminance_NONE)
                  .step(new GLSL.Float(maskThreshold));
            }

            for (let i = 0; i < lightLuminances.length; i++) {
               lightLuminances[i] = lightLuminances[i].divideFloat(all);
            }

            /**
             * @param {GLSL.Float} originLuminance
             * @param {GLSL.Float} orthogonalLuminance
             * @param {GLSL.Float} oppositeLuminance
             * @param {number} originAzimuthalAngleDeg
             * @param {number} orthogonalAzimuthalAngleDeg
             * @param {number} oppositeAzimuthalAngleDeg
             * @returns {GLSL.Vector3}
             */
            function getAnisotropicNormalVector(
               originLuminance,
               orthogonalLuminance,
               oppositeLuminance,

               originAzimuthalAngleDeg,
               orthogonalAzimuthalAngleDeg,
               oppositeAzimuthalAngleDeg
            ) {
               if (normalMapHelper.isRenderObsolete()) return;

               /**
                * @param {number} azimuthalAngleDeg
                * @param {number} polarAngleDeg
                * @returns {GLSL.Vector3}
                */
               function getLightDirectionVector(
                  azimuthalAngleDeg,
                  polarAngleDeg
               ) {
                  const polar = new GLSL.Float(polarAngleDeg).radians();
                  const azimuthal = new GLSL.Float(azimuthalAngleDeg).radians();

                  return new GLSL.Vector3([
                     polar.sin().multiplyFloat(azimuthal.cos()),
                     polar.sin().multiplyFloat(azimuthal.sin()),
                     polar.cos(),
                  ]).normalize();
               }

               const originLightDirection = getLightDirectionVector(
                  originAzimuthalAngleDeg,
                  lightPolarAngleDeg
               );
               const orthogonalLightDirection = getLightDirectionVector(
                  orthogonalAzimuthalAngleDeg,
                  lightPolarAngleDeg
               );
               const oppositeLightDirection = getLightDirectionVector(
                  oppositeAzimuthalAngleDeg,
                  lightPolarAngleDeg
               );

               const lightMatrix = new GLSL.Matrix3([
                  [
                     originLightDirection.channel(0),
                     originLightDirection.channel(1),
                     originLightDirection.channel(2),
                  ],
                  [
                     orthogonalLightDirection.channel(0),
                     orthogonalLightDirection.channel(1),
                     orthogonalLightDirection.channel(2),
                  ],
                  [
                     oppositeLightDirection.channel(0),
                     oppositeLightDirection.channel(1),
                     oppositeLightDirection.channel(2),
                  ],
               ]).inverse();

               const reflection = new GLSL.Vector3([
                  originLuminance,
                  orthogonalLuminance,
                  oppositeLuminance,
               ]);

               return lightMatrix
                  .multiplyVector3(reflection)
                  .normalize()
                  .addFloat(new GLSL.Float(1))
                  .divideFloat(new GLSL.Float(2));
            }

            /** @type {number[][]} */
            const anisotropicCombinations = [
               [180, 270, 0],
               [180, 90, 0],
               [90, 180, 270],
               [90, 0, 270],
               [225, 315, 45],
               [225, 135, 45],
               [315, 45, 135],
               [315, 225, 135],
            ];

            /** @type {GLSL.Vector3[]} */
            const normalVectors = [];

            anisotropicCombinations.forEach((combination) => {
               /**
                * @param {number} azimuthalAngle
                * @returns {GLSL.Float}
                */
               function getLightLuminance(azimuthalAngle) {
                  const lightAzimuthalAngles = [
                     0, 45, 90, 135, 180, 225, 270, 315,
                  ];
                  const id = lightAzimuthalAngles.findIndex((value) => {
                     return value === azimuthalAngle;
                  });

                  return lightLuminances[id];
               }

               normalVectors.push(
                  getAnisotropicNormalVector(
                     getLightLuminance(combination[0]),
                     getLightLuminance(combination[1]),
                     getLightLuminance(combination[2]),
                     combination[0],
                     combination[1],
                     combination[2]
                  )
               );
            });

            let normalVector = new GLSL.Vector3([
               new GLSL.Float(0),
               new GLSL.Float(0),
               new GLSL.Float(0),
            ])
               .addVector3(...normalVectors)
               .divideFloat(new GLSL.Float(normalVectors.length))
               .normalize()
               .multiplyFloat(mask);

            if (normalMapHelper.isRenderObsolete()) return;

            if (cameraVerticalShift) {
               // TODO: use cameraVerticalShift
               /*const cameraAngle = Math.atan(
                  1 / Math.tan(lightPolarAngleDeg * (Math.PI / 180))
               );

               const zero = new GLSL.Float(0);
               const one = new GLSL.Float(1);
               const sine = new GLSL.Float(Math.sin(cameraAngle));
               const cosine = new GLSL.Float(Math.cos(cameraAngle));

               const rotationMatrix = new GLSL.Matrix3([
                  [one, zero, zero],
                  [zero, cosine, sine],
                  [zero, sine.multiplyFloat(new GLSL.Float(-1)), cosine],
               ]);

               normalVector = rotationMatrix.multiplyVector3(normalVector);*/
            }

            // TODO: fix alpha
            const alpha = normalVector
               .channel(0)
               .minimum(normalVector.channel(1), normalVector.channel(2))
               .multiplyFloat(new GLSL.Float(99999))
               .minimum(new GLSL.Float(1));

            const normalMapRendering = GLSL.render(
               normalVector.getVector4(alpha)
            );

            if (normalMapHelper.isRenderObsolete()) return;

            const normalMap = await normalMapRendering.getJsImage();

            resolve(normalMap);

            if (uiImageElement && normalMap) {
               uiImageElement.src = normalMap.src;
            }

            normalMapShader.purge();
         }, 100);
      });
   }

   /**
    * @public
    * @param {HTMLImageElement} lightImage_000
    * @param {HTMLImageElement} lightImage_090
    * @param {HTMLImageElement} lightImage_180
    * @param {HTMLImageElement} lightImage_270
    * @param {HTMLImageElement} lightImage_ALL
    * @param {HTMLImageElement} lightImage_FRONT
    * @param {HTMLImageElement} lightImage_NONE
    * @param {HTMLImageElement} uiImageElement
    * @param {number} resolutionPercent
    * @returns {Promise<HTMLImageElement>}
    */
   static async calculateSphericalGradientNormalMap(
      lightImage_000,
      lightImage_090,
      lightImage_180,
      lightImage_270,
      lightImage_ALL,
      lightImage_FRONT,
      lightImage_NONE = undefined,
      uiImageElement = undefined,
      resolutionPercent = 100
   ) {
      const normalMapHelper = new NormalMapHelper();

      if (normalMapHelper.isRenderObsolete()) return;

      return new Promise((resolve) => {
         setTimeout(async () => {
            const normalMapShader = new GLSL.Shader({
               width: lightImage_000.naturalWidth * (resolutionPercent / 100),
               height: lightImage_000.naturalHeight * (resolutionPercent / 100),
            });
            normalMapShader.bind();

            let lightLuminance_ALL =
               GLSL.Image.load(lightImage_ALL).getLuminance();

            const lightLuminances = [
               GLSL.Image.load(lightImage_000).getLuminance(),
               GLSL.Image.load(lightImage_090).getLuminance(),
               GLSL.Image.load(lightImage_180).getLuminance(),
               GLSL.Image.load(lightImage_270).getLuminance(),
               GLSL.Image.load(lightImage_FRONT).getLuminance(),
            ];

            if (lightImage_NONE) {
               const lightLuminance_NONE =
                  GLSL.Image.load(lightImage_NONE).getLuminance();

               for (let i = 0; i < lightLuminances.length; i++) {
                  lightLuminances[i] =
                     lightLuminances[i].subtractFloat(lightLuminance_NONE);
               }
               lightLuminance_ALL =
                  lightLuminance_ALL.subtractFloat(lightLuminance_NONE);
            }

            for (let i = 0; i < lightLuminances.length; i++) {
               lightLuminances[i] =
                  lightLuminances[i].divideFloat(lightLuminance_ALL);
            }

            const horizontal = lightLuminances[0]
               .subtractFloat(lightLuminances[2])
               .addFloat(new GLSL.Float(1))
               .divideFloat(new GLSL.Float(2));

            const vertical = lightLuminances[3]
               .subtractFloat(lightLuminances[1])
               .addFloat(new GLSL.Float(1))
               .divideFloat(new GLSL.Float(2));

            const normalVector = new GLSL.Vector3([
               horizontal,
               vertical,
               lightLuminances[4],
            ]);

            if (normalMapHelper.isRenderObsolete()) return;

            const normalMapRendering = GLSL.render(
               normalVector.normalize().getVector4()
            );

            normalMapShader.purge();

            const normalMap = await normalMapRendering.getJsImage();

            if (uiImageElement && normalMap) {
               uiImageElement.src = normalMap.src;
            }

            resolve(normalMap);
         });
      });
   }

   /**
    * @public
    */
   static cancelRenderJobs() {
      NormalMapHelper.renderId++;
   }

   /**
    * @private
    */
   constructor() {
      this.renderId = NormalMapHelper.renderId;
   }

   /**
    * @private
    * @returns {boolean}
    */
   isRenderObsolete() {
      return this.renderId < NormalMapHelper.renderId;
   }

   /**
    * @public
    * @param {HTMLImageElement} normalMap
    * @param {HTMLImageElement} groundTruthImage
    * @returns {Promise<number>}
    */
   static async getDifferenceValue(normalMap, groundTruthImage) {
      const differenceImage = await NormalMapHelper.getDifferenceMap(
         normalMap,
         groundTruthImage
      );

      const width = differenceImage.width;
      const height = differenceImage.height;

      const imageCanvas = document.createElement("canvas");
      imageCanvas.width = width;
      imageCanvas.height = height;
      const imageContext = imageCanvas.getContext("2d");
      imageContext.drawImage(differenceImage, 0, 0, width, height);
      const imageData = imageContext.getImageData(0, 0, width, height).data;

      let differenceValue = 0;
      for (let x = 0; x < width - 1; x++) {
         for (let y = 0; y < height - 1; y++) {
            const index = (x + y * width) * 4;
            const localDifference = imageData[index] / 255;
            differenceValue += localDifference;
         }
      }
      differenceValue /= width * height;

      return differenceValue;
   }

   /**
    * @public
    * @param {HTMLImageElement} normalMap
    * @param {HTMLImageElement} groundTruthImage
    * @returns {Promise<HTMLImageElement>}
    */
   static async getDifferenceMap(normalMap, groundTruthImage) {
      return new Promise((resolve) => {
         const differenceShader = new GLSL.Shader({
            width: normalMap.width,
            height: normalMap.height,
         });
         differenceShader.bind();

         const normalImage = GLSL.Image.load(normalMap);
         const groundTruthShaderImage = GLSL.Image.load(groundTruthImage);

         let normal = new GLSL.Vector3([
            normalImage.channel(0),
            normalImage.channel(1),
            normalImage.channel(2),
         ]);

         let groundTruth = new GLSL.Vector3([
            groundTruthShaderImage.channel(0),
            groundTruthShaderImage.channel(1),
            groundTruthShaderImage.channel(2),
         ]);

         const zeroAsErrorSummand = new GLSL.Float(1).subtractFloat(
            normal.length().step().divideFloat(groundTruth.length().step())
         );

         groundTruth = groundTruth.normalize();

         const differenceAngle = normal
            .dot(groundTruth)
            .acos()
            .abs()
            .addFloat(zeroAsErrorSummand);

         normal = normal.normalize();

         const differenceMap = new Image();
         differenceMap.addEventListener("load", () => {
            resolve(differenceMap);
         });
         differenceMap.src = GLSL.render(
            new GLSL.Vector4([
               differenceAngle,
               differenceAngle,
               differenceAngle,
               new GLSL.Float(1),
            ])
         ).getDataUrl();

         differenceShader.purge();
      });
   }
}

NormalMapHelper.renderId = 0;

b56f0a76a3f60b92554f674853241e3604fbc2b6

Check if kernel is quadratic.

Check if kernel is quadratic.

// TODO Check if kernel is quadratic.

/* exported GLSL */

/** @type {{RED: number, GREEN: number, BLUE: number}} */
const LUMINANCE_CHANNEL_QUANTIFIER = {
   RED: 0.2126,
   GREEN: 0.7152,
   BLUE: 0.0722,
};

/**
 * @typedef {string} FLOAT_PRECISION
 * @enum {FLOAT_PRECISION}
 */
const GPU_GL_FLOAT_PRECISION = {
   MEDIUM: "medium" + "p",
   HIGH: "high" + "p",
};

/** @type {FLOAT_PRECISION} */
const FLOAT_PRECISION = GPU_GL_FLOAT_PRECISION.HIGH;

/**
 * @typedef {string} GLSL_VARIABLE
 */
const GLSL_VARIABLE = {
   UV: "uv",
   UV_U: "uv[0]",
   UV_V: "uv[1]",
   TEX: "tex",
   POS: "pos",
   OUT: "fragColor",
};

/**
 * @typedef {string} GLSL_VARIABLE_TYPE
 */
const GLSL_VARIABLE_TYPE = {
   FLOAT: "float",
   VECTOR2: "vec2",
   VECTOR3: "vec3",
   VECTOR4: "vec4",
   MATRIX3: "mat3",
   INTEGER: "int",
};

/**
 * @typedef {number} OPERATOR_TYPE
 */
const OPERATOR_TYPE = {
   SYMBOL: 0,
   METHOD: 1,
   CUSTOM: 2,
};

/**
 * @typedef {{GLSL_NAME: string, TYPE: OPERATOR_TYPE}} GLSL_OPERATOR
 */
const GLSL_OPERATOR = {
   ADD: { GLSL_NAME: " + ", TYPE: OPERATOR_TYPE.SYMBOL },
   SUBTRACT: { GLSL_NAME: " - ", TYPE: OPERATOR_TYPE.SYMBOL },
   MULTIPLY: { GLSL_NAME: " * ", TYPE: OPERATOR_TYPE.SYMBOL },
   DIVIDE: { GLSL_NAME: " / ", TYPE: OPERATOR_TYPE.SYMBOL },

   ABS: { GLSL_NAME: "abs", TYPE: OPERATOR_TYPE.METHOD },
   MAXIMUM: { GLSL_NAME: "max", TYPE: OPERATOR_TYPE.METHOD },
   MINIMUM: { GLSL_NAME: "min", TYPE: OPERATOR_TYPE.METHOD },
   DOT: { GLSL_NAME: "dot", TYPE: OPERATOR_TYPE.METHOD },
   INVERSE: { GLSL_NAME: "inverse", TYPE: OPERATOR_TYPE.METHOD },
   NORMALIZE: { GLSL_NAME: "normalize", TYPE: OPERATOR_TYPE.METHOD },
   LENGTH: { GLSL_NAME: "length", TYPE: OPERATOR_TYPE.METHOD },
   SINE: { GLSL_NAME: "sin", TYPE: OPERATOR_TYPE.METHOD },
   COSINE: { GLSL_NAME: "cos", TYPE: OPERATOR_TYPE.METHOD },
   ARC_COSINE: { GLSL_NAME: "acos", TYPE: OPERATOR_TYPE.METHOD },
   RADIANS: { GLSL_NAME: "radians", TYPE: OPERATOR_TYPE.METHOD },
   SIGN: { GLSL_NAME: "sign", TYPE: OPERATOR_TYPE.METHOD },
   STEP: { GLSL_NAME: "step", TYPE: OPERATOR_TYPE.METHOD },
   DISTANCE: { GLSL_NAME: "distance", TYPE: OPERATOR_TYPE.METHOD },

   LUMINANCE: { GLSL_NAME: "luminance", TYPE: OPERATOR_TYPE.CUSTOM },
   CHANNEL: { GLSL_NAME: "channel", TYPE: OPERATOR_TYPE.CUSTOM },
   VEC3_TO_VEC4: { GLSL_NAME: "vec3_to_vec4", TYPE: OPERATOR_TYPE.CUSTOM },
};

class Shader {
   /**
    * @param  {{ width: number, height: number }} dimensions
    */
   constructor(dimensions) {
      /**
       * @private
       * @type {GlslShader}
       */
      this.glslShader = null;
      this.dimensions = dimensions;
   }

   bind() {
      if (this.glslShader !== null) {
         console.warn("Shader is already bound!");
      }
      this.glslShader = new GlslShader(this.dimensions);
   }

   unbind() {
      GlslShader.currentShader = null;
      this.glslShader = null;
   }

   purge() {
      if (this.glslShader === null) {
         console.warn("No shader bound to purge!");
      } else {
         this.glslShader.reset();
         this.unbind();
      }
   }
   /**
    * @returns {GlslVector2}
    */
   getUV() {
      return new GlslVector2(undefined, GLSL_VARIABLE.UV);
   }
}

class GlslShader {
   /**
    * @public
    * @param  {{ width: number, height: number }} dimensions
    */
   constructor(dimensions) {
      GlslShader.currentShader = this;
      /**
       * @private
       * @type {GPU_GL_FLOAT_PRECISION}
       */
      this.floatPrecision = FLOAT_PRECISION;
      /**
       * @private
       * @type {GlslImage[]}
       */
      this.glslImages = [];
      /**
       * @private
       * @type {string[]}
       */
      this.glslCommands = [];
      this.glslContext = new GlslContext(dimensions);
   }
   /**
    * @public
    * @static
    * @returns {GlslShader}
    */
   static getCurrentShader() {
      return GlslShader.currentShader;
   }
   /**
    * @public
    */
   reset() {
      this.glslContext.reset();
      GlslShader.currentShader = null;
   }
   /**
    * @public
    * @static
    * @param  {string} glslCommand
    * @returns {void}
    */
   static addGlslCommandToCurrentShader(glslCommand) {
      GlslShader.getCurrentShader().addGlslCommand(glslCommand);
   }
   /**
    * @static
    * @param  {GlslImage} glslImage
    * @returns {void}
    */
   static addGlslImageToCurrentShader(glslImage) {
      GlslShader.getCurrentShader().addGlslImage(glslImage);
   }
   /**
    * @static
    * @returns {GlslContext}
    */
   static getGlslContext() {
      return GlslShader.getCurrentShader().glslContext;
   }
   /**
    * @returns {GlslImage[]}
    */
   getGlslImages() {
      return this.glslImages;
   }
   /**
    * @returns {string}
    */
   getVertexShaderSource() {
      return [
         "#version 300 es",
         "",
         "in vec3 " + GLSL_VARIABLE.POS + ";",
         "in vec2 " + GLSL_VARIABLE.TEX + ";",
         "",
         "out vec2 " + GLSL_VARIABLE.UV + ";",
         "",
         "void main() {",
         GLSL_VARIABLE.UV + " = " + GLSL_VARIABLE.TEX + ";",
         "gl_Position = vec4(" + GLSL_VARIABLE.POS + ", 1.0);",
         "}",
      ].join("\n");
   }
   /**
    * @param  {GlslVector4} outVariable
    * @returns {string}
    */
   getFragmentShaderSource(outVariable) {
      let imageDefinitions = [];
      for (let i = 0; i < this.glslImages.length; i++) {
         imageDefinitions.push(this.glslImages[i].getGlslDefinition());
      }
      return [
         "#version 300 es",
         "precision " + this.floatPrecision + " float;",
         "",
         "in vec2 " + GLSL_VARIABLE.UV + ";",
         "out vec4 " + GLSL_VARIABLE.OUT + ";",
         "",
         ...imageDefinitions,
         "",
         "float luminance(vec4 image) {",
         "return image.r * " +
            GlslFloat.getJsNumberAsString(LUMINANCE_CHANNEL_QUANTIFIER.RED) +
            " + image.g * " +
            GlslFloat.getJsNumberAsString(LUMINANCE_CHANNEL_QUANTIFIER.GREEN) +
            " + image.b * " +
            GlslFloat.getJsNumberAsString(LUMINANCE_CHANNEL_QUANTIFIER.BLUE) +
            ";",
         "}",
         "",
         "void main() {",
         ...this.glslCommands,
         GLSL_VARIABLE.OUT + " = " + outVariable.getGlslName() + ";",
         "}",
      ].join("\n");
   }
   /**
    * @private
    * @param  {string} glslCommand
    * @returns {void}
    */
   addGlslCommand(glslCommand) {
      this.glslCommands.push(glslCommand);
   }
   /**
    * @private
    * @param  {GlslImage} glslImage
    * @returns {void}
    */
   addGlslImage(glslImage) {
      this.glslImages.push(glslImage);
   }
}
/**
 * @static
 * @type {GlslShader}
 */
GlslShader.currentShader;

class GlslContext {
   /**
    * @param  {{ width: number, height: number }} dimensions
    */
   constructor(dimensions) {
      this.glslShader = GlslShader.getCurrentShader();
      this.glCanvas = document.createElement("canvas");
      this.glCanvas.width = dimensions.width;
      this.glCanvas.height = dimensions.height;
      this.glContext = this.glCanvas.getContext("webgl2");
   }

   reset() {
      this.glContext.flush();
      this.glContext.finish();
      this.glCanvas.remove();
      this.glContext.getExtension("WEBGL_lose_context").loseContext();
   }
   /**
    * @returns {WebGL2RenderingContext}
    */
   getGlContext() {
      return this.glContext;
   }
   /**
    * @param  {GlslVector4} outVariable
    * @returns {Uint8Array}
    */
   renderPixelArray(outVariable) {
      return this.renderToPixelArray(outVariable);
   }
   /**
    * @returns {string}
    */
   renderDataUrl() {
      return this.glCanvas.toDataURL();
   }
   /**
    * @private
    * @param  {GlslVector4} outVariable
    * @returns {WebGLProgram}
    */
   createShaderProgram(outVariable) {
      let vertexShader = this.glContext.createShader(
         this.glContext.VERTEX_SHADER
      );
      let fragmentShader = this.glContext.createShader(
         this.glContext.FRAGMENT_SHADER
      );
      const vertexShaderSource = this.glslShader.getVertexShaderSource();
      const fragmentShaderSource =
         this.glslShader.getFragmentShaderSource(outVariable);
      //console.log(vertexShaderSource);
      //console.log(fragmentShaderSource);
      this.glContext.shaderSource(vertexShader, vertexShaderSource);
      this.glContext.shaderSource(fragmentShader, fragmentShaderSource);
      //console.log("Compiling shader program.");
      this.glContext.compileShader(vertexShader);
      this.glContext.compileShader(fragmentShader);
      let shaderProgram = this.glContext.createProgram();
      this.glContext.attachShader(shaderProgram, vertexShader);
      this.glContext.attachShader(shaderProgram, fragmentShader);
      this.glContext.linkProgram(shaderProgram);
      return shaderProgram;
   }
   /**
    * @private
    * @param  {WebGLProgram} shaderProgram
    * @returns {void}
    */
   loadGlslImages(shaderProgram) {
      const glslImages = this.glslShader.getGlslImages();
      //console.log("Loading " + glslImages.length + " image(s) for gpu.");
      for (let i = 0; i < glslImages.length; i++) {
         glslImages[i].loadIntoShaderProgram(this.glContext, shaderProgram, i);
      }
   }
   /**
    * @param  {WebGLProgram} shaderProgram
    * @returns {WebGLVertexArrayObject}
    */
   getFrameVAO(shaderProgram) {
      const framePositionLocation = this.glContext.getAttribLocation(
         shaderProgram,
         GLSL_VARIABLE.POS
      );
      const frameTextureLocation = this.glContext.getAttribLocation(
         shaderProgram,
         GLSL_VARIABLE.TEX
      );
      const FLOAT_SIZE = Float32Array.BYTES_PER_ELEMENT;
      const frameVertices = [-1, 1, 1, 1, -1, -1, -1, -1, 1, 1, 1, -1];
      const frameTextCoords = [0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1];
      let vaoFrame = this.glContext.createVertexArray();
      this.glContext.bindVertexArray(vaoFrame);
      let vboFrameV = this.glContext.createBuffer();
      this.glContext.bindBuffer(this.glContext.ARRAY_BUFFER, vboFrameV);
      this.glContext.bufferData(
         this.glContext.ARRAY_BUFFER,
         new Float32Array(frameVertices),
         this.glContext.STATIC_DRAW
      );
      this.glContext.vertexAttribPointer(
         framePositionLocation,
         2,
         this.glContext.FLOAT,
         false,
         2 * FLOAT_SIZE,
         0
      );
      this.glContext.enableVertexAttribArray(framePositionLocation);
      this.glContext.bindBuffer(this.glContext.ARRAY_BUFFER, null);
      let vboFrameT = this.glContext.createBuffer();
      this.glContext.bindBuffer(this.glContext.ARRAY_BUFFER, vboFrameT);
      this.glContext.bufferData(
         this.glContext.ARRAY_BUFFER,
         new Float32Array(frameTextCoords),
         this.glContext.STATIC_DRAW
      );
      this.glContext.vertexAttribPointer(
         frameTextureLocation,
         2,
         this.glContext.FLOAT,
         false,
         2 * FLOAT_SIZE,
         0
      );
      this.glContext.enableVertexAttribArray(frameTextureLocation);
      this.glContext.bindBuffer(this.glContext.ARRAY_BUFFER, null);
      this.glContext.bindVertexArray(null);
      return vaoFrame;
   }

   /**
    * @param  {WebGLVertexArrayObject} vaoFrame
    * @returns {void}
    */
   drawArraysFromVAO(vaoFrame) {
      this.glContext.viewport(0, 0, this.glCanvas.width, this.glCanvas.height);
      this.glContext.clearColor(0, 0, 0, 0);
      this.glContext.clear(
         this.glContext.COLOR_BUFFER_BIT | this.glContext.DEPTH_BUFFER_BIT
      );
      this.glContext.blendFunc(this.glContext.SRC_ALPHA, this.glContext.ONE);
      this.glContext.enable(this.glContext.BLEND);
      this.glContext.disable(this.glContext.DEPTH_TEST);
      this.glContext.bindVertexArray(vaoFrame);
      this.glContext.drawArrays(this.glContext.TRIANGLES, 0, 6);
      this.glContext.bindVertexArray(null);
   }
   /**
    * @private
    * @returns {Uint8Array}
    */
   readToPixelArray() {
      let pixelArray = new Uint8Array(
         this.glCanvas.width * this.glCanvas.height * 4
      );
      this.glContext.readPixels(
         0,
         0,
         this.glCanvas.width,
         this.glCanvas.height,
         this.glContext.RGBA,
         this.glContext.UNSIGNED_BYTE,
         pixelArray
      );
      return pixelArray;
   }
   /**
    * @param  {GlslVector4} outVariable
    * @returns {Uint8Array}
    */
   renderToPixelArray(outVariable) {
      this.drawCall(outVariable);
      const pixelArray = this.readToPixelArray();
      return pixelArray;
   }
   /**
    * @param  {GlslVector4} outVariable
    * @returns {void}
    */
   drawCall(outVariable) {
      const shaderProgram = this.createShaderProgram(outVariable);
      this.glContext.useProgram(shaderProgram);
      this.loadGlslImages(shaderProgram);
      //console.log("Rendering on gpu.");
      const vaoFrame = this.getFrameVAO(shaderProgram);
      this.drawArraysFromVAO(vaoFrame);
   }
}

class GlslRendering {
   /**
    * @param  {GlslContext} glslContext
    * @param  {GlslVector4} outVariable
    */
   constructor(glslContext, outVariable) {
      this.glslContext = glslContext;
      this.outVariable = outVariable;
   }
   /**
    * @static
    * @param  {GlslVector4} outVariable
    * @returns {GlslRendering}
    */
   static render(outVariable) {
      return new GlslRendering(GlslShader.getGlslContext(), outVariable);
   }
   /**
    * @returns {Uint8Array}
    */
   getPixelArray() {
      if (!this.pixelArray) {
         this.pixelArray = this.glslContext.renderPixelArray(this.outVariable);
      }
      return this.pixelArray;
   }
   /**
    * @returns {string}
    */
   getDataUrl() {
      if (!this.dataUrl) {
         this.getPixelArray();
         this.dataUrl = this.glslContext.renderDataUrl();
      }
      return this.dataUrl;
   }
   /**
    * @returns {Promise<HTMLImageElement>}
    */
   async getJsImage() {
      if (!this.jsImage) {
         const thisDataUrl = this.getDataUrl();
         this.jsImage = await new Promise((resolve) => {
            const image = new Image();
            image.addEventListener("load", () => {
               resolve(image);
            });
            image.src = thisDataUrl;
         });
      }
      return this.jsImage;
   }
}

class GlslOperation {
   /**
    * @param  {GlslVariable} callingParameter
    * @param  {GlslVariable} result
    * @param  {GlslVariable[]} parameters
    * @param  {GLSL_OPERATOR} glslOperator
    */
   constructor(callingParameter, result, parameters, glslOperator) {
      this.callingParameter = callingParameter;
      this.result = result;
      this.parameters = parameters;
      this.glslOperator = glslOperator;
   }
   /**
    * @private
    * @static
    * @param  {string} methodName
    * @param  {string[]} params
    * @returns {string}
    */
   static getGlslExpressionOfParams(methodName, params) {
      if (params.length === 1) {
         return params[0];
      } else if (params.length === 2) {
         return methodName + "(" + params[0] + ", " + params[1] + ")";
      } else {
         return (
            methodName +
            "(" +
            params.pop() +
            ", " +
            GlslOperation.getGlslExpressionOfParams(methodName, params) +
            ")"
         );
      }
   }
   /**
    * @returns {string}
    */
   getDeclaration() {
      const glslNames = GlslVariable.getGlslNamesOfGlslVariables(
         this.parameters
      );
      if (this.glslOperator.TYPE === OPERATOR_TYPE.SYMBOL) {
         glslNames.unshift(this.callingParameter.getGlslName());
         return (
            this.result.getGlslName() +
            " = " +
            glslNames.join(this.glslOperator.GLSL_NAME) +
            ";"
         );
      } else if (this.glslOperator.TYPE === OPERATOR_TYPE.METHOD) {
         if (
            this.glslOperator === GLSL_OPERATOR.MAXIMUM ||
            this.glslOperator === GLSL_OPERATOR.MINIMUM
         ) {
            glslNames.unshift(this.callingParameter.getGlslName());
            return (
               this.result.getGlslName() +
               " = " +
               GlslOperation.getGlslExpressionOfParams(
                  this.glslOperator.GLSL_NAME,
                  glslNames
               ) +
               ";"
            );
         }
         return (
            this.result.getGlslName() +
            " = " +
            this.glslOperator.GLSL_NAME +
            "(" +
            glslNames.join(", ") +
            ");"
         );
      } else if (this.glslOperator.TYPE === OPERATOR_TYPE.CUSTOM) {
         if (this.glslOperator === GLSL_OPERATOR.CHANNEL) {
            return (
               this.result.getGlslName() +
               " = " +
               glslNames[0] +
               "[" +
               glslNames[1] +
               "];"
            );
         } else if (this.glslOperator === GLSL_OPERATOR.VEC3_TO_VEC4) {
            return (
               this.result.getGlslName() +
               " = vec4(" +
               glslNames[0] +
               ", " +
               glslNames[1] +
               ");"
            );
         } else if (this.glslOperator === GLSL_OPERATOR.LUMINANCE) {
            return (
               this.result.getGlslName() +
               " = " +
               this.glslOperator.GLSL_NAME +
               "(" +
               glslNames[0] +
               ");"
            );
         }
      }
   }
}

class GlslUniform {
   constructor() {
      this.glslName = GlslVariable.getUniqueName("uniform");
      this.context = GlslShader.getGlslContext().getGlContext();
      this.shader = GlslShader.getCurrentShader();
   }

   /**
    * @protected
    * @returns {string}
    */
   getGlslName() {
      return this.glslName;
   }

   /**
    * @protected
    * @returns {WebGL2RenderingContext}
    */
   getContext() {
      return this.context;
   }

   /**
    * @protected
    * @returns {WebGLProgram}
    */
   getShader() {
      return this.getShader;
   }

   /**
    * @abstract
    * @name setValue
    * @param {undefined} value
    */

   /**
    * @abstract
    * @name getValue
    * @returns {undefined}
    */
}

class GlslUniformImage extends GlslUniform {
   /**
    * @param {HTMLImageElement} initialValue
    */
   constructor(initialValue) {
      super();
      this.glslImage = new GlslImage(initialValue);
   }

   /**
    * @public
    * @param {HTMLImageElement} jsImage
    */
   setValue(jsImage) {
      this.glslImage.setImage(jsImage);
   }

   /**
    * @public
    * @returns {GlslImage}
    */
   getValue() {
      return this.glslImage;
   }
}

class GlslImage {
   /**
    * @public
    * @param  {HTMLImageElement} jsImage
    */
   constructor(jsImage) {
      this.jsImage = jsImage;
      this.uniformGlslName = GlslVariable.getUniqueName("uniform");
      this.glslVector4 = new GlslVector4(
         null,
         "texture(" + this.uniformGlslName + ", " + GLSL_VARIABLE.UV + ")"
      );
      GlslShader.addGlslImageToCurrentShader(this);
   }
   /**
    * @public
    * @static
    * @param  {HTMLImageElement} jsImage
    * @returns {GlslVector4}
    */
   static load(jsImage) {
      let glslImage = new GlslImage(jsImage);
      return glslImage.glslVector4;
   }
   /**
    * @public
    * @param {HTMLImageElement} jsImage
    */
   setImage(jsImage) {
      this.jsImage = jsImage;
      const context = GlslShader.getGlslContext().getGlContext();
      this.createBaseTexture(context);
   }
   /**
    * @returns {string}
    */
   getGlslDefinition() {
      return "uniform sampler2D " + this.uniformGlslName + ";";
   }
   /**
    * @param  {WebGL2RenderingContext} glContext
    * @returns {WebGLTexture}
    */
   createBaseTexture(glContext) {
      let texture = glContext.createTexture();
      glContext.bindTexture(glContext.TEXTURE_2D, texture);
      glContext.texParameteri(
         glContext.TEXTURE_2D,
         glContext.TEXTURE_WRAP_S,
         glContext.CLAMP_TO_EDGE
      );
      glContext.texParameteri(
         glContext.TEXTURE_2D,
         glContext.TEXTURE_WRAP_T,
         glContext.CLAMP_TO_EDGE
      );
      glContext.texParameteri(
         glContext.TEXTURE_2D,
         glContext.TEXTURE_MIN_FILTER,
         glContext.LINEAR
      );
      glContext.texParameteri(
         glContext.TEXTURE_2D,
         glContext.TEXTURE_MAG_FILTER,
         glContext.LINEAR
      );
      glContext.texImage2D(
         glContext.TEXTURE_2D,
         0,
         glContext.RGBA,
         glContext.RGBA,
         glContext.UNSIGNED_BYTE,
         this.jsImage
      );
      return texture;
   }

   /**
    * @param  {WebGL2RenderingContext} glContext
    * @param  {WebGLProgram} shaderProgram
    * @param  {number} textureUnit
    * @returns {void}
    */
   loadIntoShaderProgram(glContext, shaderProgram, textureUnit) {
      glContext.activeTexture(glContext.TEXTURE0 + textureUnit);
      glContext.bindTexture(
         glContext.TEXTURE_2D,
         this.createBaseTexture(glContext)
      );
      glContext.uniform1i(
         glContext.getUniformLocation(shaderProgram, this.uniformGlslName),
         textureUnit
      );
   }

   /**
    * @param {number[][]} kernel Convolution matrix (NxN).
    * @param {boolean} normalize
    * @returns {GlslVector4}
    */
   applyFilter(kernel, normalize = false) {
      // TODO Check if kernel is quadratic.

      let filtered = new GlslVector4([
         new GlslFloat(0),
         new GlslFloat(0),
         new GlslFloat(0),
         new GlslFloat(0),
      ]);

      if (normalize) {
         let kernelSum = 0;

         kernel.forEach((row) => {
            row.forEach((value) => {
               kernelSum += value;
            });
         });

         if (kernelSum !== 0) {
            kernel.forEach((row, rowIndex) => {
               row.forEach((value, columnIndex) => {
                  kernel[rowIndex][columnIndex] = value / kernelSum;
               });
            });
         }
      }

      const kernelMiddle = (kernel.length - 1) / 2;

      kernel.forEach((row, rowIndex) => {
         row.forEach((value, columnIndex) => {
            if (value !== 0) {
               filtered = filtered.addVector4(
                  new GlslFloat(value).multiplyVector4(
                     this.getNeighborPixel(
                        columnIndex - kernelMiddle,
                        rowIndex - kernelMiddle
                     )
                  )
               );
            }
         });
      });

      return new GlslVector4([
         filtered.channel(0),
         filtered.channel(1),
         filtered.channel(2),
         new GlslFloat(1),
      ]);
   }

   /**
    * @public
    * @param {number} offsetX
    * @param {number} offsetY
    * @returns {GlslVector4}
    */
   getNeighborPixel(offsetX, offsetY) {
      const u = (1 / this.jsImage.width) * offsetX;
      const v = (1 / this.jsImage.height) * offsetY;

      const glslOffset = {
         u: GlslFloat.getJsNumberAsString(u),
         v: GlslFloat.getJsNumberAsString(v),
      };

      return new GlslVector4(
         null,
         "texture(" +
            this.uniformGlslName +
            ", vec2(" +
            GLSL_VARIABLE.UV_U +
            " + " +
            glslOffset.u +
            ", " +
            GLSL_VARIABLE.UV_V +
            " + " +
            glslOffset.v +
            "))"
      );
   }
}

/**
 * @abstract
 */
class GlslVariable {
   /**
    * @abstract
    * @param {string} [customDeclaration=""]
    */
   constructor(customDeclaration = "") {
      this.glslName = GlslVariable.getUniqueName(this.getGlslVarType());
      if (customDeclaration !== null) {
         if (customDeclaration !== "") {
            customDeclaration = " = " + customDeclaration;
         }
         GlslShader.addGlslCommandToCurrentShader(
            this.getGlslVarType() +
               " " +
               this.getGlslName() +
               customDeclaration +
               ";"
         );
      }
   }
   /**
    * @static
    * @param  {string} prefix
    * @returns {string}
    */
   static getUniqueName(prefix) {
      GlslVariable.uniqueNumber++;
      return prefix + "_" + GlslVariable.uniqueNumber.toString();
   }
   /**
    * @static
    * @param  {GlslVariable[]} glslVariables
    * @returns {string[]}
    */
   static getGlslNamesOfGlslVariables(glslVariables) {
      let glslNames = [];
      if (glslVariables !== null) {
         for (let i = 0; i < glslVariables.length; i++) {
            glslNames.push(glslVariables[i].getGlslName());
         }
      }
      return glslNames;
   }
   /**
    * @returns {string}
    */
   getGlslName() {
      return this.glslName;
   }
   /**
    * @private
    * @param  {GlslOperation} glslOperation
    * @returns {void}
    */
   declareGlslResult(glslOperation) {
      GlslShader.addGlslCommandToCurrentShader(glslOperation.getDeclaration());
   }
   /**
    * @protected
    * @param  {GlslVariable[]} operants
    * @param  {GLSL_OPERATOR} operator
    * @returns {GlslFloat}
    */
   getGlslFloatResult(operants, operator) {
      const glslResult = new GlslFloat();
      this.declareGlslResult(
         new GlslOperation(this, glslResult, operants, operator)
      );
      return glslResult;
   }
   /**
    * @protected
    * @param  {GlslVariable[]} operants
    * @param  {GLSL_OPERATOR} operator
    * @returns {GlslVector3}
    */
   getGlslVector3Result(operants, operator) {
      const glslResult = new GlslVector3();
      this.declareGlslResult(
         new GlslOperation(this, glslResult, operants, operator)
      );
      return glslResult;
   }
   /**
    * @protected
    * @param  {GlslVariable[]} operants
    * @param  {GLSL_OPERATOR} operator
    * @returns {GlslVector4}
    */
   getGlslVector4Result(operants, operator) {
      const glslResult = new GlslVector4();
      this.declareGlslResult(
         new GlslOperation(this, glslResult, operants, operator)
      );
      return glslResult;
   }
   /**
    * @protected
    * @param  {GlslVariable[]} operants
    * @param  {GLSL_OPERATOR} operator
    * @returns {GlslMatrix3}
    */
   getGlslMatrix3Result(operants, operator) {
      const glslResult = new GlslMatrix3();
      this.declareGlslResult(
         new GlslOperation(this, glslResult, operants, operator)
      );
      return glslResult;
   }

   /**
    * @name getGlslVarType
    * @abstract
    * @returns {GLSL_VARIABLE_TYPE}
    */
   getGlslVarType() {
      throw new Error("Cannot call an abstract method.");
   }

   /**
    * @name addFloat
    * @abstract
    * @param  {...GlslFloat[]} addends
    * @returns {GlslVariable}
    */
   /**
    * @name addVector3
    * @abstract
    * @param  {...GlslVector3[]} addends
    * @returns {GlslVariable}
    */
   /**
    * @name addVector4
    * @abstract
    * @param  {...GlslVector4[]} addends
    * @returns {GlslVariable}
    */
   /**
    * @name addMatrix3
    * @abstract
    * @param  {...GlslMatrix3[]} addends
    * @returns {GlslVariable}
    */
   /**
    * @name subtractFloat
    * @abstract
    * @param  {...GlslFloat[]} subtrahends
    * @returns {GlslVariable}
    */
   /**
    * @name subtractVector3
    * @abstract
    * @param  {...GlslVector3[]} subtrahends
    * @returns {GlslVariable}
    */
   /**
    * @name subtractVector4
    * @abstract
    * @param  {...GlslVector4[]} subtrahends
    * @returns {GlslVariable}
    */
   /**
    * @name subtractMatrix3
    * @abstract
    * @param  {...GlslMatrix3[]} subtrahends
    * @returns {GlslVariable}
    */
   /**
    * @name multiplyFloat
    * @abstract
    * @param  {...GlslFloat[]} factors
    * @returns {GlslVariable}
    */
   /**
    * @name multiplyVector3
    * @abstract
    * @param  {...GlslVector3[]} factors
    * @returns {GlslVariable}
    */
   /**
    * @name multiplyVector4
    * @abstract
    * @param  {...GlslVector4[]} factors
    * @returns {GlslVariable}
    */
   /**
    * @name multiplyMatrix3
    * @abstract
    * @param  {...GlslMatrix3[]} factors
    * @returns {GlslVariable}
    */
   /**
    * @name divideFloat
    * @abstract
    * @param  {...GlslFloat[]} divisors
    * @returns {GlslVariable}
    */
}

GlslVariable.uniqueNumber = 0;

/** @abstract */
class GlslVector extends GlslVariable {
   /**
    * @param {number} channel
    * @returns {GlslFloat}
    */
   channel(channel) {
      return this.getGlslFloatResult(
         [this, new GlslInteger(channel)],
         GLSL_OPERATOR.CHANNEL
      );
   }

   /**
    * @abstract
    * @name abs
    * @returns {GlslVector}
    */
   /**
    * @abstract
    * @name normalize
    * @returns {GlslVector}
    */
}

/** @abstract */
class GlslMatrix extends GlslVariable {
   /**
    * @abstract
    * @name inverse
    * @returns {GlslMatrix}
    */
}

class GlslInteger extends GlslVariable {
   /**
    * @param  {number} jsNumber
    */
   constructor(jsNumber = null) {
      if (jsNumber !== null) {
         super(null);
         this.glslName = Math.floor(jsNumber).toString();
      } else {
         super();
      }
   }
   /**
    * @returns {GLSL_VARIABLE_TYPE}
    */
   getGlslVarType() {
      return GLSL_VARIABLE_TYPE.INTEGER;
   }
}

class GlslFloat extends GlslVariable {
   /**
    * @static
    * @param  {number} number
    * @returns {string}
    */
   static getJsNumberAsString(number) {
      if (Math.trunc(number) === number) {
         return "(" + number.toString() + ".0)";
      }
      if (number.toString().includes("e-")) {
         //console.warn(number.toString() + " is converted to zero.");
         return "0.0";
      }
      return "(" + number.toString() + ")";
   }
   /**
    * @param  {number} [jsNumber=null]
    */
   constructor(jsNumber = null) {
      if (jsNumber !== null) {
         super(null);
         this.glslName = GlslFloat.getJsNumberAsString(jsNumber);
      } else {
         super();
      }
   }
   /**
    * @returns {string}
    */
   getGlslName() {
      return this.glslName;
   }
   /**
    * @returns {GLSL_VARIABLE_TYPE}
    */
   getGlslVarType() {
      return GLSL_VARIABLE_TYPE.FLOAT;
   }
   /**
    * @param  {...GlslFloat} addends
    * @returns {GlslFloat}
    */
   addFloat(...addends) {
      return this.getGlslFloatResult(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @param  {...GlslVector3} addends
    * @returns {GlslVector3}
    */
   addVector3(...addends) {
      return this.getGlslVector3Result(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @param  {...GlslVector4} addends
    * @returns {GlslVector4}
    */
   addVector4(...addends) {
      return this.getGlslVector4Result(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @param  {...GlslMatrix3} addends
    * @returns {GlslMatrix3}
    */
   addMatrix3(...addends) {
      return this.getGlslMatrix3Result(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @param  {...GlslFloat} subtrahends
    * @returns {GlslFloat}
    */
   subtractFloat(...subtrahends) {
      return this.getGlslFloatResult(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @param  {...GlslVector3} subtrahends
    * @returns {GlslVector3}
    */
   subtractVector3(...subtrahends) {
      return this.getGlslVector3Result(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @param  {...GlslVector4} subtrahends
    * @returns {GlslVector4}
    */
   subtractVector4(...subtrahends) {
      return this.getGlslVector4Result(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @param  {...GlslMatrix3} subtrahends
    * @returns {GlslMatrix3}
    */
   subtractMatrix3(...subtrahends) {
      return this.getGlslMatrix3Result(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @param  {...GlslFloat} factors
    * @returns {GlslFloat}
    */
   multiplyFloat(...factors) {
      return this.getGlslFloatResult(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @param  {...GlslVector3} factors
    * @returns {GlslVector3}
    */
   multiplyVector3(...factors) {
      return this.getGlslVector3Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @param  {...GlslVector4} factors
    * @returns {GlslVector4}
    */
   multiplyVector4(...factors) {
      return this.getGlslVector4Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @param  {...GlslMatrix3} factors
    * @returns {GlslMatrix3}
    */
   multiplyMatrix3(...factors) {
      return this.getGlslMatrix3Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @param  {...GlslFloat} divisors
    * @returns {GlslFloat}
    */
   divideFloat(...divisors) {
      return this.getGlslFloatResult(divisors, GLSL_OPERATOR.DIVIDE);
   }
   /**
    * @returns {GlslFloat}
    */
   abs() {
      return this.getGlslFloatResult([this], GLSL_OPERATOR.ABS);
   }
   /**
    * @param  {...GlslVariable} parameters
    * @returns {GlslFloat}
    */
   maximum(...parameters) {
      return this.getGlslFloatResult(parameters, GLSL_OPERATOR.MAXIMUM);
   }
   /**
    * @param  {...GlslVariable} parameters
    * @returns {GlslFloat}
    */
   minimum(...parameters) {
      return this.getGlslFloatResult(parameters, GLSL_OPERATOR.MINIMUM);
   }
   /**
    * @returns {GlslFloat}
    */
   radians() {
      return this.getGlslFloatResult([this], GLSL_OPERATOR.RADIANS);
   }
   /**
    * @returns {GlslFloat}
    */
   sin() {
      return this.getGlslFloatResult([this], GLSL_OPERATOR.SINE);
   }
   /**
    * @returns {GlslFloat}
    */
   cos() {
      return this.getGlslFloatResult([this], GLSL_OPERATOR.COSINE);
   }
   /**
    * @returns {GlslFloat}
    */
   acos() {
      return this.getGlslFloatResult([this], GLSL_OPERATOR.ARC_COSINE);
   }
   /**
    * @returns {GlslFloat} Is one when the input is positive,
    * zero when the input is zero and minus one when the
    * input is negative.
    */
   sign() {
      return this.getGlslFloatResult([this], GLSL_OPERATOR.SIGN);
   }
   /**
    * @param {GlslFloat} edge
    * @returns {GlslFloat} Is zero if input is smaller than edge and otherwise one.
    */
   step(edge = new GLSL.Float(0.5)) {
      return this.getGlslFloatResult([edge, this], GLSL_OPERATOR.STEP);
   }
}

class GlslVector2 extends GlslVector {
   /**
    * @param  {GlslFloat[]} vector2
    * @param {string} customDeclaration
    */
   constructor(vector2 = undefined, customDeclaration = undefined) {
      if (!customDeclaration) {
         customDeclaration = "";
         if (vector2) {
            let vector2GlslNames = [];

            vector2GlslNames.push(vector2[0].getGlslName());
            vector2GlslNames.push(vector2[1].getGlslName());

            customDeclaration =
               GLSL_VARIABLE_TYPE.VECTOR2 +
               "(" +
               vector2GlslNames.join(", ") +
               ")";
         }
      }
      super(customDeclaration);
   }
   /**
    * @returns {GLSL_VARIABLE_TYPE}
    */
   getGlslVarType() {
      return GLSL_VARIABLE_TYPE.VECTOR2;
   }

   /**
    * @param {GlslVector2} point
    * @returns {GlslFloat}
    */
   distance(point) {
      return this.getGlslFloatResult([this, point], GLSL_OPERATOR.DISTANCE);
   }
}

class GlslVector3 extends GlslVector {
   /**
    * @param  {GlslFloat[]} vector3
    */
   constructor(vector3 = undefined) {
      let customDeclaration = "";
      if (vector3) {
         let vector3GlslNames = [];

         vector3GlslNames.push(vector3[0].getGlslName());
         vector3GlslNames.push(vector3[1].getGlslName());
         vector3GlslNames.push(vector3[2].getGlslName());

         customDeclaration =
            GLSL_VARIABLE_TYPE.VECTOR3 +
            "(" +
            vector3GlslNames.join(", ") +
            ")";
      }
      super(customDeclaration);
   }
   /**
    * @returns {GLSL_VARIABLE_TYPE}
    */
   getGlslVarType() {
      return GLSL_VARIABLE_TYPE.VECTOR3;
   }
   /**
    * @param  {...GlslFloat} addends
    * @returns {GlslVector3}
    */
   addFloat(...addends) {
      return this.getGlslVector3Result(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @param  {...GlslVector3} addends
    * @returns {GlslVector3}
    */
   addVector3(...addends) {
      return this.getGlslVector3Result(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @throws {Error} Not possible to add vec4 to vec3.
    */
   addVector4() {
      throw new Error("Not possible to add vec4 to vec3.");
   }
   /**
    * @throws {Error} Not possible to add mat3 to vec3.
    */
   addMatrix3() {
      throw new Error("Not possible to add mat3 to vec3.");
   }
   /**
    * @param  {...GlslFloat} subtrahends
    * @returns {GlslVector3}
    */
   subtractFloat(...subtrahends) {
      return this.getGlslVector3Result(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @param  {...GlslVector3} subtrahends
    * @returns {GlslVector3}
    */
   subtractVector3(...subtrahends) {
      return this.getGlslVector3Result(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @throws {Error} Not possible to subtract vec4 from vec3.
    */
   subtractVector4() {
      throw new Error("Not possible to subtract vec4 from vec3.");
   }
   /**
    * @throws {Error} Not possible to subtract mat3 from vec3.
    */
   subtractMatrix3() {
      throw new Error("Not possible to subtract mat3 from vec3.");
   }
   /**
    * @param  {...GlslFloat} factors
    * @returns {GlslVector3}
    */
   multiplyFloat(...factors) {
      return this.getGlslVector3Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @param  {...GlslVector3} factors
    * @returns {GlslVector3}
    */
   multiplyVector3(...factors) {
      return this.getGlslVector3Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @throws {Error} Not possible to multiply vec4 with vec3.
    */
   multiplyVector4() {
      throw new Error("Not possible to multiply vec4 with vec3.");
   }
   /**
    * @param  {...GlslMatrix3} factors
    * @returns {GlslVector3}
    */
   multiplyMatrix3(...factors) {
      return this.getGlslVector3Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @param  {...GlslFloat} divisors
    * @returns {GlslVector3}
    */
   divideFloat(...divisors) {
      return this.getGlslVector3Result(divisors, GLSL_OPERATOR.DIVIDE);
   }
   /**
    * @returns {GlslFloat}
    */
   length() {
      return this.getGlslFloatResult([this], GLSL_OPERATOR.LENGTH);
   }
   /**
    * @returns {GlslVector3}
    */
   abs() {
      return this.getGlslVector3Result([this], GLSL_OPERATOR.ABS);
   }
   /**
    * @returns {GlslVector3}
    */
   normalize() {
      return this.getGlslVector3Result([this], GLSL_OPERATOR.NORMALIZE);
   }
   /**
    * @param  {...GlslVariable} parameters
    * @returns {GlslVector3}
    */
   maximum(...parameters) {
      return this.getGlslVector3Result(parameters, GLSL_OPERATOR.MAXIMUM);
   }
   /**
    * @param  {...GlslVariable} parameters
    * @returns {GlslVector3}
    */
   minimum(...parameters) {
      return this.getGlslVector3Result(parameters, GLSL_OPERATOR.MINIMUM);
   }
   /**
    * @param  {GlslVector} parameter
    * @returns {GlslFloat}
    */
   dot(parameter) {
      return this.getGlslFloatResult([this, parameter], GLSL_OPERATOR.DOT);
   }
   /**
    * @param  {GlslFloat} fourthChannel
    * @returns {GlslVector4}
    */
   getVector4(fourthChannel = new GlslFloat(1)) {
      return this.getGlslVector4Result(
         [this, fourthChannel],
         GLSL_OPERATOR.VEC3_TO_VEC4
      );
   }
}

class GlslVector4 extends GlslVector {
   /**
    * @param  {GlslFloat[]} vector4
    * @param  {string} customDeclaration
    */
   constructor(vector4 = undefined, customDeclaration = "") {
      if (customDeclaration === "") {
         if (vector4) {
            let vector4GlslNames = [];

            vector4GlslNames.push(vector4[0].getGlslName());
            vector4GlslNames.push(vector4[1].getGlslName());
            vector4GlslNames.push(vector4[2].getGlslName());
            vector4GlslNames.push(vector4[3].getGlslName());

            customDeclaration =
               GLSL_VARIABLE_TYPE.VECTOR4 +
               "(" +
               vector4GlslNames.join(", ") +
               ")";
         }
      }
      super(customDeclaration);
   }
   /**
    * @returns {GLSL_VARIABLE_TYPE}
    */
   getGlslVarType() {
      return GLSL_VARIABLE_TYPE.VECTOR4;
   }
   /**
    * @param  {...GlslFloat} addends
    * @returns {GlslVector4}
    */
   addFloat(...addends) {
      return this.getGlslVector4Result(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @throws {Error} Not possible to add vec3 to vec4.
    */
   addVector3() {
      throw new Error("Not possible to add vec3 to vec4.");
   }
   /**
    * @param  {...GlslVector4} addends
    * @returns {GlslVector4}
    */
   addVector4(...addends) {
      return this.getGlslVector4Result(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @throws {Error} Not possible to add mat3 to vec4.
    */
   addMatrix3() {
      throw new Error("Not possible to add mat3 to vec4.");
   }
   /**
    * @param  {...GlslFloat} subtrahends
    * @returns {GlslVector4}
    */
   subtractFloat(...subtrahends) {
      return this.getGlslVector4Result(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @throws {Error} Not possible to subtract vec3 from vec4.
    */
   subtractVector3() {
      throw new Error("Not possible to subtract vec3 from vec4.");
   }
   /**
    * @param  {...GlslVector4} subtrahends
    * @returns {GlslVector4}
    */
   subtractVector4(...subtrahends) {
      return this.getGlslVector4Result(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @throws {Error} Not possible to subtract mat3 from vec4.
    */
   subtractMatrix3() {
      throw new Error("Not possible to subtract mat3 from vec4.");
   }
   /**
    * @param  {...GlslFloat} factors
    * @returns {GlslVector4}
    */
   multiplyFloat(...factors) {
      return this.getGlslVector4Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @throws {Error} Not possible to multiply vec3 with vec4.
    */
   multiplyVector3() {
      throw new Error("Not possible to multiply vec3 with vec4.");
   }
   /**
    * @param  {...GlslVector4} factors
    * @returns {GlslVector4}
    */
   multiplyVector4(...factors) {
      return this.getGlslVector4Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @throws {Error} Not possible to multiply mat3 with vec4.
    */
   multiplyMatrix3() {
      throw new Error("Not possible to multiply mat3 with vec4.");
   }
   /**
    * @param  {...GlslFloat} divisors
    * @returns {GlslVector4}
    */
   divideFloat(...divisors) {
      return this.getGlslVector4Result(divisors, GLSL_OPERATOR.DIVIDE);
   }
   /**
    * @returns {GlslVector4}
    */
   abs() {
      return this.getGlslVector4Result([this], GLSL_OPERATOR.ABS);
   }
   /**
    * @returns {GlslFloat}
    */
   length() {
      return this.getGlslFloatResult([this], GLSL_OPERATOR.LENGTH);
   }
   /**
    * @returns {GlslVector4}
    */
   normalize() {
      return this.getGlslVector4Result([this], GLSL_OPERATOR.NORMALIZE);
   }
   /**
    * @param  {...GlslVariable} parameters
    * @returns {GlslVector4}
    */
   maximum(...parameters) {
      return this.getGlslVector4Result(parameters, GLSL_OPERATOR.MAXIMUM);
   }
   /**
    * @param  {...GlslVariable} parameters
    * @returns {GlslVector4}
    */
   minimum(...parameters) {
      return this.getGlslVector4Result(parameters, GLSL_OPERATOR.MINIMUM);
   }
   /**
    * @param  {GlslVector} parameter
    * @returns {GlslFloat}
    */
   dot(parameter) {
      return this.getGlslFloatResult([this, parameter], GLSL_OPERATOR.DOT);
   }
   /**
    * @returns {GlslFloat}
    */
   getLuminance() {
      return this.getGlslFloatResult([this], GLSL_OPERATOR.LUMINANCE);
   }
}

class GlslMatrix3 extends GlslMatrix {
   /**
    * @param  {GlslFloat[][]} matrix3
    */
   constructor(matrix3 = undefined) {
      let customDeclaration = "";
      if (matrix3 !== undefined) {
         let matrix3GlslNames = [
            [null, null, null],
            [null, null, null],
            [null, null, null],
         ];
         for (let r = 0; r < matrix3.length; r++) {
            for (let c = 0; c < matrix3[0].length; c++) {
               matrix3GlslNames[r][c] = matrix3[r][c].getGlslName();
            }
         }
         if (matrix3 !== undefined) {
            customDeclaration =
               GLSL_VARIABLE_TYPE.MATRIX3 +
               "(" +
               matrix3GlslNames[0][0] +
               ", " +
               matrix3GlslNames[1][0] +
               ", " +
               matrix3GlslNames[2][0] +
               ", " +
               matrix3GlslNames[0][1] +
               ", " +
               matrix3GlslNames[1][1] +
               ", " +
               matrix3GlslNames[2][1] +
               ", " +
               matrix3GlslNames[0][2] +
               ", " +
               matrix3GlslNames[1][2] +
               ", " +
               matrix3GlslNames[2][2] +
               ")";
         }
      }
      super(customDeclaration);
   }
   /**
    * @returns {GLSL_VARIABLE_TYPE}
    */
   getGlslVarType() {
      return GLSL_VARIABLE_TYPE.MATRIX3;
   }
   /**
    * @param  {...GlslFloat} addends
    * @returns {GlslMatrix3}
    */
   addFloat(...addends) {
      return this.getGlslMatrix3Result(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @throws {Error} Not possible to add vec3 to mat3.
    */
   addVector3() {
      throw new Error("Not possible to add vec3 to mat3.");
   }
   /**
    * @throws Not possible to add vec4 to mat3.
    */
   addVector4() {
      throw new Error("Not possible to add vec4 to mat3.");
   }
   /**
    * @param  {...GlslMatrix3} addends
    * @returns {GlslMatrix3}
    */
   addMatrix3(...addends) {
      return this.getGlslMatrix3Result(addends, GLSL_OPERATOR.ADD);
   }
   /**
    * @param  {...GlslFloat} subtrahends
    * @returns {GlslMatrix3}
    */
   subtractFloat(...subtrahends) {
      return this.getGlslMatrix3Result(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @throws Not possible to subtract vec3 from mat3.
    */
   subtractVector3() {
      throw new Error("Not possible to subtract vec3 from mat3.");
   }
   /**
    * @throws {Error} Not possible to subtract vec4 from mat3.
    */
   subtractVector4() {
      throw new Error("Not possible to subtract vec4 from mat3.");
   }
   /**
    * @param  {...GlslMatrix3} subtrahends
    * @returns {GlslMatrix3}
    */
   subtractMatrix3(...subtrahends) {
      return this.getGlslMatrix3Result(subtrahends, GLSL_OPERATOR.SUBTRACT);
   }
   /**
    * @param  {...GlslFloat} factors
    * @returns {GlslVariable}
    */
   multiplyFloat(...factors) {
      return this.getGlslMatrix3Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @param  {...GlslVector3} factors
    * @returns {GlslVector3}
    */
   multiplyVector3(...factors) {
      return this.getGlslVector3Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @throws {Error} Not possible to multiply vec4 with mat3.
    */
   multiplyVector4() {
      throw new Error("Not possible to multiply vec4 with mat3.");
   }
   /**
    * @param  {...GlslMatrix3} factors
    * @returns {GlslMatrix3}
    */
   multiplyMatrix3(...factors) {
      return this.getGlslMatrix3Result(factors, GLSL_OPERATOR.MULTIPLY);
   }
   /**
    * @param  {...GlslFloat} divisors
    * @returns {GlslMatrix3}
    */
   divideFloat(...divisors) {
      return this.getGlslMatrix3Result(divisors, GLSL_OPERATOR.DIVIDE);
   }
   /**
    * @returns {GlslMatrix3}
    */
   inverse() {
      return this.getGlslMatrix3Result([this], GLSL_OPERATOR.INVERSE);
   }
}

/**
 * @global
 * @typedef {Shader} GLSL.Shader
 * @typedef {GlslRendering.render} GLSL.render
 * @typedef {GlslFloat} GLSL.Float
 * @typedef {GlslImage} GLSL.image
 * @typedef {GlslVector2} GLSL.Vector2
 * @typedef {GlslVector3} GLSL.Vector3
 * @typedef {GlslVector4} GLSL.Vector4
 * @typedef {GlslMatrix3} GLSL.Matrix3
 * @typedef {GlslUniformImage} GLSL.Uniform.Image
 */
const GLSL = {
   Shader: Shader,
   render: GlslRendering.render,
   Image: GlslImage,
   Integer: GlslInteger,
   Float: GlslFloat,
   Vector2: GlslVector2,
   Vector3: GlslVector3,
   Vector4: GlslVector4,
   Matrix3: GlslMatrix3,
   Uniform: { Image: GlslUniformImage },
};

9e92e756113c4e4550a67baa5cd34e5a45f668ce

Remove hard code.

Remove hard code.

// TODO Remove hard code.

      this.handleResize();
      this.updateCameraPlanes();
   }
}
/** @constant */
VirtualInputRenderer.MIN_CAMERA_PLANES_DISTANCE = 0.5;
/** @type {VirtualInputRenderer[]} */
VirtualInputRenderer.instances = [];

class PhotometricStereoRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }

   /**
    * @override
    * @protected
    */
   async initialize() {
      if (this.initialized || (!this.initialized && this.initializing)) {
         return;
      }
      this.initializing = true;

      super.initialize();

      this.lights = new Array(8);
      this.lightHelpers = new Array(8);

      for (let i = 0; i < 8; i++) {
         // TODO Remove hard code.
         this.lights[i] = new THREE.PointLight("white", 0.25);
         this.lights[i].castShadow = true;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.width = 512 * 2;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.height = 512 * 2;
         // TODO Remove hard code.
         this.lightHelpers[i] = new THREE.PointLightHelper(this.lights[i], 0.2);
         this.scene.add(this.lights[i]);
         this.scene.add(this.lightHelpers[i]);
      }

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @override
    * @public
    * @returns {Promise<HTMLImageElement[]>}
    */
   async render() {
      this.renderId++;
      const renderId = this.renderId;

      await this.initialize();

      if (this.isRenderObsolete(renderId)) return;

      const lightCount = this.lights.length;
      const renderPromises = [];

      for (let i = 0; i < lightCount; i++) {
         this.lightHelpers[i].visible = false;
      }
      this.cameraHelper.visible = false;

      for (let i = 0; i < lightCount; i++) {
         for (let i = 0; i < lightCount; i++) {
            this.lights[i].visible = false;
         }
         this.lights[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 25 * (this.cameraDistance / 8);

         if (this.isRenderObsolete(renderId)) return;

         this.renderer.render(this.scene, this.camera);
         const renderImageDataUrl = this.renderer.domElement.toDataURL();

         renderPromises.push(
            new Promise((resolve) => {
               setTimeout(() => {
                  const image = new Image();
                  image.addEventListener("load", () => {
                     resolve(image);
                  });
                  image.src = renderImageDataUrl;
               });
            })
         );
      }

      for (let i = 0; i < lightCount; i++) {
         this.lights[i].visible = true;
         this.lightHelpers[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 0.25;
      }
      this.cameraHelper.visible = true;

      this.updateCameraPlanes();
      this.uiRenderer.render(this.scene, this.uiCamera);

      return Promise.all(renderPromises);
   }

   /**
    * @public
    * @param {number} lightPolarAngleDeg
    */
   async setLightPolarAngleDeg(lightPolarAngleDeg) {
      await this.initialize();
      this.lightPolarAngleDeg = lightPolarAngleDeg;
      this.updateLightPositions();
   }

   /**
    * @override
    * @public
    * @param {number} lightDistance
    */
   async setLightDistance(lightDistance) {
      await this.initialize();

      this.lightDistance = lightDistance;
      for (let i = 0; i < this.lights.length; i++) {
         this.lights[i].distance = this.lightDistance * 2;
      }
      this.updateLightPositions();
   }

   async updateLightPositions() {
      await this.initialize();

      const correctedLightPolarDegree = 360 - this.lightPolarAngleDeg;

      /**
       * @param {THREE.Light} light
       * @param {number} lightAzimuthalDegree
       */
      const setSingleLightAzimuthalAngle = (light, lightAzimuthalDegree) => {
         let lightVector = new THREE.Vector3(this.lightDistance, 0, 0);

         let lightPolarRotationAxis = new THREE.Vector3(0, 1, 0).normalize();
         lightVector.applyAxisAngle(
            lightPolarRotationAxis,
            correctedLightPolarDegree * (Math.PI / 180)
         );

         const lightRotation = lightAzimuthalDegree * (Math.PI / 180);
         const lightRotationAxis = new THREE.Vector3(0, 0, 1).normalize();
         lightVector.applyAxisAngle(lightRotationAxis, lightRotation);

         light.position.set(lightVector.x, lightVector.y, lightVector.z);
      };

      const lightCount = this.lights.length;
      for (let i = 0; i < lightCount; i++) {
         setSingleLightAzimuthalAngle(this.lights[i], i * (360 / lightCount));
      }
   }
}

class SphericalGradientRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }
}

f39176ee8b4b393b919c0a86b9e008ac8a522b6e

use cameraVerticalShift

use cameraVerticalShift

// TODO: use cameraVerticalShift

            if (normalMapHelper.isRenderObsolete()) return;

            if (cameraVerticalShift) {
               // TODO: use cameraVerticalShift
               /*const cameraAngle = Math.atan(
                  1 / Math.tan(lightPolarAngleDeg * (Math.PI / 180))
               );

52b8d6cd22a6870d8ffb73d0db1e757928da5df1

remove preserve to enable swapping for better performance

remove preserve to enable swapping for better performance

// TODO: remove preserve to enable swapping for better performance

      this.cameraHelper = new THREE.CameraHelper(this.camera);
      this.scene.add(this.cameraHelper);

      // TODO: remove preserve to enable swapping for better performance
      this.renderer = new THREE.WebGLRenderer({
         preserveDrawingBuffer: true,
      });

02dd92f75f25e94e4b7d327cf5b37d45748aed3f

Remove hard code.

Remove hard code.

// TODO Remove hard code.

      this.handleResize();
      this.updateCameraPlanes();
   }
}
/** @constant */
VirtualInputRenderer.MIN_CAMERA_PLANES_DISTANCE = 0.5;
/** @type {VirtualInputRenderer[]} */
VirtualInputRenderer.instances = [];

class PhotometricStereoRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }

   /**
    * @override
    * @protected
    */
   async initialize() {
      if (this.initialized || (!this.initialized && this.initializing)) {
         return;
      }
      this.initializing = true;

      super.initialize();

      this.lights = new Array(8);
      this.lightHelpers = new Array(8);

      for (let i = 0; i < 8; i++) {
         // TODO Remove hard code.
         this.lights[i] = new THREE.PointLight("white", 0.25);
         this.lights[i].castShadow = true;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.width = 512 * 2;
         // TODO Remove hard code.
         this.lights[i].shadow.mapSize.height = 512 * 2;
         // TODO Remove hard code.
         this.lightHelpers[i] = new THREE.PointLightHelper(this.lights[i], 0.2);
         this.scene.add(this.lights[i]);
         this.scene.add(this.lightHelpers[i]);
      }

      this.initialized = true;
      this.initializing = false;
   }

   /**
    * @override
    * @public
    * @returns {Promise<HTMLImageElement[]>}
    */
   async render() {
      this.renderId++;
      const renderId = this.renderId;

      await this.initialize();

      if (this.isRenderObsolete(renderId)) return;

      const lightCount = this.lights.length;
      const renderPromises = [];

      for (let i = 0; i < lightCount; i++) {
         this.lightHelpers[i].visible = false;
      }
      this.cameraHelper.visible = false;

      for (let i = 0; i < lightCount; i++) {
         for (let i = 0; i < lightCount; i++) {
            this.lights[i].visible = false;
         }
         this.lights[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 25 * (this.cameraDistance / 8);

         if (this.isRenderObsolete(renderId)) return;

         this.renderer.render(this.scene, this.camera);
         const renderImageDataUrl = this.renderer.domElement.toDataURL();

         renderPromises.push(
            new Promise((resolve) => {
               setTimeout(() => {
                  const image = new Image();
                  image.addEventListener("load", () => {
                     resolve(image);
                  });
                  image.src = renderImageDataUrl;
               });
            })
         );
      }

      for (let i = 0; i < lightCount; i++) {
         this.lights[i].visible = true;
         this.lightHelpers[i].visible = true;
         // TODO Remove hard code.
         this.lights[i].intensity = 0.25;
      }
      this.cameraHelper.visible = true;

      this.updateCameraPlanes();
      this.uiRenderer.render(this.scene, this.uiCamera);

      return Promise.all(renderPromises);
   }

   /**
    * @public
    * @param {number} lightPolarAngleDeg
    */
   async setLightPolarAngleDeg(lightPolarAngleDeg) {
      await this.initialize();
      this.lightPolarAngleDeg = lightPolarAngleDeg;
      this.updateLightPositions();
   }

   /**
    * @override
    * @public
    * @param {number} lightDistance
    */
   async setLightDistance(lightDistance) {
      await this.initialize();

      this.lightDistance = lightDistance;
      for (let i = 0; i < this.lights.length; i++) {
         this.lights[i].distance = this.lightDistance * 2;
      }
      this.updateLightPositions();
   }

   async updateLightPositions() {
      await this.initialize();

      const correctedLightPolarDegree = 360 - this.lightPolarAngleDeg;

      /**
       * @param {THREE.Light} light
       * @param {number} lightAzimuthalDegree
       */
      const setSingleLightAzimuthalAngle = (light, lightAzimuthalDegree) => {
         let lightVector = new THREE.Vector3(this.lightDistance, 0, 0);

         let lightPolarRotationAxis = new THREE.Vector3(0, 1, 0).normalize();
         lightVector.applyAxisAngle(
            lightPolarRotationAxis,
            correctedLightPolarDegree * (Math.PI / 180)
         );

         const lightRotation = lightAzimuthalDegree * (Math.PI / 180);
         const lightRotationAxis = new THREE.Vector3(0, 0, 1).normalize();
         lightVector.applyAxisAngle(lightRotationAxis, lightRotation);

         light.position.set(lightVector.x, lightVector.y, lightVector.z);
      };

      const lightCount = this.lights.length;
      for (let i = 0; i < lightCount; i++) {
         setSingleLightAzimuthalAngle(this.lights[i], i * (360 / lightCount));
      }
   }
}

class SphericalGradientRenderer extends VirtualInputRenderer {
   /**
    * @public
    * @param {HTMLCanvasElement} uiCanvas
    * @param {string} modelUrl
    * @param {{width: number, height: number}} renderDimensions
    */
   constructor(
      uiCanvas,
      modelUrl = "./test-datasets/models/monkey.glb",
      renderDimensions = { width: 300, height: 300 }
   ) {
      super(uiCanvas, modelUrl, renderDimensions);
   }
}

f39176ee8b4b393b919c0a86b9e008ac8a522b6e

Better design.

Better design.

// TODO Better design.

/* global THREE */
/* exported PointCloudHelper */

/**
 * @global
 */
class PointCloudHelper {
   /**
    * This functions calculates a point cloud by a given
    * depth mapping.
    *
    * @public
    * @param {HTMLImageElement} depthMapImage The depth
    * mapping that is used to calculate the point cloud.
    * @param {HTMLCanvasElement} renderCanvas The
    * UI-canvas to display the point cloud.
    * @param {number} depthFactor The factor that is
    * multiplied with the z-coordinate (depth-coordinate).
    * @param {HTMLImageElement} textureImage The texture
    * that is used for the point cloud vertex color.
    * @returns {Promise<number[]>} The vertices of the
    * calculated point cloud in an array. [x1, y1, z1, x2,
    * y2, z2, ...]
    */
   static async calculatePointCloud(
      depthMapImage,
      renderCanvas,
      depthFactor = 0.15,
      textureImage = depthMapImage
   ) {
      const pointCloudHelper = new PointCloudHelper(renderCanvas);

      return new Promise((resolve) => {
         setTimeout(async () => {
            if (depthMapImage.naturalWidth === 0) return;

            if (pointCloudHelper.isRenderObsolete()) return;
            await pointCloudHelper.renderingContext.initialize();
            if (pointCloudHelper.isRenderObsolete()) return;

            const dataCanvas = document.createElement("canvas");
            dataCanvas.width = depthMapImage.naturalWidth;
            dataCanvas.height = depthMapImage.naturalHeight;
            const dataContext = dataCanvas.getContext("2d");

            dataContext.drawImage(depthMapImage, 0, 0);

            if (pointCloudHelper.isRenderObsolete()) return;

            const imageData = dataContext.getImageData(
               0,
               0,
               dataCanvas.width,
               dataCanvas.height
            ).data;

            dataContext.drawImage(textureImage, 0, 0);

            if (pointCloudHelper.isRenderObsolete()) return;

            const textureData = dataContext.getImageData(
               0,
               0,
               dataCanvas.width,
               dataCanvas.height
            ).data;

            const vertices = [];
            const vertexColors = [];

            const maxDimension = Math.max(dataCanvas.width, dataCanvas.height);
            /*
            const aspectWidth = dataCanvas.width / dataCanvas.height;
            const aspectHeight = dataCanvas.height / dataCanvas.width;
            */

            for (let x = 0; x < dataCanvas.width; x++) {
               for (let y = 0; y < dataCanvas.height; y++) {
                  const index = (y * dataCanvas.width + x) * 4;

                  const r = textureData[index + 0];
                  const g = textureData[index + 1];
                  const b = textureData[index + 2];

                  if (r !== 0 || g !== 0 || b !== 0) {
                     const xC = (x / maxDimension - 0.5) * 100;
                     const yC = (1 - y / maxDimension - 0.75) * 100;
                     const zC = (imageData[index] / 255) * 100 * depthFactor;

                     vertices.push(xC, yC, zC);
                     vertexColors.push(r / 255, g / 255, b / 255);
                  }
               }
               if (pointCloudHelper.isRenderObsolete()) return;
            }

            PointCloudHelper.vertices = vertices;
            // TODO Better design
            resolve(vertices);

            pointCloudHelper.renderingContext.geometry.setAttribute(
               "position",
               new THREE.Float32BufferAttribute(vertices, 3)
            );
            pointCloudHelper.renderingContext.geometry.setAttribute(
               "color",
               new THREE.Float32BufferAttribute(vertexColors, 3)
            );

            if (pointCloudHelper.isRenderObsolete()) return;

            pointCloudHelper.renderingContext.geometry.attributes.position.needsUpdate = true;
            pointCloudHelper.renderingContext.geometry.attributes.color.needsUpdate = true;

            pointCloudHelper.renderingContext.render();

            pointCloudHelper.renderingContext.handleResize();
         }, 100);
      });
   }

   static async downloadOBJ() {
      // TODO Better design.
      const vertices = PointCloudHelper.vertices;

      if (vertices.length > 3) {
         await new Promise((resolve) => {
            setTimeout(() => {
               const filename = "point_cloud.obj";
               let objString = "";

               for (
                  let i = 0, vertexCount = vertices.length;
                  i < vertexCount;
                  i += 3
               ) {
                  const x = vertices[i];
                  const y = vertices[i + 1];
                  const z = vertices[i + 2];
                  objString += "v " + x + " " + y + " " + z + "\n";
               }

               let element = document.createElement("a");
               element.style.display = "none";

               let blob = new Blob([objString], {
                  type: "text/plain; charset = utf-8",
               });

               let url = window.URL.createObjectURL(blob);
               element.setAttribute("href", window.URL.createObjectURL(blob));
               element.setAttribute("download", filename);

               document.body.appendChild(element);

               element.click();

               window.URL.revokeObjectURL(url);
               element.remove();

               resolve();
            });
         });
      }
   }

   /**
    * @public
    */
   static cancelRenderJobs() {
      PointCloudHelper.renderId++;
   }

   /**
    * @public
    * @param {HTMLCanvasElement} canvas
    */
   static clearCanvas(canvas) {
      const pointCloudHelperRenderingContext =
         PointCloudHelperRenderingContext.getInstance(canvas);

      pointCloudHelperRenderingContext.geometry.setAttribute(
         "position",
         new THREE.Float32BufferAttribute([], 3)
      );
      pointCloudHelperRenderingContext.geometry.setAttribute(
         "color",
         new THREE.Float32BufferAttribute([], 3)
      );

      pointCloudHelperRenderingContext.geometry.attributes.position.needsUpdate = true;
      pointCloudHelperRenderingContext.geometry.attributes.color.needsUpdate = true;

      pointCloudHelperRenderingContext.render();
   }

   /**
    * @private
    * @param {HTMLCanvasElement} renderCanvas
    */
   constructor(renderCanvas) {
      this.renderId = PointCloudHelper.renderId;

      this.renderingContext =
         PointCloudHelperRenderingContext.getInstance(renderCanvas);
   }

   /**
    * @private
    * @returns {boolean}
    */
   isRenderObsolete() {
      return this.renderId < PointCloudHelper.renderId;
   }
}
PointCloudHelper.renderId = 0;

/** @type {PointCloudHelperRenderingContext[]} */
const PointCloudHelperRenderingContext_instances = [];

class PointCloudHelperRenderingContext {
   /**
    * @public
    * @param {HTMLCanvasElement} renderCanvas
    * @returns {PointCloudHelperRenderingContext}
    */
   static getInstance(renderCanvas) {
      for (
         let i = 0;
         i < PointCloudHelperRenderingContext_instances.length;
         i++
      ) {
         const testInstance = PointCloudHelperRenderingContext_instances[i];
         if (testInstance.renderCanvas === renderCanvas) {
            const instance = testInstance;
            return instance;
         }
      }

      const instance = new PointCloudHelperRenderingContext(renderCanvas);
      return instance;
   }

   /**
    * @private
    * @param {HTMLCanvasElement} renderCanvas
    */
   constructor(renderCanvas) {
      this.initialized = false;
      this.renderCanvas = renderCanvas;

      this.renderer = new THREE.WebGLRenderer({
         canvas: renderCanvas,
         alpha: true,
         antialias: true,
      });
      this.camera = new THREE.PerspectiveCamera(
         50,
         renderCanvas.width / renderCanvas.height,
         0.01,
         1000
      );

      // @ts-ignore
      this.controls = new THREE.OrbitControls(this.camera, this.renderCanvas);
      this.scene = new THREE.Scene();
      this.geometry = new THREE.BufferGeometry();
      this.material = new THREE.PointsMaterial({
         size: 2,
         vertexColors: true,
      });
      this.pointCloud = new THREE.Points(this.geometry, this.material);

      this.pointCloud.rotateX(35 * (Math.PI / 180));
      this.pointCloud.translateY(15);

      PointCloudHelperRenderingContext_instances.push(this);

      if (
         PointCloudHelperRenderingContext_instances.length >
         PointCloudHelperRenderingContext.MAX_INSTANCES
      ) {
         console.warn(
            "PointCloudHelperRenderingContext exceeded maximum render canvas instance count. The last instance gets deleted."
         );
         PointCloudHelperRenderingContext_instances.shift();
      }
   }

   async render() {
      await this.initialize();
      this.renderer.render(this.scene, this.camera);
   }

   /**
    * @public
    */
   async initialize() {
      if (this.initialized) {
         return;
      }
      await new Promise((resolve) => {
         setTimeout(() => {
            this.scene.add(this.pointCloud);

            this.camera.position.z = 75;
            this.camera.position.y = -100;

            this.controls.target = new THREE.Vector3(0, 0, 0);

            this.initialized = true;
            resolve();

            this.controls.addEventListener("change", () => {
               this.render();
            });
            window.addEventListener("resize", this.handleResize.bind(this));

            this.controls.update();
            this.handleResize();
         });
      });
   }

   /**
    * @public
    */
   handleResize() {
      const width = this.renderCanvas.clientWidth;
      const height = this.renderCanvas.clientHeight;
      const needResize =
         this.renderCanvas.width !== width ||
         this.renderCanvas.height !== height;
      if (needResize) {
         this.renderer.setSize(width, height);

         this.camera.aspect =
            this.renderCanvas.width / this.renderCanvas.height;
         this.camera.updateProjectionMatrix();
         this.render();
      }
   }
}

/** @type {number[]} */
PointCloudHelper.vertices = [];

/** @constant */
PointCloudHelperRenderingContext.MAX_INSTANCES = 8;

ecd72448843bffb473efb8a8e4a2e4ec69834e2b

y-flipping?

y-flipping?

eslint-disable-next-line no-unused-vars

// TODO y-flipping?

/* global GLSL */
/* exported depthMap */

/**
 * @global
 * @typedef {{x: number, y: number}} Pixel
 * @typedef {{x: number, y: number, slope: number}} LinePixel
 * @typedef {LinePixel[]} PixelLine
 */
class DepthMapHelper {
   /**
    * This functions calculates a depth mapping by a given
    * normal mapping.
    *
    * @public
    * @param {ImageBitmap} normalMap The normal mapping
    * that is used to calculate the depth mapping.
    * @param {number} qualityPercent The quality in percent
    * defines how many anisotropic integrals are taken into
    * account to archive a higher quality depth mapping.
    * @returns {Promise<ImageBitmap>} A depth mapping
    * according to the input normal mapping.
    */
   static async depthMap(normalMap, qualityPercent) {
      console.time("calculate depth mapping");
      const startTime = performance.now();

      const maximumThreadCount = Math.pow(2, 8);
      const dimensions = {
         width: normalMap.width,
         height: normalMap.height,
      };
      const maximumAngleCount = dimensions.width * 2 + dimensions.height * 2;
      const angleCount = Math.round(maximumAngleCount * qualityPercent);
      const azimuthalAngles = [];

      for (let frac = 1; frac < angleCount; frac *= 2) {
         for (let angle = 0; angle < 360; angle += 360 / frac) {
            if (!azimuthalAngles.includes(angle)) {
               azimuthalAngles.push(angle);
               azimuthalAngles.push(180 + angle);
            }
         }
      }

      const gradientPixelArray = await DepthMapHelper.getLocalGradientFactor(
         normalMap
      );

      const anglesCount = azimuthalAngles.length;

      let promisesResolvedCount = 0;
      /** @type {number[]} */
      const integral = new Array(dimensions.width * dimensions.height).fill(0);

      const edgeFramePixels = DepthMapHelper.getEdgeFramePixels(dimensions);

      for (let angleIndex = 0; angleIndex < anglesCount; angleIndex++) {
         while (angleIndex - promisesResolvedCount >= maximumThreadCount) {
            await new Promise((resolve) => {
               setTimeout(resolve, Math.random() * 1000);
            });
         }

         new Promise((resolve) => {
            let azimuthalAngle = azimuthalAngles[angleIndex];

            // Inverse and thus, line FROM and NOT TO azimuthal angle.
            azimuthalAngle += 180;
            const azimuthalAngleInRadians = azimuthalAngle * (Math.PI / 180);

            const stepVector = {
               x: Math.cos(azimuthalAngleInRadians),
               y: Math.sin(azimuthalAngleInRadians),
            };

            const minimumStep = 0.00000001;

            if (Math.abs(stepVector.x) < minimumStep) {
               stepVector.x = 0;
            }
            if (Math.abs(stepVector.y) < minimumStep) {
               stepVector.y = 0;
            }

            for (
               let framePixelIndex = 0,
                  edgeFramePixelsCount = edgeFramePixels.length;
               framePixelIndex < edgeFramePixelsCount;
               framePixelIndex++
            ) {
               const startPixel = edgeFramePixels[framePixelIndex];

               const stepOffset = {
                  x: startPixel.x,
                  y: startPixel.y,
               };

               const pixel = {
                  x: startPixel.x,
                  y: startPixel.y,
               };

               const nextPixel = { x: pixel.x, y: pixel.y };

               let inDimensions;
               let integralValue = 0;

               do {
                  do {
                     stepOffset.x += stepVector.x;
                     stepOffset.y += stepVector.y;
                     nextPixel.x = Math.round(stepOffset.x);
                     nextPixel.y = Math.round(stepOffset.y);
                  } while (nextPixel.x === pixel.x && nextPixel.y === pixel.y);

                  pixel.x = nextPixel.x;
                  pixel.y = nextPixel.y;
                  inDimensions =
                     pixel.x < dimensions.width &&
                     pixel.y < dimensions.height &&
                     pixel.x >= 0 &&
                     pixel.y >= 0;

                  if (inDimensions) {
                     // TODO y-flipping?
                     const index =
                        pixel.x +
                        (dimensions.height - 1 - pixel.y) * dimensions.width;
                     const colorIndex =
                        (pixel.x + pixel.y * dimensions.width) * 4;

                     let pixelSlope = 0;

                     if (gradientPixelArray[colorIndex + 2] !== 0) {
                        const rightSlope =
                           gradientPixelArray[colorIndex + 0] +
                           DepthMapHelper.SLOPE_SHIFT;
                        const topSlope =
                           gradientPixelArray[colorIndex + 1] +
                           DepthMapHelper.SLOPE_SHIFT;

                        pixelSlope =
                           stepVector.x * rightSlope + stepVector.y * topSlope;
                     }

                     integralValue -= pixelSlope;
                     integral[index] += integralValue;
                  }
               } while (inDimensions);
            }
            resolve();
         }).then(async () => {
            promisesResolvedCount++;

            const percent = (promisesResolvedCount / anglesCount) * 100;
            //nodeCallback.setProgressPercent(percent);

            const ETA =
               ((performance.now() - startTime) / promisesResolvedCount) *
               (anglesCount - promisesResolvedCount);

            let ETAsec = String(Math.floor((ETA / 1000) % 60));
            const ETAmin = String(Math.floor(ETA / (60 * 1000)));

            if (ETAsec.length < 2) {
               ETAsec = "0" + ETAsec;
            }

            const etaString = "ETA in " + ETAmin + ":" + ETAsec + " min";
            console.log(percent + " " + etaString);
         });
      }

      while (promisesResolvedCount < anglesCount) {
         await new Promise((resolve) => {
            setTimeout(resolve, 500);
         });
      }

      const normalizedIntegral =
         await DepthMapHelper.getNormalizedIntegralAsGrayscale(
            integral,
            dimensions
         );

      const depthMap = await DepthMapHelper.getDepthMapImage(
         normalizedIntegral,
         dimensions
      );

      console.timeEnd("calculate depth mapping");
      return depthMap;
   }

   /**
    * @deprecated
    */
   constructor() {}

   /**
    * @private
    * @param {Uint8ClampedArray} integral
    * @param {{width:number, height:number}} dimensions
    * @returns {Promise<ImageBitmap>}
    */
   static async getDepthMapImage(integral, dimensions) {
      if (Math.min(dimensions.width, dimensions.height) > 0) {
         const dim = new Uint32Array(2);
         dim[0] = dimensions.width;
         dim[1] = dimensions.height;
         const canvas = new OffscreenCanvas(dim[0], dim[1]);
         const context = canvas.getContext("2d");

         canvas.width = dimensions.width;
         canvas.height = dimensions.height;

         const imageData = new ImageData(
            integral,
            dimensions.width,
            dimensions.height
         );

         context.putImageData(imageData, 0, 0);

         return createImageBitmap(canvas);
      }
   }

   /**
    * @private
    * @param {number[]} integral
    * @param {{width:number, height:number}} dimensions
    * @returns {Promise<Uint8ClampedArray>}
    */
   static async getNormalizedIntegralAsGrayscale(integral, dimensions) {
      return new Promise((resolve) => {
         setTimeout(() => {
            const colorImageArrayLength =
               dimensions.width * dimensions.height * 4;
            const normalizedIntegral = new Uint8ClampedArray(
               new ArrayBuffer(colorImageArrayLength)
            );

            let min = Number.MAX_VALUE;
            let max = Number.MIN_VALUE;

            integral.forEach((value) => {
               if (value > max) max = value;
               if (value < min) min = value;
            });

            const maxMinDelta = max - min;

            integral.forEach((value, index) => {
               const normalizedValue = ((value - min) * 255) / maxMinDelta;
               const colorIndex = index * 4;
               normalizedIntegral[colorIndex + 0] = normalizedValue;
               normalizedIntegral[colorIndex + 1] = normalizedValue;
               normalizedIntegral[colorIndex + 2] = normalizedValue;
               normalizedIntegral[colorIndex + 3] = 255;
            });

            resolve(normalizedIntegral);
         });
      });
   }

   /**
    * @private
    * @param {ImageBitmap} normalMap
    * @returns {Promise<Uint8Array>}
    */
   static getLocalGradientFactor(normalMap) {
      return new Promise((resolve) => {
         setTimeout(() => {
            const depthMapShader = new GLSL.Shader({
               width: normalMap.width,
               height: normalMap.height,
            });
            depthMapShader.bind();

            const glslNormalMap = GLSL.Image.load(normalMap);
            const red = glslNormalMap.channel(0);
            const green = glslNormalMap.channel(1);
            const blue = glslNormalMap.channel(2);
            const result = new GLSL.Vector3([
               red.divideFloat(blue),
               green.divideFloat(blue),
               blue.minimum(red, green),
            ]);

            const gradientPixelArray = GLSL.render(
               result.getVector4()
            ).getPixelArray();

            resolve(gradientPixelArray);

            depthMapShader.purge();
         });
      });
   }

   /**
    * @private
    * @param {{width:number, height:number}} dimensions
    * @returns {Pixel[]}
    */
   static getEdgeFramePixels(dimensions) {
      /** @type {Pixel[]} */
      const edgeFramePixels = [];

      const topY = -1;
      const bottomY = dimensions.height;
      const leftX = -1;
      const rightX = dimensions.width;

      for (let x = 0; x < dimensions.width; x++) {
         edgeFramePixels.push({ x: x, y: topY });
         edgeFramePixels.push({ x: x, y: bottomY });
      }
      for (let y = 0; y < dimensions.height; y++) {
         edgeFramePixels.push({ x: leftX, y: y });
         edgeFramePixels.push({ x: rightX, y: y });
      }

      return edgeFramePixels;
   }
}

/** @constant */
DepthMapHelper.SLOPE_SHIFT = -255 / 2;

// @ts-ignore
// eslint-disable-next-line no-unused-vars
const depthMap = DepthMapHelper.depthMap;

465f3d77d7af7bd9ebb393971efdbb45ecae476f

Fix initial update and thus, remove second call.

Fix initial update and thus, remove second call.

// TODO Fix initial update and thus, remove second call.

);

inputOrCalculationTypeChange();
// TODO Fix initial update and thus, remove second call.
setTimeout(inputOrCalculationTypeChange, 1000);

Array.from(
   DOM_ELEMENT.PIPELINE_AREA.getElementsByClassName("userInput")

3beeb2e78b74d47457cf63354a1ee047dd778baf

Check if kernel is quadratic.

Check if kernel is quadratic.

// TODO Check if kernel is quadratic.

    * @returns {GlslVector4}
    */
   applyFilter(kernel, normalize = false) {
      // TODO Check if kernel is quadratic.

      let filtered = new GlslVector4([
         new GlslFloat(0),
         new GlslFloat(0),
         new GlslFloat(0),
         new GlslFloat(0),
      ]);

      if (normalize) {
         let kernelSum = 0;

ad1d0b1bbd216c32c5744e16bfeeb9da2c669d86

fix alpha

fix alpha

// TODO: fix alpha

/* global GLSL */
/* exported NormalMapHelper */

/**
 * @global
 */
class NormalMapHelper {
   /**
    * @public
    * @param {number} lightPolarAngleDeg
    * @param {HTMLImageElement} lightImage_000
    * @param {HTMLImageElement} lightImage_045
    * @param {HTMLImageElement} lightImage_090
    * @param {HTMLImageElement} lightImage_135
    * @param {HTMLImageElement} lightImage_180
    * @param {HTMLImageElement} lightImage_225
    * @param {HTMLImageElement} lightImage_270
    * @param {HTMLImageElement} lightImage_315
    * @param {HTMLImageElement} lightImage_NONE
    * @param {HTMLImageElement} uiImageElement
    * @param {number} resolutionPercent
    * @param {boolean} cameraVerticalShift
    * @param {number} maskThresholdPercent
    * @returns {Promise<HTMLImageElement>}
    */
   static async calculatePhotometricStereoNormalMap(
      lightPolarAngleDeg,
      lightImage_000,
      lightImage_045,
      lightImage_090,
      lightImage_135,
      lightImage_180,
      lightImage_225,
      lightImage_270,
      lightImage_315,
      lightImage_NONE = undefined,
      uiImageElement = undefined,
      resolutionPercent = 100,
      cameraVerticalShift = false,
      maskThresholdPercent = 5
   ) {
      const maskThreshold = maskThresholdPercent / 100;

      const normalMapHelper = new NormalMapHelper();

      return new Promise((resolve) => {
         setTimeout(async () => {
            if (
               lightImage_000.naturalWidth < 1 ||
               lightImage_045.naturalWidth < 1 ||
               lightImage_090.naturalWidth < 1 ||
               lightImage_180.naturalWidth < 1 ||
               lightImage_225.naturalWidth < 1 ||
               lightImage_270.naturalWidth < 1 ||
               lightImage_315.naturalWidth < 1
            )
               return;

            if (normalMapHelper.isRenderObsolete()) return;

            const normalMapShader = new GLSL.Shader({
               width: lightImage_000.naturalWidth * (resolutionPercent / 100),
               height: lightImage_000.naturalHeight * (resolutionPercent / 100),
            });

            if (normalMapHelper.isRenderObsolete()) return;

            normalMapShader.bind();

            const lightLuminances = [
               GLSL.Image.load(lightImage_000).getLuminance(),
               GLSL.Image.load(lightImage_045).getLuminance(),
               GLSL.Image.load(lightImage_090).getLuminance(),
               GLSL.Image.load(lightImage_135).getLuminance(),
               GLSL.Image.load(lightImage_180).getLuminance(),
               GLSL.Image.load(lightImage_225).getLuminance(),
               GLSL.Image.load(lightImage_270).getLuminance(),
               GLSL.Image.load(lightImage_315).getLuminance(),
            ];

            const all = new GLSL.Float(0).maximum(...lightLuminances);

            let mask = new GLSL.Float(1);

            if (
               lightImage_NONE &&
               Math.min(
                  lightImage_NONE.naturalWidth,
                  lightImage_NONE.naturalHeight
               ) > 0
            ) {
               const lightLuminance_NONE =
                  GLSL.Image.load(lightImage_NONE).getLuminance();

               for (let i = 0; i < lightLuminances.length; i++) {
                  lightLuminances[i] =
                     lightLuminances[i].subtractFloat(lightLuminance_NONE);
               }

               mask = all
                  .subtractFloat(lightLuminance_NONE)
                  .step(new GLSL.Float(maskThreshold));
            }

            for (let i = 0; i < lightLuminances.length; i++) {
               lightLuminances[i] = lightLuminances[i].divideFloat(all);
            }

            /**
             * @param {GLSL.Float} originLuminance
             * @param {GLSL.Float} orthogonalLuminance
             * @param {GLSL.Float} oppositeLuminance
             * @param {number} originAzimuthalAngleDeg
             * @param {number} orthogonalAzimuthalAngleDeg
             * @param {number} oppositeAzimuthalAngleDeg
             * @returns {GLSL.Vector3}
             */
            function getAnisotropicNormalVector(
               originLuminance,
               orthogonalLuminance,
               oppositeLuminance,

               originAzimuthalAngleDeg,
               orthogonalAzimuthalAngleDeg,
               oppositeAzimuthalAngleDeg
            ) {
               if (normalMapHelper.isRenderObsolete()) return;

               /**
                * @param {number} azimuthalAngleDeg
                * @param {number} polarAngleDeg
                * @returns {GLSL.Vector3}
                */
               function getLightDirectionVector(
                  azimuthalAngleDeg,
                  polarAngleDeg
               ) {
                  const polar = new GLSL.Float(polarAngleDeg).radians();
                  const azimuthal = new GLSL.Float(azimuthalAngleDeg).radians();

                  return new GLSL.Vector3([
                     polar.sin().multiplyFloat(azimuthal.cos()),
                     polar.sin().multiplyFloat(azimuthal.sin()),
                     polar.cos(),
                  ]).normalize();
               }

               const originLightDirection = getLightDirectionVector(
                  originAzimuthalAngleDeg,
                  lightPolarAngleDeg
               );
               const orthogonalLightDirection = getLightDirectionVector(
                  orthogonalAzimuthalAngleDeg,
                  lightPolarAngleDeg
               );
               const oppositeLightDirection = getLightDirectionVector(
                  oppositeAzimuthalAngleDeg,
                  lightPolarAngleDeg
               );

               const lightMatrix = new GLSL.Matrix3([
                  [
                     originLightDirection.channel(0),
                     originLightDirection.channel(1),
                     originLightDirection.channel(2),
                  ],
                  [
                     orthogonalLightDirection.channel(0),
                     orthogonalLightDirection.channel(1),
                     orthogonalLightDirection.channel(2),
                  ],
                  [
                     oppositeLightDirection.channel(0),
                     oppositeLightDirection.channel(1),
                     oppositeLightDirection.channel(2),
                  ],
               ]).inverse();

               const reflection = new GLSL.Vector3([
                  originLuminance,
                  orthogonalLuminance,
                  oppositeLuminance,
               ]);

               return lightMatrix
                  .multiplyVector3(reflection)
                  .normalize()
                  .addFloat(new GLSL.Float(1))
                  .divideFloat(new GLSL.Float(2));
            }

            /** @type {number[][]} */
            const anisotropicCombinations = [
               [180, 270, 0],
               [180, 90, 0],
               [90, 180, 270],
               [90, 0, 270],
               [225, 315, 45],
               [225, 135, 45],
               [315, 45, 135],
               [315, 225, 135],
            ];

            /** @type {GLSL.Vector3[]} */
            const normalVectors = [];

            anisotropicCombinations.forEach((combination) => {
               /**
                * @param {number} azimuthalAngle
                * @returns {GLSL.Float}
                */
               function getLightLuminance(azimuthalAngle) {
                  const lightAzimuthalAngles = [
                     0, 45, 90, 135, 180, 225, 270, 315,
                  ];
                  const id = lightAzimuthalAngles.findIndex((value) => {
                     return value === azimuthalAngle;
                  });

                  return lightLuminances[id];
               }

               normalVectors.push(
                  getAnisotropicNormalVector(
                     getLightLuminance(combination[0]),
                     getLightLuminance(combination[1]),
                     getLightLuminance(combination[2]),
                     combination[0],
                     combination[1],
                     combination[2]
                  )
               );
            });

            let normalVector = new GLSL.Vector3([
               new GLSL.Float(0),
               new GLSL.Float(0),
               new GLSL.Float(0),
            ])
               .addVector3(...normalVectors)
               .divideFloat(new GLSL.Float(normalVectors.length))
               .normalize()
               .multiplyFloat(mask);

            if (normalMapHelper.isRenderObsolete()) return;

            if (cameraVerticalShift) {
               // TODO: use cameraVerticalShift
               /*const cameraAngle = Math.atan(
                  1 / Math.tan(lightPolarAngleDeg * (Math.PI / 180))
               );

               const zero = new GLSL.Float(0);
               const one = new GLSL.Float(1);
               const sine = new GLSL.Float(Math.sin(cameraAngle));
               const cosine = new GLSL.Float(Math.cos(cameraAngle));

               const rotationMatrix = new GLSL.Matrix3([
                  [one, zero, zero],
                  [zero, cosine, sine],
                  [zero, sine.multiplyFloat(new GLSL.Float(-1)), cosine],
               ]);

               normalVector = rotationMatrix.multiplyVector3(normalVector);*/
            }

            // TODO: fix alpha
            const alpha = normalVector
               .channel(0)
               .minimum(normalVector.channel(1), normalVector.channel(2))
               .multiplyFloat(new GLSL.Float(99999))
               .minimum(new GLSL.Float(1));

            const normalMapRendering = GLSL.render(
               normalVector.getVector4(alpha)
            );

            if (normalMapHelper.isRenderObsolete()) return;

            const normalMap = await normalMapRendering.getJsImage();

            resolve(normalMap);

            if (uiImageElement && normalMap) {
               uiImageElement.src = normalMap.src;
            }

            normalMapShader.purge();
         }, 100);
      });
   }

   /**
    * @public
    * @param {HTMLImageElement} lightImage_000
    * @param {HTMLImageElement} lightImage_090
    * @param {HTMLImageElement} lightImage_180
    * @param {HTMLImageElement} lightImage_270
    * @param {HTMLImageElement} lightImage_ALL
    * @param {HTMLImageElement} lightImage_FRONT
    * @param {HTMLImageElement} lightImage_NONE
    * @param {HTMLImageElement} uiImageElement
    * @param {number} resolutionPercent
    * @returns {Promise<HTMLImageElement>}
    */
   static async calculateSphericalGradientNormalMap(
      lightImage_000,
      lightImage_090,
      lightImage_180,
      lightImage_270,
      lightImage_ALL,
      lightImage_FRONT,
      lightImage_NONE = undefined,
      uiImageElement = undefined,
      resolutionPercent = 100
   ) {
      const normalMapHelper = new NormalMapHelper();

      if (normalMapHelper.isRenderObsolete()) return;

      return new Promise((resolve) => {
         setTimeout(async () => {
            const normalMapShader = new GLSL.Shader({
               width: lightImage_000.naturalWidth * (resolutionPercent / 100),
               height: lightImage_000.naturalHeight * (resolutionPercent / 100),
            });
            normalMapShader.bind();

            let lightLuminance_ALL =
               GLSL.Image.load(lightImage_ALL).getLuminance();

            const lightLuminances = [
               GLSL.Image.load(lightImage_000).getLuminance(),
               GLSL.Image.load(lightImage_090).getLuminance(),
               GLSL.Image.load(lightImage_180).getLuminance(),
               GLSL.Image.load(lightImage_270).getLuminance(),
               GLSL.Image.load(lightImage_FRONT).getLuminance(),
            ];

            if (lightImage_NONE) {
               const lightLuminance_NONE =
                  GLSL.Image.load(lightImage_NONE).getLuminance();

               for (let i = 0; i < lightLuminances.length; i++) {
                  lightLuminances[i] =
                     lightLuminances[i].subtractFloat(lightLuminance_NONE);
               }
               lightLuminance_ALL =
                  lightLuminance_ALL.subtractFloat(lightLuminance_NONE);
            }

            for (let i = 0; i < lightLuminances.length; i++) {
               lightLuminances[i] =
                  lightLuminances[i].divideFloat(lightLuminance_ALL);
            }

            const horizontal = lightLuminances[0]
               .subtractFloat(lightLuminances[2])
               .addFloat(new GLSL.Float(1))
               .divideFloat(new GLSL.Float(2));

            const vertical = lightLuminances[3]
               .subtractFloat(lightLuminances[1])
               .addFloat(new GLSL.Float(1))
               .divideFloat(new GLSL.Float(2));

            const normalVector = new GLSL.Vector3([
               horizontal,
               vertical,
               lightLuminances[4],
            ]);

            if (normalMapHelper.isRenderObsolete()) return;

            const normalMapRendering = GLSL.render(
               normalVector.normalize().getVector4()
            );

            normalMapShader.purge();

            const normalMap = await normalMapRendering.getJsImage();

            if (uiImageElement && normalMap) {
               uiImageElement.src = normalMap.src;
            }

            resolve(normalMap);
         });
      });
   }

   /**
    * @public
    */
   static cancelRenderJobs() {
      NormalMapHelper.renderId++;
   }

   /**
    * @private
    */
   constructor() {
      this.renderId = NormalMapHelper.renderId;
   }

   /**
    * @private
    * @returns {boolean}
    */
   isRenderObsolete() {
      return this.renderId < NormalMapHelper.renderId;
   }

   /**
    * @public
    * @param {HTMLImageElement} normalMap
    * @param {HTMLImageElement} groundTruthImage
    * @returns {Promise<number>}
    */
   static async getDifferenceValue(normalMap, groundTruthImage) {
      const differenceImage = await NormalMapHelper.getDifferenceMap(
         normalMap,
         groundTruthImage
      );

      const width = differenceImage.width;
      const height = differenceImage.height;

      const imageCanvas = document.createElement("canvas");
      imageCanvas.width = width;
      imageCanvas.height = height;
      const imageContext = imageCanvas.getContext("2d");
      imageContext.drawImage(differenceImage, 0, 0, width, height);
      const imageData = imageContext.getImageData(0, 0, width, height).data;

      let differenceValue = 0;
      for (let x = 0; x < width - 1; x++) {
         for (let y = 0; y < height - 1; y++) {
            const index = (x + y * width) * 4;
            const localDifference = imageData[index] / 255;
            differenceValue += localDifference;
         }
      }
      differenceValue /= width * height;

      return differenceValue;
   }

   /**
    * @public
    * @param {HTMLImageElement} normalMap
    * @param {HTMLImageElement} groundTruthImage
    * @returns {Promise<HTMLImageElement>}
    */
   static async getDifferenceMap(normalMap, groundTruthImage) {
      return new Promise((resolve) => {
         const differenceShader = new GLSL.Shader({
            width: normalMap.width,
            height: normalMap.height,
         });
         differenceShader.bind();

         const normalImage = GLSL.Image.load(normalMap);
         const groundTruthShaderImage = GLSL.Image.load(groundTruthImage);

         let normal = new GLSL.Vector3([
            normalImage.channel(0),
            normalImage.channel(1),
            normalImage.channel(2),
         ]);

         let groundTruth = new GLSL.Vector3([
            groundTruthShaderImage.channel(0),
            groundTruthShaderImage.channel(1),
            groundTruthShaderImage.channel(2),
         ]);

         const zeroAsErrorSummand = new GLSL.Float(1).subtractFloat(
            normal.length().step().divideFloat(groundTruth.length().step())
         );

         groundTruth = groundTruth.normalize();

         const differenceAngle = normal
            .dot(groundTruth)
            .acos()
            .abs()
            .addFloat(zeroAsErrorSummand);

         normal = normal.normalize();

         const differenceMap = new Image();
         differenceMap.addEventListener("load", () => {
            resolve(differenceMap);
         });
         differenceMap.src = GLSL.render(
            new GLSL.Vector4([
               differenceAngle,
               differenceAngle,
               differenceAngle,
               new GLSL.Float(1),
            ])
         ).getDataUrl();

         differenceShader.purge();
      });
   }
}

NormalMapHelper.renderId = 0;

df9c54e4722be7fb8d3b77dbf9a06bb78498e1ab

fix alpha

fix alpha

// TODO: fix alpha

               normalVector = rotationMatrix.multiplyVector3(normalVector);*/
            }

            // TODO: fix alpha
            const alpha = normalVector
               .channel(0)
               .minimum(normalVector.channel(1), normalVector.channel(2))

73563dc7d2d7f5e37050edcac223b5b31114f900

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.