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来控制动画的启停,避免重复创建循环。
修复方案:
- 先创建一个
ref保存动画ID,用来控制动画的启停 - 用
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
相关产品推荐
相关产品推荐

