页面加载时移动鼠标致WebGL流体模拟脚本失效的解决求助
WebGL流体模拟React实现的崩溃问题修复求助
我用到了一个WebGL流体模拟的React实现脚本,它基于Pavel Dobryakov的WebGL流体模拟项目。
已解决的第一个问题
最初遇到的问题是:当光标和模拟上方的文本交互时,模拟会崩溃,原因是事件监听器绑定在canvas元素上。我将监听器改绑到window上解决了这个问题,代码对比如下:
// 原代码 canvas.addEventListener('mousemove', function (e) { let pointer = pointers[0]; if (pointer.down) { return; } let posX = scaleByPixelRatio(e.offsetX); let posY = scaleByPixelRatio(e.offsetY); updatePointerMoveData(pointer, posX, posY); }); // 修复后代码 window.addEventListener('mousemove', function (e) { let pointer = pointers[0]; if (pointer.down) { return; } let posX = scaleByPixelRatio(e.offsetX); let posY = scaleByPixelRatio(e.offsetY); updatePointerMoveData(pointer, posX, posY); });
当前遇到的新问题
现在出现了新问题:当页面加载过程中(比如刷新页面后)移动鼠标,脚本会崩溃,流体模拟永远无法在canvas上绘制;但如果在脚本加载完成前,鼠标保持在窗口外或静止不动,模拟就能正常运行。
我尝试过将mousedown事件改为mouseover,虽然能正常运行,但性能下降明显,不是理想方案。
相关代码片段
fluid-init.js 核心代码
let canvas = document.getElementsByClassName('fluid-canvas')[0]; resizeCanvas(); /* 其余核心模拟代码省略 */ canvas.addEventListener('mousedown', function (e) { let posX = scaleByPixelRatio(e.offsetX); let posY = scaleByPixelRatio(e.offsetY); let pointer = pointers.find(function (p) { return p.id == -1; }); if (pointer == null) { pointer = new pointerPrototype(); } updatePointerDownData(pointer, -1, posX, posY); }); window.addEventListener('mousemove', function (e) { let pointer = pointers[0]; if (pointer.down) { return; } let posX = scaleByPixelRatio(e.clientX); let posY = scaleByPixelRatio(e.clientY); updatePointerMoveData(pointer, posX, posY); }); window.addEventListener('mouseup', function () { updatePointerUpData(pointers[0]); }); canvas.addEventListener('touchstart', function (e) { e.preventDefault(); let touches = e.targetTouches; while (touches.length >= pointers.length) { pointers.push(new pointerPrototype()); } for (let i = 0; i < touches.length; i++) { let posX = scaleByPixelRatio(touches[i].pageX); let posY = scaleByPixelRatio(touches[i].pageY); updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY); } }); canvas.addEventListener( 'touchmove', function (e) { e.preventDefault(); let touches = e.targetTouches; for (let i = 0; i < touches.length; i++) { let pointer = pointers[i + 1]; if (!pointer.down) { continue; } let posX = scaleByPixelRatio(touches[i].pageX); let posY = scaleByPixelRatio(touches[i].pageY); updatePointerMoveData(pointer, posX, posY); } }, false ); window.addEventListener('touchend', function (e) { let touches = e.changedTouches; let loop = function (i) { let pointer = pointers.find(function (p) { return p.id == touches[i].identifier; }); if (pointer == null) { return; } updatePointerUpData(pointer); }; for (let i = 0; i < touches.length; i++) loop(i); }); function updatePointerDownData(pointer, id, posX, posY) { pointer.id = id; pointer.down = true; pointer.moved = false; pointer.texcoordX = posX / (canvas.width / 2); pointer.texcoordY = 1 - posY / (canvas.height / 2); pointer.prevTexcoordX = pointer.texcoordX; pointer.prevTexcoordY = pointer.texcoordY; pointer.deltaX = 0; pointer.deltaY = 0; pointer.color = generateColor(); } function updatePointerMoveData(pointer, posX, posY) { pointer.prevTexcoordX = pointer.texcoordX; pointer.prevTexcoordY = pointer.texcoordY; pointer.texcoordX = posX / canvas.width; pointer.texcoordY = 1.0 - posY / canvas.height; pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; } function updatePointerUpData(pointer) { pointer.down = false; } function correctDeltaX(delta) { let aspectRatio = canvas.width / canvas.height; if (aspectRatio < 1) { delta *= aspectRatio; } return delta; } function correctDeltaY(delta) { let aspectRatio = canvas.width / canvas.height; if (aspectRatio > 1) { delta /= aspectRatio; } return delta; }
Fluid.jsx 组件代码
import React from 'react'; export const Fluid = () => { React.useEffect(() => { const script = document.createElement('script'); script.src = '/src/assets/fluid-init.js'; script.async = true; script.type = 'module'; document.body.appendChild(script); return () => { document.body.removeChild(script); }; }, []); return <canvas className="fluid-canvas"></canvas>; };
可行修复方案
1. 给事件回调添加初始化校验
在window的mousemove和mouseup事件回调开头,先判断核心数据是否已初始化,避免访问未定义变量:
window.addEventListener('mousemove', function (e) { // 校验pointers数组及首个指针是否存在 if (!pointers || pointers.length === 0 || !pointers[0]) { return; } let pointer = pointers[0]; if (pointer.down) { return; } let posX = scaleByPixelRatio(e.clientX); let posY = scaleByPixelRatio(e.clientY); updatePointerMoveData(pointer, posX, posY); }); window.addEventListener('mouseup', function () { if (!pointers || pointers.length === 0 || !pointers[0]) { return; } updatePointerUpData(pointers[0]); });
2. 延迟绑定全局事件监听器
不要在脚本开头就绑定window事件,而是等pointers和pointerPrototype初始化完成后再绑定。比如找到原脚本中初始化指针的代码段,之后再添加事件监听:
// 假设原脚本中初始化指针的代码 let pointerPrototype = function() { // 指针对象的构造逻辑 }; let pointers = [new pointerPrototype()]; // 初始化完成后再绑定全局事件 window.addEventListener('mousemove', function(e) { /* ... */ }); window.addEventListener('mouseup', function() { /* ... */ });
3. 优化React组件的脚本加载时机
通过useRef确保canvas元素挂载完成后再加载脚本,避免脚本执行时canvas未就绪:
import React, { useEffect, useRef } from 'react'; export const Fluid = () => { const canvasRef = useRef(null); useEffect(() => { if (!canvasRef.current) return; const script = document.createElement('script'); script.src = '/src/assets/fluid-init.js'; script.async = true; script.type = 'module'; document.body.appendChild(script); return () => { document.body.removeChild(script); // 可选:清理全局事件监听器,避免内存泄漏 }; }, []); return <canvas ref={canvasRef} className="fluid-canvas"></canvas>; };
4. 给指针操作函数添加参数校验
在updatePointerMoveData和updatePointerUpData开头添加参数校验,防止传入无效指针:
function updatePointerMoveData(pointer, posX, posY) { if (!pointer) return; // 原函数逻辑 } function updatePointerUpData(pointer) { if (!pointer) return; // 原函数逻辑 }
这些修改的核心思路是:确保只有当流体模拟的核心数据结构(指针数组、指针原型)初始化完成后,才会处理鼠标事件,避免因访问未定义变量导致脚本崩溃。
内容的提问来源于stack exchange,提问作者fallenech
相关产品推荐
相关产品推荐

