You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

p5.js绘图应用油漆桶工具递归栈溢出问题的解决办法

解决递归洪水填充的栈溢出问题,非递归实现完全可行

递归版的洪水填充在处理大面积区域时,很容易因为调用栈深度超过浏览器上限触发Maximum Call Stack Size Exceeded错误——改成非递归实现不仅可行,还是这类场景的标准解决方案,下面直接给你修复方案和优化后的代码:

核心问题分析

递归实现的每一次调用都会占用调用栈空间,当填充区域覆盖几百上千个像素时,递归深度会迅速超出浏览器的栈容量(通常在1000-10000之间),直接触发溢出错误。用栈/队列模拟递归过程,就能彻底避开这个问题。

修复&优化后的代码

function BucketTool(){
    var self = this;
    self.icon = "assets/bucket.jpg";
    self.name = "Bucket";
    var d = pixelDensity();
    
    var oldColor;
    var currentColor;
    var searchDirections = [[1,0],[-1,0],[0,1],[0,-1]];
    // 用二维数组记录已访问像素,替代原数组的indexOf查询(O(1)效率)
    var visited;
    var pixelsToFill = [];

    // 初始化visited数组
    function initVisited() {
        visited = new Array(height);
        for(let y = 0; y < height; y++){
            visited[y] = new Array(width).fill(false);
        }
    }
    
    self.checkBoundary = function(currentX, currentY, localOldColor) {
        // 先判断坐标是否在画布范围内,再检查颜色和是否已访问
        if (currentX < 0 || currentY < 0 || currentX >= width || currentY >= height) {
            return false;
        }
        if (visited[currentY][currentX]) {
            return false;
        }
        const currentPixel = self.getPixelAtXYPosition(currentX, currentY);
        // 优化颜色对比:直接比较RGB值,避免toString()的性能损耗
        return currentPixel[0] === localOldColor[0] && 
               currentPixel[1] === localOldColor[1] && 
               currentPixel[2] === localOldColor[2] && 
               currentPixel[3] === localOldColor[3];
    };
    
    // 非递归版洪水填充(深度优先,和原递归逻辑一致)
    self.floodFill = function(startX, startY, localOldColor) {
        const stack = [[startX, startY]];
        visited[startY][startX] = true;

        while(stack.length > 0){
            const [currentX, currentY] = stack.pop();
            // 标记为已访问并加入待绘制列表
            pixelsToFill.push([currentX, currentY]);

            // 遍历四个方向
            for (let i = 0; i < searchDirections.length; i++){
                const newX = currentX + searchDirections[i][0];
                const newY = currentY + searchDirections[i][1];
                if(self.checkBoundary(newX, newY, localOldColor)){
                    visited[newY][newX] = true;
                    stack.push([newX, newY]);
                }
            }
        }
    };
    
    self.getPixelAtXYPosition = function(x, y) {
        var colour = [];
        for (var i = 0; i < d; i++) {
          for (var j = 0; j < d; j++) {
            index = 4 * ((y * d + j) * width * d + (x * d + i));
            colour[0] = pixels[index];
            colour[1] = pixels[index+1];
            colour[2] = pixels[index+2];
            colour[3] = pixels[index+3];
          }
        }
        return colour;
    }
    
    self.drawTheNeededPixels = function(){
        // 直接操作pixels数组,比多次调用point()效率高
        loadPixels();
        for(let i = 0; i < pixelsToFill.length; i++){
            const [x, y] = pixelsToFill[i];
            for (let i = 0; i < d; i++) {
                for (let j = 0; j < d; j++) {
                    const index = 4 * ((y * d + j) * width * d + (x * d + i));
                    pixels[index] = currentColor[0];
                    pixels[index+1] = currentColor[1];
                    pixels[index+2] = currentColor[2];
                    pixels[index+3] = currentColor[3];
                }
            }
        }
        updatePixels();
    }
    
    self.draw = function () {
        if(mouseIsPressed){
            pixelsToFill = [];
            initVisited(); // 每次点击重置访问记录
            
            loadPixels();
            oldColor = self.getPixelAtXYPosition(mouseX, mouseY);
            // 获取当前选中的绘图颜色
            currentColor = [red(color()), green(color()), blue(color()), alpha(color())];
            
            self.floodFill(mouseX, mouseY, oldColor);
            self.drawTheNeededPixels();
        }
    };
}

关键优化点说明

  1. 非递归实现:用栈(Stack)模拟递归调用,所有待处理的像素坐标都存在栈中,循环处理直到栈为空,彻底避免栈溢出。
  2. 访问记录优化:用二维数组visited替代原代码的pixelsToFill.indexOf(),查询效率从O(n)提升到O(1),大幅提升大区域填充的性能。
  3. 颜色对比优化:直接对比RGBa数组的每个值,避免toString()带来的性能损耗和潜在的字符串匹配错误。
  4. 绘制效率优化:直接操作p5.js的pixels数组批量更新颜色,再调用updatePixels(),比多次调用point()效率高得多。

可选:广度优先实现

如果想要填充顺序从点击点向外扩散(和递归的深度优先顺序不同,但最终填充效果一致),只需要把栈改成队列,用shift()取出元素即可:

// 非递归广度优先版本
self.floodFill = function(startX, startY, localOldColor) {
    const queue = [[startX, startY]];
    visited[startY][startX] = true;

    while(queue.length > 0){
        const [currentX, currentY] = queue.shift();
        pixelsToFill.push([currentX, currentY]);

        for (let i = 0; i < searchDirections.length; i++){
            const newX = currentX + searchDirections[i][0];
            const newY = currentY + searchDirections[i][1];
            if(self.checkBoundary(newX, newY, localOldColor)){
                visited[newY][newX] = true;
                queue.push([newX, newY]);
            }
        }
    }
};

内容的提问来源于stack exchange,提问作者Caleb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 17:57:14