一个功能相对完善的前端弹幕系统。这个系统将支持半屏、全屏、1/4屏显示模式,以及不同颜色的滚动弹幕和固定位置弹幕(可定时消失或常驻)。
核心思路:
-
HTML 结构: 包含视频(或占位符)、弹幕渲染区域、控制按钮(模式切换)、输入区域。
-
CSS 样式: 定义容器、弹幕层、不同模式下的样式、弹幕自身样式(颜色、字体、动画)、固定弹幕位置。
-
JavaScript 逻辑:
- 弹幕类 (Barrage Class): 封装单条弹幕的数据(文本、颜色、类型、速度、位置)和行为(移动、渲染、判断是否移出屏幕)。
- 弹幕管理器 (Barrage Manager): 负责接收新弹幕、管理弹幕轨道(避免严重重叠)、控制弹幕的创建、动画循环、销毁、模式切换、处理固定弹幕的计时。
- 动画循环: 使用
requestAnimationFrame实现流畅的弹幕移动。 - 事件处理: 处理发送按钮点击、模式切换按钮点击。
完整 HTML 文件 (包含 CSS 和 JavaScript)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>功能完善的弹幕系统</title>
<style>
/* CSS 样式 */
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: #f0f0f0;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 20px;
}
/* 弹幕容器总父级 */
.barrage-container-wrapper {
width: 80%; /* 或者根据需要设置更具体的宽度 */
max-width: 960px; /* 最大宽度 */
margin-bottom: 20px;
/* 为了演示方便,给个背景色 */
background-color: #eee;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
/* 弹幕容器,相对定位是关键,作为弹幕项绝对定位的基准 */
.barrage-container {
position: relative;
width: 100%;
/* 高度由视频或内容决定,这里用aspect-ratio模拟 */
aspect-ratio: 16 / 9;
background-color: #333; /* 模拟视频区域背景 */
overflow: hidden; /* 隐藏超出容器的弹幕 */
border-radius: 3px;
}
/* 视频占位符 (可以用实际 <video> 替换) */
.video-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 24px;
background-color: #222; /* 深一点的背景 */
z-index: 1; /* 在弹幕层下方 */
}
/* 弹幕渲染层,覆盖在视频/内容之上 */
.barrage-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%; /* 默认全屏 */
z-index: 10; /* 确保在视频之上 */
pointer-events: none; /* 允许点击穿透到视频播放器 */
overflow: hidden; /* 再次确认隐藏溢出 */
transition: height 0.3s ease, top 0.3s ease; /* 为模式切换添加过渡动画 */
}
/* --- 屏幕模式 --- */
.barrage-overlay.full-screen {
top: 0;
height: 100%;
}
.barrage-overlay.half-screen {
top: 0;
height: 50%; /* 半屏 */
}
.barrage-overlay.quarter-screen {
top: 0;
height: 25%; /* 1/4屏 */
}
/* --- 弹幕项基础样式 --- */
.barrage-item {
position: absolute; /* 绝对定位,相对于 .barrage-overlay */
white-space: nowrap; /* 防止弹幕文字换行 */
color: #fff; /* 默认白色 */
font-size: 20px; /* 默认字号,可调整 */
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); /* 文字描边,增强可读性 */
will-change: transform; /* 性能优化:提示浏览器该元素会变化 */
padding: 2px 5px;
border-radius: 3px;
background-color: rgba(0, 0, 0, 0.2); /* 轻微背景,可选 */
/* 默认从右侧进入 */
transform: translateX(0); /* JS会更新这个值 */
left: 100%; /* 起始位置在容器右侧外部 */
}
/* --- 滚动弹幕特定样式 --- */
.barrage-item.scroll {
/* 速度由JS控制 */
}
/* --- 固定弹幕特定样式 --- */
.barrage-item.fixed-top,
.barrage-item.fixed-bottom {
/* 固定弹幕不从右侧进入,直接定位 */
left: 50%;
transform: translateX(-50%); /* 水平居中 */
text-align: center;
}
.barrage-item.fixed-top {
top: 10px; /* 距离顶部距离 */
}
.barrage-item.fixed-bottom {
bottom: 10px; /* 距离底部距离 */
/* 注意:如果 .barrage-overlay 高度变化(如半屏),bottom 依然相对于 overlay 的底部 */
}
/* --- 控制区域 --- */
.barrage-controls,
.barrage-input-area {
margin-top: 15px;
display: flex;
gap: 10px; /* 元素间距 */
justify-content: center;
align-items: center;
flex-wrap: wrap; /* 允许换行 */
}
.barrage-controls button,
.barrage-input-area button {
padding: 8px 15px;
font-size: 14px;
cursor: pointer;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
transition: background-color 0.2s ease;
}
.barrage-controls button:hover,
.barrage-input-area button:hover {
background-color: #0056b3;
}
.barrage-controls button.active {
background-color: #28a745; /* 激活状态 */
}
.barrage-input-area input[type="text"] {
padding: 8px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
flex-grow: 1; /* 占据多余空间 */
min-width: 150px;
}
.barrage-input-area input[type="color"] {
padding: 0;
border: none;
width: 40px;
height: 36px; /* 与按钮高度接近 */
cursor: pointer;
vertical-align: middle; /* 对齐 */
}
.barrage-input-area select {
padding: 8px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
height: 36px; /* 与按钮高度接近 */
}
/* 响应式调整 - 简单示例 */
@media (max-width: 768px) {
.barrage-container-wrapper {
width: 95%;
}
.barrage-item {
font-size: 16px; /* 移动端适当减小字号 */
}
.barrage-controls,
.barrage-input-area {
flex-direction: column; /* 窄屏时垂直排列 */
align-items: stretch; /* 拉伸以填充宽度 */
}
.barrage-input-area input[type="text"] {
min-width: unset; /* 移除最小宽度限制 */
width: 100%; /* 占据全部宽度 */
box-sizing: border-box; /* 防止 padding 导致溢出 */
}
.barrage-input-area > * { /* 让输入区域所有子元素宽度一致 */
width: 100%;
box-sizing: border-box;
margin-bottom: 5px; /* 添加一些间距 */
}
.barrage-input-area input[type="color"]{
width: 100%; /* 颜色选择器也撑满 */
}
}
</style>
</head>
<body>
<div class="barrage-container-wrapper">
<!-- 弹幕容器 -->
<div class="barrage-container">
<!-- 视频占位符 -->
<div class="video-placeholder">模拟视频区域</div>
<!-- 弹幕渲染层 -->
<div class="barrage-overlay full-screen" id="barrage-overlay">
<!-- 弹幕将动态添加到这里 -->
</div>
</div>
<!-- 控制按钮 -->
<div class="barrage-controls">
<span>显示模式:</span>
<button id="mode-full" class="active" data-mode="full-screen">全屏</button>
<button id="mode-half" data-mode="half-screen">半屏</button>
<button id="mode-quarter" data-mode="quarter-screen">1/4屏</button>
<button id="btn-pause">暂停弹幕</button>
<button id="btn-resume" style="display: none;">恢复弹幕</button>
<button id="btn-clear">清空弹幕</button>
</div>
<!-- 输入区域 -->
<div class="barrage-input-area">
<input type="text" id="barrage-text" placeholder="输入弹幕内容...">
<input type="color" id="barrage-color" value="#FFFFFF" title="选择颜色">
<select id="barrage-type">
<option value="scroll" selected>滚动</option>
<option value="top">顶部固定</option>
<option value="bottom">底部固定</option>
</select>
<input type="number" id="barrage-duration" placeholder="固定时长(秒, 0=常驻)" min="0" style="width: 150px;">
<button id="send-barrage">发送弹幕</button>
</div>
</div>
<script>
// JavaScript 逻辑
(function() {
"use strict"; // 启用严格模式
// --- 配置项 ---
const CONFIG = {
TRACK_HEIGHT: 30, // 每条弹幕轨道的高度(用于计算轨道数量)
DEFAULT_SPEED: 2, // 滚动弹幕默认速度 (像素/帧)
MAX_SPEED_OFFSET: 1, // 滚动弹幕速度随机偏移量上限
MIN_SPEED_OFFSET: -0.5, // 滚动弹幕速度随机偏移量下限
FIXED_DURATION: 5000, // 固定弹幕默认显示时长 (毫秒)
BUFFER_TIME: 3000, // 轨道预留时间 (毫秒),防止弹幕刚出现就重叠
FONT_SIZE: 20, // 基础字号,应与CSS同步或动态获取
};
// --- DOM 元素获取 ---
const barrageOverlay = document.getElementById('barrage-overlay');
const barrageInput = document.getElementById('barrage-text');
const colorInput = document.getElementById('barrage-color');
const typeSelect = document.getElementById('barrage-type');
const durationInput = document.getElementById('barrage-duration');
const sendButton = document.getElementById('send-barrage');
const modeButtons = document.querySelectorAll('.barrage-controls button[data-mode]');
const pauseButton = document.getElementById('btn-pause');
const resumeButton = document.getElementById('btn-resume');
const clearButton = document.getElementById('btn-clear');
// --- 状态变量 ---
let isPaused = false;
let animationFrameId = null;
let activeBarrages = []; // 存储当前屏幕上所有弹幕对象
let tracks = []; // 存储滚动弹幕轨道信息
let overlayWidth = barrageOverlay.offsetWidth;
let overlayHeight = barrageOverlay.offsetHeight;
let trackCount = Math.floor(overlayHeight / CONFIG.TRACK_HEIGHT);
// --- 弹幕类 (Barrage Class) ---
class Barrage {
constructor(text, options = {}) {
this.text = text;
this.color = options.color || '#FFFFFF';
this.type = options.type || 'scroll'; // 'scroll', 'top', 'bottom'
this.duration = options.duration; // 固定弹幕的显示时长(ms), undefined 表示常驻或滚动
this.id = `barrage-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // 唯一ID
this.el = this.createElement(); // 创建DOM元素
if (this.type === 'scroll') {
this.speed = options.speed || (CONFIG.DEFAULT_SPEED + Math.random() * (CONFIG.MAX_SPEED_OFFSET - CONFIG.MIN_SPEED_OFFSET) + CONFIG.MIN_SPEED_OFFSET);
this.track = options.track; // 分配的轨道号
this.yPos = this.track * CONFIG.TRACK_HEIGHT + Math.random() * (CONFIG.TRACK_HEIGHT - CONFIG.FONT_SIZE); // 在轨道内随机垂直位置
this.xPos = overlayWidth; // 初始X坐标在屏幕右侧外
this.el.style.top = `${Math.max(0, Math.min(this.yPos, overlayHeight - CONFIG.FONT_SIZE))}px`; // 限制在可视区
this.el.style.left = '100%'; // CSS left 控制初始位置
this.el.style.transform = `translateX(0px)`; // JS通过transform移动
} else {
// 固定弹幕
this.el.classList.add(this.type === 'top' ? 'fixed-top' : 'fixed-bottom');
// 位置由CSS控制 (left: 50%, transform: translateX(-50%), top/bottom)
if (typeof this.duration === 'number' && this.duration > 0) {
this.timeoutId = setTimeout(() => {
this.remove();
}, this.duration);
}
// 注意:固定弹幕不参与滚动动画循环的位置更新
}
}
// 创建弹幕DOM元素
createElement() {
const el = document.createElement('div');
el.id = this.id;
el.classList.add('barrage-item', this.type); // 添加基础类和类型类
el.textContent = this.text;
el.style.color = this.color;
// el.style.fontSize = `${CONFIG.FONT_SIZE}px`; // 如果需要动态字号
// 性能优化:对于频繁移动的元素,使用translateZ(0)或will-change可以提升性能
el.style.willChange = 'transform';
// el.style.transform = 'translateZ(0)'; // 强制GPU加速(可能过度优化)
return el;
}
// 渲染到屏幕 (添加到DOM)
render() {
barrageOverlay.appendChild(this.el);
// 获取实际宽度,用于判断何时离开屏幕以及轨道占用计算
this.width = this.el.offsetWidth;
}
// 移动弹幕 (仅滚动类型)
move() {
if (this.type !== 'scroll' || isPaused) return;
this.xPos -= this.speed;
this.el.style.transform = `translateX(${this.xPos - overlayWidth}px)`; // 相对于初始left:100%的位置移动
// console.log(`Moving ${this.id}: xPos=${this.xPos}`); // Debugging
}
// 判断弹幕是否完全移出屏幕左侧 (滚动类型)
isOffscreen() {
// 当弹幕的右边缘移动到屏幕左边缘之外时,视为移出
return this.type === 'scroll' && (this.xPos + this.width < 0);
}
// 从DOM中移除并清理资源
remove() {
// console.log(`Removing ${this.id}`); // Debugging
if (this.el && this.el.parentNode) {
this.el.parentNode.removeChild(this.el);
}
// 如果是定时固定的弹幕,清除定时器
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
// 从 activeBarrages 数组中移除自身
const index = activeBarrages.findIndex(b => b.id === this.id);
if (index > -1) {
activeBarrages.splice(index, 1);
}
}
} // End of Barrage Class
// --- 弹幕管理器 功能 ---
// 初始化轨道信息
function initializeTracks() {
tracks = [];
overlayHeight = barrageOverlay.offsetHeight; // 获取当前实际高度
trackCount = Math.max(1, Math.floor(overlayHeight / CONFIG.TRACK_HEIGHT)); // 至少1条轨道
console.log(`Overlay height: ${overlayHeight}px, Track count: ${trackCount}`);
for (let i = 0; i < trackCount; i++) {
// lastBarrageEndTime: 记录该轨道最后一条弹幕的尾部离开屏幕右侧(即刚完全进入屏幕)的时间戳
// 用于防止新弹幕在轨道刚空闲时就立即发射,造成视觉重叠
tracks.push({ id: i, lastBarrageEndTime: 0 });
}
}
// 寻找一个合适的滚动弹幕轨道
function findAvailableTrack() {
const now = Date.now();
// 尝试寻找完全空闲或等待时间最短的轨道
let bestTrack = -1;
let minEndTime = Infinity;
for (let i = 0; i < tracks.length; i++) {
if (tracks[i].lastBarrageEndTime <= now) {
// 找到一个当前可用的轨道
bestTrack = i;
break; // 优先使用立即可用的
} else if (tracks[i].lastBarrageEndTime < minEndTime) {
// 如果没有立即可用的,找一个最快可用的
minEndTime = tracks[i].lastBarrageEndTime;
bestTrack = i;
}
}
// 如果所有轨道都在缓冲期,随机选一个(或选择等待时间最短的那个)
if (bestTrack === -1 && tracks.length > 0) {
// 简单策略:轮流分配或随机分配
// bestTrack = Math.floor(Math.random() * tracks.length);
// 或者就用上面找到的 minEndTime 对应的 bestTrack
console.warn("All tracks busy, might cause overlap. Assigning to track:", bestTrack);
} else if (tracks.length === 0) {
console.error("No tracks available!");
return -1; // 没有轨道可用
}
return bestTrack;
}
// 更新轨道占用信息
function updateTrackUsage(trackIndex, barrageWidth, speed) {
if (trackIndex < 0 || trackIndex >= tracks.length) return;
// 计算弹幕完全进入屏幕所需时间 (ms)
const enterDuration = (barrageWidth / speed) * (1000 / 60); // 假设60fps
// 计算弹幕尾部离开屏幕右侧的时间戳
// 加上一个缓冲时间,避免紧挨着发射
const endTime = Date.now() + enterDuration + CONFIG.BUFFER_TIME;
tracks[trackIndex].lastBarrageEndTime = Math.max(tracks[trackIndex].lastBarrageEndTime, endTime);
// console.log(`Track ${trackIndex} updated, next available at ${new Date(endTime).toLocaleTimeString()}`);
}
// 添加一条新弹幕
function addBarrage(text, options = {}) {
if (!text || text.trim() === "") return; // 不添加空弹幕
const barrageOptions = { ...options }; // 复制一份,避免修改原对象
if (barrageOptions.type === 'scroll') {
const trackIndex = findAvailableTrack();
if (trackIndex === -1) {
console.warn("No available track for scroll barrage:", text);
return; // 没有可用轨道,暂时不发送
}
barrageOptions.track = trackIndex;
const newBarrage = new Barrage(text, barrageOptions);
activeBarrages.push(newBarrage);
newBarrage.render(); // 添加到DOM
// 渲染后才能获取宽度
requestAnimationFrame(() => {
if (newBarrage.el) { // 确保元素还在
newBarrage.width = newBarrage.el.offsetWidth;
updateTrackUsage(trackIndex, newBarrage.width, newBarrage.speed);
}
});
} else { // 固定弹幕 (top/bottom)
// 可以考虑限制同位置固定弹幕数量,这里简化处理,直接添加
const newBarrage = new Barrage(text, barrageOptions);
activeBarrages.push(newBarrage);
newBarrage.render();
}
// console.log("Added barrage:", text, barrageOptions);
}
// 动画循环
function animationLoop() {
if (isPaused) {
animationFrameId = requestAnimationFrame(animationLoop);
return;
}
// 清理旧的(已移出屏幕的)滚动弹幕
activeBarrages = activeBarrages.filter(barrage => {
if (barrage.type === 'scroll' && barrage.isOffscreen()) {
barrage.remove(); // 从DOM移除
return false; // 从数组移除
}
return true;
});
// 移动所有活动的滚动弹幕
activeBarrages.forEach(barrage => {
if (barrage.type === 'scroll') {
barrage.move();
}
});
// 继续下一帧
animationFrameId = requestAnimationFrame(animationLoop);
}
// 暂停弹幕
function pauseBarrages() {
if (isPaused) return;
isPaused = true;
pauseButton.style.display = 'none';
resumeButton.style.display = 'inline-block';
// 注意:这里只暂停了滚动弹幕的移动,固定弹幕的定时器不受影响
// 如果需要暂停定时器,需要额外逻辑记录剩余时间
console.log("Barrages paused.");
}
// 恢复弹幕
function resumeBarrages() {
if (!isPaused) return;
isPaused = false;
pauseButton.style.display = 'inline-block';
resumeButton.style.display = 'none';
// 无需重新启动 animationLoop,因为它内部会检查 isPaused 状态
console.log("Barrages resumed.");
}
// 清空所有弹幕
function clearBarrages() {
console.log("Clearing all barrages...");
// 停止动画循环以防在移除过程中添加新弹幕
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
// 移除所有弹幕元素并清除定时器
activeBarrages.forEach(barrage => barrage.remove()); // remove方法会处理定时器
activeBarrages = []; // 清空数组
// 重置轨道状态
initializeTracks();
// 如果是暂停状态,恢复按钮状态
isPaused = false;
pauseButton.style.display = 'inline-block';
resumeButton.style.display = 'none';
// 重新启动动画循环(如果需要,比如希望清空后还能继续接收新弹幕)
startAnimationLoop();
console.log("All barrages cleared.");
}
// 设置显示模式
function setMode(mode) {
barrageOverlay.className = `barrage-overlay ${mode}`; // 更新CSS类
// 更新当前激活按钮的样式
modeButtons.forEach(button => {
button.classList.toggle('active', button.dataset.mode === mode);
});
// 模式改变后,overlay尺寸可能变化,需要重新计算轨道
// 延迟一点执行,等待CSS过渡完成(如果需要精确)
// 或者立即执行,但要知道尺寸可能不是最终过渡后的尺寸
setTimeout(() => {
overlayWidth = barrageOverlay.offsetWidth;
// 注意:高度变化会影响轨道计算,需要重新初始化
initializeTracks();
// 可选:是否清空现有弹幕?或者让它们在旧尺寸下继续?
// 清空可能是最简单的处理方式
// clearBarrages(); // 取消注释则切换模式时清空
console.log(`Mode changed to ${mode}. Overlay dimensions: ${overlayWidth}x${overlayHeight}. Tracks re-initialized.`);
}, 50); // 短暂延迟,可以根据CSS过渡时间调整
}
// 处理发送按钮点击
function handleSendBarrage() {
const text = barrageInput.value;
const color = colorInput.value;
const type = typeSelect.value;
let duration = durationInput.value ? parseInt(durationInput.value, 10) * 1000 : undefined; // 转毫秒
if (type !== 'scroll' && duration === undefined) {
// 如果是固定弹幕且未指定时长,给个默认值或设为0表示常驻
duration = CONFIG.FIXED_DURATION; // 或者设为 0
// durationInput.value = CONFIG.FIXED_DURATION / 1000; // 更新输入框显示默认值
}
if (type !== 'scroll' && duration === 0) {
duration = undefined; // 0 表示常驻,用 undefined 传递给 Barrage 类
}
addBarrage(text, { color, type, duration });
barrageInput.value = ''; // 清空输入框
}
// 启动动画循环
function startAnimationLoop() {
if (!animationFrameId) {
animationFrameId = requestAnimationFrame(animationLoop);
}
}
// --- 事件监听器 ---
sendButton.addEventListener('click', handleSendBarrage);
barrageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleSendBarrage();
}
});
modeButtons.forEach(button => {
button.addEventListener('click', () => {
setMode(button.dataset.mode);
});
});
pauseButton.addEventListener('click', pauseBarrages);
resumeButton.addEventListener('click', resumeBarrages);
clearButton.addEventListener('click', clearBarrages);
// 当窗口大小改变时,重新计算尺寸和轨道(节流处理更佳)
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
console.log("Window resized, recalculating...");
overlayWidth = barrageOverlay.offsetWidth;
// 高度变化需要重新初始化轨道
initializeTracks();
// 可选:是否清空或调整现有弹幕?
}, 250); // 延迟250ms执行,避免频繁触发
});
// --- 初始化 ---
function init() {
console.log("Initializing Barrage System...");
initializeTracks(); // 初始化轨道信息
startAnimationLoop(); // 启动动画循环
// 设置默认模式激活状态
setMode('full-screen'); // 默认全屏
// 添加一些示例弹幕
addBarrage("欢迎来到弹幕世界!", { color: "#FFD700", type: "scroll" });
setTimeout(() => addBarrage("这是一条滚动弹幕~", { color: "#00FF00" }), 1000);
setTimeout(() => addBarrage("顶部固定弹幕 (5秒)", { color: "#FF69B4", type: "top", duration: 5000 }), 2000);
setTimeout(() => addBarrage("底部常驻弹幕", { color: "#1E90FF", type: "bottom", duration: 0 }), 3000); // duration 0 表示常驻
setTimeout(() => addBarrage("1/4屏模式试试看?", { color: "#FFA500" }), 5000);
console.log("Barrage System Initialized.");
}
// --- 启动 ---
// 等待DOM加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init(); // DOM已加载
}
})(); // 立即执行函数表达式 (IIFE),避免污染全局作用域
</script>
</body>
</html>
代码讲解:
-
HTML 结构 (
<body>部分):.barrage-container-wrapper: 包裹整个弹幕显示区域和控制区域,方便整体布局和响应式设计。.barrage-container: 核心容器,position: relative是关键,作为内部绝对定位元素的参考。aspect-ratio模拟视频比例,overflow: hidden防止弹幕溢出容器。.video-placeholder: 一个简单的div模拟视频区域,实际应用中可以替换为<video>标签。z-index: 1确保它在弹幕层下方。.barrage-overlay: 弹幕实际渲染的图层,position: absolute覆盖在.barrage-container上。z-index: 10确保它在最上层。pointer-events: none允许鼠标事件穿透到下方的视频播放器。这个元素的height和top会根据模式(全屏、半屏、1/4屏)通过添加/移除 CSS 类来改变。.barrage-controls: 包含模式切换、暂停/恢复、清空按钮。.barrage-input-area: 包含文本输入框、颜色选择器、类型下拉框、固定时长输入框和发送按钮。
-
CSS 样式 (
<style>部分):-
基础布局: 使用 Flexbox 居中
.barrage-container-wrapper。 -
容器样式: 设置
.barrage-container和.barrage-overlay的定位、尺寸、背景、层级。transition属性为.barrage-overlay的height和top变化添加平滑过渡效果。 -
屏幕模式:
.full-screen,.half-screen,.quarter-screen类通过修改height和top来控制弹幕显示区域的大小和位置。 -
弹幕项样式 (
.barrage-item):position: absolute: 相对于.barrage-overlay定位。white-space: nowrap: 防止文字换行。color,font-size,font-weight,text-shadow: 定义外观。text-shadow模拟描边效果,提高在复杂背景下的可读性。will-change: transform: 性能优化提示,告知浏览器transform属性会频繁变动。left: 100%: 滚动弹幕的初始水平位置在容器右侧外部。transform: translateX(0): 初始变换值,JavaScript 将通过修改translateX的值来实现向左移动。
-
固定弹幕样式:
.fixed-top,.fixed-bottom类覆盖了部分.barrage-item样式。它们使用left: 50%; transform: translateX(-50%);实现水平居中,并用top或bottom属性固定垂直位置。 -
控件样式: 美化按钮、输入框等。
.active类用于高亮当前选中的模式按钮。 -
响应式设计: 使用
@media (max-width: 768px)为窄屏幕(如手机)提供不同的布局,例如将控制按钮和输入区域垂直排列,输入框宽度调整为100%。
-
-
JavaScript 逻辑 (
<script>部分):-
IIFE (立即执行函数表达式):
(function() { ... })();创建了一个独立的作用域,避免定义的变量和函数污染全局命名空间。 -
严格模式:
"use strict";启用 JavaScript 的严格模式,有助于捕捉常见错误。 -
配置项 (CONFIG): 将一些可调参数(如轨道高度、速度、默认时长等)集中管理,方便修改。
-
DOM 元素获取: 获取需要操作的 HTML 元素的引用。
-
状态变量:
isPaused: 标记弹幕是否处于暂停状态。animationFrameId: 存储requestAnimationFrame返回的 ID,用于取消动画帧。activeBarrages: 数组,存储当前所有在屏幕上活动(包括即将进入和正在显示)的Barrage对象实例。tracks: 数组,用于管理滚动弹幕的水平轨道,避免过度重叠。每个轨道对象记录了该轨道预计何时可用 (lastBarrageEndTime)。overlayWidth,overlayHeight,trackCount: 缓存弹幕区域的尺寸和计算出的轨道数量,窗口大小变化时会更新。
-
Barrage类:constructor: 初始化弹幕对象的属性(文本、颜色、类型、时长、唯一ID)。创建 DOM 元素 (this.el)。根据类型 (scroll,top,bottom) 设置不同的初始样式和行为。滚动弹幕计算初始 Y 坐标和速度;固定弹幕设置对应的 CSS 类,并根据duration设置setTimeout定时移除。createElement: 创建弹幕的div元素,设置 ID、CSS 类、文本内容、颜色和性能优化相关的样式。render: 将创建的 DOM 元素 (this.el) 添加到.barrage-overlay中,使其在页面上可见。渲染后获取元素的实际宽度 (this.width),这对于计算滚动弹幕何时离开屏幕和轨道占用时间很重要。move: (仅用于滚动弹幕) 更新弹幕的xPos(逻辑 X 坐标),并通过修改style.transform = translateX(...)来实际移动 DOM 元素。注意translateX的值是相对于初始left: 100%的偏移量。isOffscreen: (仅用于滚动弹幕) 判断弹幕是否已经完全移动到屏幕左侧之外。remove: 从 DOM 中移除弹幕元素。如果存在定时器 (timeoutId),则清除它。同时,从activeBarrages数组中移除该弹幕对象。这是弹幕生命周期的终点。
-
弹幕管理器功能 (全局函数):
-
initializeTracks: 根据当前.barrage-overlay的高度和CONFIG.TRACK_HEIGHT计算可用轨道数量,并初始化tracks数组,将每个轨道的lastBarrageEndTime设为 0。 -
findAvailableTrack: 为新的滚动弹幕寻找一个合适的轨道。优先选择已经完全空闲 (lastBarrageEndTime <= now) 的轨道。如果没有立即可用的,则选择lastBarrageEndTime最小(即最快会变空闲)的轨道。这是避免弹幕重叠的关键逻辑之一。 -
updateTrackUsage: 当一个新的滚动弹幕被分配到某个轨道后,根据其宽度和速度计算它完全进入屏幕所需的时间,并加上一个缓冲时间 (CONFIG.BUFFER_TIME),更新该轨道的lastBarrageEndTime。这能有效减少连续发射的弹幕在起点就重叠的问题。 -
addBarrage: 核心函数,用于添加新弹幕。接收文本和选项对象。根据type创建Barrage实例。如果是滚动弹幕,先调用findAvailableTrack获取轨道号,如果找到则创建弹幕、添加到activeBarrages、渲染到 DOM,并在下一帧获取宽度后调用updateTrackUsage。如果是固定弹幕,直接创建、添加、渲染。 -
animationLoop: 使用requestAnimationFrame实现的主动画循环。每一帧执行:- 过滤
activeBarrages数组,移除已经isOffscreen()的滚动弹幕(调用其remove方法)。 - 遍历剩余的
activeBarrages,对所有滚动弹幕调用move()方法更新位置。 - 递归调用
requestAnimationFrame(animationLoop)请求下一帧。 - 如果
isPaused为true,则不执行移动和移除逻辑,但仍然请求下一帧,以便在恢复时能继续。
- 过滤
-
pauseBarrages,resumeBarrages,clearBarrages: 控制弹幕的暂停、恢复和清空。clearBarrages会停止动画循环,移除所有弹幕 DOM 元素,清除定时器,清空activeBarrages数组,重置轨道信息,然后可以重新启动动画循环。 -
setMode: 处理模式切换。更新.barrage-overlay的 CSS 类,更新按钮激活状态,并在短暂延迟后重新获取overlay尺寸并重新初始化轨道 (initializeTracks)。 -
handleSendBarrage: 当用户点击发送按钮或在输入框按回车时触发。获取输入框、颜色选择器、类型下拉框、时长输入框的值,构造options对象,调用addBarrage。清空输入框。处理固定弹幕时长(将秒转为毫秒,0 或空表示常驻)。 -
startAnimationLoop: 启动(或重新启动)动画循环。
-
-
事件监听器:
- 为发送按钮、输入框回车、模式按钮、暂停/恢复/清空按钮绑定相应的处理函数。
- 监听
window的resize事件。当窗口大小改变时,使用setTimeout进行节流处理,延迟执行尺寸重新计算和轨道重新初始化。
-
初始化 (
init函数):- 在 DOM 加载完成后执行。
- 调用
initializeTracks初始化轨道。 - 调用
startAnimationLoop启动动画。 - 设置默认的显示模式(例如全屏)。
- 可以添加一些示例弹幕,演示不同类型和颜色。
-
启动: 判断 DOM 是否已加载,如果未加载则监听
DOMContentLoaded事件,否则直接调用init。
-
总结与可扩展性:
这个实现提供了一个功能相对完整的弹幕系统基础。它包含了多种显示模式、不同类型的弹幕(滚动、顶部固定、底部固定)、颜色自定义、定时消失或常驻、暂停/恢复/清空等功能。代码结构清晰,使用了类来封装弹幕对象,并通过管理器函数来协调整个系统。
可进一步扩展的方向:
-
性能优化:
- 对象池: 对于频繁创建和销毁的弹幕对象,使用对象池可以减少垃圾回收压力。
- Canvas 渲染: 对于极大量的弹幕,使用 Canvas 绘制通常比 DOM 操作性能更好,但实现更复杂。
- 更精细的轨道管理: 实现更复杂的碰撞检测和避让算法。
- 节流/防抖: 对用户输入(发送弹幕)和窗口大小调整进行更严格的节流或防抖处理。
-
功能增强:
- 用户交互: 点击弹幕进行点赞、举报等。
- 特殊弹幕: 如带有特殊效果(发光、放大)、图标或用户头像的弹幕。
- 弹幕密度控制: 根据屏幕上的弹幕数量动态调整是否接受新弹幕或调整弹幕速度。
- 与视频同步: 将弹幕的显示与视频播放时间精确关联。这通常需要监听视频播放器的
timeupdate事件,并将弹幕数据与时间戳绑定。 - 后端集成: 从服务器加载弹幕数据,并将用户发送的弹幕提交到服务器。
- 弹幕过滤/屏蔽: 基于关键词、用户 ID 等屏蔽某些弹幕。
- 字体大小/透明度设置: 允许用户自定义弹幕的显示效果。