1

I have an ASCII art "pathfinding visualizer" which I am modeling off of a popular one seen here. The ASCII art displays a n by m size board with n*m number of nodes on it.

My current goal is to slowly change the appearance of the text on the user-facing board, character by character, until the "animation" is finished. I intend to animate both the "scanning" of the nodes by the pathfinding algorithm and the shortest path from the start node to the end node. The animation, which is just changing text in a series of divs, should take a few seconds. I also plan to add a CSS animation with color or something.

Basically the user ends up seeing something like this, where * is the start node, x is the end node, and + indicates the path:

....
..*.
..+.
.++.
.x..
.... (. represents an empty space)

After doing some research on both setTimeout, promises and other options, I can tell you:

JavaScript really isn't designed to allow someone to delay code execution in the browser.

I've found lots of ways to freeze the browser. I also tried to set a series of promises set to resolve after setTimeout(resolve, milliseconds) occurs, where the milliseconds steadily increases (see below code). My expectation was that numOfAnimationsForPath number of promises would be set and trigger a change in the appearance of the board when each one resolved (forming the appearance of a path). But, they all seem to resolve instantly (?) as I see the path show as soon as I click the "animate" button

const shortestPathAndScanningOrder = dijkstras(grid);
promisesRendering(1000, shortestPathAndScanningOrder[0].path, shortestPathAndScanningOrder[1])

function promisesRendering(animationDelay, algoPath, scanTargets) {
    const numOfAnimationsForScanning = scanTargets.length;
    const numOfAnimationsForPath = algoPath.length;

    for (let i = 1; i < numOfAnimationsForPath - 1; i++) {
        const xCoordinate = algoPath[i][0];
        const yCoordinate = algoPath[i][1];

        renderAfterDelay(animationDelay * i).then(renderNode(xCoordinate, yCoordinate, "path"))
    }
}

function renderAfterDelay(milliseconds) {
    return new Promise(resolve => setTimeout(resolve, milliseconds))
}

function renderNode(x, y, type) {
    if (type === "scan") {
        const targetDiv = getLocationByCoordinates(x, y);
        targetDiv.innerHTML = VISITED_NODE;
    } else if (type === "path") {
        const targetDiv = getLocationByCoordinates(x, y);
        targetDiv.innerHTML = SHORTEST_PATH_NODE;
    } else {
        throw "passed incorrect parameter to 'type' argument"
    }
}

In my other attempt, I tried to generate pathLength number of setTimeouts as in:

const shortestPathAndScanningOrder = dijkstras(grid);
    renderByTimer(10000, shortestPathAndScanningOrder[0].path, shortestPathAndScanningOrder[1])

function renderByTimer(animationDelay, algoPath, scanTargets) {
    const numOfAnimations = algoPath.length;

    for (let i = 1; i < numOfAnimations - 1; i++) {
        const xCoordinate = algoPath[i][0];
        const yCoordinate = algoPath[i][1];
        setTimeout(i * animationDelay, updateCoordinatesWithTrailMarker(xCoordinate, yCoordinate))
    }
}

...but this also resulted in the path being "animated" instantly instead of over a few seconds as I want it to be.

I believe what I want is possible because the Pathfinding Visualizer linked at the start of the post animates its board slowly, but I cannot figure out how to do it with text.

So basically:

If anyone knows how I can convince my browser to send an increasing delay value a series of function executions, I'm all ears...

And if you think it can't be done, I'd like to know that too in the comments, just so I know I have to choose an alternative to changing the text slowly.

edit: a friend tells me setTimeout should be able to do it... I'll update this w/ a solution if I figure it out

Edit2: Here is the modified version of @torbinsky's code that ended up doing the job for me...

function renderByTimer(algoPath, scanTargets) {
    const numOfAnimations = algoPath.length - 1; // - 1 because we don't wanna animate the TARGET_NODE at the end
    let frameNum = 1;

    // Renders the current frame and schedules the next frame
    // This repeats until we have exhausted all frames
    function renderIn() {
        if (frameNum >= numOfAnimations) {
            // end recursion
            console.log("Done!")
            return
        }

        // Immediately render the current frame
        const xCoordinate = algoPath[frameNum][0];
        const yCoordinate = algoPath[frameNum][1];
        frameNum = frameNum + 1;
        updateCoordinatesWithTrailMarker(xCoordinate, yCoordinate);

        // Schedule the next frame for rendering
        setTimeout(function () {
            renderIn(1000)
        }, 1000);
    }
    // Render first frame
    renderIn()
}

Thanks @torbinsky!

Roly Poly
  • 349
  • 4
  • 14

2 Answers2

1

This should absolutely be doable using setTimeout. Probably the issue is that you are immediately registering 10,000 timeouts. The longer your path, the worse this approach becomes.

So instead of scheduling all updates right away, you should use a recursive algorithm where each "frame" schedules the timeout for the next frame. Something like this:

const shortestPathAndScanningOrder = dijkstras(grid);
    renderByTimer(10000, shortestPathAndScanningOrder[0].path, shortestPathAndScanningOrder[1])

function renderByTimer(animationDelay, algoPath, scanTargets) {
    const numOfAnimations = algoPath.length;

    // Renders the current frame and schedules the next frame
    // This repeats until we have exhausted all frames
    function renderIn(msToNextFrame, frameNum){
        if(frameNum >= numOfAnimations){
            // end recursion
            return
        }

        // Immediately render the current frame
        const xCoordinate = algoPath[frameNum][0];
        const yCoordinate = algoPath[frameNum][1];
        updateCoordinatesWithTrailMarker(xCoordinate, yCoordinate);

        // Schedule the next frame for rendering
        setTimeout(msToNextFrame, function(){
            renderIn(msToNextFrame, frameNum + 1)
        });
    }
    // Render first frame
    renderIn(1000, 1)
}

Note: I wrote this code in the StackOverflow code snipppet. So I was not able to test it as I did not have the rest of your code to fully run this. Treat it more like pseudo-code even though it probably works ;)

In any case, the approach I've used is to only have 1 timeout scheduled at any given time. This way you don't overload the browser with 1000's of timeouts scheduled at the same time. This approach will support very long paths!

torbinsky
  • 1,330
  • 9
  • 17
1

This is a general animation technique and not particularly unique to ASCII art except that old-school ASCII art is rendered one (slow) character at a time instead of one fast pixel frame at a time. (I'm old enough to remember watching ASCII "movies" stream across hard-wired Gandalf modems at 9600bps to a z19 terminal from the local mainframe...everything old is new again! :) ).

Anyhow, queueing up a bunch of setTimeouts is not really the best plan IMO. What you should be doing, instead, is queueing up the next event with either window.requestAnimationFrame or setTimeout. I recommend rAF because it doesn't trigger when the browser tab is not showing.

Next, once you're in the event, you look at the clock delta (use a snapshot of performance.now()) to figure what should have been drawn between "now" and the last time your function ran. Then update the display, and trigger the next event.

This will yield a smooth animation that will play nicely with your system resources.

Wes
  • 875
  • 5
  • 12