优化圆圈的画布绘制

时间:2020-05-25 09:26:09

标签: javascript reactjs html5-canvas

我是HTML5画布的新手,并希望使一些圆圈沿随机方向移动,以在我的网站上产生奇特的效果。

我注意到,当这些圆圈移动时,CPU使用率非常高。当只有几个圆圈移动时,通常是可以的,但是当大约5个或更多时,就开始成为问题。

这是在Safari中用5个圆圈对它进行概要分析的屏幕截图。

Profile Results

这是到目前为止我的Circle组件的代码:

export default function Circle({ color = null }) {
  useEffect(() => {
    if (!color) return

    let requestId = null
    let canvas = ref.current
    let context = canvas.getContext("2d")

    let ratio = getPixelRatio(context)
    let canvasWidth = getComputedStyle(canvas).getPropertyValue("width").slice(0, -2)
    let canvasHeight = getComputedStyle(canvas).getPropertyValue("height").slice(0, -2)

    canvas.width = canvasWidth * ratio
    canvas.height = canvasHeight * ratio
    canvas.style.width = "100%"
    canvas.style.height = "100%"

    let y = random(0, canvas.height)
    let x = random(0, canvas.width)
    const height = random(100, canvas.height * 0.6)

    let directionX = random(0, 1) === 0 ? "left" : "right"
    let directionY = random(0, 1) === 0 ? "up" : "down"

    const speedX = 0.1
    const speedY = 0.1

    context.fillStyle = color

    const render = () => {
      //draw circle
      context.clearRect(0, 0, canvas.width, canvas.height)
      context.beginPath()
      context.arc(x, y, height, 0, 2 * Math.PI)

      //prevent circle from going outside of boundary
      if (x < 0) directionX = "right"
      if (x > canvas.width) directionX = "left"
      if (y < 0) directionY = "down"
      if (y > canvas.height) directionY = "up"

      //move circle
      if (directionX === "left") x -= speedX
      else x += speedX
      if (directionY === "up") y -= speedY
      else y += speedY

      //apply color
      context.fill()

      //animate
      requestId = requestAnimationFrame(render)
    }

    render()

    return () => {
      cancelAnimationFrame(requestId)
    }
  }, [color])

  let ref = useRef()
  return <canvas ref={ref} />
}

是否有使用画布绘制和移动圆的更有效的方法?

当它们不动时,CPU使用率从〜3%左右开始,然后下降到不到1%,当我从DOM中删除圆圈时,CPU使用率始终低于1%。

我知道用CSS制作这些类型的动画通常会更好(因为我相信它使用GPU而不是CPU),但是我无法弄清楚如何使用过渡CSS属性使其工作。我只能使比例转换生效。

我的幻想效果仅在屏幕上有许多圆圈移动时才看起来很“酷”,因此寻找一种更高性能的绘制和移动圆圈的方法。

这里是演示的沙箱:https://codesandbox.io/s/async-meadow-vx822(使用chrome或safari查看最佳效果)

5 个答案:

答案 0 :(得分:6)

这里是一种将圆和背景组合在一起的方法,该方法略有不同,仅具有一个画布元素来改善渲染的dom。

此组件使用与您的随机化逻辑相同的颜色和大小,但在呈现任何内容之前将所有初始值存储在circles数组中。 render函数将背景色和所有圆一起渲染,并计算它们在每个循环中的移动。

