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:
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!
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:
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:
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.
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.
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):
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.
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?.