canvas 中的文字被下面的视频 canvas 擦除

Text in canvas being wiped out with a video canvas underneath

注意:最小工作示例 here。点击change text按钮,可以看到文字会出现一帧然后消失。

我想为我的视频添加叠加层,所以我堆叠了两个 canvas 并在透明顶部填充了文字 canvas。

然而,文字不粘。它消失了。

为了测试我是否正确填写了文本,我尝试在下方使用黑色 canvas(无视频)。

我只需要填充一次文本,文本就会保留下来。

但是,下面是视频 canvas,除非我在 requestAnimationFrame 中绘制文本,否则文本不会粘住,我相信它会在每一帧中绘制相同的文本,这是不必要的。

文本 canvas 应该与视频 canvas 分开。为什么没有 requestAnimationFrame 就消失了?

我该如何解决?

最小工作示例here,但代码也如下所示。

import "./styles.css";
import { useEffect, useRef, useState } from "react";

export default function App() {
  const inputStreamRef = useRef();
  const videoRef = useRef();

  const canvasRef = useRef();
  const overlayCanvasRef = useRef();
  const [text, setText] = useState("Hello");

  function drawTextWithBackground() {
    const ctx = overlayCanvasRef.current.getContext("2d");
    ctx.clearRect(
      0,
      0,
      overlayCanvasRef.current.width,
      overlayCanvasRef.current.height
    );

    /// lets save current state as we make a lot of changes
    ctx.save();

    /// set font
    const font = "50px monospace";
    ctx.font = font;

    /// draw text from top - makes life easier at the moment
    ctx.textBaseline = "top";

    /// color for background
    ctx.fillStyle = "#FFFFFF";

    /// get width of text
    const textWidth = ctx.measureText(text).width;
    // Just note that this way of "measuring" height is not accurate.
    // You can measure height of a font by using a temporary div / span element and
    // get the calculated style from that when font and text is set for it.
    const textHeight = parseInt(font, 10);

    const textXPosition = canvasRef.current.width / 2 - textWidth / 2;
    const textYPosition = canvasRef.current.height - textHeight * 3;

    ctx.save();
    ctx.globalAlpha = 0.4;
    /// draw background rect assuming height of font
    ctx.fillRect(textXPosition, textYPosition, textWidth, textHeight);
    ctx.restore(); // this applies globalAlpha to just this?

    /// text color
    ctx.fillStyle = "#000";

    ctx.fillText(text, textXPosition, textYPosition);

    /// restore original state
    ctx.restore();
  }

  function updateCanvas() {
    const ctx = canvasRef.current.getContext("2d");

    ctx.drawImage(
      videoRef.current,
      0,
      0,
      videoRef.current.videoWidth,
      videoRef.current.videoHeight
    );

    // QUESTION: Now the text won't stick unless I uncomment this below,
    // which runs drawTextWithBackground in requestAnimationFrame.
    // Previosuly, with just a black canvas underneath, I just needed to fillText once
    // and the text stayed.
    // The text canvas is supposed to be separate. Why is this getting wiped out without requestAnimationFrame?
    // drawTextWithBackground();

    requestAnimationFrame(updateCanvas);
  }

  const CAMERA_CONSTRAINTS = {
    audio: true,
    video: {
      // the best (4k) resolution from camera
      width: 4096,
      height: 2160
    }
  };

  const enableCamera = async () => {
    inputStreamRef.current = await navigator.mediaDevices.getUserMedia(
      CAMERA_CONSTRAINTS
    );

    videoRef.current.srcObject = inputStreamRef.current;

    await videoRef.current.play();

    // We need to set the canvas height/width to match the video element.
    canvasRef.current.height = videoRef.current.videoHeight;
    canvasRef.current.width = videoRef.current.videoWidth;
    overlayCanvasRef.current.height = videoRef.current.videoHeight;
    overlayCanvasRef.current.width = videoRef.current.videoWidth;

    requestAnimationFrame(updateCanvas);
  };
  useEffect(() => {
    enableCamera();
  });

  return (
    <div className="App">
      <video
        ref={videoRef}
        style={{ visibility: "hidden", position: "absolute" }}
      />
      <div style={{ position: "relative", width: "90vw" }}>
        <canvas
          style={{ width: "100%", top: 0, left: 0, position: "static" }}
          ref={canvasRef}
          width="500"
          height="400"
        />
        <canvas
          style={{
            width: "100%",
            top: 0,
            left: 0,
            position: "absolute"
          }}
          ref={overlayCanvasRef}
          width="500"
          height="400"
        />
      </div>
      <button
        onClick={() => {
          setText(text + "c");
          drawTextWithBackground();
        }}
      >
        change text
      </button>
    </div>
  );
}

当您更新视频中的 canvas 时,canvas 中的所有像素都会更新为视频中的像素。把它想象成一堵墙:

你在墙上画了“这是我的文字”。

然后你把整面墙漆成了蓝色。 (你的文字不见了,墙上只有蓝色)。

如果您将整面墙涂成蓝色,然后重新涂上“这是我的文字”,您仍然会在蓝色墙壁上看到您的文字。

重绘 canvas 时,您必须重绘想要出现在 canvas 上的所有内容。如果视频“覆盖”了您的文字,您必须重新绘制文字。

https://github.com/dougsillars/chyronavideo 上的工作示例,它在 canvas.

的每一帧上绘制一个 chyron(新闻故事底部的栏)

非常简单的修复

由于 React 更新状态的方式,canvas 正在被清除。

修复

最简单的解决方法是只使用 canvas 并将文本绘制到用于显示视频的 canvas 上。

从工作示例中更改两个函数 updateCanvasdrawTextWithBackground 如下。

请注意,我已经删除了 drawTextWithBackground 中的所有状态保存,因为 canvas 状态由于反应而丢失,所以为什么要使用非常昂贵的状态堆栈函数。

  function drawTextWithBackground(ctx) {
    const can = ctx.canvas;
    const textH = 50;  
    ctx.font = textH + "px monospace";
    ctx.textBaseline = "top";
    ctx.textAlign = "center";
    ctx.fillStyle = "#FFFFFF66";     // alpha 0.4 as part of fillstyle
    ctx.fillStyle = "#000";
    const [W, X, Y] = [ctx.measureText(text).width, can.width, can.height - textH * 3];
    ctx.fillRect(X -  W / 2, Y, W, textH);
    ctx.fillText(text, X, Y);
  }

  function updateCanvas() {
    const vid = videoRef.current;
    const ctx = canvasRef.current.getContext("2d");
    ctx.drawImage(vid, 0, 0, vid.videoWidth,  vid.videoHeight);
    drawTextWithBackground(ctx);
    requestAnimationFrame(updateCanvas);
  }