export default function Circles() {
  useEffect(() => {
    const colorList = {
      1: ["#247ba0", "#70c1b3", "#b2dbbf", "#f3ffbd", "#ff1654"],
      2: ["#05668d", "#028090", "#00a896", "#02c39a", "#f0f3bd"]
    };
    const colors = colorList[random(1, Object.keys(colorList).length)];
    const primary = colors[random(0, colors.length - 1)];
    const circles = [];

    let requestId = null;
    let canvas = ref.current;
    let context = canvas.getContext("2d");

    let ratio = getPixelRatio(context);
    let canvasWidth = getComputedStyle(canvas)
      .getPropertyValue("width")
      .slice(0, -2);
    let canvasHeight = getComputedStyle(canvas)
      .getPropertyValue("height")
      .slice(0, -2);

    canvas.width = canvasWidth * ratio;
    canvas.height = canvasHeight * ratio;
    canvas.style.width = "100%";
    canvas.style.height = "100%";

    [...colors, ...colors].forEach(color => {
      let y = random(0, canvas.height);
      let x = random(0, canvas.width);
      const height = random(100, canvas.height * 0.6);

      let directionX = random(0, 1) === 0 ? "left" : "right";
      let directionY = random(0, 1) === 0 ? "up" : "down";

      circles.push({
        color: color,
        y: y,
        x: x,
        height: height,
        directionX: directionX,
        directionY: directionY
      });
    });

    const render = () => {
      context.fillStyle = primary;
      context.fillRect(0, 0, canvas.width, canvas.height);

      circles.forEach(c => {
        const speedX = 0.1;
        const speedY = 0.1;

        context.fillStyle = c.color;
        context.beginPath();
        context.arc(c.x, c.y, c.height, 0, 2 * Math.PI);
        if (c.x < 0) c.directionX = "right";
        if (c.x > canvas.width) c.directionX = "left";
        if (c.y < 0) c.directionY = "down";
        if (c.y > canvas.height) c.directionY = "up";
        if (c.directionX === "left") c.x -= speedX;
        else c.x += speedX;
        if (c.directionY === "up") c.y -= speedY;
        else c.y += speedY;
        context.fill();
        context.closePath();
      });

      requestId = requestAnimationFrame(render);
    };

    render();

    return () => {
      cancelAnimationFrame(requestId);
    };
  });

  let ref = useRef();
  return <canvas ref={ref} />;
}

您可以在应用程序组件中简单地用这一组件替换所有的圆圈元素和背景样式。

export default function App() {
  return (
    <>
      <div className="absolute inset-0 overflow-hidden">
          <Circles />
      </div>
      <div className="backdrop-filter-blur-90 absolute inset-0 bg-gray-900-opacity-20" />
    </>
  );
}

答案 1 :(得分:1)

我试图尽可能地汇编您的代码,看来您有缓冲区溢出(蓝色js堆),您需要在此处进行调查,这是根本原因。

最初的方法是只创建一次圆,然后从父级对子级进行动画处理,这样可以避免占用大量内存和CPU计算。

通过单击画布添加多少个圆圈,画布功劳将归入Martin

更新资料

在进行亚历山大讨论之后,可以使用setTimeout或Timeinterval(解决方案2)

解决方案#1

App.js

import React from 'react';
import { useCircle } from './useCircle';
import './App.css';

const useAnimationFrame = callback => {
  // Use useRef for mutable variables that we want to persist
  // without triggering a re-render on their change
  const requestRef = React.useRef();
  const previousTimeRef = React.useRef();

  const animate = time => {
    if (previousTimeRef.current != undefined) {
      const deltaTime = time - previousTimeRef.current;
      callback(deltaTime)
    }
    previousTimeRef.current = time;
    requestRef.current = requestAnimationFrame(animate);
  }

  React.useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []); // Make sure the effect runs only once
}
function App() {

  const [count, setCount] = React.useState(0)
  const [coordinates, setCoordinates, canvasRef, canvasWidth, canvasHeight, counts] = useCircle();
  const speedX = 1 // tunne performance by changing this
  const speedY = 1 // tunne performance by changing this
  const requestRef = React.useRef();
  const previousTimeRef = React.useRef();



  const handleCanvasClick = (event) => {
    // on each click get current mouse location 
    const currentCoord = { x: event.clientX, y: event.clientY ,directionX:"right",directionY:"down"};
    // add the newest mouse location to an array in state 
    setCoordinates([...coordinates, currentCoord]);
    // query.push(currentCoord)
    //query.push(currentCoord)
  };

  const move = () => {
    let q = [...coordinates]
    q.map(coordinate => { return { x: coordinate.x + 10, y: coordinate.y + 10 } })
    setCoordinates(q)
  }

  const handleClearCanvas = (event) => {
    setCoordinates([]);
  };

  const animate = time => {

//if (time % 2===0){

    setCount(time)
    if (previousTimeRef.current != undefined) {
      const deltaTime = time - previousTimeRef.current;

setCoordinates(coordinates => coordinates.map((coordinate)=> {

let x=coordinate.x;
let y=coordinate.y;

let directionX=coordinate.directionX

let directionY=coordinate.directionY


  if (x < 0) directionX = "right"
  if (x > canvasWidth) directionX = "left"
  if (y < 0) directionY = "down"
  if (y > canvasHeight) directionY = "up"


  if (directionX === "left") x -= speedX
  else x += speedX
  if (directionY === "up") y -= speedY
  else y += speedY

  return { x:x,y:y,directionX:directionX,directionY:directionX} 


}))

   // }
  }
    previousTimeRef.current = time;
    requestRef.current = requestAnimationFrame(animate);
  }


  React.useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []); // Make sure the effect runs only once

  return (
    <main className="App-main" >
      <div>{Math.round(count)}</div>

      <canvas
        className="App-canvas"
        ref={canvasRef}
        width={canvasWidth}
        height={canvasHeight}
        onClick={handleCanvasClick}

      />

      <div className="button" >
        <button onClick={handleClearCanvas} > CLEAR </button>
      </div>
    </main>
  );

};

