animate(now) {
    this.requestAnimationFrame();
    const ctx = this.ctx_;
    if (!this.canvas_ || !ctx || !this.captureTrack_) {
      return;
    }

    // Resize canvas based on displayed size; or if not visible, based on the
    // output video size.
    // VideoFrame prefers to have dimensions that are even numbers.
    if (this.visibility_) {
      this.canvas_.width = roundToEven(this.canvas_.clientWidth);
    } else {
      const outputVideoContainer =
          document.getElementById('outputVideoContainer');
      const outputVideo = outputVideoContainer.firstElementChild;
      if (outputVideo) {
        this.canvas_.width = roundToEven(outputVideo.clientWidth);
      }
    }
    this.canvas_.height = roundToEven(this.canvas_.width / CANVAS_ASPECT_RATIO);

    ctx.fillStyle = '#fff';
    ctx.fillRect(0, 0, this.canvas_.width, this.canvas_.height);

    const linesShown = 20;
    const millisecondsPerLine = 1000;
    const linesIncludingExtraBlank = this.text_.length + linesShown;
    const totalAnimationLength = linesIncludingExtraBlank * millisecondsPerLine;
    const currentFrame = now % totalAnimationLength;
    const firstLineIdx = Math.floor(
        linesIncludingExtraBlank * (currentFrame / totalAnimationLength) -
        linesShown);
    const lineFraction = (now % millisecondsPerLine) / millisecondsPerLine;

    const border = 20;
    const fontSize = (this.canvas_.height - 2 * border) / (linesShown + 1);
    ctx.font = `${fontSize}px sansserif`;

    const textWidth = this.canvas_.width - 2 * border;

    // first line
    if (firstLineIdx >= 0) {
      const fade = Math.floor(256 * lineFraction);
      ctx.fillStyle = `rgb(${fade},${fade},${fade})`;
      const position = (2 - lineFraction) * fontSize;
      ctx.fillText(this.text_[firstLineIdx], border, position, textWidth);
    }

    // middle lines
    for (let line = 2; line <= linesShown - 1; line++) {
      const lineIdx = firstLineIdx + line - 1;
      if (lineIdx >= 0 && lineIdx < this.text_.length) {
        ctx.fillStyle = 'black';
        const position = (line + 1 - lineFraction) * fontSize;
        ctx.fillText(this.text_[lineIdx], border, position, textWidth);
      }
    }

    // last line
    const lastLineIdx = firstLineIdx + linesShown - 1;
    if (lastLineIdx >= 0 && lastLineIdx < this.text_.length) {
      const fade = Math.floor(256 * (1 - lineFraction));
      ctx.fillStyle = `rgb(${fade},${fade},${fade})`;
      const position = (linesShown + 1 - lineFraction) * fontSize;
      ctx.fillText(this.text_[lastLineIdx], border, position, textWidth);
    }

    this.captureTrack_.requestFrame();
  }
@private
@param {number} now current animation timestamp