Текст на холсте стирается с помощью видео-холста под ним

#graphics #html5-canvas #html5-video

Вопрос:

Примечание: Минимальный рабочий пример здесь. Нажмите на change text кнопку, и вы увидите, что текст появится на один кадр и исчезнет.

Я хотел добавить наложение на свое видео, поэтому сложил два холста и заполнил текст на прозрачном верхнем холсте.

Однако текст не прилипает. Оно исчезает.

Чтобы проверить, правильно ли я заполнял текст, я попытался использовать черный холст (без видео) под ним.

Мне просто нужно было один раз заполнить текст, и текст остался.

Однако с видео-холстом под ним текст не будет прилипать, если я не нарисую текст в requestAnimationFrame, который, как я полагаю, продолжает рисовать один и тот же текст в каждом кадре, что необязательно.

Предполагается, что текстовый холст должен быть отделен от видео-холста. Почему это уничтожается без requestAnimationFrame?

Как я могу это исправить?

Минимальный рабочий пример здесь, но код также показан ниже.

 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>
  );
}
 

Ответ №1:

Очень простое исправление

Холст очищается из-за того, как реагирует состояние обновлений.

Исправление

Самое простое решение-использовать только один холст и нарисовать текст на холсте, используемом для отображения видео.

Из рабочего примера измените две функции updateCanvas и drawTextWithBackground выполните следующие действия.

Примечание.Я удалил все сохранение состояния drawTextWithBackground , так как состояние холста все равно теряется из-за реакции, так зачем использовать очень дорогие функции стека состояний.

   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);
  }
 

Комментарии:

1. Спасибо. Можете ли вы подробнее объяснить, почему это связано с «состоянием обновлений реакции»? какое состояние в моей первоначальной функции было причиной этого?

2. Кстати, это было решено автоматически, когда я переработал, чтобы извлечь компоненты canvas из своего приложения. Может быть, это потому, что я отделил ненужные состояния от полотен?

3. @EricNa Изменения состояния, которые влияют на DOM, могут привести к повторному отображению элементов (изменение свойств) . Реакция называет это Примирением reactjs.org/docs/reconciliation.html . Запись в атрибуты ширины или высоты холста очистит холст.

Ответ №2:

когда вы обновляете холст из видео, все пиксели на холсте обновляются пикселями из видео. Думай об этом как о стене:

ты нарисовал «вот мое сообщение» на стене.

Потом ты выкрасил всю стену в синий цвет. (ваш текст исчез, а стена просто синяя).

Если вы покрасите всю стену в синий цвет, а затем перекрасите «вот мой текст», вы все равно увидите свой текст на синей стене.

Когда вы перерисовываете холст, вам нужно заново нарисовать все, что вы хотите отобразить на холсте. Если видео «покрывает» ваш текст, вам нужно перерисовать текст.

рабочий пример на https://github.com/dougsillars/chyronavideo который рисует хирон (полосу в нижней части новостного сюжета) на каждом кадре холста.

Комментарии:

1. Спасибо, но я использовал отдельный холст для текста поверх холста для видео именно по этой причине. Я все еще не понимаю, почему рисование на видео-холсте стирает мой текстовый холст. Текстовое полотно прозрачно и находится сверху.

2. возможно, вам следует использовать разные переменные для каждого холста: ctx1 и ctx2 Вы можете определить константу только один раз, поэтому я думаю, что все они отображаются на одном из холстов.