<!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 ?? " "
}</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(" "), ...charsFixed];
charsLength = chars.length;
});
const play = pane.addBinding(config, "play");
play.on("change", ({ value }) =>
value ? video.play() : video.pause()
);
})();
</script>
</body>
</html>