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

页面加载时移动鼠标致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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 07:16:00