字符视频生成器

Published on
/
/趣玩前端
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>字符视频生成器</title>
    <link
      rel="stylesheet"
      href="https://public.codepenassets.com/css/normalize-5.0.0.min.css"
    />
    <style>
      :root {
        --color-primary: #ee75d2;
        --color-secondary: #75d8ee;
        --color-tertiary: #deee75;
        --color-quaternary: #9375ee;
        --color-surface: #271c22;
        --brightness: 1;
      }

      #output {
        position: relative;
        text-align: center;
        border-radius: 2rem;
        font-family: "SF Mono", monospace;
        overflow: hidden;
        filter: drop-shadow(
            0 0 10rem color-mix(in srgb, var(--color), transparent 20%)
          )
          brightness(var(--brightness));
        transition: filter 0.3s linear;
        white-space: nowrap;
        background: black;
      }
      #output div,
      #output span {
        white-space: nowrap;
      }

      #input,
      #prerender {
        display: none;
        position: absolute;
        left: 0;
        top: 0;
      }

      .file-input-container {
        position: fixed;
        top: 20px;
        left: 20px;
        z-index: 1000;
        background: var(--color-surface);
        padding: 15px;
        border-radius: 10px;
        border: 2px solid var(--color-primary);
        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
      }

      .file-input-container label {
        display: block;
        color: var(--color-primary);
        margin-bottom: 10px;
        font-weight: bold;
        font-size: 14px;
      }

      .file-input-container input[type="file"] {
        background: var(--color-secondary);
        color: var(--color-surface);
        padding: 8px 12px;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        font-size: 12px;
      }

      .file-input-container input[type="file"]:hover {
        background: var(--color-tertiary);
      }

      :root {
        font-size: 60%;
      }

      body {
        width: 100vw;
        height: 100vh;
        display: grid;
        place-items: center;
        background-color: color-mix(in srgb, var(--color-surface), black 40%);
        color: var(--color-primary);
        overflow: hidden;
      }

      a.labs-follow-me {
        left: 2rem;
        right: 2rem;
        bottom: 1rem;
        top: unset;
        text-align: center;
      }

      * {
        box-sizing: border-box;
      }

      :root {
        --tp-base-background-color: var(--color-surface);
        --tp-base-shadow-color: hsla(0, 0%, 0%, 0.2);
        --tp-button-background-color: var(--color-secondary);
        --tp-button-background-color-active: hsla(230, 7%, 85%, 1);
        --tp-button-background-color-focus: hsla(230, 7%, 80%, 1);
        --tp-button-background-color-hover: hsla(230, 7%, 75%, 1);
        --tp-button-foreground-color: hsla(230, 7%, 17%, 1);
        --tp-container-background-color: hsla(230, 7%, 75%, 0.1);
        --tp-container-background-color-active: hsla(230, 7%, 75%, 0.25);
        --tp-container-background-color-focus: hsla(230, 7%, 75%, 0.2);
        --tp-container-background-color-hover: hsla(230, 7%, 75%, 0.15);
        --tp-container-foreground-color: hsla(230, 7%, 75%, 1);
        --tp-groove-foreground-color: hsla(230, 7%, 75%, 0.1);
        --tp-input-background-color: hsla(230, 7%, 75%, 0.1);
        --tp-input-background-color-active: hsla(230, 7%, 75%, 0.25);
        --tp-input-background-color-focus: hsla(230, 7%, 75%, 0.2);
        --tp-input-background-color-hover: hsla(230, 7%, 75%, 0.15);
        --tp-input-foreground-color: var(--color-primary);
        --tp-label-foreground-color: hsla(230, 7%, 75%, 0.7);
        --tp-monitor-background-color: hsla(230, 7%, 0%, 0.2);
        --tp-monitor-foreground-color: hsla(230, 7%, 75%, 0.7);
      }
    </style>
  </head>
  <body>
    <div class="file-input-container">
      <label for="videoFile">选择本地视频文件:</label>
      <input type="file" id="videoFile" accept="video/*" />
    </div>

    <div id="output"></div>
    <video id="input" autoplay muted loop playsinline crossorigin="anonymous">
      <source src="./download.mp4" type="video/mp4" />
    </video>

    <canvas id="prerender" width="96" height="32"></canvas>

    <script type="module">
      import { Pane } from "https://cdn.jsdelivr.net/npm/tweakpane@4.0.5/dist/tweakpane.min.js";

      (() => {
        const video = document.getElementById("input");
        const canvas = document.getElementById("prerender");
        const output = document.getElementById("output");
        const fileInput = document.getElementById("videoFile");
        const ctx = canvas.getContext("2d", { willReadFrequently: true });
        const charsFixed = [
          "_",
          ".",
          ",",
          "-",
          "=",
          "+",
          ":",
          ";",
          "c",
          "b",
          "a",
          "!",
          "?",
          "0",
          "1",
          "2",
          "3",
          "4",
          "5",
          "6",
          "7",
          ["9", "8"],
          ["✚", "✚", "✚", "✚", "✚", "⚛︎"],
          ["☺︎", "☹︎"],
          "☀︎",
          ["@", "#"],
          ["X", "Y", "Z"],
          "'",
        ];
        let chars = [...charsFixed];
        let charsLength = chars.length;
        const MAX_COLOR_INDEX = 255;

        fileInput.addEventListener("change", (event) => {
          const file = event.target.files[0];
          if (file) {
            const url = URL.createObjectURL(file);
            video.src = url;
            video.onloadeddata = () => {
              video.play();
            };

            config.video = "Local";
            videoSelection.refresh();
          }
        });

        const updateCanvas = () => {
          const w = canvas.width;
          const h = canvas.height;
          ctx.drawImage(video, 0, 0, w, h);
          const data = ctx.getImageData(0, 0, w, h).data;
          let outputText = "";
          for (let y = 0; y < h; y++) {
            let row = "";
            for (let x = 0; x < w; x++) {
              const index = (x + y * w) * 4;
              const [r, g, b] = data.slice(index, index + 3);
              const c = (r + g + b) / 3;
              const charIndex = Math.floor(
                (charsLength * ((c * 100) / MAX_COLOR_INDEX)) / 100
              );
              const result = chars[charIndex];
              const char = Array.isArray(result)
                ? result[Math.floor(Math.random() * result.length) + 0]
                : result;

              row += `<span style="color: rgb(${r}, ${g}, ${b});">${
                char ?? "&nbsp;"
              }</span>`;
            }
            outputText += `<div>${row}</div>`;
          }
          output.innerHTML = outputText;
          output.style.setProperty(
            "--color",
            `rgb(${data[0]}, ${data[1]}, ${data[2]})`
          );

          setTimeout(() => requestAnimationFrame(updateCanvas), 0);
        };

        requestAnimationFrame(() => {
          video.play();
          updateCanvas();
        });

        const config = {
          speed: 1,
          zoom: 60,
          isolation: 0,
          brightness: 1,
          play: true,
        };

        const pane = new Pane({ title: "Config", expanded: false });
        const speed = pane.addBinding(config, "speed", {
          min: 0,
          max: 2,
          step: 0.1,
        });
        speed.on("change", ({ value }) => {
          video.playbackRate = value;
        });

        const brightness = pane.addBinding(config, "brightness", {
          min: 0,
          max: 2,
          step: 0.1,
        });
        brightness.on("change", ({ value }) => {
          output.style.setProperty("--brightness", `${value}`);
        });

        const zoom = pane.addBinding(config, "zoom", {
          min: 10,
          max: 200,
          step: 1,
        });
        zoom.on("change", ({ value }) => {
          document.documentElement.style.fontSize = `${value}%`;
        });

        const isolation = pane.addBinding(config, "isolation", {
          min: 0,
          max: 50,
          step: 1,
        });
        isolation.on("change", ({ value }) => {
          chars = [...new Array(value).fill("&nbsp;"), ...charsFixed];
          charsLength = chars.length;
        });

        const play = pane.addBinding(config, "play");
        play.on("change", ({ value }) =>
          value ? video.play() : video.pause()
        );
      })();
    </script>
  </body>
</html>