Immersive presentations with HTML & WebGL

While preparing for a talk I gave at Reject.js 2014, I decided that I wanted to embed my WebGL demos directly into the presentation itself, to give a more fluid and interesting talk.

If you take a quick look at the title slide, you’ll be able to see the result, full-screen 3D content running in a presentation, accessible to anyone with a WebGL-enabled browser. You can advance using the arrow keys.

WebGL presentation title slide

In this post I’ll give details of how something like this can be put together.

Making a HTML presentation

Before we can embed WebGL content into a presentation, we need a presentation. Obviously.

I’ve been using shower recently, which is a pretty nifty presentation enginethat let’s you create your slides in HTML, like this:

<section class="slide"><div>
  <h2>Why WebGL?</h2>
  <ul>
    <li>Accessible 3D for all</li>
    <li>Support across browsers and mobile increasing all the time</li>
    <li>Combination of JavaScript &amp; GLSL very effective</li>
  </ul>
  <p>Some examples: <a href="http://threejs.org">http://threejs.org</a></p>
  <img class="right" src="pictures/sidebar.png" alt="">
</div></section>

…and get out slides looking like this:

WebGL presentation example slide

Adding WebGL content

Now that we have our presentation up and running, we can add some WebGL. The example that I embedded is of a 3D model I built of the Safari logo.

Adding this is reasonably straightforward. In the original example, the 3D visuals get populated into a <div> with a specific id, all that is required to add it to the presentation is to load the JavaScript for the example, and add a <div> to the slide where the embedding takes place.

However, this doesn’t scale – what we want is for many slides to have some example embedded in them, and ideally for them to share resources, so that each slide wouldn’t start with an ugly loading screen. Adding a bunch of <div>s won’t work as the WebGL code only expects to have a single place to render to, and it seems wasteful to create multiple rendering targets (one per slide) when only a single one is ever shown at a time.

Detaching and re-attaching

The solution is pretty simple: we create a single canvas element that contains our WebGL demo, and as we advance through the slides we detach it from the slide that it was on previously and attach it to the slide we are about to show.

Shower doesn’t give us events to trigger off, however we can use shower.getCurrentSlideNumber() to find the current slide number. Given that on most slides we are already executing some sort of animation loop for the WebGL example, it’s not a problem to always check whether the slide has changed on every frame.

Shower also adds matching CSS ids to each slide, so we can grab the <section> element for the corresponding slide from JavaScript.

Combining this with some CSS selectors for picking out the canvas element from the old slide and locating the element to inject it into the new slide, we can now declaratively add a WebGL example to as many slides as necessary:

<section class="slide"><div>
  <h2>Geometry - Sphere</h2>
    <code>new THREE.SphereGeometry( 1, 32, 32 );</code></br>
    <div class="threejs-container medium">Loading...</div>
</div></section>

Matching the WebGL content to the slide

The demo I use has a basic API for interacting with it, for example, to switch to displaying the model in wireframe mode, you can do app.wireframe = true;. To synchronize the slides and the demo, we can just update the demo every time the slide changes, e.g.

  var slideNumber = shower.getCurrentSlideNumber();
  if ( slideNumber !== lastSlideNumber ) {
    // Have changed slide
    if ( slideNumber === 1 ) {
      app.wireframe = false;
    }
  }

WebGL presentation wireframe

Styling with CSS

One gotcha I hit with sharing the WebGL canvas in this way, is that the canvas container size changes between slides.

I use a library, THREE.js to do the rendering, and this was not notified of this change and as such the rendering results were off. To resolve this, I added a utility function, renderer.setContainer() to the renderer class which let me update the DOM container that WebGL canvas is a child of. Using this, the renderer could reset the its size and aspect ratio.

This function is invoked whenever the slide is changed, to make sure things display correctly.

Hover effects

Finally, I wanted to allow users to expand a demo on a slide, from a thumbnail like this…

WebGL presentation thumbnail

…into a fullscreen version like this:

WebGL presentation full

The most intuitive way to do this is to provide an on hover CSS rule on the thumbnail, like so:

.small:hover {
    position: absolute;
    right: 2%;
    bottom: 3%;
    height: 94%;
    width: 96%;

This just expands the demo to fill pretty much the whole slide, whenever the user hovers over the thumbnail.

As before, the slight gotcha is that we need to update the renderer every time this happens, but this is achieved fairly simply with a listener:

var onHover = function() {
  renderer.setContainer( this );
};
container.addEventListener( 'mouseover', onHover );
container.addEventListener( 'mouseout', onHover );

That’s all

All in all I was quite happy with how well all this performed, and not having to switch tabs to show a demo mid-presentation definitely made giving the talk easier. If you’d like to use my presentation as reference, the code is up on Github.