如何在 React.js 中使用这个 Javascript 动画

问题描述

我有下面的代码,我想知道让它在 React.js 中工作的最佳方式。我的主要问题是我无法理解 React 中的面向对象原则。使用 JS 非常简单地制作我的 Particle 类的许多实例。但是如何做到 React?非常感谢任何帮助!

const canvas = document.getElementById('canvas1')
const ctx = canvas.getContext('2d')
canvas.width = window.innerWidth
canvas.height = window.innerHeight


class Particle {
  constructor(x,y,size,weight) {
    this.x = x
    this.y = y
    this.size = size
    this.weight = weight
  }
  update() {
    if (this.y > canvas.height) {
      this.y = 0 - this.size
      this.x = Math.random() * 60 + 200
      this.weight = Math.random() * 0.5 + 1
    }
    this.y += this.weight
    //console.log('y is inside the canvas',this.y)
  }
  draw() {
    ctx.fillStyle = 'blue'
    ctx.beginPath()
    ctx.arc(this.x,this.y,this.size,2 * Math.PI)
    //ctx.fill()
    ctx.stroke()
  }
}
const particleArray = []
const numberOfBalls = 10

function createParticles() {
  for (let i = 0; i < numberOfBalls; i++) {
    const x = Math.random() * 60 + 200
    const y = Math.random() * canvas.height
    const size = Math.random() * 20 + 5
    const weight = Math.random() * 0.5 + 1
    particleArray.push(new Particle(x,weight))
  }
}
createParticles()

// animate canvas
function animate() {
  ctx.clearRect(0,canvas.height,canvas.width)
  for (let i = 0; i < particleArray.length; i++) {
    particleArray[i].update()
    particleArray[i].draw()
  }
  requestAnimationFrame(animate)
}

animate()
window.addEventListener('resize',(e) => {
  canvas.width = window.innerWidth
  canvas.height = window.innerHeight
})
<canvas id="canvas1"></canvas>

解决方法

拥有类和相当数量的代码(游戏、动画等)不应该需要对 React 进行重大更改。

关键是重构您的代码,以便将画布对象传递给您的动画/游戏代码,而不是作为 React 无法管理的全局变量。即使您不使用 React,避免使用全局变量也是很好的设计。

此外,particleArraynumberOfBallscreateParticlesanimate 在全局范围内是松散的,但实际上,所有这些都协同工作来表示动画,应该分组一个类、闭包、对象等

在重构动画代码以接受画布参数后,您的组件会呈现画布元素,将其传递到动画中并跟踪 requestAnimationFrame,以便它可以在卸载时调用 cancelAnimationFrame

键盘和鼠标事件也由 React 管理。您可以使用普通的处理程序 onKeyDownonClickonMouseMove 等。您的游戏或动画管理器将公开函数来响应这些事件。

您还需要在 React 中处理窗口大小调整。 Rerender view on browser resize with React 中有代码,但我会将其调整为钩子并使用去抖动而不是节流。

这是一个快速、不雅的草图,对您的代码进行了最少的调整:

body { margin: 0; }
<script type="text/babel" defer>
const {useEffect,useRef,useState} = React;

class Particle {
  constructor(canvas,x,y,size,weight) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.x = x
    this.y = y
    this.size = size
    this.weight = weight
  }
  update() {
    if (this.y > this.canvas.height) {
      this.y = 0 - this.size
      this.x = Math.random() * 60 + 200
      this.weight = Math.random() * 0.5 + 1
    }
    
    this.y += this.weight
  }
  draw() {
    const {x,ctx} = this;
    ctx.fillStyle = 'blue'
    ctx.beginPath()
    ctx.arc(x,2 * Math.PI)
    ctx.stroke()
  }
}

const particleArray = []
const numberOfBalls = 10

function createParticles(canvas) {
  for (let i = 0; i < numberOfBalls; i++) {
    const x = Math.random() * 60 + 200
    const y = Math.random() * canvas.height
    const size = Math.random() * 20 + 5
    const weight = Math.random() * 0.5 + 1
    particleArray.push(new Particle(canvas,weight))
  }
}