export default App;

userCircle.js

import React, { useState, useEffect, useRef } from 'react';

var circle = new Path2D();
circle.arc(100, 100, 50, 0, 2 * Math.PI);
const SCALE = 1;
const OFFSET = 80;
export const canvasWidth = window.innerWidth * .5;
export const canvasHeight = window.innerHeight * .5;

export const counts=0;

export function draw(ctx, location) {
  console.log("attempting to draw")
  ctx.fillStyle = 'red';
  ctx.shadowColor = 'blue';
  ctx.shadowBlur = 15;
  ctx.save();
  ctx.scale(SCALE, SCALE);
  ctx.translate(location.x / SCALE - OFFSET, location.y / SCALE - OFFSET);
  ctx.rotate(225 * Math.PI / 180);
  ctx.fill(circle);
  ctx.restore();

};

export function useCircle() {
  const canvasRef = useRef(null);
  const [coordinates, setCoordinates] = useState([]);

  useEffect(() => {
    const canvasObj = canvasRef.current;
    const ctx = canvasObj.getContext('2d');
    ctx.clearRect(0, 0, canvasWidth, canvasHeight);
    coordinates.forEach((coordinate) => {
      draw(ctx, coordinate)
    }
    );
  });

  return [coordinates, setCoordinates, canvasRef, canvasWidth, canvasHeight,counts];
}

使用间隔的方法#2

IntervalExample.js(应用程序)9个示例圆

import React, { useState, useEffect } from 'react';

import Circlo from './Circlo'


const IntervalExample = () => {
  const [seconds, setSeconds] = useState(0);

  const [circules, setCircules] = useState([]);


  let arr =[
    {x:19,y:15, r:3,directionX:'left',directionY:'down'},
    {x:30,y:10,r:4,directionX:'left',directionY:'down'},
    {x:35,y:20,r:5,directionX:'left',directionY:'down'},
    {x:0,y:15, r:3,directionX:'left',directionY:'down'},
    {x:10,y:30,r:4,directionX:'left',directionY:'down'},
    {x:20,y:50,r:5,directionX:'left',directionY:'down'},
    {x:70,y:70, r:3,directionX:'left',directionY:'down'},
    {x:80,y:80,r:4,directionX:'left',directionY:'down'},
    {x:10,y:20,r:5,directionX:'left',directionY:'down'},
  ]



const reno =(arr)=>{
  const table = Array.isArray(arr) && arr.map(item => <Circlo x={item.x} y={item.y} r={item.r} />);
return(table)
}
  const speedX = 0.1 // tunne performance by changing this
  const speedY = o.1 // tunne performance by changing this

  const move = (canvasHeight,canvasWidth) => {


 let xarr=   arr.map(((coordinate)=> {

      let x=coordinate.x;
      let y=coordinate.y;

      let directionX=coordinate.directionX
      let directionY=coordinate.directionY
      let r=coordinate.r
        if (x < 0) directionX = "right"
        if (x > canvasWidth) directionX = "left"
        if (y < 0) directionY = "down"
        if (y > canvasHeight) directionY = "up"
        if (directionX === "left") x -= speedX
        else x += speedX
        if (directionY === "up") y -= speedY
        else y += speedY

        return { x:x,y:y,directionX:directionX,directionY:directionY,r:r} 

      }))
      return xarr;

  }

  useEffect(() => {
    const interval = setInterval(() => {

     arr =move(100,100)

      setCircules( arr)
      setSeconds(seconds => seconds + 1);


    }, 10);
    return () => clearInterval(interval);
  }, []);

  return (
    <div className="App">
      <p>
        {seconds} seconds have elapsed since mounting.
      </p>


<svg viewBox="0 0 100 100">
{ reno(circules)}
  </svg>     

    </div>
  );
};

