How to Build a Danmaku (Bullet Comment) System with HTML, CSS, and Canvas
This article explains the concept of danmaku (bullet comments), why they improve user experience, and provides a detailed guide on implementing a danmaku system using HTML + CSS or Canvas, including stage design, track management, collision handling, and reusable code examples.
Background
To create a richer multimedia experience, many video platforms have added a social feature that lets users post comments at specific points on the video timeline, known as danmaku (bullet comments). Originating from Japan's Niconico, danmaku are now supported on Chinese sites such as Bilibili, AcFun, Tencent Video, iQiyi, Youku, and Migu.
Form
A single danmaku can appear in three basic modes:
Scrolling: moves from right to left across the screen.
Top: static and centered at the top.
Bottom: static and centered at the bottom.
Why Danmaku?
Before Danmaku
Traditional interaction uses separate comment sections or chat windows, forcing users to split their visual attention between the video and the comment area, which degrades both video focus and comment readability.
After Danmaku
Danmaku keep the viewer’s focus on the video while displaying comments in a single, left‑to‑right scrolling direction that matches reading habits, eliminating visual obstacles.
Additional Benefits
Strong Interaction (On‑Demand) : Viewers can instantly share thoughts that appear as scrolling comments, creating real‑time interaction.
Live Interaction : Streamers can gauge audience feedback directly from on‑screen comments.
Atmosphere Enhancement : Highlighted “high‑energy” comments add excitement to suspenseful or horror content.
Implementation Methods
Most major sites implement danmaku with HTML + CSS; a small portion uses Canvas.
HTML + CSS Approach
Using DOM elements makes styling easy via CSS and leverages the browser’s native event system for interactions such as likes, reports, hover, and click.
Canvas Approach
Canvas offers smoother animation but requires custom event handling and is more complex for developers less familiar with it.
Design Overview
The system consists of three main parts: the stage, tracks, and the barrage pool.
Stage
The stage controls multiple tracks, a waiting queue, and the pool. Each frame it checks for empty tracks, moves barrages from the queue to appropriate tracks, and renders them.
export default abstract class BaseStage<T extends BarrageObject> extends EventEmitter {
protected trackWidth: number
protected trackHeight: number
protected duration: number
protected maxTrack: number
protected tracks: Track<T>[] = []
waitingQueue: T[] = []
// Add barrage to waiting queue
abstract add(barrage: T): boolean
// Find suitable track
abstract _findTrack(): number
// Extract barrage from queue to track
abstract _extractBarrage(): void
// Render function
abstract render(): void
// Reset
abstract reset(): void
}Canvas Stage
export default abstract class BaseCanvasStage<T extends BarrageObject> extends BaseStage<T> {
protected canvas: HTMLCanvasElement
protected ctx: CanvasRenderingContext2D
constructor(canvas: HTMLCanvasElement, config: Config) {
super(config)
this.canvas = canvas
this.ctx = canvas.getContext('2d')!
}
}HTML + CSS Stage
export default abstract class BaseCssStage<T extends BarrageObject> extends BaseStage<T> {
el: HTMLDivElement
objToElm: WeakMap<T, HTMLElement> = new WeakMap()
elmToObj: WeakMap<HTMLElement, T> = new WeakMap()
freezeBarrage: T | null = null
domPool: HTMLElement[] = []
constructor(el: HTMLDivElement, config: Config) {
super(config)
this.el = el
const wrapper = config.wrapper
if (wrapper && config.interactive) {
wrapper.addEventListener('mousemove', this._mouseMoveEventHandler.bind(this))
wrapper.addEventListener('click', this._mouseClickEventHandler.bind(this))
}
}
// ...methods for creating, removing, and handling mouse events...
}Track
class BarrageTrack<T extends BarrageObject> {
barrages: T[] = []
offset: number = 0
forEach(handler: TrackForEachHandler<T>) {
for (let i = 0; i < this.barrages.length; ++i) {
handler(this.barrages[i], i, this.barrages)
}
}
reset() { this.barrages = []; this.offset = 0 }
push(...items: T[]) { this.barrages.push(...items) }
removeTop() { this.barrages.shift() }
remove(index: number) { if (index >= 0 && index < this.barrages.length) this.barrages.splice(index, 1) }
updateOffset() {
const endBarrage = this.barrages[this.barrages.length - 1]
if (endBarrage) {
const { speed } = endBarrage
this.offset -= speed
}
}
}Collision Handling
Two common strategies:
Uniform speed for all barrages (no collision).
Variable speeds with collision detection, often solved by a pursuit‑problem formula to compute a maximum speed, then adding randomness.
S = Math.max(VB, Random * DefaultSpeed)Where DefaultSpeed is the baseline speed for the first barrage on a track.
Demo Links
CSS implementation: https://logcas.github.io/a-barrage/example/css3.html
Canvas implementation: https://logcas.github.io/a-barrage/example/canvas.html
References
https://w3c.github.io/danmaku/usecase.zh.html
https://juejin.cn/post/6867689680670818317
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
