繁体   English   中英

优化canvas画圆

[英]Optimise canvas drawing of a circle

我是 HTML5 canvas 的新手,并希望在我的网站上随机移动几个圆圈以获得奇特的效果。

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

这是在 Safari 中用 5 个圆圈对其进行分析几秒钟的屏幕截图。

配置文件结果

这是到目前为止我的 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} />
}

有没有更高效的方法来使用 canvas 绘制和移动圆圈?

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

我知道用 CSS 做这些类型的动画通常会更好(因为我相信它使用 GPU 而不是 CPU),但我不知道如何使用转换 Z2C56C360580420D293D78 属性让它工作。 我只能让规模转换工作。

只有当屏幕上有很多圆圈在移动时,我的奇特效果才会看起来“酷”,因此寻找一种更高效的方式来绘制和移动圆圈。

这是一个用于演示的沙箱: https://codesandbox.io/s/async-meadow-vx822 (在 chrome 或 safari 中查看以获得最佳效果)

这是一种稍微不同的方法来组合圆圈和背景,只有一个 canvas 元素来改善渲染的 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" />
    </>
  );
}

我尽量把你的代码组装起来,看来你有缓冲区溢出(蓝色js堆),你需要在这里调查,这些是根本原因。

最初的方法是只创建一次圆圈,然后从父级为子级设置动画,这样可以避免密集的 memory 和 CPU 计算。

通过单击 canvas 添加多少圈,canvas 归功于Martin

更新

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

解决方案#1

应用程序.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];
}

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 (app) 9 示例圆

import React from 'react';

export default function Circlo(props) {

    return (

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

}

Circlo.js

 import React from 'react'; export default function Circlo(props) { return ( <circle cx={props.x} cy={props.y} r={props.r} fill="red" /> ) }

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

首先,效果不错!

曾经说过,我仔细阅读了您的代码,看起来还不错。 恐怕有许多 canvas 和透明胶片,高 CPU 负载是不可避免的......

为了优化您的效果,您可以尝试两种方法:

  1. 尝试只使用一个 canvas
  2. 尝试仅使用 CSS,最后您使用 canvas 仅使用固定集合中的颜色绘制实心圆:您可以使用具有预先绘制的相同圆的图像并使用或多或少相同的代码来简单地更改样式属性图片

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

希望我给了你一些想法。

我强烈推荐阅读 Mozilla Developer's Network 网站上的文章优化 Canvas 具体来说,在不进行实际编码的情况下,不建议在 canvas 中重复执行昂贵的渲染操作。 Alternatively, you can create a virtual canvas inside your circle class and perform the drawing on there when you initially create the circle, then scale your Circle canvas and blit it the main canvas, or blit it and then scale it on the canvas you are blitting至。 您可以使用 CanvasRenderingContext2d.getImageData 和.putImageData 从一个 canvas 复制到另一个。 你如何实现它取决于你,但想法是不要在绘制一次时重复绘制图元,相比之下复制像素数据相当快。

更新

我试着弄乱你的例子,但我没有任何反应经验,所以我不确定发生了什么。 Anyway, I cooked up a pure Javascript example without using virtual canvasses, but rather drawing to a canvas, adding it to the document, and animating the canvas itself inside the constraints of the original canvas. 这似乎工作最快和最顺利(按 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>

酷炫的效果。 我真的很惊讶@Sam Erkiner 提出的解决方案对我来说并没有比你原来的效果好得多。 我本来希望单个 canvas 效率更高。 我决定用新的 animation 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} />;
}

以下是 Safari 在我 6 岁的 Macbook 上的性能统计数据: 在此处输入图像描述

也许通过一些额外的调整可以将其推入绿色区域? 您的原始解决方案位于红色区域的开头,单个 canvas 解决方案位于能源影响图表黄色区域的末尾。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM