npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sweet-danmaku

v0.1.2

Published

Lightweight video danmaku (bullet comments) engine for web: scroll/top/bottom modes, React/Vue adapters, sweet-player plugin

Downloads

451

Readme

sweet-danmaku

npm version license

Lightweight danmaku (bullet comment) engine for web video. Zero runtime dependencies. Works with a plain <video>, and optionally plugs into sweet-player.

One package for all usage modes:

  • Core / Vanilla JS: sweet-danmaku
  • React: sweet-danmaku/react
  • Vue: sweet-danmaku/vue
  • sweet-player plugin: createDanmakuPlugin

Features

  • Modes — scroll (right → left), top fixed, bottom fixed
  • Track allocation — collision-aware lanes; pending retry when full
  • Timeline sync — cursor-based scan on sorted list; seek clears stage; late items are not back-filled
  • Runtime controls — opacity / fontSize / speed / display area / filter
  • Send & load — batch load() or realtime send() (binary insert)
  • Eventsready / show / click / destroy (on returns unsubscribe; late ready is replayed)
  • Framework adapters — React hook & Vue composable
  • sweet-player plugin — optional settings rows (toggle + opacity), no hard dependency on @sweet-player/core
  • Lightweight — ESM/CJS dual build, tree-shakeable, no WASM

Install

npm install sweet-danmaku

Quick Start

import { SweetDanmaku } from 'sweet-danmaku'

const video = document.querySelector('video')!
const dm = new SweetDanmaku(video, {
  comments: [
    { text: '前排', time: 1 },
    { text: '高能', time: 3, mode: 'top', color: '#ff4d6d' },
  ],
  opacity: 1,
  fontSize: 25,
  speed: 1,
  area: 0.85,
})

// ready 在 microtask 触发,构造后立刻订阅即可
dm.on('ready', () => console.log('danmaku ready', dm.ready))
dm.on('show', (item) => console.log('show', item.text))
dm.on('destroy', () => console.log('destroyed'))

dm.send({ text: '我来了', time: video.currentTime, border: true })

Core options (SweetDanmakuOptions)

| Option | Type | Default | Description | |--------|------|---------|-------------| | comments | DanmakuItem[] | [] | Initial list | | opacity | number | 1 | 0–1 | | fontSize | number | 25 | Default font size (px) | | speed | number | 1 | Scroll speed multiplier | | fixedDuration | number | 4 | top/bottom hold seconds | | area | number | 1 | Vertical usable ratio from top (0.1–1) | | maxTracks | number | auto | Max lanes | | visible | boolean | true | Initial visibility | | container | HTMLElement | video parent | Overlay host | | filter | (item) => boolean | — | Return false to skip |

Core methods

| Method | Description | |--------|-------------| | load(comments) | Replace all comments (sorted by time) | | send(item) | Append one (binary insert); shows immediately if near current time | | clear() | Clear list + stage | | show() / hide() | Toggle visibility | | setOpacity(0–1) | Opacity | | setFontSize(px) | Default font size | | setSpeed(n) | Scroll speed multiplier | | setArea(0.1–1) | Vertical usable ratio from top | | setFilter(fn?) | Skip items returning false | | on / off / once | Events | | destroy() | Teardown (emits destroy first) | | ready | boolean getter — whether ready already fired | | layer | Overlay root element |

Events

| Event | Payload | When | |-------|---------|------| | ready | void | After construct (microtask). Late subscribers are replayed once. | | show | DanmakuItem | A bullet starts displaying | | click | DanmakuItem | User clicks a bullet | | destroy | void | destroy() called, before listeners are cleared | | error | Error | Reserved for recoverable failures. Fatal construct errors throw instead. |

DanmakuItem:

{
  id?: string
  text: string
  time: number          // video seconds
  mode?: 'scroll' | 'top' | 'bottom'
  color?: string
  fontSize?: number
  border?: boolean      // self-sent highlight
}

React

import { useRef } from 'react'
import { useSweetDanmaku } from 'sweet-danmaku/react'