export default IntervalExample;

Circlo.js

import React from 'react';

export default function Circlo(props) {

    return (

        <circle cx={props.x} cy={props.y} r={props.r} fill="red" />
    )

}

enter image description here

enter image description here

enter image description here

答案 2 :(得分:0)

首先,效果不错!

一次,我仔细阅读了您的代码,看起来还不错。恐怕许多画布和透明胶片都无法避免高CPU负载...

要优化效果,您可以尝试以下两种方法:

  1. 尝试仅使用一张画布
  2. 尝试仅使用CSS,最后只使用画布从固定集中绘制带有颜色的填充圆:您可以使用带有预先绘制的相同圆的图像,并使用或多或少相同的代码来简单地改变样式图片的属性

可能使用着色器,您可以在节省大量CPU的情况下获得相同的效果,但是不幸的是,我不精通着色器,因此无法为您提供任何相关提示。

希望我给你一些想法。

答案 3 :(得分:0)

我强烈建议您阅读Mozilla开发人员网络网站上的文章Optimizing the Canvas。具体来说,在不进行实际编码的情况下,建议不要在画布中重复执行昂贵的渲染操作。或者,您可以在圆类中创建一个虚拟画布,并在最初创建圆时在该画布上执行绘图,然后缩放“圆”画布并对其进行主框的涂色,或者对其进行涂色然后在您要涂色的画布上进行缩放至。您可以使用CanvasRenderingContext2d.getImageData和.putImageData从一个画布复制到另一个画布。如何实现它取决于您,但是这种想法不是在绘制一次时重复绘制图元,并且相比之下复制像素数据的速度相当快。

更新

我尝试弄乱您的示例,但是我没有任何反应的经验,因此我不确定到底发生了什么。无论如何,我编写了一个纯Javascript示例,而不使用虚拟画布,而是绘制到画布,将其添加到文档中,并在原始画布的约束范围内对画布本身进行动画处理。这似乎最快,最流畅(按c键添加圆圈,按d键删除圆圈):

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Buffer Canvas</title>
        <style>
            body, html {
                background-color: aquamarine;
                padding: 0;
                margin: 0;
            }
            canvas {
                border: 1px solid black;
                padding: 0;
                margin: 0;
                box-sizing: border-box;
            }
        </style>
        <script>
            function randInt(min, max) {
                return min + Math.floor(Math.random() * max);
            }
            class Circle {
                constructor(x, y, r) {
                    this._canvas = document.createElement('canvas');
                    this.x = x;
                    this.y = y;
                    this.r = r;
                    this._canvas.width = 2*this.r;
                    this._canvas.height = 2*this.r;
                    this._canvas.style.width = this._canvas.width+'px';
                    this._canvas.style.height = this._canvas.height+'px';
                    this._canvas.style.border = '0px';
                    this._ctx = this._canvas.getContext('2d');
                    this._ctx.beginPath();
                    this._ctx.ellipse(this.r, this.r, this.r, this.r, 0, 0, Math.PI*2);
                    this._ctx.fill();
                    document.querySelector('body').appendChild(this._canvas);
                    const direction = [-1, 1];
                    this.vx = 2*direction[randInt(0, 2)];
                    this.vy = 2*direction[randInt(0, 2)];
                    this._canvas.style.position = "absolute";
                    this._canvas.style.left = this.x + 'px';
                    this._canvas.style.top = this.y + 'px';
                    this._relativeElem = document.querySelector('body').getBoundingClientRect();
                }
                relativeTo(elem) {
                    this._relativeElem = elem;
                }
                getImageData() {
                    return this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
                }
                right() {
                    return this._relativeElem.left + this.x + this.r;
                }
                left() {
                    return this._relativeElem.left + this.x - this.r;
                }
                top() {
                    return this._relativeElem.top + this.y - this.r
                }
                bottom() {
                    return this._relativeElem.top + this.y + this.r;
                }
                moveX() {
                    this.x += this.vx;
                    this._canvas.style.left = this.x - this.r  + 'px';
                }
                moveY() {
                    this.y += this.vy;
                    this._canvas.style.top = this.y - this.r + 'px';
                }
                move() {
                    this.moveX();
                    this.moveY();
                }
                reverseX() {
                    this.vx = -this.vx;
                }
                reverseY() {
                    this.vy = -this.vy;
                }
            }

            let canvas, ctx, width, height, c, canvasRect;

            window.onload = preload;
            let circles = [];

            function preload() {
                canvas = document.createElement('canvas');
                canvas.style.backgroundColor = "antiquewhite";
                ctx = canvas.getContext('2d');
                width = canvas.width = 800;
                height = canvas.height = 600;
                document.querySelector('body').appendChild(canvas);
                canvasRect = canvas.getBoundingClientRect();
                document.addEventListener('keypress', function(e) {
                   if (e.key === 'c') {
                       let radius = randInt(10, 50);
                       let c = new Circle(canvasRect.left + canvasRect.width / 2 - radius, canvasRect.top + canvasRect.height / 2 - radius, radius);
                       c.relativeTo(canvasRect);
                       circles.push(c);
                   } else if (e.key === 'd') {
                       let c = circles.pop();
                       c._canvas.parentNode.removeChild(c._canvas);
                   }
                });
                render();
            }

            function render() {
                // Draw
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                circles.forEach((c) => {
                    // Check position and change direction if we hit the edge
                    if (c.left() <= canvasRect.left || c.right() >= canvasRect.right) {
                        c.reverseX();
                    }
                    if (c.top() <= canvasRect.top || c.bottom() >= canvasRect.bottom) {
                        c.reverseY();
                    }

                    // Update position for next render
                    c.move();
                });

                requestAnimationFrame(render);
            }
        </script>
    </head>
    <body>

    </body>
