弹幕留言墙
输入文字即可发射彩色弹幕,支持随机速度 / 颜色 / 位置,可接入直播间或留言墙。
弹幕区 · 发一条试试
弹幕使用 CSS transform 动画驱动,配合 requestAnimationFrame 移除越界元素,性能友好。
使用体验
- 在输入框输入内容(≤40 字),点击“发射”,弹幕从右向左飞过。
- 每条弹幕随机分配颜色(取自预设色板)、纵向位置与速度;点击预设词条可快速体验。
- “清屏”立即移除全部弹幕。演示仅本地展示,接入真实弹幕需 WebSocket 广播。
代码要点
js
const COLORS = ["#6fb3a8", "#7aa2f7", "#e0af68", "#f7768e", "#9ece6a"];
function fire(text) {
const el = document.createElement("div");
el.className = "dm-item";
el.textContent = text;
el.style.color = COLORS[Math.floor(Math.random() * COLORS.length)];
el.style.top = Math.random() * 82 + "%"; // 纵向随机(避开顶部 18%)
const speed = 90 + Math.random() * 120; // px/s
const startX = stage.clientWidth;
const endX = -el.offsetWidth;
el.style.left = startX + "px";
stage.appendChild(el);
let x = startX, last = performance.now();
(function step(now) {
const dt = (now - last) / 1000; last = now;
x -= speed * dt;
el.style.transform = "translateX(" + (x - startX) + "px)";
if (x > endX) requestAnimationFrame(step);
else el.remove(); // 越界即移除,避免 DOM 膨胀
})(last);
}完整可运行工程
可直接运行纯前端 · 零依赖双击 index.html 即用
把演示区的「弹幕留言墙」抽成独立工程:样式随机(颜色 / 速度 / 高度)+ requestAnimationFrame 推进 + 出画回收,另附接直播间的改造点。
danmaku-wall/
├─ index.html 输入区 + 弹幕舞台
├─ app.js 样式随机 + 推进回收
├─ styles.css 样式(跟随系统深浅色)
└─ README.md 运行方式与扩展点
index.html
输入框 / 速度选择 / 弹幕舞台
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>弹幕留言墙 · 独立工程版</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="wrap">
<h1>弹幕留言墙</h1>
<p class="sub">输入文字即可发射彩色弹幕,随机速度 / 颜色 / 高度。</p>
<section class="panel">
<div class="row">
<input id="text" type="text" maxlength="40" placeholder="说点什么…" style="flex:1;min-width:180px">
<select id="speed">
<option value="normal">速度:随机</option>
<option value="slow">速度:慢</option>
<option value="fast">速度:快</option>
</select>
<button id="send" type="button" class="btn">发射</button>
<button id="clear" type="button" class="btn ghost">清空</button>
</div>
<p class="msg" id="status">已发射 0 条。</p>
</section>
<h2>弹幕舞台</h2>
<div class="panel" style="position:relative;overflow:hidden;height:260px" id="stage"></div>
</main>
<script src="app.js"></script>
</body>
</html>
app.js
样式随机(pickStyle 可测)+ 推进与回收
javascript
/* app.js —— 弹幕留言墙:样式随机 + 舞台推进(node 中可 require 做逻辑自测) */
var COLORS = ["#6fb3a8", "#7aa2f7", "#e0af68", "#f7768e", "#9ece6a", "#bb9af7"];
/* pickStyle:纯函数(rand 可注入,便于测试),返回一条弹幕的随机外观 */
function pickStyle(rand, speedMode) {
rand = rand || Math.random;
var base = speedMode === "slow" ? 40 : speedMode === "fast" ? 220 : 60 + rand() * 140;
return {
color: COLORS[Math.floor(rand() * COLORS.length) % COLORS.length],
speed: Math.round(base),
top: Math.floor(rand() * 80) + 8
};
}
function makeDanmaku(text, style, stageWidth) {
return { text: text, color: style.color, speed: style.speed, top: style.top, x: stageWidth };
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { COLORS: COLORS, pickStyle: pickStyle, makeDanmaku: makeDanmaku };
}
/* ---- 浏览器交互层 ---- */
if (typeof document !== "undefined") {
document.addEventListener("DOMContentLoaded", function () {
var stage = document.getElementById("stage"), input = document.getElementById("text"),
sendBtn = document.getElementById("send"), clearBtn = document.getElementById("clear"),
speedSel = document.getElementById("speed"), status = document.getElementById("status");
var items = [], sent = 0, raf = 0, last = 0;
function spawn(text) {
var style = pickStyle(Math.random, speedSel.value);
var d = makeDanmaku(text || "弹幕 " + (sent + 1), style, stage.clientWidth);
var el = document.createElement("span");
el.textContent = d.text;
el.style.cssText = "position:absolute;white-space:nowrap;font-size:15px;font-weight:600;" +
"color:" + d.color + ";top:" + d.top + "%;left:" + d.x + "px;text-shadow:0 1px 2px rgba(0,0,0,.35)";
stage.appendChild(el);
items.push({ d: d, el: el });
sent++;
status.textContent = "已发射 " + sent + " 条。";
}
function step(now) {
var dt = last ? (now - last) / 1000 : 0;
last = now;
for (var i = items.length - 1; i >= 0; i--) {
var it = items[i];
it.d.x -= it.d.speed * dt;
it.el.style.left = it.d.x + "px";
if (it.d.x < -it.el.offsetWidth) {
it.el.remove();
items.splice(i, 1);
}
}
raf = requestAnimationFrame(step);
}
sendBtn.addEventListener("click", function () {
var v = input.value.trim();
if (!v) { input.focus(); return; }
spawn(v);
input.value = "";
input.focus();
});
input.addEventListener("keydown", function (e) { if (e.key === "Enter") sendBtn.click(); });
clearBtn.addEventListener("click", function () {
items.forEach(function (it) { it.el.remove(); });
items = [];
status.textContent = "已清空(累计发射 " + sent + " 条)。";
});
raf = requestAnimationFrame(step);
});
}
styles.css
样式:跟随系统深浅色
css
:root {
--bg: #0e1014; --panel: #171a20; --ink: #e8ecf1; --muted: #9aa4b2; --line: #262b33; --brand: #6fb3a8;
}
@media (prefers-color-scheme: light) {
:root { --bg: #f6f7f8; --panel: #ffffff; --ink: #1a1d21; --muted: #5b636e; --line: #e3e6ea; }
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 24px 16px 48px; background: var(--bg); color: var(--ink);
font: 15px/1.6 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
}
.wrap { max-width: 760px; margin: 0 auto; }
h1 { font-size: 1.5rem; margin: 0 0 6px; }
h2 { font-size: 1.05rem; margin: 22px 0 10px; }
.sub { color: var(--muted); margin: 0 0 18px; }
.panel, .log-wrap { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px; }
.q { font-size: 1.06rem; margin: 0 0 12px; }
.btns, .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.btn {
padding: 8px 16px; border-radius: 8px; border: 1px solid var(--brand);
background: var(--brand); color: #0b0f12; cursor: pointer; font-size: .92rem;
}
.btn.ghost { background: transparent; color: var(--brand); }
.msg { color: var(--muted); margin: 12px 0 0; font-size: .92rem; }
.log { max-height: 200px; overflow: auto; font-size: .88rem; color: var(--muted); }
.log div { padding: 3px 0; border-bottom: 1px dashed var(--line); }
input, select {
padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line);
background: var(--bg); color: var(--ink); font: inherit;
}
canvas { width: 100%; display: block; border-radius: 10px; }
README.md
运行方式与接入 WebSocket 的改造点
markdown
# 弹幕留言墙(独立可运行工程)
作品站 fun 区「弹幕留言墙」演示的独立工程版:纯前端、零依赖。
## 文件
- `index.html`:输入框 + 速度选择 + 舞台
- `app.js`:样式随机(颜色 / 速度 / 高度)、弹幕推进与回收
- `styles.css`:样式
## 运行
1. 双击 `index.html`(或 `python -m http.server 8000`)
2. 逻辑自测:`node -e "console.log(require('./app.js').pickStyle(() => 0.5, 'normal'))"`
→ 期望 `{ color: ..., speed: 130, top: 48 }`
## 扩展点
- 接入直播间:把 `spawn()` 换成 WebSocket 消息回调即可(服务端推送 `{ text }`)
- 长弹幕防重叠:按 `top` 分轨道,记录每条弹幕的"尾部到达时间"再分配轨道
运行方式
- 新建一个目录,把上面全部文件按文件名分别保存(用「一键复制整份工程」可一次拿全)。
- 前端部分:直接双击
index.html即可运行;若浏览器限制本地文件读取,执行python -m http.server 8000后访问http://localhost:8000/。 - 逻辑自测:各文件
README.md中给出了对应的 Node / Python 自测命令(均为真实可跑的最小用例)。
标注:可直接运行、无需构建;要改造只需动 app.js 顶部的常量与 styles.css 里的变量。
依赖清单:无第三方依赖 —— 纯前端(Canvas + DOM),无需安装任何包
可直接运行:可直接运行 双击 index.html 即可发弹幕;命令行自测 node -e "console.log(require('./app.js').pickStyle(() => 0.5, 'normal'))"
需先改的配置项:无配置项需要修改。要接直播间时把 spawn() 换成 WebSocket 消息回调
来源参考
实现涉及的官方文档(可直接访问):