2D graphics and animation with time-travel debugging in JavaScript

Leporello.js now allows programming 2D graphics and animation using the Canvas 2D API and debugging them with time-travel debugging.

Check out a short demo:

The beautiful tree image in the video is generated by concise and elegant JavaScript code, borrowed from the excellent article Recursive Drawing by Sarah Bricault. The tree is drawn recursively - each branch is drawn by the same function, with only the parameters of size, color, and tilt changing.

The code runs in the Leporello.js environment. At the top, you see the code editor. At the bottom, you see the Call tree view, displaying the function call tree of the program. You can navigate the call tree view by clicking with the mouse or using the keyboard arrows. By selecting a specific function call, you travel back in time to the moment of that call. You can inspect the values of function arguments, local variables, and all intermediate expressions at the chosen call moment. Meanwhile, the 2D canvas displays the image as it looked at the time of the function call.

You can try this example online here. After clicking the link, press the "Re(open) app window" button:

Open app window

After clicking the button, a new browser tab will open, where the image with the tree will be rendered. You can detach this tab into a separate window and position it on your monitor as you prefer. Then, click on the calltree elements with the mouse or navigate them with the keyboard arrows, and observe how the tree image is drawn as the program executes!

Time-travel using console.log

You may notice that before drawing a branch of the tree, we call the console.log function:

console.log('draw branch', x1, y1, x2, y2)

The output of the console.log function is displayed at the bottom in the Logs panel:

Logs view

By clicking on the log lines, you move to the corresponding console.log function call in the editor. Simultaneously, the canvas image rolls back to the moment of the console.log function call. Holding and pressing the up or down arrow on the keyboard quickly moves through console.log calls, and the displayed image changes accordingly. You can move both forward and backward:

Animating drawing

Now, let's modify the code a bit to make the drawing not instant but with a small delay before each segment. Add the async modifier to the branch drawing function and call it using the await keyword. Additionally, add a sleep function and call it every time after drawing a segment:

function sleep() {
  return new Promise(resolve => setTimeout(resolve, 3))
}

//...

await sleep()

Thus, we will get such an animation as in the video below. You can try this example yourself by following the link:

Note that debugging the program using the calltree view remains unchanged compared to the previous example with no animation. Leporello can visualize the calltree for both synchronous and asynchronous code.

To restart the animation, refresh (Ctrl-R) the window where the application is running.

Animation using setInterval

Leporello.js also allows debugging animations in a time-travel manner. For JavaScript animation, the setInterval(callback, delay) function is used, which repeatedly calls the callback function after a delay.

Consider an example borrowed from the freeCodeCamp website. You can try this example yourself here:

In the calltree view, you can see a section called deferred calls. These are function calls that were not the result of module execution but occurred due to the browser's event loop in response to events.

In our example, we see in the deferred calls list the createCircles function call in response to a canvas click, followed by the animate function call, triggered by the setInterval function. You can navigate forward and backward through the deferred calls list with keyboard arrows or mouse clicks. The canvas image rolls back to the selected call moment:

Please note that the example code is structured so that the animation stops when the focus leaves the window. This is done to avoid consuming a large amount of memory. If the animation plays for too long, redrawing while navigating the calltree view can become very slow. Leporello.js does not know that the animation consists of individual frames, and each frame starts with a complete canvas clearing. Therefore, Leporello.js has to reapply all drawing operations from the first frame to the selected frame inclusively.

Time-travel debugging and mutable data

Leporello.js incorporates a time-travel engine that allows inspecting data at a selected point in time. Let's demonstrate how this works in the above animation example. Select the first createCircles function call in the deferred calls list and move to the editor. Look at the code creating bubbles:

circles = circles.concat(Array.from({length: 50}, () => (
   {
    x: event.pageX,
    y: event.pageY,
    radius: Math.random() * 50,
    dx: Math.random() * 0.3,
    dy: Math.random() * 0.7,
    hue: 200,
  }
)))

By selecting with the Ctrl-ArrowDown shortcut the circles identifier to the right of the assignment =, you see an empty array (the initial value of the circles variable). By selecting the circles identifier to the left of the =, you see the next value (an array of 50 elements):

Programming education

Graphics and animation are excellent ways to teach programming. Leporello.js allows visualizing program execution at a new level. For example, recursive drawing illustrates how recursion works. This makes Leporello.js an excellent environment for teaching programming.

Towards next-level debugging tools

The 2D graphics example demonstrates how a time-travel debugger can be integrated with external stateful systems. Any external stateful system can be connected to the debugger, similar to how canvas is currently connected. You can read more about how Leporello.js changes our views on program debugging in the article Can Debugging Be Liberated from the von Neumann Style?.

Back to the blog's table of contents