function animate(canvas) {
  const ctx = canvas.getContext("2d");
  ctx.clearRect(0,canvas.width,canvas.height);

  for (let i = 0; i < particleArray.length; i++) {
    particleArray[i].update();
    particleArray[i].draw();
  }
}

const Animation = () => {
  const canvasRef = useRef();
  const requestRef = useRef();

  const update = () => {
    animate(canvasRef.current);
    requestRef.current = requestAnimationFrame(update);
  };

  const handleResize = debounce(() => {
    canvasRef.current.width = innerWidth;
    canvasRef.current.height = innerHeight;
  },100);
  
  useEffect(() => {
    createParticles(canvasRef.current);
    handleResize();
    window.addEventListener("resize",handleResize);
    requestRef.current = requestAnimationFrame(update);
    return () => {
      cancelAnimationFrame(requestRef.current);
      window.removeEventListener("resize",handleResize);
    };
  },[]);
  
  return <canvas ref={canvasRef}></canvas>;
};

ReactDOM.render(<Animation />,document.body);

function debounce(fn,ms) {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args),ms);
  };
}

</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>

这行得通,但由于前面提到的松散的全局函数和变量,设计不是很好。

从 React 的角度来看,代码可以使用自定义钩子来处理动画帧和窗口大小调整以清理主要组件。

顺便说一句,您的 clearRect 调用混淆了宽度和高度参数,因此它只会在方形屏幕上准确地清除屏幕。


为了完整起见,这里有一个简单的草图,它移除了动画全局变量、显示处理事件并将逻辑移动到钩子。分成模块时应该是干净的。

body { margin: 0; }
<script type="text/babel" defer>
const {useEffect,useState} = React;

class Particle {
  constructor(ctx,r) {
    this.ctx = ctx;
    this.x = x;
    this.y = y;
    this.r = r;
    this.vx = 0;
    this.vy = 0;
  }

  moveTo(x,y) {
    this.x = x;
    this.y = y;
  }

  render() {
    this.ctx.beginPath();
    this.ctx.fillStyle = "white";
    this.ctx.arc(this.x,this.y,this.r,Math.PI * 2);
    this.ctx.fill();
  }
}

class SimpleAnimation {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.particles = [
      new Particle(this.ctx,canvas.width / 2,canvas.height / 2,20)
    ];
  }

  onMouseMove(evt) {
    this.mouseX = evt.clientX;
    this.mouseY = evt.clientY;
  }

  tick() {
    this.particles.forEach(p => p.moveTo(this.mouseX,this.mouseY));
    this.ctx.fillStyle = "black";
    this.ctx.fillRect(0,this.canvas.width,this.canvas.height);
    this.particles.forEach(p => p.render());
  }
}

const Animation = () => {
  const animationRef = useRef();
  const canvasRef = useRef();

  useAnimationFrame(() => animationRef.current.tick());

  const resize = () => {
    canvasRef.current.width = innerWidth;
    canvasRef.current.height = innerHeight;
  };
  useWindowResize(resize);

  useEffect(() => {
    resize();
    animationRef.current = new SimpleAnimation(canvasRef.current);
  },[]);

  return (
    <canvas 
      onMouseMove={evt => animationRef.current.onMouseMove(evt)} 
      ref={canvasRef} 
    />
  );
};

ReactDOM.render(<Animation />,ms);
  };
}

function useWindowResize(fn,ms=100) {
  const handleResize = debounce(fn,ms);
  useEffect(() => {
    window.addEventListener("resize",handleResize);
    return () => {
      window.removeEventListener("resize",[]);
}

function useAnimationFrame(fn) {
  const rafRef = useRef();
  
  const animate = time => {
    fn(time);
    rafRef.current = requestAnimationFrame(animate);
  };
  
  useEffect(() => {
    rafRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  },[]);
}

</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>