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

React中基于State自动循环绘制并更新Canvas水波纹直至波纹全部消失的实现问题

React中基于State自动循环绘制并更新Canvas水波纹直至波纹全部消失的实现问题

嘿,我完全懂你现在的困扰——在React里结合Canvas做动画,很容易在State管理和动画循环的衔接上踩坑。咱们一步步拆解问题,把你的水波纹效果搞定!

第一个问题:为什么会出现Cannot read properties of undefined (reading 'x')?

你在updateCircles里用map处理数组时,当圆形半径超过maxRadius时没有返回任何值,这会导致map生成的数组里出现undefined元素。后续drawRipples里遍历这个数组时,自然就会读到undefined的x属性。

修复方案:
把「更新半径」和「过滤超出范围的圆形」分开做,先更新所有符合条件的圆形半径,再过滤掉超出maxRadius的项。另外,React的State是不可变的,别直接修改原对象,要返回新的对象:

function updateCircles() {
  const maxRadius = Math.sqrt(canvasWidth * canvasWidth + canvasHeight * canvasHeight) / 6;
  const rippleSpeed = maxRadius / 1500;

  setCircles((currentCircles) => {
    return currentCircles
      // 先更新符合条件的圆形半径,返回新对象
      .map(circle => {
        if (circle.radius < maxRadius) {
          return { ...circle, radius: circle.radius + rippleSpeed };
        }
        return circle;
      })
      // 过滤掉超出最大半径的圆形
      .filter(circle => circle.radius < maxRadius);
  });
}

第二个问题:动画循环导致的最大深度/重复触发问题

你之前的useEffect依赖circles,每次State更新都会触发新的requestAnimationFrame,这会导致多个动画帧叠加执行,甚至触发无限循环。咱们需要用一个ref来控制动画的启停,避免重复创建循环。

修复方案:

  1. 先创建一个ref保存动画ID,用来控制动画的启停
  2. 用useEffect监听circles的变化,动态启动/停止动画循环
// 在组件内部定义
const animationIdRef = useRef(null);
const canvasWidth = 300;
const canvasHeight = 200;

// 调整后的useEffect
useEffect(() => {
  // 当没有波纹时,停止动画并清空画布
  if (circles.length === 0) {
    if (animationIdRef.current) {
      cancelAnimationFrame(animationIdRef.current);
      animationIdRef.current = null;
    }
    const canvas = canvasRef.current;
    if (canvas) {
      const ctx = canvas.getContext('2d');
      ctx.clearRect(0, 0, canvas.width, canvas.height);
    }
    return;
  }

  // 当有波纹且动画未运行时,启动循环
  if (!animationIdRef.current) {
    const renderLoop = () => {
      drawRipples();
      updateCircles();
      // 继续请求下一帧,直到波纹全部消失
      animationIdRef.current = requestAnimationFrame(renderLoop);
    };
    animationIdRef.current = requestAnimationFrame(renderLoop);
  }

  // 组件卸载时清理动画
  return () => {
    if (animationIdRef.current) {
      cancelAnimationFrame(animationIdRef.current);
    }
  };
}, [circles]);

额外优化:统一常量与绘制逻辑

把maxRadius、rippleSpeed这些常量提到组件顶部,避免在drawRipples和updateCircles里重复计算,保证逻辑一致性:

// 组件顶部定义常量
const canvasWidth = 300;
const canvasHeight = 200;
const maxRadius = Math.sqrt(canvasWidth * canvasWidth + canvasHeight * canvasHeight) / 6;
const rippleSpeed = maxRadius / 1500;

// 简化后的drawRipples
function drawRipples() {
  const canvas = canvasRef.current;
  if (!canvas) return;
  const context = canvas.getContext('2d');
  
  context.clearRect(0, 0, canvasWidth, canvasHeight);
  
  circles.forEach(circle => {
    context.beginPath();
    context.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
    const transparency = 1 - (circle.radius / maxRadius);
    context.strokeStyle = `rgba(0, 150, 255, ${transparency})`;
    context.stroke();
  });
}

这样调整后,你的动画循环就能正常工作:点击添加波纹后,会自动持续绘制并更新波纹,直到所有波纹超出最大半径被移除,动画自动停止,画布清空。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 09:39:35