Using double buffering to get live updating math with MathJax

One very important topic to Jon and myself is making math look good everywhere we need to render it. This is reasonably straight-forward with static math (with some unfortunate caveats) but what about math that updates in response to user action?

For this post the example we'll be looking at is very simple. It's just a text field and a div. The desired behavior is that as the user types AsciiMath [LINK] expressions into the box, the updates are rendered live into the div. Here’s the HTML snippet for that:

<div class="container">
  <p>
    Type some ASCIIMath code:<br /> 
    <input oninput="UpdateMath(this.value)" />
  </p>

  <p>You typed:</p>
  <div>
    <div id="MathOutput">` `</div>
  </div>
</div>

By default, Mathjax just renders the math present on the page at the time when mathjax loads. We need to explicitly ask mathjax to render the new math when it updates.

Let's take a look at what a naive implementation of dynamically rendering the user's input looks like.

<script>
  const HUB = MathJax.Hub;  // shorthand for the hub
  let math;

  //
  //  Get the element jax when MathJax has produced it.
  //
  HUB.Queue(function () {
    math = HUB.getAllJax("MathOutput")[0];
  });

  //
  //  The naive oninput event handler that queues MathJax to
  //  queue the rendering of the new Math.
  //
  window.UpdateMath = function (asciiMathText) {
    HUB.Queue(["Text", math, asciiMathText]);
  }
</script>

This is hard to make look good. The annoyance of the popping effect depicted in the link above is magnified about 1000 fold when it's happening live as you type.

Type some ASCIIMath code:

You typed:

` `

How can we improve on this?

Obviously this is not an acceptable solution. But what is causing this and what can we do to improve it?

The problem is that by default, most configurations of mathjax do a fast preview pass before rendering the full version. If you can believe it, turning fast preview off entirely makes it look even worse since the math disappears altogether between updates:

0001.gif

Essentially, what we want to do is tell mathjax to render the new math without getting rid of the last math.

As the title suggests, we're going to be using a technique from computer graphics called double buffering which does just that.

Double buffering

In computer graphics, you have two buffers which represent the screen. While one buffer is being shown to the users, the computer draws the next frame into the other buffer. Once it's done drawing, it can quickly swap the new buffer to the screen providing the user with a seamless experience.

Getting back to our previous example, our two “buffers” in this case, are going to be two divs that we’ll ask Mathjax to render into. So in terms of the structure of the HTML, this is what that looks like:

<div class="container">
  <p>
    Type some ASCIIMath code:<br /> 
    <input 
        id="MathInput" 
        oninput="UpdateMath(this.value)"
    />
  </p>

  <p>You typed:</p>
  <div class="box" id="box0" style="display:none">
    <div id="MathOutput0" class="output">` `</div>
  </div>
  <div class="box" id="box1" style="display:none">
    <div id="MathOutput1" class="output">` `</div>
  </div>
</div>

When one div is presented to the user and the user updates with field, we ask mathjax to render the new math into the hidden div. Then once mathjax indicates it's done rendering we hide the one div and show the newly rendered div.

Here's the updated code:

<script>
  const HUB = MathJax.Hub;  // shorthand for the hub
  const NUM_BUFFERS = 2;
  let maths = [], box = [], hidden = 0;

  //
  //  Show the requested buffer.
  //
  const SHOW = (idx) => {
    box[idx].style.display = "block";
    hidden = (idx + 1) % NUM_BUFFERS;
    box[hidden].style.display = "none";
  }

  //
  //  Queue some initialization to run when MathJax is done
  //  initializing.
  //
  HUB.Queue(() => {
    for (let idx = 0; idx < NUM_BUFFERS; idx++) {
      maths[idx] = HUB.getAllJax("MathOutput" + idx)[0];
      box[idx] = document.getElementById("box" + idx);
    }
    SHOW(hidden); // box is initially hidden
  });
//
  //  The oninput event handler that typesets the math entered
  //  by the user.  Render's the requested math into the hidden
  //  buffer with a callback to show the div with the newly
  //  rendered math (and hide the other one) when it's done.
  //
  window.UpdateMath = (amath) => {
    HUB.Queue(["Text", maths[hidden], amath, [SHOW, hidden]]);
    hidden = (hidden + 1) % NUM_BUFFERS;
  }

</script>

And this is how it looks:

Type some ASCIIMath code:

You typed:

Future improvements

Using double buffering was a big improvement for the interactivity of the page. One further enhancement I'm thinking about is rendering the math as you type, for example, how to get the math to render in the text field itself.

But for now, the irritation factor is low enough I wouldn't object to having this on the site.

Rest assured, if I figure out how to implement a text field like Desmos’, I'll be back with another post!

Matthew KellerComment