如何用setTimeout延迟递归分形树绘制?含requestAnimationFrame疑问
分步绘制分形树的解决方案及问题解析
为什么直接用setTimeout包裹递归调用无效?
这其实是Canvas上下文状态的异步冲突导致的:
- 原来的同步递归中,
ctx.save()和ctx.restore()是严格成对嵌套的:父级保存上下文→绘制当前分支→同步调用子分支(子分支会复用父级的上下文状态)→父级恢复上下文,整个流程的上下文状态是连贯的。 - 当你用
setTimeout把递归调用改成异步后,父级的ctx.restore()会在子分支的回调执行前就完成了。等子分支的回调触发时,它拿到的已经是被父级恢复后的原始上下文,之前的translate和rotate状态都没了,自然会把分支画到错误的位置。 - 另外,异步递归会同时触发多个子分支的绘制任务,它们共享同一个Canvas上下文,状态互相干扰,进一步导致绘制混乱。
方案一:用任务队列+setTimeout实现分步绘制
核心思路是先同步收集所有分支的绘制参数(全局坐标),再逐个异步绘制,彻底避免上下文状态的嵌套依赖:
var canvas = document.getElementById('canvas_main'); canvas.width = 600; canvas.height = 600; var ctx = canvas.getContext('2d'); // 存储所有分支的绘制任务(每个任务包含全局坐标、长度、角度) const drawQueue = []; // 第一步:收集所有分支的绘制参数(广度优先,保证绘制顺序是从主干到分支) function collectBranchTasks() { // 初始任务:树干 const queue = [{ x: 300, y: 600, len: 120, ang: 0 }]; while (queue.length > 0) { const task = queue.shift(); drawQueue.push(task); // 分支长度小于10时停止递归 if (task.len < 10) continue; // 计算子分支的全局坐标(避免依赖父级上下文变换) const rad = task.ang * Math.PI / 180; const childX = task.x + task.len * Math.sin(rad); const childY = task.y - task.len * Math.cos(rad); // 添加左右子分支任务 queue.push({ x: childX, y: childY, len: task.len * 0.8, ang: task.ang - 15 }); queue.push({ x: childX, y: childY, len: task.len * 0.8, ang: task.ang + 15 }); } } // 第二步:逐个执行队列中的任务,实现延迟绘制 function drawNextBranch() { if (drawQueue.length === 0) return; const task = drawQueue.shift(); ctx.save(); ctx.beginPath(); ctx.translate(task.x, task.y); ctx.rotate(task.ang * Math.PI / 180); ctx.moveTo(0, 0); ctx.lineTo(0, -task.len); ctx.stroke(); ctx.restore(); // 每隔100ms绘制一个分支,可调整速度 setTimeout(drawNextBranch, 100); } // 启动流程 collectBranchTasks(); drawNextBranch();
方案二:用requestAnimationFrame实现更流畅的生长动画
requestAnimationFrame会和浏览器的刷新频率同步,绘制更流畅,适合做动画效果:
var canvas = document.getElementById('canvas_main'); canvas.width = 600; canvas.height = 600; var ctx = canvas.getContext('2d'); const drawQueue = []; let animationId; // 同样先收集所有分支任务(和方案一的collectBranchTasks一致) function collectBranchTasks() { const queue = [{ x: 300, y: 600, len: 120, ang: 0 }]; while (queue.length > 0) { const task = queue.shift(); drawQueue.push(task); if (task.len < 10) continue; const rad = task.ang * Math.PI / 180; const childX = task.x + task.len * Math.sin(rad); const childY = task.y - task.len * Math.cos(rad); queue.push({ x: childX, y: childY, len: task.len * 0.8, ang: task.ang - 15 }); queue.push({ x: childX, y: childY, len: task.len * 0.8, ang: task.ang + 15 }); } } // 用requestAnimationFrame驱动绘制 function animateGrowth() { if (drawQueue.length === 0) { cancelAnimationFrame(animationId); return; } const task = drawQueue.shift(); ctx.save(); ctx.beginPath(); ctx.translate(task.x, task.y); ctx.rotate(task.ang * Math.PI / 180); ctx.moveTo(0, 0); ctx.lineTo(0, -task.len); ctx.stroke(); ctx.restore(); animationId = requestAnimationFrame(animateGrowth); } // 启动流程 collectBranchTasks(); animateGrowth();
补充说明
- 如果想要深度优先的绘制顺序(先画完一侧所有分支再画另一侧),可以把收集任务的逻辑改成递归深度优先的方式,只需要调整
collectBranchTasks的实现即可。 - 两种方案都把上下文状态的管理限制在单个分支的绘制中,每个分支的
save和restore都是独立的,彻底解决了异步导致的上下文混乱问题。
内容的提问来源于stack exchange,提问作者swyveu
相关产品推荐
相关产品推荐

