UI support

Create a user interface for our WebGL code to handle mouse events and make choices.

Creating a UI for our WebGL program

The user interface is a fairly simple HTML page, providing a canvas, some directions, and a few buttons. This article describes the events and code that supports the HTML.

UI mouse support

There are a number of UI support functions in the Warp example. The main UI that needs to be handled is the mouse support that controls the stretch and distortion of the photo. A set of handlers are created for the mousedown, mouseup, mouseout and mousemove events when the program first starts.

var inputHandler = new function() {
    var move; // Pointer to a uniform variable in the renderer object

    this.init = function () {
        var canvas = document.getElementById("2dcanvas");
      //  Set up mouse events on the canvas object. 
        canvas.onmousedown = function (e) {
          console.log("onmousedown");
          this.move = renderer.newMove(getMousePoint(e));            
        }

        canvas.onmouseup = function (e) {
          console.log("onmouseup");
            this.move = undefined;
            renderer.render();
        }

        canvas.onmouseout = function (e) {
          console.log("onmouseout");
          this.move = undefined;
            renderer.render();
        };

        canvas.onmousemove = function (e) {
          console.log("onmousemove");

          var point = getMousePoint(e);

            if (typeof this.move != 'undefined')
            {
                if (typeof point != 'undefined')
                {
                    this.move.move(point);
                }
                renderer.render();
            }

        };

These events track the coordinates of the mouse movement. When the mousedown event fires, it calls the newMove function with the results of the getMousePoint function. The newMove function is called with getMousePoint as a parameter to capture the latest mouse move.

// (point) is where the mouse was clicked
this.newMove = function(point) // Where the warp starts (-1 to 1 range)
{
    var move = new Move(point);
    // Adds move to beginning of moves array (pushes onto array)
    moves.unshift(move);

    return move;
}

The getMousePoint function calls the normalizedPoint function, which takes the pixel-based values that the mouse returns and converts them to a -1 to 1 coordinate system for WebGL. The normalized x and y coordinates are then returned to newMove, which pushes the values onto the moves[] array.

// getMousePoint
// input - mouse event e
function getMousePoint(e) {
    var x;
    var y;
    // The standard way to get mouse coordinates
    if (e.offsetX) {
        x = e.offsetX;
        y = e.offsetY;
    }
    // LayerX and layerY are provided for cross-browser compatibility
    else if (e.layerX) {
        x = e.layerX;
        y = e.layerY;
    }
    else {
        return undefined; //Work around Chrome
    }

    return normalizedPoint(x, y); // Converts pixels to -1 to 1
}
function normalizedPoint(x, y) 
{
  // converts screen coordinates to -1 to 1
  var canvas = document.getElementById("2dcanvas");
    x = (x / canvas.width) * 2 - 1;
    y = (1 - (y / canvas.height)) * 2 - 1;
    
    return new Point(x, y);
}

Under the photo are buttons to open a photo, undo a step at a time, start over, or save the image.

The Open a photo button uses a little style trick. When the button is pressed, the style for the button is set to style.display="none", and a <span> element is set to style.display="inline" so it's displayed. The <span> contains an input type=file element and a prompt to pick a file. The input type=file element displays a browse button which brings up the system's file picker. When you pick a file and click open from the file dialog box, the selected file is returned. The HTML5 FileReader object then reads the file. The Open a photo button is set back to style.display="inline", and the span display is set to style.display="none".

function handleFileSelect(evt) {

  document.getElementById("openphoto1").style.display = "inline";
  document.getElementById("openphoto2").style.display = "none";
  
  var files = evt.target.files; // FileList object
  
  // files is a FileList of File objects. List some properties.
  var file = files[0];
  
  var reader = new FileReader();
  
  // Closure to capture the file information.
  reader.onload = function (e) {
      renderer.loadImageX(this.result);
  };
  
  // Read in the image file as a data URL.
  
  reader.readAsDataURL(file);
}

document.getElementById('files').addEventListener('change', handleFileSelect, false);


function OpenPhoto1() {
  document.getElementById("openphoto1").style.display = "none";
  document.getElementById("openphoto2").style.display = "inline";
}

The Undo and Reset buttons control the contents of the moves[] array, which tracks the last 10 mouse points. Undo pops the last point off the array using the JavaScript shift method. Reset just clears the moves[] array. Both functions then call the render function to redisplay the image.

this.reset = function () {
    moves = [];
    this.render();
}

this.undo = function () {
    // Removes the first element in moves array (pops off array)
    moves.shift();
    this.render();
}

Saving the file

The Save button saves your masterpiece to the downloads folder in Internet Explorer 11. When you click the button, it calls the save function, which in turn calls the renderer member function save(). The save function initially converts the canvas image to a png format dataURL using the toDataURL method. The dataURL string is then converted to a blob, and finally saved using the saveBlob method. If blob isn't supported, the image/png mime type recorded into the initial dataURL is substituted with image/octet-stream and returned to the browser as an href. With this, browsers that don't support the saveBlob method can still save the image.

    this.save = function () {
    // First create a dataURL string from the canvas in jpeg format.
      var dataURL = document.getElementById("webglcanvas").toDataURL("image/png");

      // Split the dataURL and decode it from ASCII to base-64 binary.
      var binArray = atob(dataURL.split(',')[1]);

      // Create an 8-bit unsigned array
      var array = [];
    // Add the unicode numeric value of each element to the new array.
      for (var i = 0; i < binArray.length; i++) {
        array.push(binArray.charCodeAt(i));
      }

      var blobObject = new Blob([new Uint8Array(array)], { type: 'image/png' }); 

      if (window.navigator.msSaveBlob) {
        window.navigator.msSaveBlob(blobObject, 'warpedphoto.png');
      }else if (window.navigator.saveBlob) {
        window.navigator.saveBlob(blobObject, 'warpedphoto.png');
      }
      else {
        dataURL = dataURL.replace("image/png", "image/octet-stream");
        window.location.href = dataURL;
        // alert("Sorry, your browser does not support navigator.saveBlob");
      }
  }

In IE11, your image is saved to the download folder with the name warpedphoto.png for the first save, followed by warpedphoto (i).png, where i is a sequential number.

Showing uniforms over the WebGL image

If you check the Show uniform points check box, when you drag your mouse over the grid or photo, it shows two dots and a line connecting them. This is the approximate location of the uniform points (p1, p2 in earlier code). It's a straight 2D canvas rendering overlaid onto the WebGL content. This is the purpose of the second canvas that was mentioned in WebGL context and setup. We're not going to describe how it works as it's standard canvas 2D code, but here it is. For more information on canvas and the 2D rendering context, see CanvasRenderingContext2D and associated reference content.

<!-- The show uniform points checkbox -->
   <label><input type="checkbox" name="showUniforms" id="showUniforms" onchange="renderer.changeMode();renderer.render()" />Show uniform points</label> 
  // This code checks the Show uniform points checkbox and changes the mode and initializes the canvas.
this.modeOff = 0;
this.modeHint = 1;
this.modeHint2 = 2;
this.modeUniform = 3;

  //  Modes tell the app to show uniforms or not
this.canvasMode = this.modeHint;

this.changeMode = function () {
    if (document.getElementById("showUniforms").checked) {
        this.canvasMode = this.modeUniform;
    }
    else {
        this.canvasMode = this.modeOff;
        var canvas = document.getElementById("2dcanvas");
        var ctx = canvas.getContext("2d");
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

}
    // Draw uniform points 
if (this.canvasMode == this.modeUniform) {
      var canvas = document.getElementById("2dcanvas");
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);

  for (var i = 0; i < MAXMOVES; i++) {
    if (moves[i]) {
      var x1 = (moves[i].point1.x + 1) * canvas.width / 2;
      var y1 = (-moves[i].point1.y + 1) * canvas.height / 2;
      var x2 = (moves[i].point2.x + 1) * canvas.width / 2;
      var y2 = (-moves[i].point2.y + 1) * canvas.height / 2;

  // The raio is used here to show where the pixel started and ended
      var ratio = 0.3;
      x2 = x1 + (x2 - x1) * ratio;
      y2 = y1 + (y2 - y1) * ratio;

      var radius = 6;            
      ctx.beginPath();  // Start a fresh path 

  // Create a 2D gradient 
      var grd = ctx.createLinearGradient(x1, y1, x2, y2);
      grd.addColorStop(0, 'pink'); // Set one side to pink
      grd.addColorStop(1, 'red');  // The other side to red

      ctx.setLineDash([5, 5]);  // Use a dotted line
      ctx.lineWidth = radius / 2; 
      ctx.moveTo(x1, y1); // Create a line from start to end poing
      ctx.lineTo(x2, y2);
      ctx.strokeStyle = grd;
      ctx.stroke();

      ctx.beginPath(); // Start a new path for pink dot
      ctx.arc(x1, y1, radius, 0, 2 * Math.PI, false); // full circle (2*pi)
      ctx.fillStyle = 'pink';
      ctx.fill();

      ctx.beginPath(); // Start a new path for red dot
      ctx.arc(x2, y2, radius, 0, 2 * Math.PI, false);
      ctx.fillStyle = 'red';
      ctx.fill();
    }
  }
}

Where to now?

In WebGL demos, references, and resources, you'll find a collection of references to help you learn more about WebGL, libraries, and 3D graphics. You can also see the full listing of the example code we've used here.

WebGL demos, references, and resources