</html>

答案 4 :(得分:0)

很酷的效果!我真的很惊讶@Sam Erkiner提出的解决方案对我来说没有比您原来的要好得多。我本来希望单个画布更加有效。 我决定尝试使用新的动画API和纯DOM元素进行尝试,看看效果如何。 这是我的解决方案(仅更改Circle.js文件):

import React, { useEffect, useRef, useMemo } from "react";
import { random } from "lodash";

const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;  

export default function Circle({ color = null }) {
  let ref = useRef();

  useEffect(() => {
    let y = random(0, HEIGHT);
    let x = random(0, WIDTH);
    let directionX = random(0, 1) === 0 ? "left" : "right";
    let directionY = random(0, 1) === 0 ? "up" : "down";

    const speed = 0.5;

    const render = () => {
      if (x <= 0) directionX = "right";
      if (x >= WIDTH) directionX = "left";
      if (y <= 0) directionY = "down";
      if (y >= HEIGHT) directionY = "up";

      let targetX = directionX === 'right' ? WIDTH : 0;
      let targetY = directionY === 'down' ? HEIGHT : 0;

      const minSideDistance = Math.min(Math.abs(targetX - x), Math.abs(targetY - y));
      const duration = minSideDistance / speed;

      targetX = directionX === 'left' ? x - minSideDistance : x + minSideDistance;
      targetY = directionY === 'up' ? y - minSideDistance : y + minSideDistance;

      ref.current.animate([
        { transform: `translate(${x}px, ${y}px)` }, 
        { transform: `translate(${targetX}px, ${targetY}px)` }
      ], {
          duration: duration,
      });

      setTimeout(() => {
        x = targetX;
        y = targetY;
        ref.current.style.transform = `translate(${targetX}px, ${targetY}px)`;
      }, duration - 10);

      setTimeout(() => {
        render();
      }, duration);
    };
    render();
  }, [color]);

  const diameter = useMemo(() => random(0, 0.6 * Math.min(WIDTH, HEIGHT)), []);
  return <div style={{
    background: color,
    position: 'absolute',
    width: `${diameter}px`,
    height: `${diameter}px`,
    top: 0,
    left: 0
  }} ref={ref} />;
}

以下是我6岁的Macbook上Safari的性能统计信息: enter image description here

也许还有一些其他调整可以推到绿色区域? 您原来的解决方案是在能源影响图上的红色区域的开头,而单一画布的解决方案是在黄色区域的末尾。