首页 > 解决方案 > 如何在不冻结浏览器的情况下一次一个节点地在 ASCII 艺术网格中“动画化”变化?

问题描述

我有一个 ASCII 艺术“寻路可视化器”,我正在根据这里看到的一个流行的模型来建模。ASCII 艺术显示一个 m 大小的板,上面有 n*m 个节点。

我目前的目标是逐个字符地慢慢改变面向用户的板上文本的外观,直到“动画”完成。我打算通过寻路算法对节点的“扫描”和从开始节点到结束节点的最短路径进行动画处理。只是在一系列 div 中更改文本的动画应该需要几秒钟。我还计划添加带有颜色或其他内容的 CSS 动画。

基本上用户最终会看到这样的东西,其中 * 是开始节点,x 是结束节点,+ 表示路径:

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

在对 setTimeout、promises和其他选项进行 了一些 研究之后,我可以告诉你:

JavaScript 的设计并不是为了让某人延迟浏览器中的代码执行。

我找到了很多冻结浏览器的方法。我还尝试设置一系列承诺设置为在setTimeout(resolve, milliseconds)发生后解决,其中milliseconds稳步增加(见下面的代码)。我的期望是,numOfAnimationsForPath当每个承诺解决(形成路径的外观)时,将设置承诺的数量并触发董事会外观的变化。但是,它们似乎都立即解决了(?),因为我一点击“动画”按钮就会看到路径显示

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"
    }
}

在我的另一次尝试中,我尝试生成s 的pathLength数量,setTimeout如下所示:

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))
    }
}

...但这也导致路径立即“动画化”,而不是像我想要的那样超过几秒钟。

我相信我想要的是可能的,因为在帖子开头链接的 Pathfinding Visualizer 会缓慢地为其板设置动画,但我无法弄清楚如何使用文本来做到这一点。

所以基本上:

如果有人知道我如何说服我的浏览器发送一个增加延迟值的一系列函数执行,我会全神贯注......

如果您认为无法完成,我也想在评论中知道这一点,这样我就知道我必须选择一个替代方案来慢慢更改文本。

编辑:一位朋友告诉我 setTimeout 应该能够做到...如果我想通了,我会用解决方案更新这个

Edit2:这是@torbinsky 代码的修改版本,它最终为我完成了这项工作......

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()
}

谢谢@torbinsky!

标签: javascripthtml

解决方案


这应该绝对可以使用setTimeout. 问题可能是您立即注册了 10,000 次超时。你的路径越长,这种方法就越糟糕。

因此,您不应立即安排所有更新,而应使用递归算法,其中每个“帧”安排下一帧的超时。像这样的东西:

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)
}

注意:我在 StackOverflow 代码片段中编写了此代码。所以我无法测试它,因为我没有你的其余代码来完全运行它。把它当作伪代码对待,即使它可能有效;)

无论如何,我使用的方法是在任何给定时间只安排 1 次超时。这样,您就不会同时安排 1000 次超时使浏览器过载。这种方法将支持很长的路径!


推荐阅读