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

How to use this Javascript animation in React.js

我有下面的代码,我想知道让它在 React.js 中运行的最佳方法。我的主要问题是我无法理解 React 中的面向对象原则。使用 JS 可以非常简单地创建我的 Particle class 的许多实例。但是 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, 0, 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, y, size, weight))
  }
}
createParticles()

// animate canvas
function animate() {
  ctx.clearRect(0, 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>

拥有 classes 和相当数量的代码(游戏、动画等)不需要进行重大更改即可与 React 一起工作。

关键是重构您的代码,以便 canvas 对象可以传递给您的 animation/game 代码,而不是作为 React 不管理的全局变量。即使你不使用 React,避免全局变量也是一个好的设计。

此外,particleArraynumberOfBallscreateParticlesanimate 在全局范围内是松散的,但实际上,它们一起工作来表示动画并且应该是分组为class、闭包、对象等

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

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

您还需要在 React 中处理 window 大小调整。 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, y, size, ctx} = this;
    ctx.fillStyle = 'blue'
    ctx.beginPath()
    ctx.arc(x, y, size, 0, 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, x, y, size, weight))
  }
}

function animate(canvas) {
  const ctx = canvas.getContext("2d");
  ctx.clearRect(0, 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 的角度来看,代码可以使用自定义挂钩来处理动画帧和 window 调整大小以清理主要组件。

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


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

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

class Particle {
  constructor(ctx, x, y, 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, 0, 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, 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 />, document.body);

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

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

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>