移动端 B 站弹幕功能实现

文章目录

    • 效果展示
    • [HTML 结构](#HTML 结构)
    • 样式设计
    • [Canvas 与弹幕逻辑](#Canvas 与弹幕逻辑)
      • [1. 初始化 Canvas](#1. 初始化 Canvas)
      • [2. 弹幕行数计算](#2. 弹幕行数计算)
      • [3. 弹幕类 Barrage](#3. 弹幕类 Barrage)
      • [4. 弹幕管理](#4. 弹幕管理)
      • [5. 自动发射弹幕](#5. 自动发射弹幕)
      • [6. 用户发送弹幕](#6. 用户发送弹幕)
      • [7. 控制按钮逻辑](#7. 控制按钮逻辑)
      • [8. 渲染循环](#8. 渲染循环)
    • 案例源码

在现代网页中,弹幕(Barrage)是一种非常流行的用户互动方式,尤其在直播、短视频和互动课堂中广泛使用。下面讲解如何使用原生 HTML5 Canvas + JavaScript 实现一个高密度弹幕系统,支持 图标背景、动画效果、暂停/隐藏功能

效果展示

  • 弹幕自动随机出现,颜色随机
  • 支持用户输入发送自定义弹幕
  • 支持暂停/恢复和隐藏/显示弹幕
  • 弹幕背景带圆角矩形,实现图标/文字区分
  • 弹幕密集且不遮挡底部输入框

如图所示:

HTML 结构

HTML 部分主要包括三个区域:

  1. Canvas 渲染区:用于绘制弹幕。
  2. 控制按钮:暂停/恢复和隐藏/显示弹幕。
  3. 输入区域:用于发送自定义弹幕。
html 复制代码
<canvas id="barrageCanvas"></canvas>

<div class="controls">
  <button id="togglePause" class="ctrl-btn">⏸ 暂停弹幕</button>
  <button id="toggleHide" class="ctrl-btn">👁 隐藏弹幕</button>
</div>

<div class="input-area">
  <input type="text" id="barrageInput" placeholder="发送弹幕..." />
  <button id="sendBtn">发送</button>
</div>
  • canvas 是绘制弹幕的核心区域。

  • controls 包含两个按钮:

    • 暂停/恢复:暂停弹幕移动,但仍显示已存在弹幕。
    • 隐藏/显示:隐藏或显示弹幕,但不影响位置更新。
  • input-area 用于输入用户自定义弹幕。

样式设计

CSS 样式确保弹幕在全屏显示,且输入区域固定在底部,不被弹幕遮挡。

css 复制代码
body {
  margin: 0;
  background: #000;
  overflow: hidden;
  height: 100vh;
  font-family: sans-serif;
}

canvas {
  position: absolute;
  top: 0;
  left: 0;
}

.input-area {
  position: fixed;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  width: 90%;
  display: flex;
  gap: 8px;
  z-index: 10;
}

.controls {
  position: fixed;
  top: 12px;
  right: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  z-index: 20;
}
  • canvas 使用绝对定位,覆盖整个页面。
  • input-area 使用 fixed 固定在底部,并保证弹幕不会遮挡输入框。
  • controls 固定在右上角,并加入半透明和模糊效果,提高视觉层次。

Canvas 与弹幕逻辑

1. 初始化 Canvas

js 复制代码
const canvas = document.getElementById("barrageCanvas");
const ctx = canvas.getContext("2d");

function resizeCanvas() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  updateLineCount();
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
  • resizeCanvas 保证弹幕在屏幕大小变化时自适应。
  • 使用 updateLineCount 动态计算可用弹幕行数,避免弹幕覆盖输入框。

2. 弹幕行数计算

js 复制代码
const inputHeight = 80;
const topMargin = 20;
const lineHeight = 28;
let lineCount;

function updateLineCount() {
  lineCount = Math.floor((canvas.height - inputHeight - topMargin) / lineHeight);
}
  • lineHeight 决定每条弹幕在纵向的间距。
  • lineCount 用于随机选择弹幕所在的行。

3. 弹幕类 Barrage

js 复制代码
class Barrage {
  constructor(text, lineIndex) {
    this.text = text;
    this.color = `hsl(${Math.random() * 360},80%,60%)`;
    this.fontSize = 18;
    this.x = canvas.width;
    this.lineIndex = lineIndex;
    this.y = topMargin + lineHeight * (lineIndex + 1);
    this.speed = 1.5 + Math.random() * 2.5;
    this.padding = 8;
  }

  draw() {
    ctx.font = `${this.fontSize}px sans-serif`;
    const textWidth = ctx.measureText(this.text).width;

    // 绘制半透明圆角矩形背景
    ctx.fillStyle = "rgba(0,0,0,0.4)";
    ctx.beginPath();
    ctx.roundRect(this.x - this.padding / 2, this.y - this.fontSize, textWidth + this.padding, this.fontSize + 8, 8);
    ctx.fill();

    // 绘制文本
    ctx.fillStyle = this.color;
    ctx.fillText(this.text, this.x, this.y);
  }

  update() {
    this.x -= this.speed;
  }
}
  • draw() 负责渲染文字和背景矩形,背景矩形带圆角,实现图标/文字区分。
  • update() 负责弹幕移动。
  • 弹幕颜色随机,增加视觉丰富度。
  • 弹幕的 speed 随机,避免所有弹幕移动速度一致。

4. 弹幕管理

js 复制代码
const barrages = [];
const preset = ["太精彩了🔥", "前排打卡", "哈哈哈哈哈", "好顶赞👍", "稳了!", "无敌!", "冲啊~", "哇塞好炫", "牛啊哥们"];
let isPaused = false;
let isHidden = false;
  • barrages 存储当前页面所有弹幕。
  • preset 是随机弹幕库。
  • isPausedisHidden 控制暂停与隐藏状态。

5. 自动发射弹幕

js 复制代码
setInterval(() => {
  if (isPaused) return;
  const count = Math.floor(Math.random() * 3) + 1;
  for (let i = 0; i < count; i++) {
    const text = preset[Math.floor(Math.random() * preset.length)];
    const lineIndex = Math.floor(Math.random() * lineCount);
    barrages.push(new Barrage(text, lineIndex));
  }
}, 300);
  • 每 300ms 自动生成 1~3 条弹幕。
  • 随机选择文字和行号。
  • 暂停时不会生成新弹幕

6. 用户发送弹幕

js 复制代码
sendBtn.onclick = () => {
  const text = input.value.trim();
  if (text) {
    const lineIndex = Math.floor(Math.random() * lineCount);
    barrages.push(new Barrage(text, lineIndex));
    input.value = "";
  }
};
  • 用户输入后随机选择行显示。
  • 清空输入框,方便连续发送。

7. 控制按钮逻辑

js 复制代码
togglePause.onclick = () => {
  isPaused = !isPaused;
  togglePause.textContent = isPaused ? "▶ 恢复弹幕" : "⏸ 暂停弹幕";
};

toggleHide.onclick = () => {
  isHidden = !isHidden;
  toggleHide.textContent = isHidden ? "🚫 隐藏弹幕" : "👁 显示弹幕";
};
  • 暂停弹幕时,已存在弹幕仍显示,但不移动。
  • 隐藏弹幕时,仍在计算位置,但不渲染。

8. 渲染循环

js 复制代码
function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (let i = barrages.length - 1; i >= 0; i--) {
    const b = barrages[i];
    if (!isHidden) b.draw();
    if (!isPaused) b.update();
    if (b.x + ctx.measureText(b.text).width < 0) barrages.splice(i, 1);
  }
  requestAnimationFrame(render);
}
render();
  • 清空画布,重新绘制所有弹幕。
  • 遍历弹幕数组,从后往前遍历,方便删除已移出屏幕的弹幕。
  • requestAnimationFrame 保证动画平滑。

案例源码

html 复制代码
<!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>
      body {
        margin: 0;
        background: #000;
        overflow: hidden;
        height: 100vh;
        font-family: sans-serif;
      }

      canvas {
        position: absolute;
        top: 0;
        left: 0;
      }

      .input-area {
        position: fixed;
        bottom: 10px;
        left: 50%;
        transform: translateX(-50%);
        width: 90%;
        display: flex;
        gap: 8px;
        z-index: 10;
      }

      .input-area input {
        flex: 1;
        padding: 8px 10px;
        font-size: 16px;
        border-radius: 6px;
        border: none;
      }

      .input-area button {
        padding: 8px 16px;
        border: none;
        border-radius: 6px;
        background: #ff4081;
        color: white;
        font-weight: bold;
      }

      .controls {
        position: fixed;
        top: 12px;
        right: 12px;
        display: flex;
        flex-direction: column;
        gap: 10px;
        z-index: 20;
      }

      .ctrl-btn {
        display: flex;
        align-items: center;
        gap: 6px;
        justify-content: center;
        background: rgba(255, 255, 255, 0.15);
        color: #fff;
        border: 1px solid rgba(255, 255, 255, 0.4);
        border-radius: 8px;
        padding: 6px 10px;
        font-size: 14px;
        backdrop-filter: blur(6px);
        cursor: pointer;
        transition: transform 0.1s;
      }

      .ctrl-btn:active {
        transform: scale(0.95);
      }
    </style>
  </head>
  <body>
    <canvas id="barrageCanvas"></canvas>

    <div class="controls">
      <button id="togglePause" class="ctrl-btn">⏸ 暂停弹幕</button>
      <button id="toggleHide" class="ctrl-btn">👁 隐藏弹幕</button>
    </div>

    <div class="input-area">
      <input type="text" id="barrageInput" placeholder="发送弹幕..." />
      <button id="sendBtn">发送</button>
    </div>

    <script>
      const canvas = document.getElementById("barrageCanvas");
      const ctx = canvas.getContext("2d");
      const input = document.getElementById("barrageInput");
      const sendBtn = document.getElementById("sendBtn");
      const togglePause = document.getElementById("togglePause");
      const toggleHide = document.getElementById("toggleHide");

      const inputHeight = 80;
      const topMargin = 20;
      const lineHeight = 28;
      let lineCount;

      function resizeCanvas() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        updateLineCount();
      }
      function updateLineCount() {
        lineCount = Math.floor((canvas.height - inputHeight - topMargin) / lineHeight);
      }

      window.addEventListener("resize", resizeCanvas);
      resizeCanvas();

      class Barrage {
        constructor(text, lineIndex) {
          this.text = text;
          this.color = `hsl(${Math.random() * 360},80%,60%)`;
          this.fontSize = 18;
          this.x = canvas.width;
          this.lineIndex = lineIndex;
          this.y = topMargin + lineHeight * (lineIndex + 1);
          this.speed = 1.5 + Math.random() * 2.5;
          this.padding = 8;
        }
        draw() {
          ctx.font = `${this.fontSize}px sans-serif`;
          const textWidth = ctx.measureText(this.text).width;

          ctx.fillStyle = "rgba(0,0,0,0.4)";
          ctx.beginPath();
          ctx.roundRect(this.x - this.padding / 2, this.y - this.fontSize, textWidth + this.padding, this.fontSize + 8, 8);
          ctx.fill();

          ctx.fillStyle = this.color;
          ctx.fillText(this.text, this.x, this.y);
        }
        update() {
          this.x -= this.speed;
        }
      }

      const barrages = [];
      const preset = ["太精彩了🔥", "前排打卡", "哈哈哈哈哈", "好顶赞👍", "稳了!", "无敌!", "冲啊~", "哇塞好炫", "牛啊哥们"];

      let isPaused = false;
      let isHidden = false;

      // 发射弹幕
      setInterval(() => {
        if (isPaused) return;
        const count = Math.floor(Math.random() * 3) + 1;
        for (let i = 0; i < count; i++) {
          const text = preset[Math.floor(Math.random() * preset.length)];
          const lineIndex = Math.floor(Math.random() * lineCount);
          barrages.push(new Barrage(text, lineIndex));
        }
      }, 300);

      sendBtn.onclick = () => {
        const text = input.value.trim();
        if (text) {
          const lineIndex = Math.floor(Math.random() * lineCount);
          barrages.push(new Barrage(text, lineIndex));
          input.value = "";
        }
      };

      // 暂停/恢复
      togglePause.onclick = () => {
        isPaused = !isPaused;
        togglePause.textContent = isPaused ? "▶ 恢复弹幕" : "⏸ 暂停弹幕";
      };

      // 隐藏/显示
      toggleHide.onclick = () => {
        isHidden = !isHidden;
        toggleHide.textContent = isHidden ? "🚫 隐藏弹幕" : "👁 显示弹幕";
      };

      // 渲染循环
      function render() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        for (let i = barrages.length - 1; i >= 0; i--) {
          const b = barrages[i];
          if (!isHidden) b.draw(); // 暂停仍显示
          if (!isPaused) b.update(); // 暂停不更新位置
          if (b.x + ctx.measureText(b.text).width < 0) barrages.splice(i, 1);
        }
        requestAnimationFrame(render);
      }
      render();
    </script>
  </body>
</html>

👉点击进入 我的网站

相关推荐
闪耀之光M785 小时前
前端依赖自动导入:unplugin-auto-import
前端
Ricon组态薄荷糖5 小时前
Ricon组态系统:工业组件开发指南与实践
前端·后端·物联网
flash俊杰5 小时前
Electron 桌面应用的进程模型:为什么 fork Next.js standalone
前端
沙蒿同学6 小时前
我把架构约定编译成了会变红的测试:Wails v2 + Go + Vue3 桌面脚手架实战
前端·后端·github
cpolar技术支持6 小时前
本地 Playwright 测试报告怎么远程复盘?Trace Viewer 跑起来后,用 cpolar 分享失败现场
前端·自动化测试·测试工具·cpolar·playwright
wordbaby6 小时前
企业级后台管理系统路由设计与最佳实践指南
前端
胡志辉的博客6 小时前
【完全开源】IP 纯净度检测 可一键部署到自己的CF
前端·javascript·chrome·ip·chromium
Hilaku6 小时前
作为面试官,我最怕遇到什么样的候选人?
前端·javascript·程序员
TiDi7 小时前
Pinia优化重复请求
前端
web3d5207 小时前
01-用 Leafletjs 10 分钟搭一张水利一张图(Vue3 + Vite 实战)
前端·javascript