function Player() {
  const videoRef = useRef<HTMLVideoElement>(null)
  const { danmaku, ready, send, show, hide } = useSweetDanmaku(videoRef, {
    comments: [{ text: 'hello', time: 0 }],
    opacity: 1,
    fontSize: 25,
    onReady: () => console.log('ready'),
    onShow: (item) => console.log(item.text),
    onClick: (item) => console.log('click', item.text),
    // onError: reserved; construct failures throw
  })

  return (
    <div style={{ position: 'relative' }}>
      <video ref={videoRef} src="..." controls />
      <button
        disabled={!ready || !danmaku}
        onClick={() => send({ text: 'hi', time: videoRef.current!.currentTime })}
      >
        Send
      </button>
    </div>
  )
}

useSweetDanmaku options

Inherits core options (opacity, fontSize, speed, fixedDuration, area, maxTracks, container, filter, …) plus:

| Option | Type | Description | |--------|------|-------------| | comments | DanmakuItem[] | Reloads when reference changes | | visible | boolean | show/hide when changes | | onReady | () => void | Ready callback | | onError | (err) => void | Reserved (error event) | | onShow | (item) => void | Bullet shown | | onClick | (item) => void | Bullet clicked |

Returns: { danmaku, ready, load, send, show, hide, clear, setOpacity, setFontSize, setSpeed, destroy }.

Vue

<script setup lang="ts">
import { ref } from 'vue'
import { useSweetDanmaku } from 'sweet-danmaku/vue'

const videoRef = ref<HTMLVideoElement | null>(null)
const { danmaku, ready, send } = useSweetDanmaku(videoRef, {
  comments: [{ text: 'hello', time: 0 }],
  onReady: () => console.log('ready'),
})
</script>

<template>
  <div style="position: relative">
    <video ref="videoRef" src="..." controls />
    <button v-if="ready" @click="send({ text: 'hi', time: videoRef!.currentTime })">
      Send
    </button>
  </div>
</template>

useSweetDanmaku (Vue) options

Same as React, but comments / visible accept Ref / plain values (MaybeRef).

sweet-player plugin

import { SweetPlayer } from '@sweet-player/core'
import { createDanmakuPlugin } from 'sweet-danmaku'

const danmaku = createDanmakuPlugin({
  comments: [/* ... */],
  opacity: 1,
  fontSize: 25,
  speed: 1,
  area: 0.85,
  settings: true, // register toggle + opacity in settings panel (default true)
  labels: {
    toggle: '弹幕',
    opacity: '弹幕透明度',
  },
})

const player = new SweetPlayer({
  container: '#player',
  src: 'https://example.com/video.m3u8',
  plugins: [danmaku.plugin],
})

danmaku.send({ text: '来了', time: 0, border: true })
danmaku.hide()
danmaku.getInstance() // SweetDanmaku | null (after apply)

CreateDanmakuPluginOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | …core options | | | All SweetDanmakuOptions except container (auto = player.container) | | comments | DanmakuItem[] | — | Initial list | | settings | boolean | true | Register settings rows if addSettingsRow exists | | labels.toggle | string | 弹幕 | Toggle label | | labels.opacity | string | 弹幕透明度 | Opacity slider label |

Handle methods: plugin, getInstance, load, send, show, hide, clear, setOpacity, setFontSize, setSpeed.

The plugin only duck-types player.container / player.video / player.on / optional addSettingsRow — no package dependency on @sweet-player/core.

Playground

pnpm install
pnpm dev

Opens Vite at http://localhost:5184.

Release

# bump version in packages/core/package.json, update CHANGELOG.md
pnpm run release:one
# = version:sync → typecheck → build → release:dry → publish

| Script | Description | |--------|-------------| | pnpm build | Build npm package | | pnpm test | Vitest | | pnpm typecheck | tsc --noEmit | | pnpm release:dry | npm publish dry-run | | pnpm release | Publish sweet-danmaku to npm |

Release notes template: .github/RELEASE_TEMPLATE.md

Non-goals (for now)

  • Built into @sweet-player/core (stays a separate package)
  • Server protocol / WebSocket (bring your own; call send / load)
  • Canvas / WASM renderer (DOM + measureText first)

License

MIT