Intro to (images in) Go – UI

This post is part of a series. For a listing of all the posts, as well as instructions on running the code, see here.

A few posts back, we looked at how to set up a simple HTTP server and also how to communicate with a web page using WebSockets. Today we’ll combine this with the fractal generation code we looked at last time to make an application, with the backend powered by Go, while the frontend is displayed using web technologies.

Here’s what we’ll wind up with, if all goes well:
Fractal app

Why do this?

While Go was originally meant as a systems programming language, people have started using it for other things. However the standard library does not include a UI layer. Using HTML/CSS/JavaScript gives us the flexibility to deploy on any platform that can run a web browser, and gives us the flexibility to separate our client from the server, if that model fits our use case better.

Missing pieces

It turns out that the code in the WebSockets post almost did everything we needed to make a functional application. The two bits missing were:

  • A way to send messages from UI layer back to the Go application
  • A sane way of sending large images to the client. Pixel-by-pixel just isn’t going to cut it

Fractal 1

Talking to Go

With our WebSockets transport up and running, sending a message from the client is dead simple:

ws.send( "READY" );

While in our handler on the Go end, we’d receive this like so:

func UIHandler(ws *websocket.Conn) {
  for {
    var msg string
    websocket.Message.Receive(ws, &msg)
    fmt.Println("Message:", msg)
  }
}

Our application is going to display a fractal image and let us zoom in and out, re-center the image and change the gradient. For each of these, we’ll send a command to the server and it will generate a new fractal for us, which we will then display.

Once we’ve got one of these functions working, the others will be easy, and follow the same pattern.

Building the UI

For now, let’s focus on zooming. As before we’ll use a Canvas tag to display the fractal image. To control zooming we’ll have two buttons, making our HTML look something like this:

<body>
    <h2>Fractal Explorer</h2>
    <canvas id="canvas" width="512" height="512">
        Sorry, your browser does not support Canvas
    </canvas>
    <hr>
    <button id="zoomin">Zoom in</button>
    <button id="zoomout">Zoom out</button>
</body>
</html> 

To actually make our buttons do something, we’ll need to add listeners to them using JavaScript, e.g. zooming in:

var zoomInBtn = document.getElementById( "zoomin" );
zoomInBtn.addEventListener( "click", function() {
  ws.send( "ZOOMIN" );
});

Then on the Go end, we’ll need to handle this message, so we modify our handler to:

func UIHandler(ws *websocket.Conn) {
  for {
    var msg string
    websocket.Message.Receive(ws, &msg)

    // Parse message
    fields := strings.Fields(msg)
    cmd := fields[0]
    if cmd == "ZOOMIN" {
      zoom *= 2.0
    }

    // Draw fractal and send to client
  }
}

OK great, we can now control our Go code with a click of a button. How about drawing that fractal?

Fractal drawing

The fractal post gave us a function to draw a fractal based on some parameters:

drawInvMandelbrot(canvas, zoom, center, colorizer)

I actually re-factored the code a bit and moved it into a separate file, to make it easier to use from different projects. So given a canvas, we can use this function to draw a fractal at an arbitrary zoom level, center and gradient.

Fractal 2

All that remains is sending the fractal to the client. Previously, we sent images pixel by pixel, using a command like PIXEL 4 5 #ff0000, which worked fine for plotting graphs, but was slow for a large image. Based on a suggestion by cryptix, I’ve instead opted for sending the image encoded as a base64 string, and then drawing it onto the Canvas in one go. To do this, I’ve added a method to the Canvas (in Go) class:

func (c *Canvas) ToBase64() string {
  imgBuf := new(bytes.Buffer)
  imgEncoder := base64.NewEncoder(base64.StdEncoding, imgBuf)
  png.Encode(imgEncoder, c)
  imgEncoder.Close()
  return imgBuf.String()
}

With this in place we can now send the entire contents of our Canvas in Go to our JavaScript frontend:

drawInvMandelbrot(canvas, zoom, center, colorizer)
drawMsg := fmt.Sprintf("DRAW %s", canvas.ToBase64())
io.WriteString(ws, drawMsg)

Notice, that rather than using the PIXEL command, we’ve added a new DRAW command. To make our client understand this new command and to draw the image to the HTML Canvas, we’ll do:

ws.onmessage = function( e ) {
  var data = e.data.split("\n");
  for ( var line in data ) {
    var msg = data[line].split(" ");
    var cmd = msg[0];
    // ...other commands
    } else if ( cmd == "DRAW" ) {
      var imgData = msg[1];
      var img = new Image();
      img.src = "data:image/png;base64," + imgData;
      ctx.drawImage( img, 0, 0 );
    }
  }
}

Capturing mouse clicks

So now we have the full loop, the app displays a fractal, the user clicks a button, the server receives this input, draws a new fractal and sends it back to the client, ready for the virtuous circle to begin again.

Adding more functionality is now more or less repeating what we’ve already done. For changing the gradient used, we’ll add a few buttons, add click handlers and send the relevant message to the server for processing.

Re-centering the fractal is a little different, so I’ll pull it out specifically here. Rather than using a simple on click handler, we’ll want to detect where the user has clicked in the HTML Canvas:

canvas.addEventListener( "mousedown", function(e) {
  ws.send( "MOUSEDOWN " + e.offsetX + " " + e.offsetY );
});

Which will send a message of the format MOUSEDOWN 4 12 when the user clicks on the Canvas 4 pixels from the left, and 12 from the top. As this will arrive as a string on the Go end, we’ll want to parse integers using the strconv package:

fields := strings.Fields(msg)
cmd := fields[0]
// ...other commands
} else if cmd == "MOUSEDOWN" {
  x, _ := strconv.ParseInt(fields[1], 10, 0)
  y, _ := strconv.ParseInt(fields[2], 10, 0)
  // ...

Fractal 3

Finished

And that’s more or less it. To play around with the full app, check out the source on github for the server, as well as the client. To run the app, execute go run ws_fractals.go mandelbrot.go canvas.go vector.go, and then navigate to http://localhost:1234/ui.html in your browser.

The app we built today was designed for displaying fractals, but because the web client is so thin, the same code could be reused for other applications, by modifying the Go code, but keeping the web client more or less the same.

For example, you could switch out the gradient images for thumbnails of pictures, and you’d have an image browsing application. Or rather than just capturing mousedown events, we could also capture when the mouse moves, and use this to draw on the Canvas.

Finally, by splitting our business and display logic like we have, we can style our app using CSS without worrying that the Go backend will be affected.

  • James Garfield

    Excellent series of posts! I was interested in playing around with the source, but itt doesn’t look like ws_fractal.go or ui.html made it into github. Is it possible that github is behind your local branch?

    • pheelicks

      You’re completely right, I forgot to push the branch with the new code. Should be there now.