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(); } }; }
关键优化点说明
- 非递归实现:用栈(Stack)模拟递归调用,所有待处理的像素坐标都存在栈中,循环处理直到栈为空,彻底避免栈溢出。
- 访问记录优化:用二维数组
visited替代原代码的pixelsToFill.indexOf(),查询效率从O(n)提升到O(1),大幅提升大区域填充的性能。 - 颜色对比优化:直接对比RGBa数组的每个值,避免
toString()带来的性能损耗和潜在的字符串匹配错误。 - 绘制效率优化:直接操作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
相关产品推荐
相关产品推荐

