├── .github
└── workflows
│ └── deploy.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierrc
├── LICENSE
├── README.md
├── demo
├── .gitignore
├── README.md
├── components.json
├── index.html
├── package.json
├── postcss.config.js
├── public
│ └── favicon.svg
├── src
│ ├── App.tsx
│ ├── assets
│ │ ├── avatar.jpg
│ │ ├── github.svg
│ │ └── mask.svg
│ ├── components
│ │ ├── danmu
│ │ │ ├── Container.tsx
│ │ │ ├── Danmaku.tsx
│ │ │ ├── TopBar.tsx
│ │ │ └── Transmitter.tsx
│ │ ├── sidebar
│ │ │ ├── SidebarAreaX.tsx
│ │ │ ├── SidebarAreaY.tsx
│ │ │ ├── SidebarDirection.tsx
│ │ │ ├── SidebarFreeze.tsx
│ │ │ ├── SidebarFrequency.tsx
│ │ │ ├── SidebarGap.tsx
│ │ │ ├── SidebarModeSelect.tsx
│ │ │ ├── SidebarMoveDuration.tsx
│ │ │ ├── SidebarNumbers.tsx
│ │ │ ├── SidebarOcclusion.tsx
│ │ │ ├── SidebarOpacity.tsx
│ │ │ ├── SidebarRate.tsx
│ │ │ ├── SidebarShowAndHide.tsx
│ │ │ ├── SidebarSpeed.tsx
│ │ │ ├── SidebarStartAndStop.tsx
│ │ │ └── index.tsx
│ │ └── ui
│ │ │ ├── avatar.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── button.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── select.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── toast.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── tooltip.tsx
│ │ │ └── use-toast.ts
│ ├── globals.css
│ ├── i18n
│ │ ├── en.ts
│ │ └── zh.ts
│ ├── lib
│ │ └── utils.ts
│ ├── main.tsx
│ ├── manager.tsx
│ ├── types.ts
│ └── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts
├── docs
├── .vitepress
│ ├── config
│ │ ├── en.ts
│ │ ├── index.ts
│ │ ├── shared.ts
│ │ └── zh.ts
│ └── theme
│ │ ├── index.ts
│ │ └── main.css
├── en
│ ├── cases
│ │ ├── anti-occlusion.md
│ │ ├── cooldown.md
│ │ ├── custom-container.md
│ │ ├── custom-danmaku.md
│ │ ├── filter-keyword.md
│ │ ├── fixed.md
│ │ ├── image.md
│ │ ├── like.md
│ │ ├── loop.md
│ │ ├── recommend.md
│ │ ├── simplified-mode.md
│ │ ├── track-settings.md
│ │ └── uniform-speed.md
│ ├── guide
│ │ ├── create-plugin.md
│ │ ├── getting-started.md
│ │ └── typescript-interface.md
│ ├── index.md
│ └── reference
│ │ ├── container-api.md
│ │ ├── danmaku-api.md
│ │ ├── danmaku-hooks.md
│ │ ├── danmaku-properties.md
│ │ ├── manager-api.md
│ │ ├── manager-configuration.md
│ │ ├── manager-hooks.md
│ │ ├── manager-properties.md
│ │ └── track-api.md
├── package.json
├── public
│ ├── favicon.svg
│ └── logo.svg
└── zh
│ ├── cases
│ ├── anti-occlusion.md
│ ├── cooldown.md
│ ├── custom-container.md
│ ├── custom-danmaku.md
│ ├── filter-keyword.md
│ ├── fixed.md
│ ├── image.md
│ ├── like.md
│ ├── loop.md
│ ├── recommend.md
│ ├── simplified-mode.md
│ ├── track-settings.md
│ └── uniform-speed.md
│ ├── guide
│ ├── create-plugin.md
│ ├── getting-started.md
│ └── typescript-interface.md
│ ├── index.md
│ └── reference
│ ├── container-api.md
│ ├── danmaku-api.md
│ ├── danmaku-hooks.md
│ ├── danmaku-properties.md
│ ├── manager-api.md
│ ├── manager-configuration.md
│ ├── manager-hooks.md
│ ├── manager-properties.md
│ └── track-api.md
├── jest.config.js
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── release.ts
├── rollup.config.mjs
├── src
├── __tests__
│ └── index.spec.ts
├── container.ts
├── danmaku
│ ├── facile.ts
│ └── flexible.ts
├── engine.ts
├── global.d.ts
├── index.ts
├── lifeCycle.ts
├── manager.ts
├── track.ts
├── types.ts
└── utils.ts
└── tsconfig.json
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v2
15 |
16 | - name: Set up Node.js
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: '20'
20 |
21 | - name: Install pnpm
22 | uses: pnpm/action-setup@v2
23 | with:
24 | version: 9.1.3
25 |
26 | - name: Install dependencies
27 | run: pnpm install
28 |
29 | - name: Build src
30 | run: pnpm run build:core
31 |
32 | - name: Build demo
33 | run: pnpm run build:demo
34 |
35 | - name: Build docs
36 | run: pnpm run build:docs
37 |
38 | - name: Deploy demo to GitHub Pages
39 | uses: peaceiris/actions-gh-pages@v3
40 | with:
41 | github_token: ${{ secrets.GITHUB_TOKEN }}
42 | publish_dir: ./demo/dist
43 |
44 | - name: Deploy docs to GitHub Pages
45 | uses: peaceiris/actions-gh-pages@v3
46 | with:
47 | github_token: ${{ secrets.GITHUB_TOKEN }}
48 | publish_dir: ./docs/.vitepress/dist
49 | destination_dir: ./document
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | dist
4 | cache
5 | test.js
6 | test.ts
7 | node_modules
8 | tsconfig.tsbuildinfo
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "printWidth": 80,
4 | "singleQuote": true,
5 | "endOfLine": "lf",
6 | "trailingComma": "all"
7 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024-present, chentao.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
danmu
3 |
4 | [](https://www.npmjs.com/package/danmu) [](https://github.com/imtaotao/danmu/actions/workflows/deploy.yml)
5 |
6 |
7 |
8 | Welcome to the Danmu project! 🎉 This repository is dedicated to providing a robust and user-friendly danmaku solution. Whether you are building an interactive video streaming platform or want to enhance live interactions during events, Danmu got you covered!
9 |
10 | - **Example Project**: https://imtaotao.github.io/danmu/
11 |
12 | - **Official Documentation**: https://imtaotao.github.io/danmu/document/en/
13 |
14 |
15 |
16 | ## Why Choose Danmu? 💭
17 |
18 | - 🍃 **Lightweight**: Danmu is designed to be minimal yet powerful.
19 | - 🧩 **Customizable**: Tailor the functionality to fit your specific needs with extensive customization options.
20 | - 🌺 **User Friendly**: Easy to integrate and use, elevating user experience on your platform.
21 | - 🎯 **Full-Featured**: Provides APIs that meet a variety of business needs.
22 |
23 | ## Getting Started 🌟
24 |
25 | To get started with Danmu, please refer to our [**official documentation**](https://imtaotao.github.io/danmu/document/en/guide/getting-started.html), which provides detailed instructions on installation and configurations.
26 |
27 | Thank you for considering Danmu for your project! We are excited to see what you will build with it 😁.
28 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # Demo
2 |
3 | This is a demo, deployed on gh-pages.
4 |
--------------------------------------------------------------------------------
/demo/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Danmu 🌘 Collision detection, highly customized danmu screen styles, you deserve it
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "private": true,
4 | "scripts": {
5 | "dev": "vite",
6 | "a": "pnpx shadcn-ui add",
7 | "build": "tsc -b && vite build",
8 | "preview": "vite preview"
9 | },
10 | "dependencies": {
11 | "@radix-ui/react-alert-dialog": "^1.1.1",
12 | "@radix-ui/react-avatar": "^1.1.0",
13 | "@radix-ui/react-checkbox": "^1.1.1",
14 | "@radix-ui/react-dialog": "^1.1.1",
15 | "@radix-ui/react-label": "^2.1.0",
16 | "@radix-ui/react-menubar": "^1.1.1",
17 | "@radix-ui/react-popover": "^1.1.1",
18 | "@radix-ui/react-radio-group": "^1.2.0",
19 | "@radix-ui/react-select": "^2.1.1",
20 | "@radix-ui/react-slider": "^1.2.0",
21 | "@radix-ui/react-slot": "^1.1.0",
22 | "@radix-ui/react-switch": "^1.1.0",
23 | "@radix-ui/react-tabs": "^1.1.0",
24 | "@radix-ui/react-toast": "^1.2.1",
25 | "@radix-ui/react-toggle": "^1.1.0",
26 | "@radix-ui/react-toggle-group": "^1.1.0",
27 | "@radix-ui/react-tooltip": "^1.1.2",
28 | "aidly": "^1.9.0",
29 | "class-variance-authority": "^0.7.0",
30 | "clsx": "^2.1.1",
31 | "danmu": "workspace:*",
32 | "i18next": "^23.12.2",
33 | "i18next-browser-languagedetector": "^8.0.0",
34 | "lucide-react": "^0.396.0",
35 | "react": "^18.3.1",
36 | "react-dom": "^18.3.1",
37 | "react-i18next": "^15.0.0",
38 | "react-resizable-panels": "^2.0.19",
39 | "tailwind-merge": "^2.3.0",
40 | "tailwindcss-animate": "^1.0.7"
41 | },
42 | "devDependencies": {
43 | "@types/react": "^18.3.3",
44 | "@types/react-dom": "^18.3.0",
45 | "@vitejs/plugin-react": "^4.3.1",
46 | "autoprefixer": "^10.4.19",
47 | "postcss": "^8.4.38",
48 | "tailwindcss": "^3.4.4",
49 | "typescript": "^5.8.3",
50 | "vite": "^5.3.1"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/demo/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/demo/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/src/App.tsx:
--------------------------------------------------------------------------------
1 | import type { Manager } from 'danmu';
2 | import type { DanmakuValue } from '@/types';
3 | import { cn, isMobile } from '@/lib/utils';
4 | import { Sidebar } from '@/components/sidebar';
5 | import { Toaster } from '@/components/ui/toaster';
6 | import { TopBar } from '@/components/danmu/TopBar';
7 | import { Container } from '@/components/danmu/Container';
8 | import { Transmitter } from '@/components/danmu/Transmitter';
9 |
10 | export function App({ manager }: { manager: Manager }) {
11 | return (
12 |
13 |
19 | {isMobile ? null : (
20 |
21 |
22 |
23 | )}
24 |
25 |
31 |
32 |
33 |
34 | {isMobile ? null : (
35 |
36 |
37 |
38 | )}
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/demo/src/assets/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imtaotao/danmu/9ca0f4e567526db43766fb2f522a155efe814ab6/demo/src/assets/avatar.jpg
--------------------------------------------------------------------------------
/demo/src/assets/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/src/assets/mask.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/demo/src/components/danmu/Container.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, memo } from 'react';
2 | import { Maximize } from 'lucide-react';
3 | import type { Manager } from 'danmu';
4 | import type { DanmakuValue } from '@/types';
5 |
6 | export const Container = memo(
7 | ({ manager }: { manager: Manager }) => {
8 | const ref = useRef(null);
9 |
10 | useEffect(() => {
11 | const format = () => {
12 | manager.nextFrame(() => {
13 | if (ref.current) {
14 | manager.mount(ref.current);
15 | }
16 | });
17 | };
18 |
19 | if (ref.current) {
20 | manager.mount(ref.current);
21 | manager.startPlaying();
22 | document.addEventListener('fullscreenchange', format);
23 | }
24 |
25 | return () => {
26 | document.removeEventListener('fullscreenchange', format);
27 | };
28 | }, []);
29 |
30 | return (
31 |
37 | {
42 | if (!document.fullscreenElement) {
43 | if (ref.current) {
44 | manager.each((dm) => {
45 | dm.destroy();
46 | });
47 | ref.current.requestFullscreen();
48 | }
49 | } else {
50 | document.exitFullscreen();
51 | }
52 | }}
53 | />
54 |
55 | );
56 | },
57 | );
58 |
--------------------------------------------------------------------------------
/demo/src/components/danmu/Danmaku.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { ThumbsUp } from 'lucide-react';
3 | import { useTranslation } from 'react-i18next';
4 | import type { Danmaku, Manager } from 'danmu';
5 | import type { Statuses, DanmakuValue } from '@/types';
6 | import { cn } from '@/lib/utils';
7 | import avatarPath from '@/assets/avatar.jpg';
8 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
9 | import {
10 | Popover,
11 | PopoverContent,
12 | PopoverTrigger,
13 | } from '@/components/ui/popover';
14 |
15 | // Render file:demo/src/manager.tsx
16 | export const DanmakuComponent = ({
17 | manager,
18 | danmaku,
19 | }: {
20 | danmaku: Danmaku;
21 | manager: Manager;
22 | }) => {
23 | const { t } = useTranslation();
24 | const [open, setOpen] = useState(false);
25 | let isSelf;
26 | let content;
27 | if (typeof danmaku.data === 'string') {
28 | isSelf = true;
29 | content = danmaku.data;
30 | } else {
31 | isSelf = danmaku.data.isSelf;
32 | content = danmaku.data.content;
33 | }
34 |
35 | danmaku.use({
36 | pause() {
37 | setOpen(true);
38 | },
39 | resume() {
40 | setOpen(false);
41 | },
42 | beforeMove(dm) {
43 | for (const key in manager.statuses) {
44 | dm.setStyle(
45 | key as keyof Statuses,
46 | manager.statuses[key as keyof Statuses],
47 | );
48 | }
49 | },
50 | });
51 |
52 | return (
53 |
54 |
55 |
56 | danmaku.pause()}
58 | onMouseLeave={() => {
59 | if (!manager.isFreeze()) {
60 | danmaku.resume();
61 | }
62 | }}
63 | onClick={() => {
64 | setOpen(false);
65 | setTimeout(() => danmaku.destroy('mark'), 100);
66 | }}
67 | className={cn(
68 | isSelf ? 'border-2 border-solid border-teal-500' : '',
69 | 'py-[5px] px-3 rounded-xl font-bold text-slate-900 text-center cursor-pointer bg-gray-300 hover:bg-gray-400 flex items-center',
70 | )}
71 | >
72 |
73 |
74 | CN
75 |
76 |
77 | {danmaku.type === 'flexible'
78 | ? `${t('flexibleDanmaku')} -- ${content}`
79 | : content}
80 |
81 |
82 |
83 |
84 |
85 | {t('thisIsA')}
86 |
87 | {danmaku.type === 'flexible'
88 | ? t('flexibleDanmaku')
89 | : t('facileDanmaku')}
90 | {danmaku.isFixedDuration ? ` (${t('correctedDuration')})` : ''}
91 |
92 |
93 |
94 |
95 | );
96 | };
97 |
--------------------------------------------------------------------------------
/demo/src/components/danmu/TopBar.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next';
2 | import { cn, isMobile } from '@/lib/utils';
3 | import githubLogo from '@/assets/github.svg';
4 | import {
5 | Select,
6 | SelectContent,
7 | SelectItem,
8 | SelectTrigger,
9 | SelectValue,
10 | } from '@/components/ui/select';
11 |
12 | export function TopBar() {
13 | const { t, i18n } = useTranslation();
14 |
15 | return (
16 |
17 |
22 |
27 |
28 |
i18n.changeLanguage(e)}>
29 |
30 |
34 |
35 |
36 | English
37 | 简体中文
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarAreaX.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { Dog } from 'lucide-react';
3 | import { useTranslation } from 'react-i18next';
4 | import type { Manager } from 'danmu';
5 | import type { DanmakuValue } from '@/types';
6 | import { Label } from '@/components/ui/label';
7 | import { Slider } from '@/components/ui/slider';
8 |
9 | export const SidebarAreaX = memo(
10 | ({ manager }: { manager: Manager }) => {
11 | const { t } = useTranslation();
12 |
13 | return (
14 |
15 |
16 |
17 | {t('setArea')} (X)
18 |
19 | {
26 | manager.setArea({
27 | x: {
28 | end: `${v[1]}%`,
29 | start: `${v[0]}%`,
30 | },
31 | });
32 | }}
33 | />
34 |
35 | );
36 | },
37 | );
38 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarAreaY.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { Dog } from 'lucide-react';
3 | import { useTranslation } from 'react-i18next';
4 | import type { Manager } from 'danmu';
5 | import type { DanmakuValue } from '@/types';
6 | import { Label } from '@/components/ui/label';
7 | import { Slider } from '@/components/ui/slider';
8 |
9 | export const SidebarAreaY = memo(
10 | ({ manager }: { manager: Manager }) => {
11 | const { t } = useTranslation();
12 |
13 | return (
14 |
15 |
16 |
17 | {t('setArea')} (Y)
18 |
19 | {
26 | manager.setArea({
27 | y: {
28 | end: `${v[1]}%`,
29 | start: `${v[0]}%`,
30 | },
31 | });
32 | }}
33 | />
34 |
35 | );
36 | },
37 | );
38 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarDirection.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { Bone } from 'lucide-react';
3 | import { useTranslation } from 'react-i18next';
4 | import type { Manager } from 'danmu';
5 | import type { DanmakuValue } from '@/types';
6 | import { Label } from '@/components/ui/label';
7 | import { Switch } from '@/components/ui/switch';
8 |
9 | export const SidebarDirection = memo(
10 | ({ manager }: { manager: Manager }) => {
11 | const { t } = useTranslation();
12 |
13 | return (
14 |
15 |
19 |
20 | {t('setDirection')}
21 |
22 |
26 | manager.updateOptions({
27 | direction: v ? 'right' : 'left',
28 | })
29 | }
30 | />
31 |
32 | );
33 | },
34 | );
35 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarFreeze.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { Shell } from 'lucide-react';
3 | import { useTranslation } from 'react-i18next';
4 | import type { Manager } from 'danmu';
5 | import type { DanmakuValue } from '@/types';
6 | import { Label } from '@/components/ui/label';
7 | import { Switch } from '@/components/ui/switch';
8 |
9 | export const SidebarFreeze = memo(
10 | ({ manager }: { manager: Manager }) => {
11 | const { t } = useTranslation();
12 |
13 | return (
14 |
15 |
19 |
20 | {t('setFreeze')}
21 |
22 | {
25 | // Do not trigger the built-in `pause` and `resume` events
26 | const options = { preventEvents: ['pause', 'resume'] };
27 | v ? manager.freeze(options) : manager.unfreeze(options);
28 | }}
29 | />
30 |
31 | );
32 | },
33 | );
34 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarFrequency.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { throttle } from 'aidly';
3 | import { Fish, CircleAlert } from 'lucide-react';
4 | import { useTranslation } from 'react-i18next';
5 | import type { Manager } from 'danmu';
6 | import type { DanmakuValue } from '@/types';
7 | import { Label } from '@/components/ui/label';
8 | import { Input } from '@/components/ui/input';
9 | import {
10 | Tooltip,
11 | TooltipContent,
12 | TooltipProvider,
13 | TooltipTrigger,
14 | } from '@/components/ui/tooltip';
15 |
16 | export const SidebarFrequency = memo(
17 | ({ manager }: { manager: Manager }) => {
18 | const { t } = useTranslation();
19 |
20 | return (
21 |
22 |
23 |
24 |
25 | {t('setInterval')} (ms)
26 |
27 |
28 |
29 |
30 |
31 | {t('setIntervalTip')}
32 |
33 |
34 |
35 |
36 |
{
42 | manager.updateOptions({ interval: Number(e.target.value) });
43 | })}
44 | />
45 |
46 | );
47 | },
48 | );
49 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarGap.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { throttle } from 'aidly';
3 | import { useTranslation } from 'react-i18next';
4 | import { Squirrel, CircleAlert } from 'lucide-react';
5 | import type { Manager } from 'danmu';
6 | import type { DanmakuValue } from '@/types';
7 | import { Label } from '@/components/ui/label';
8 | import { Input } from '@/components/ui/input';
9 | import {
10 | Tooltip,
11 | TooltipContent,
12 | TooltipProvider,
13 | TooltipTrigger,
14 | } from '@/components/ui/tooltip';
15 |
16 | export const SidebarGap = memo(
17 | ({ manager }: { manager: Manager }) => {
18 | const { t } = useTranslation();
19 |
20 | return (
21 |
22 |
23 |
24 |
25 | {t('setGap')}
26 |
27 |
28 |
29 |
30 |
31 | {t('setGapTip')}
32 |
33 |
34 |
35 |
36 |
{
42 | manager.updateOptions({ gap: Number(e.target.value) });
43 | })}
44 | />
45 |
46 | );
47 | },
48 | );
49 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarModeSelect.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { Snail, CircleAlert } from 'lucide-react';
4 | import type { Mode, Manager } from 'danmu';
5 | import type { DanmakuValue } from '@/types';
6 | import { Label } from '@/components/ui/label';
7 | import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
8 | import {
9 | Tooltip,
10 | TooltipContent,
11 | TooltipProvider,
12 | TooltipTrigger,
13 | } from '@/components/ui/tooltip';
14 |
15 | export const SidebarModeSelect = memo(
16 | ({ manager }: { manager: Manager }) => {
17 | const { t } = useTranslation();
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | {t('setMode')}
25 |
26 |
27 |
28 |
29 |
30 |
31 | {t('setModeTipTitle')}
32 |
33 | 1. {t('setModeTipOne')}
34 |
35 | 2. {t('setModeTipTwo')}
36 |
37 | 3. {t('setModeTipThree')}
38 |
39 |
40 |
41 |
42 |
43 |
46 | manager.updateOptions({
47 | mode: e.target.textContent?.trim() as Mode,
48 | })
49 | }
50 | >
51 |
52 |
53 | none
54 |
55 |
56 | strict
57 |
58 |
59 | adaptive
60 |
61 |
62 |
63 |
64 | );
65 | },
66 | );
67 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarMoveDuration.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { throttle } from 'aidly';
3 | import { useTranslation } from 'react-i18next';
4 | import { Rabbit, CircleAlert } from 'lucide-react';
5 | import type { Manager } from 'danmu';
6 | import type { DanmakuValue } from '@/types';
7 | import { Label } from '@/components/ui/label';
8 | import { Input } from '@/components/ui/input';
9 | import {
10 | Tooltip,
11 | TooltipContent,
12 | TooltipProvider,
13 | TooltipTrigger,
14 | } from '@/components/ui/tooltip';
15 |
16 | export const SidebarMoveDuration = memo(
17 | ({ manager }: { manager: Manager }) => {
18 | const { t } = useTranslation();
19 |
20 | return (
21 |
63 | );
64 | },
65 | );
66 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarNumbers.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useState, useEffect } from 'react';
2 | import { Asterisk } from 'lucide-react';
3 | import { useTranslation } from 'react-i18next';
4 | import type { Manager } from 'danmu';
5 | import type { DanmakuValue } from '@/types';
6 | import { Label } from '@/components/ui/label';
7 | import { Badge } from '@/components/ui/badge';
8 |
9 | export const SidebarNumbers = memo(
10 | ({ manager }: { manager: Manager }) => {
11 | const { t } = useTranslation();
12 | const [allNumber, setAllNumber] = useState(0);
13 | const [stashNumber, setStashNumber] = useState(0);
14 | const [renderNumber, setRenderNumber] = useState(0);
15 |
16 | useEffect(() => {
17 | const name = 'DanmakuNumber';
18 | const update = () => {
19 | const { all, view, stash } = manager.len();
20 | setAllNumber(all);
21 | setStashNumber(stash);
22 | setRenderNumber(view);
23 | };
24 | manager.use({
25 | name,
26 | push: () => update(),
27 | clear: () => update(),
28 | $destroyed: () => update(),
29 | $beforeMove: () => update(),
30 | });
31 | return () => {
32 | manager.remove(name);
33 | };
34 | }, [manager]);
35 |
36 | return (
37 | <>
38 |
39 |
40 |
41 | {t('setNumbersTitle')}
42 |
43 |
{renderNumber}
44 |
45 |
46 |
47 |
48 | {t('stashNumber')}
49 |
50 |
{stashNumber}
51 |
52 |
53 |
54 |
55 | {t('allNumber')}
56 |
57 |
{allNumber}
58 |
59 | >
60 | );
61 | },
62 | );
63 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarOcclusion.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { VenetianMask } from 'lucide-react';
3 | import { useTranslation } from 'react-i18next';
4 | import type { Manager } from 'danmu';
5 | import type { DanmakuValue } from '@/types';
6 | import { Label } from '@/components/ui/label';
7 | import { Switch } from '@/components/ui/switch';
8 | import maskPath from '@/assets/mask.svg';
9 |
10 | export const SidebarOcclusion = memo(
11 | ({ manager }: { manager: Manager }) => {
12 | const { t } = useTranslation();
13 |
14 | return (
15 |
16 |
20 |
21 | {t('preventOcclusion')}
22 |
23 | {
26 | // The second parameter is optional, If not passed, the default is the built-in danmaku container.
27 | // But it should be noted that the danmaku container will change with the display area, so the second parameter may be required.
28 | manager.updateOccludedUrl(v ? maskPath : '', '#RenderContainer');
29 | }}
30 | />
31 |
32 | );
33 | },
34 | );
35 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarOpacity.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { throttle } from 'aidly';
3 | import { PawPrint } from 'lucide-react';
4 | import { useTranslation } from 'react-i18next';
5 | import type { Manager } from 'danmu';
6 | import type { DanmakuValue } from '@/types';
7 | import { Label } from '@/components/ui/label';
8 | import { Slider } from '@/components/ui/slider';
9 |
10 | export const SidebarOpacity = memo(
11 | ({ manager }: { manager: Manager }) => {
12 | const { t } = useTranslation();
13 |
14 | return (
15 |
16 |
17 |
18 | {t('opacity')}
19 |
20 |
{
26 | manager.setOpacity(v[0] / 100);
27 | })}
28 | />
29 |
30 | );
31 | },
32 | );
33 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarRate.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { throttle } from 'aidly';
3 | import { useTranslation } from 'react-i18next';
4 | import { Squirrel, CircleAlert } from 'lucide-react';
5 | import type { Manager } from 'danmu';
6 | import type { DanmakuValue } from '@/types';
7 | import { Label } from '@/components/ui/label';
8 | import { Input } from '@/components/ui/input';
9 | import {
10 | Tooltip,
11 | TooltipContent,
12 | TooltipProvider,
13 | TooltipTrigger,
14 | } from '@/components/ui/tooltip';
15 |
16 | export const SidebarRate = memo(
17 | ({ manager }: { manager: Manager }) => {
18 | const { t } = useTranslation();
19 |
20 | return (
21 |
22 |
23 |
24 |
25 | {t('setRate')}
26 |
27 |
28 |
29 |
30 |
31 | {t('setRateTip')}
32 |
33 |
34 |
35 |
36 |
{
42 | manager.setRate(Number(e.target.value));
43 | })}
44 | />
45 |
46 | );
47 | },
48 | );
49 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarShowAndHide.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { Turtle } from 'lucide-react';
3 | import { useTranslation } from 'react-i18next';
4 | import type { Manager } from 'danmu';
5 | import type { DanmakuValue } from '@/types';
6 | import { Label } from '@/components/ui/label';
7 | import { Switch } from '@/components/ui/switch';
8 |
9 | export const SidebarShowAndHide = memo(
10 | ({ manager }: { manager: Manager }) => {
11 | const { t } = useTranslation();
12 |
13 | return (
14 |
15 |
19 |
20 | {t('setShow')}
21 |
22 | {
26 | v ? manager.show() : manager.hide();
27 | }}
28 | />
29 |
30 | );
31 | },
32 | );
33 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarSpeed.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { throttle } from 'aidly';
3 | import { useTranslation } from 'react-i18next';
4 | import { Squirrel, CircleAlert } from 'lucide-react';
5 | import type { Manager } from 'danmu';
6 | import type { DanmakuValue } from '@/types';
7 | import { Label } from '@/components/ui/label';
8 | import { Input } from '@/components/ui/input';
9 | import {
10 | Tooltip,
11 | TooltipContent,
12 | TooltipProvider,
13 | TooltipTrigger,
14 | } from '@/components/ui/tooltip';
15 |
16 | export const SidebarSpeed = memo(
17 | ({ manager }: { manager: Manager }) => {
18 | const { t } = useTranslation();
19 |
20 | return (
21 |
22 |
23 |
24 |
25 | {t('setSpeed')}
26 |
27 |
28 |
29 |
30 |
31 | {t('setSpeedTip')}
32 |
33 |
34 |
35 |
36 |
{
42 | manager.setSpeed(Number(e.target.value));
43 | })}
44 | />
45 |
46 | );
47 | },
48 | );
49 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/SidebarStartAndStop.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useState, useEffect } from 'react';
2 | import { Bird } from 'lucide-react';
3 | import { useTranslation } from 'react-i18next';
4 | import type { Manager } from 'danmu';
5 | import type { DanmakuValue } from '@/types';
6 | import { Label } from '@/components/ui/label';
7 | import { Switch } from '@/components/ui/switch';
8 |
9 | export const SidebarStartAndStop = memo(
10 | ({ manager }: { manager: Manager }) => {
11 | const { t } = useTranslation();
12 | const [checked, setChecked] = useState(manager.isPlaying());
13 |
14 | useEffect(() => {
15 | manager.use({
16 | stop: () => setChecked(false),
17 | start: () => setChecked(true),
18 | });
19 | }, [manager]);
20 |
21 | return (
22 |
23 |
27 |
28 | {t('setStart')}
29 |
30 |
34 | v ? manager.startPlaying() : manager.stopPlaying()
35 | }
36 | />
37 |
38 | );
39 | },
40 | );
41 |
--------------------------------------------------------------------------------
/demo/src/components/sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import type { Manager } from 'danmu';
2 | import type { DanmakuValue } from '@/types';
3 | import { SidebarGap } from '@/components/sidebar/SidebarGap';
4 | import { SidebarRate } from '@/components/sidebar/SidebarRate';
5 | import { SidebarSpeed } from '@/components/sidebar/SidebarSpeed';
6 | import { SidebarAreaX } from '@/components/sidebar/SidebarAreaX';
7 | import { SidebarAreaY } from '@/components/sidebar/SidebarAreaY';
8 | import { SidebarFreeze } from '@/components/sidebar/SidebarFreeze';
9 | import { SidebarOpacity } from '@/components/sidebar/SidebarOpacity';
10 | import { SidebarNumbers } from '@/components/sidebar/SidebarNumbers';
11 | import { SidebarDirection } from '@/components/sidebar/SidebarDirection';
12 | import { SidebarFrequency } from '@/components/sidebar/SidebarFrequency';
13 | import { SidebarOcclusion } from '@/components/sidebar/SidebarOcclusion';
14 | import { SidebarModeSelect } from '@/components/sidebar/SidebarModeSelect';
15 | import { SidebarShowAndHide } from '@/components/sidebar/SidebarShowAndHide';
16 | import { SidebarMoveDuration } from '@/components/sidebar/SidebarMoveDuration';
17 | import { SidebarStartAndStop } from '@/components/sidebar/SidebarStartAndStop';
18 |
19 | export const Sidebar = ({ manager }: { manager: Manager }) => {
20 | return (
21 | <>
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | >
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/demo/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AvatarPrimitive from '@radix-ui/react-avatar';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/demo/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground',
18 | },
19 | },
20 | defaultVariants: {
21 | variant: 'default',
22 | },
23 | },
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/demo/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-9 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8',
26 | icon: 'h-10 w-10',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | },
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button';
45 | return (
46 |
51 | );
52 | },
53 | );
54 | Button.displayName = 'Button';
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/demo/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = 'Input';
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/demo/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import { cva, type VariantProps } from 'class-variance-authority';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const labelVariants = cva(
10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/demo/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as PopoverPrimitive from '@radix-ui/react-popover';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/demo/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SheetPrimitive from '@radix-ui/react-dialog';
5 | import { cva, type VariantProps } from 'class-variance-authority';
6 | import { X } from 'lucide-react';
7 |
8 | import { cn } from '@/lib/utils';
9 |
10 | const Sheet = SheetPrimitive.Root;
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger;
13 |
14 | const SheetClose = SheetPrimitive.Close;
15 |
16 | const SheetPortal = SheetPrimitive.Portal;
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ));
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
32 |
33 | const sheetVariants = cva(
34 | 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
35 | {
36 | variants: {
37 | side: {
38 | top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
39 | bottom:
40 | 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
41 | left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
42 | right:
43 | 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
44 | },
45 | },
46 | defaultVariants: {
47 | side: 'right',
48 | },
49 | },
50 | );
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = 'right', className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ));
75 | SheetContent.displayName = SheetPrimitive.Content.displayName;
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | );
89 | SheetHeader.displayName = 'SheetHeader';
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | );
103 | SheetFooter.displayName = 'SheetFooter';
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ));
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName;
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ));
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName;
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | };
141 |
--------------------------------------------------------------------------------
/demo/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SliderPrimitive from '@radix-ui/react-slider';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ));
27 | Slider.displayName = SliderPrimitive.Root.displayName;
28 |
29 | export { Slider };
30 |
--------------------------------------------------------------------------------
/demo/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SwitchPrimitives from '@radix-ui/react-switch';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/demo/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TabsPrimitive from '@radix-ui/react-tabs';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/demo/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | },
21 | );
22 | Textarea.displayName = 'Textarea';
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/demo/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from '@/components/ui/toast';
11 | import { useToast } from '@/components/ui/use-toast';
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/demo/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/demo/src/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/demo/src/i18n/en.ts:
--------------------------------------------------------------------------------
1 | export const enMap = {
2 | time: 'Time',
3 | position: 'Position',
4 | language: 'English',
5 | direction: 'Direction',
6 | selectDirection: 'Select direction',
7 | randomColor: 'Random color',
8 | clearDanmaku: 'Clear danmaku',
9 | currentlyFrozen: 'Currently frozen',
10 | notEmptyValue: 'Danmaku value cannot be empty',
11 | inputDanmaku: 'Enter your danmaku content.',
12 | setFlexiblePosition: 'Set flexible danmaku position information',
13 | facileDanmaku: 'Facile danmaku',
14 | flexibleDanmaku: 'Advanced danmaku',
15 | correctedDuration: 'Corrected duration',
16 | thisIsA: 'This is a ',
17 | danmakuGap: 'Danmaku gap',
18 | opacity: 'Opacity',
19 | setArea: 'Rendering area',
20 | setDirection: 'Direction (left/right)',
21 | setFreeze: 'Unfreeze/Freeze',
22 | setInterval: 'Rendering frequency',
23 | setIntervalTip:
24 | 'The manager will have a timer to poll and push normal danmaku, please set an appropriate value',
25 | setGap: 'Gap between danmaku',
26 | setGapTip:
27 | 'In the case of collision detection on the same track, the minimum distance between the following danmaku and the previous danmaku',
28 | setMode: 'Mode',
29 | setModeTipTitle:
30 | 'The rendering mode determines the collision detection rules and the timing of danmaku rendering',
31 | setModeTipOne:
32 | 'When set to `none` mode, there will be no collision detection, and the danmaku will be rendered immediately',
33 | setModeTipTwo:
34 | 'When set to `strict` mode, strict collision detection will be performed, and if the conditions are not met, rendering will be delayed',
35 | setModeTipThree:
36 | 'When set to `adaptive` mode, collision detection will be performed as much as possible under the premise of immediate rendering (recommended)',
37 | setDuration: 'Duration',
38 | setDurationTip:
39 | 'Normal danmaku will randomly take a value between these two values as the movement duration',
40 | setNumbersTitle: 'Real-time rendering danmaku',
41 | stashNumber: 'Stashed danmaku',
42 | allNumber: 'Total danmaku',
43 | preventOcclusion: 'Cancel/Prevent occlusion',
44 | setRate: 'Rate',
45 | setRateTip:
46 | 'The default rate is 1, and the movement speed of the danmaku is equal to the original speed * rate',
47 | setSpeed: 'Speed',
48 | setSpeedTip:
49 | 'The danmuku will move at a constant speed, and the `timeRange` configuration will be invalid',
50 | setShow: 'Hide/Show',
51 | setStart: 'Stop/Start',
52 | mockDanmuContent0: 'Wow!',
53 | mockDanmuContent1: 'Unbelievable!',
54 | mockDanmuContent2: 'This is amazing!',
55 | mockDanmuContent3: 'Stunned!',
56 | mockDanmuContent4: 'So magical!',
57 | mockDanmuContent5: 'What is this operation?',
58 | mockDanmuContent6: 'What did I just see?',
59 | mockDanmuContent7: 'What is the principle behind this?',
60 | mockDanmuContent8: 'I don’t quite understand.',
61 | mockDanmuContent9: 'What is this operation?',
62 | };
63 |
--------------------------------------------------------------------------------
/demo/src/i18n/zh.ts:
--------------------------------------------------------------------------------
1 | export const zhMap = {
2 | time: '时间',
3 | position: '位置',
4 | language: '简体中文',
5 | direction: '移动方向',
6 | selectDirection: '选择方向',
7 | randomColor: '随机颜色',
8 | clearDanmaku: '清空弹幕',
9 | currentlyFrozen: '当前处于冻结状态',
10 | notEmptyValue: '弹幕值不能为空',
11 | inputDanmaku: '输入你的弹幕内容。',
12 | setFlexiblePosition: '设置高级弹幕的位置信息',
13 | facileDanmaku: '普通弹幕',
14 | flexibleDanmaku: '高级弹幕',
15 | correctedDuration: '被修正过运动时间',
16 | thisIsA: '这个是一个',
17 | danmakuGap: '弹幕间距',
18 | opacity: '透明度',
19 | setArea: '渲染区域',
20 | setDirection: '方向(左/右)',
21 | setFreeze: '恢复/冻结',
22 | setInterval: '渲染频率',
23 | setIntervalTip: 'manager 会有一个定时器来轮询 push 普通弹幕,请设置合适的值',
24 | setGap: '弹幕之间的间距',
25 | setGapTip:
26 | '同一条轨道在碰撞检测的啥情况下,后一条弹幕与前一条弹幕最小相隔的距离',
27 | setMode: '渲染模式',
28 | setModeTipTitle: '渲染模式决定着碰撞检测的规则和弹幕渲染的时机',
29 | setModeTipOne: '当设置为 none 模式时,不会有任何碰撞检测,弹幕会立即渲染',
30 | setModeTipTwo:
31 | '当设置为 strict 模式时,会进行严格的碰撞检测,如果不满足条件则会推迟渲染',
32 | setModeTipThree:
33 | '当设置为 adaptive 模式时,在满足立即渲染的前提下,会尽力进行碰撞检测(推荐)',
34 | setDuration: '运动时长',
35 | setDurationTip: '普通弹幕会从这两个值之间随机取一个值作为弹幕运动的时间',
36 | setNumbersTitle: '实时渲染弹幕',
37 | stashNumber: '暂存区弹幕',
38 | allNumber: '弹幕总量(包含暂存区)',
39 | preventOcclusion: '取消/防遮挡',
40 | setRate: '设置速率',
41 | setRateTip: '速率默认为 1,弹幕的运动速度等于原始速度 * 速率',
42 | setSpeed: '设置速度',
43 | setSpeedTip: '弹幕将以恒定的速度进行运动,此时 `timeRange` 配置会失效',
44 | setShow: '隐藏/显示',
45 | setStart: '停止/启动',
46 | mockDanmuContent0: '哇塞!',
47 | mockDanmuContent1: '不可思议!',
48 | mockDanmuContent2: '这也太厉害了吧!',
49 | mockDanmuContent3: '惊呆了!',
50 | mockDanmuContent4: '太神奇了!',
51 | mockDanmuContent5: '这是什么操作?',
52 | mockDanmuContent6: '我看到了什么?',
53 | mockDanmuContent7: '这是什么原理?',
54 | mockDanmuContent8: '我有点看不懂了。',
55 | mockDanmuContent9: '这是什么操作?',
56 | };
57 |
--------------------------------------------------------------------------------
/demo/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export const isMobile = (() => {
5 | return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
6 | navigator.userAgent || navigator.vendor || '',
7 | );
8 | })();
9 |
10 | export function cn(...inputs: ClassValue[]) {
11 | return twMerge(clsx(inputs));
12 | }
13 |
--------------------------------------------------------------------------------
/demo/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import i18next from 'i18next';
4 | import { initReactI18next } from 'react-i18next';
5 | import LanguageDetector from 'i18next-browser-languagedetector';
6 | import { enMap } from '@/i18n/en';
7 | import { zhMap } from '@/i18n/zh';
8 | import { App } from '@/App';
9 | import { mock, autoFormat, initManager } from '@/manager';
10 | import '@/globals.css';
11 |
12 | // https://github.com/i18next/i18next-browser-languageDetector
13 | // https://www.i18next.com/overview/configuration-options
14 | i18next
15 | .use(LanguageDetector)
16 | .use(initReactI18next)
17 | .init({
18 | fallbackLng: 'en',
19 | supportedLngs: ['zh', 'en'],
20 | interpolation: {
21 | escapeValue: false,
22 | },
23 | resources: {
24 | en: {
25 | translation: enMap,
26 | },
27 | zh: {
28 | translation: zhMap,
29 | },
30 | },
31 | });
32 |
33 | const manager = ((window as any).manager = initManager());
34 |
35 | ReactDOM.createRoot(document.getElementById('root')!).render(
36 |
37 |
38 | ,
39 | );
40 |
41 | mock(manager);
42 | autoFormat(manager);
43 |
--------------------------------------------------------------------------------
/demo/src/manager.tsx:
--------------------------------------------------------------------------------
1 | import { uuid } from 'aidly';
2 | import { t } from 'i18next';
3 | import ReactDOM from 'react-dom/client';
4 | import { type Manager, create } from 'danmu';
5 | import type { Statuses, DanmakuValue } from '@/types';
6 | import { DanmakuComponent } from '@/components/danmu/Danmaku';
7 |
8 | export const initManager = () => {
9 | const manager = create({
10 | interval: 100,
11 | trackHeight: 40,
12 | durationRange: [10000, 13000],
13 | plugin: {
14 | init(manager) {
15 | 'shadow shadow-slate-200 bg-slate-100'.split(' ').forEach((c) => {
16 | manager.container.node.classList.add(c);
17 | });
18 |
19 | document.addEventListener('visibilitychange', () => {
20 | const options = { preventEvents: ['pause', 'resume'] };
21 | document.visibilityState === 'hidden'
22 | ? manager.freeze(options)
23 | : manager.unfreeze(options);
24 | });
25 | },
26 |
27 | $createNode(dm) {
28 | if (!dm.node) return;
29 | ReactDOM.createRoot(dm.node).render(
30 | ,
31 | );
32 | },
33 | },
34 | });
35 | return manager;
36 | };
37 |
38 | export const mock = (manager: Manager) => {
39 | setInterval(() => {
40 | for (let i = 0; i < 10; i++) {
41 | manager.push({
42 | id: uuid(),
43 | isSelf: false,
44 | content: t(`mockDanmuContent${i}`),
45 | });
46 | }
47 | }, 1000);
48 | };
49 |
50 | export const autoFormat = (manager: Manager) => {
51 | const resizeObserver = new ResizeObserver(() => manager.format());
52 | resizeObserver.observe(document.body);
53 | };
54 |
--------------------------------------------------------------------------------
/demo/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface DanmakuValue {
2 | id: string;
3 | content: string;
4 | isSelf?: boolean;
5 | }
6 |
7 | export type Statuses = {
8 | background: string;
9 | };
10 |
--------------------------------------------------------------------------------
/demo/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/demo/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ['class'],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | prefix: '',
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: '2rem',
15 | screens: {
16 | '2xl': '1400px',
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: 'hsl(var(--border))',
22 | input: 'hsl(var(--input))',
23 | ring: 'hsl(var(--ring))',
24 | background: 'hsl(var(--background))',
25 | foreground: 'hsl(var(--foreground))',
26 | primary: {
27 | DEFAULT: 'hsl(var(--primary))',
28 | foreground: 'hsl(var(--primary-foreground))',
29 | },
30 | secondary: {
31 | DEFAULT: 'hsl(var(--secondary))',
32 | foreground: 'hsl(var(--secondary-foreground))',
33 | },
34 | destructive: {
35 | DEFAULT: 'hsl(var(--destructive))',
36 | foreground: 'hsl(var(--destructive-foreground))',
37 | },
38 | muted: {
39 | DEFAULT: 'hsl(var(--muted))',
40 | foreground: 'hsl(var(--muted-foreground))',
41 | },
42 | accent: {
43 | DEFAULT: 'hsl(var(--accent))',
44 | foreground: 'hsl(var(--accent-foreground))',
45 | },
46 | popover: {
47 | DEFAULT: 'hsl(var(--popover))',
48 | foreground: 'hsl(var(--popover-foreground))',
49 | },
50 | card: {
51 | DEFAULT: 'hsl(var(--card))',
52 | foreground: 'hsl(var(--card-foreground))',
53 | },
54 | },
55 | borderRadius: {
56 | lg: 'var(--radius)',
57 | md: 'calc(var(--radius) - 2px)',
58 | sm: 'calc(var(--radius) - 4px)',
59 | },
60 | keyframes: {
61 | 'accordion-down': {
62 | from: { height: '0' },
63 | to: { height: 'var(--radix-accordion-content-height)' },
64 | },
65 | 'accordion-up': {
66 | from: { height: 'var(--radix-accordion-content-height)' },
67 | to: { height: '0' },
68 | },
69 | },
70 | animation: {
71 | 'accordion-down': 'accordion-down 0.2s ease-out',
72 | 'accordion-up': 'accordion-up 0.2s ease-out',
73 | },
74 | },
75 | },
76 | plugins: [require('tailwindcss-animate')],
77 | };
78 |
--------------------------------------------------------------------------------
/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 | "paths": {
10 | "@/*": ["./src/*"],
11 | },
12 |
13 | /* Bundler mode */
14 | "moduleResolution": "bundler",
15 | "allowImportingTsExtensions": true,
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "moduleDetection": "force",
19 | "noEmit": true,
20 | "jsx": "react-jsx",
21 |
22 | /* Linting */
23 | "strict": true,
24 | "noUnusedLocals": true,
25 | "noUnusedParameters": true,
26 | "noFallthroughCasesInSwitch": true
27 | },
28 | "include": ["src"]
29 | }
30 |
--------------------------------------------------------------------------------
/demo/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { defineConfig } from 'vite';
3 | import react from '@vitejs/plugin-react';
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | base: '/danmu',
8 | plugins: [react()],
9 | server: {
10 | watch: {
11 | ignored: '!**/node_modules/danmu/dist/**',
12 | },
13 | },
14 | resolve: {
15 | alias: {
16 | '@': path.resolve(__dirname, 'src'),
17 | },
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/docs/.vitepress/config/en.ts:
--------------------------------------------------------------------------------
1 | import { DefaultTheme, LocaleSpecificConfig } from 'vitepress';
2 |
3 | export const en: LocaleSpecificConfig = {
4 | themeConfig: {
5 | nav: [
6 | { text: 'Home', link: '/en' },
7 | { text: 'Start Learning', link: '/en/guide/getting-started' },
8 | ],
9 | sidebar: [
10 | {
11 | text: 'Getting Started',
12 | collapsed: false,
13 | items: [
14 | { text: 'Getting Started', link: '/en/guide/getting-started' },
15 | { text: 'Writing Plugins', link: '/en/guide/create-plugin' },
16 | {
17 | text: 'Typescript Interface',
18 | link: '/en/guide/typescript-interface',
19 | },
20 | ],
21 | },
22 | {
23 | text: 'Manager',
24 | collapsed: false,
25 | items: [
26 | {
27 | text: 'Manager Configuration',
28 | link: '/en/reference/manager-configuration',
29 | },
30 | {
31 | text: 'Manager Hooks',
32 | link: '/en/reference/manager-hooks',
33 | },
34 | {
35 | text: 'Manager Properties',
36 | link: '/en/reference/manager-properties',
37 | },
38 | {
39 | text: 'Manager API',
40 | link: '/en/reference/manager-api',
41 | },
42 | ],
43 | },
44 | {
45 | text: 'Danmaku',
46 | collapsed: false,
47 | items: [
48 | {
49 | text: 'Danmaku Hooks',
50 | link: '/en/reference/danmaku-hooks',
51 | },
52 | {
53 | text: 'Danmaku Properties',
54 | link: '/en/reference/danmaku-properties',
55 | },
56 | {
57 | text: 'Danmaku API',
58 | link: '/en/reference/danmaku-api',
59 | },
60 | ],
61 | },
62 | {
63 | text: 'basic',
64 | collapsed: false,
65 | items: [
66 | {
67 | text: 'Track API',
68 | link: '/en/reference/track-api',
69 | },
70 | {
71 | text: 'Container API',
72 | link: '/en/reference/container-api',
73 | },
74 | ],
75 | },
76 | {
77 | text: 'Common Use Cases',
78 | collapsed: false,
79 | items: [
80 | {
81 | text: 'Track Settings',
82 | link: '/en/cases/track-settings',
83 | },
84 | {
85 | text: 'Uniform Speed',
86 | link: '/en/cases/uniform-speed',
87 | },
88 | {
89 | text: 'Cooldown',
90 | link: '/en/cases/cooldown',
91 | },
92 | {
93 | text: 'Simplified Mode',
94 | link: '/en/cases/simplified-mode',
95 | },
96 | {
97 | text: 'Filter Keywords',
98 | link: '/en/cases/filter-keyword',
99 | },
100 | {
101 | text: 'Like/Dislike',
102 | link: '/en/cases/like',
103 | },
104 | {
105 | text: 'Send Looping Danmaku',
106 | link: '/en/cases/loop',
107 | },
108 | {
109 | text: 'Send Danmaku with Images',
110 | link: '/en/cases/image',
111 | },
112 | {
113 | text: 'Custom Danmaku Styles',
114 | link: '/en/cases/custom-danmaku',
115 | },
116 | {
117 | text: 'Custom Container Styles',
118 | link: '/en/cases/custom-container',
119 | },
120 | {
121 | text: 'Pin Danmaku to the Top',
122 | link: '/en/cases/fixed',
123 | },
124 | {
125 | text: 'Anti-Blocking Feature',
126 | link: '/en/cases/anti-occlusion',
127 | },
128 | {
129 | text: 'Live Streaming and Video',
130 | link: '/en/cases/recommend',
131 | },
132 | ],
133 | },
134 | ],
135 | },
136 | };
137 |
--------------------------------------------------------------------------------
/docs/.vitepress/config/index.ts:
--------------------------------------------------------------------------------
1 | import { sharedConfig } from './shared.js';
2 | import { zh } from './zh';
3 | import { en } from './en';
4 | import { withMermaid } from 'vitepress-plugin-mermaid';
5 | import { defineConfig } from 'vitepress';
6 |
7 | export default (process.env.NODE_ENV === 'production'
8 | ? withMermaid
9 | : defineConfig)({
10 | ...sharedConfig,
11 | markdown: { math: true },
12 | locales: {
13 | root: { label: '简体中文', lang: 'zh', link: '/zh', ...zh },
14 | en: { label: 'English', lang: 'en', link: '/en', ...en },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/docs/.vitepress/config/shared.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress';
2 |
3 | const base = '/danmu/document';
4 |
5 | export const sharedConfig = defineConfig({
6 | base,
7 | title: 'The danmu Book',
8 | appearance: 'dark',
9 | description:
10 | 'Danmu 🌘 Collision detection, highly customized danmu screen styles, you deserve it',
11 | lang: 'zh',
12 | lastUpdated: true,
13 | ignoreDeadLinks: true,
14 | head: [
15 | [
16 | 'link',
17 | { rel: 'icon', type: 'image/x-icon', href: `${base}/favicon.svg` },
18 | ],
19 | ],
20 | themeConfig: {
21 | logo: '/favicon.svg',
22 | search: { provider: 'local' },
23 | outline: 'deep',
24 | socialLinks: [
25 | { icon: 'github', link: 'https://github.com/imtaotao/danmu' },
26 | ],
27 | editLink: {
28 | pattern: 'https://github.com/imtaotao/danmu/blob/master/docs/:path',
29 | text: 'Suggest changes to this page',
30 | },
31 | footer: {
32 | copyright: `Copyright © 2024-${new Date().getFullYear()} imtaotao`,
33 | message: 'Released under the MIT License.',
34 | },
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/docs/.vitepress/config/zh.ts:
--------------------------------------------------------------------------------
1 | import { DefaultTheme, LocaleSpecificConfig } from 'vitepress';
2 |
3 | export const zh: LocaleSpecificConfig = {
4 | themeConfig: {
5 | nav: [
6 | { text: '主页', link: '/zh' },
7 | { text: '快速开始', link: '/zh/guide/getting-started' },
8 | ],
9 | sidebar: [
10 | {
11 | text: '指南',
12 | collapsed: false,
13 | items: [
14 | { text: '快速开始', link: '/zh/guide/getting-started' },
15 | { text: '编写插件', link: '/zh/guide/create-plugin' },
16 | { text: 'Typescript 类型', link: '/zh/guide/typescript-interface' },
17 | ],
18 | },
19 | {
20 | text: 'manager',
21 | collapsed: false,
22 | items: [
23 | {
24 | text: 'manager 配置',
25 | link: '/zh/reference/manager-configuration',
26 | },
27 | {
28 | text: 'manager 钩子',
29 | link: '/zh/reference/manager-hooks',
30 | },
31 | {
32 | text: 'manager 属性',
33 | link: '/zh/reference/manager-properties',
34 | },
35 | {
36 | text: 'manager 方法',
37 | link: '/zh/reference/manager-api',
38 | },
39 | ],
40 | },
41 | {
42 | text: '弹幕',
43 | collapsed: false,
44 | items: [
45 | {
46 | text: '弹幕钩子',
47 | link: '/zh/reference/danmaku-hooks',
48 | },
49 | {
50 | text: '弹幕属性',
51 | link: '/zh/reference/danmaku-properties',
52 | },
53 | {
54 | text: '弹幕方法',
55 | link: '/zh/reference/danmaku-api',
56 | },
57 | ],
58 | },
59 | {
60 | text: '基础类',
61 | collapsed: false,
62 | items: [
63 | {
64 | text: '轨道 API',
65 | link: '/zh/reference/track-api',
66 | },
67 | {
68 | text: '容器 API',
69 | link: '/zh/reference/container-api',
70 | },
71 | ],
72 | },
73 | {
74 | text: '常见 case',
75 | collapsed: false,
76 | items: [
77 | {
78 | text: '轨道设置',
79 | link: '/zh/cases/track-settings',
80 | },
81 | {
82 | text: '匀速运动',
83 | link: '/zh/cases/uniform-speed',
84 | },
85 | {
86 | text: '弹幕冷却时间',
87 | link: '/zh/cases/cooldown',
88 | },
89 | {
90 | text: '弹幕精简模式',
91 | link: '/zh/cases/simplified-mode',
92 | },
93 | {
94 | text: '过滤关键字',
95 | link: '/zh/cases/filter-keyword',
96 | },
97 | {
98 | text: '点赞/点踩',
99 | link: '/zh/cases/like',
100 | },
101 | {
102 | text: '发送循环弹幕',
103 | link: '/zh/cases/loop',
104 | },
105 | {
106 | text: '发送带图片的弹幕',
107 | link: '/zh/cases/image',
108 | },
109 |
110 | {
111 | text: '自定义弹幕样式',
112 | link: '/zh/cases/custom-danmaku',
113 | },
114 | {
115 | text: '自定义容器样式',
116 | link: '/zh/cases/custom-container',
117 | },
118 | {
119 | text: '固定弹幕在容器顶部',
120 | link: '/zh/cases/fixed',
121 | },
122 | {
123 | text: '防遮挡功能的实现',
124 | link: '/zh/cases/anti-occlusion',
125 | },
126 | {
127 | text: '直播和视频场景的建议',
128 | link: '/zh/cases/recommend',
129 | },
130 | ],
131 | },
132 | ],
133 | },
134 | };
135 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { useData, inBrowser } from 'vitepress';
2 | import DefaultTheme from 'vitepress/theme-without-fonts';
3 | import './main.css';
4 |
5 | export default {
6 | ...DefaultTheme,
7 | setup() {
8 | const { lang } = useData();
9 | if (inBrowser) {
10 | document.cookie = `nf_lang=${lang.value}; expires=Mon, 1 Jan 2024 00:00:00 UTC; path=/`;
11 | }
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/docs/en/cases/anti-occlusion.md:
--------------------------------------------------------------------------------
1 | # Anti-Blocking Feature
2 |
3 | ## Description
4 |
5 | This section will teach you how to implement the anti-occlusion feature. Since the anti-occlusion feature requires defining the occluded area, it is generally done by defining an `svg` image to set the area and then using the `CSS` [**`maskImage`**](https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image) property to achieve it. There are mainly two steps you need to implement.
6 |
7 | > [!NOTE] Hint
8 | >
9 | > 1. Poll to get the `svg` image that needs to prevent occlusion, usually generated through **AI**, but it also depends on business requirements.
10 | > 2. Call the danmu library's [**`manager.updateOccludedUrl`**](../reference/manager-api/#manager-updateoccludedurl) to set the `CSS` property.
11 |
12 | ## Example
13 |
14 | ```ts {7,10}
15 | (async function update() {
16 | const { url } = await fetch('https://abc.com/svg').then((res) => res.json());
17 |
18 | // 1. Update the mask (if the second parameter is not provided,
19 | // it defaults to setting on `manager.container.node`)
20 | // 2. The url can also be a base64 image, which might be helpful for you
21 | manager.updateOccludedUrl(url, '#id');
22 |
23 | // // Polling request
24 | setTimeout(() => update(), 1000);
25 | })();
26 | ```
27 |
--------------------------------------------------------------------------------
/docs/en/cases/cooldown.md:
--------------------------------------------------------------------------------
1 | # Cooldown
2 |
3 | ## Description
4 |
5 | In live streaming scenarios, when users send danmaku at a high frequency within a short period, it may appear as spamming. This section provides a simple implementation for such scenarios. You can refer to its principles and ideas to extend it according to your business needs.
6 |
7 | > [!NOTE] Hint
8 | > We mainly rely on the [**`willRender`**](../reference/manager-hooks/#hooks-willrender) hook to achieve this.
9 |
10 | ## Implementation Based on Danmaku Content
11 |
12 | ```ts {13-23}
13 | import { create } from 'danmu';
14 |
15 | const cd = 3000;
16 | const map = Object.create(null);
17 |
18 | // Create manager, define the type of danmaku to be sent as string
19 | const manager = create({ ... });
20 |
21 | // Write a plugin specifically to handle danmaku cooldown (CD)
22 | manager.use({
23 | name: 'cd',
24 | willRender(ref) {
25 | const now = Date.now();
26 | const content = ref.danmaku.data;
27 | const prevTime = map[content];
28 |
29 | if (prevTime && now - prevTime < cd) {
30 | ref.prevent = true;
31 | console.warn(`"${content}" is blocked.`);
32 | } else {
33 | map[content] = now;
34 | }
35 | return ref;
36 | },
37 | });
38 |
39 | // ✔️ Successfully
40 | manager.push('content'); // [!code hl]
41 |
42 | // ❌ Blocked
43 | manager.push('content'); // [!code error]
44 |
45 | // ✔️ Successfully
46 | setTimeout(() => {
47 | manager.push('content'); // [!code hl]
48 | }, 3000)
49 | ```
50 |
51 | ## Implementation Based on User ID
52 |
53 | ```ts {13-23}
54 | import { create } from 'danmu';
55 |
56 | // Create manager,
57 | // define the type of danmaku to be sent as `{ userId: number, content: string }`
58 | const manager = create<{ userId: number, content: string }>({ ... });
59 |
60 | const cd = 3000;
61 | const map = Object.create(null);
62 |
63 | manager.use({
64 | name: 'cd',
65 | willRender(ref) {
66 | const now = Date.now();
67 | const { userId } = ref.danmaku.data;
68 | const prevTime = map[userId];
69 |
70 | if (prevTime && now - prevTime < cd) {
71 | ref.prevent = true;
72 | console.warn(`"${userId}" is blocked.`);
73 | } else {
74 | map[userId] = now;
75 | }
76 | return ref;
77 | },
78 | });
79 |
80 | // ✔️ Successfully
81 | manager.push({ useId: 1, content: 'content1' }); // [!code hl]
82 |
83 | // ❌ Blocked
84 | manager.push({ useId: 1, content: 'content2' }); // [!code error]
85 |
86 | // ✔️ Successfully
87 | setTimeout(() => {
88 | manager.push({ useId: 1, content: 'content3' }); // [!code hl]
89 | }, 3000)
90 | ```
91 |
--------------------------------------------------------------------------------
/docs/en/cases/custom-container.md:
--------------------------------------------------------------------------------
1 | # Custom Container Styles
2 |
3 | ## Description
4 |
5 | We mainly achieve this through the [**`manager.container.setStyle`**](../reference/manager-properties/#manager-container-setstyle) API.
6 |
7 | > [!NOTE] Hint
8 | > The styles set through the official API will only apply to the root node of the container, which is [**`manager.container.node`**](../reference/manager-properties/#manager-container-node).
9 |
10 | ## Example
11 |
12 | ```ts {14,24}
13 | import { create } from 'danmu';
14 |
15 | // Styles to be added
16 | const styles = {
17 | background: 'red',
18 | // .
19 | };
20 |
21 | const manager = create({
22 | plugin: {
23 | // You can add hooks during initialization
24 | init(manager) {
25 | for (const key in styles) {
26 | manager.container.setStyle(key, styles[key]);
27 | }
28 | // You can also add a `className` to the container DOM here
29 | manager.container.node.classList.add('className');
30 | },
31 | },
32 | });
33 |
34 | // Or directly call the API
35 | for (const key in styles) {
36 | manager.container.setStyle(key, styles[key]);
37 | }
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/en/cases/custom-danmaku.md:
--------------------------------------------------------------------------------
1 | # Custom Danmaku Styles
2 |
3 | ## Description
4 |
5 | Since we can access the DOM node of the danmaku, it is very convenient to customize CSS styles. This is mainly achieved through the [**`manager.setStyle`**](../reference/manager-api/#manager-setstyle) and [**`danmaku.setStyle`**](../reference/danmaku-api/#danmaku-setstyle) APIs.
6 |
7 | > [!NOTE] Hint
8 | > The styles set through the official API will only apply to the root node of the danmaku, which is [**`danmaku.node`**](../reference/danmaku-props/#danmaku-node).
9 |
10 | ## Setting Styles via `manager.setStyle`
11 |
12 | ```ts {14}
13 | import { create } from 'danmu';
14 |
15 | // Styles to be added
16 | const styles = {
17 | color: 'red',
18 | fontSize: '15px',
19 | // .
20 | };
21 |
22 | const manager = create();
23 |
24 | // Subsequent rendered danmaku and currently rendered danmaku will have these styles applied.
25 | for (const key in styles) {
26 | manager.setStyle(key, styles[key]);
27 | }
28 | ```
29 |
30 | ## Setting Styles via `danmaku.setStyle`
31 |
32 | In this implementation, you might need to leverage [**`manager.statuses`**](../reference/manager-properties/#manager-statuses) to simplify the implementation in real business scenarios.
33 |
34 | ```ts {15,26}
35 | import { create } from 'danmu';
36 |
37 | // Styles to be added
38 | const styles = {
39 | color: 'red',
40 | fontSize: '15px',
41 | // .
42 | };
43 |
44 | // Add hooks during initialization so that new danmaku will automatically have these styles applied when rendered
45 | const manager = create({
46 | plugin: {
47 | $beforeMove(danmaku) {
48 | for (const key in styles) {
49 | danmaku.setStyle(key, styles[key]);
50 | }
51 | // You can also add a `className` to the container DOM here
52 | danmaku.node.classList.add('className');
53 | },
54 | },
55 | });
56 |
57 | // Add styles to the currently rendered danmaku
58 | manager.asyncEach((danmaku) => {
59 | for (const key in styles) {
60 | danmaku.setStyle(key, styles[key]);
61 | }
62 | });
63 | ```
64 |
--------------------------------------------------------------------------------
/docs/en/cases/filter-keyword.md:
--------------------------------------------------------------------------------
1 | # Filter Keywords
2 |
3 | ## Description
4 |
5 | The implementation of the keyword filtering feature has already been covered in the [**Writing Plugins**](../guide/create-plugin) chapter.
6 |
7 | > [!NOTE] Hint
8 | > The implementation of keyword filtering also relies on the [**`willRender`**](../reference/manager-hooks/#hooks-willrender) hook.
9 |
10 | ## Example
11 |
12 | ```ts {4,12}
13 | import { create } from 'danmu';
14 |
15 | // Define the keyword list
16 | const keywords = ['a', 'c', 'e'];
17 |
18 | // Create `manager`, define the type of danmaku to be sent as `string`
19 | const manager = create({
20 | plugin: {
21 | willRender(ref) {
22 | for (const word of keywords) {
23 | if (ref.danmaku.data.includes(word)) {
24 | ref.prevent = true;
25 | break;
26 | }
27 | }
28 | return ref;
29 | },
30 | },
31 | });
32 |
33 | // ❌ Will be filtered
34 | manager.push('ab'); // [!code error]
35 |
36 | // ✔️ Will not be filtered
37 | manager.push('bd'); // [!code hl]
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/en/cases/fixed.md:
--------------------------------------------------------------------------------
1 | # Pin Danmaku to the Top
2 |
3 | ## Description
4 |
5 | This section will introduce how to fix danmaku at a specific position, using **`top`** and **`left`** as examples. Since we need to customize the position, we will use the capabilities of flexible danmaku.
6 |
7 | > [!NOTE] Hint
8 | > You can copy and paste the following code into the console of the online [**demo**](https://imtaotao.github.io/danmu/) to see the effect.
9 |
10 | ## Fixing Danmaku at the Top
11 |
12 | **1. Fixed at the very top:**
13 |
14 | ```ts {7-8}
15 | // This danmaku will hover 10px from the top, centered, for 5 seconds
16 | manager.pushFlexibleDanmaku('弹幕内容', {
17 | duration: 5000,
18 | direction: 'none',
19 | position(danmaku, container) {
20 | return {
21 | x: `50% - ${danmaku.getWidth() / 2}`,
22 | y: 10, // `10px` from the top of the container
23 | };
24 | },
25 | });
26 | ```
27 |
28 | **2. Fixed on the 2nd track from the top:**
29 |
30 | ```ts {9-10}
31 | // This danmaku will hover in the center of the second track for 5s
32 | manager.pushFlexibleDanmaku('content', {
33 | duration: 5000,
34 | direction: 'none',
35 | position(danmaku, container) {
36 | // Render in the 3rd track
37 | const { middle } = manager.getTrack(2).location;
38 | return {
39 | x: `50% - ${danmaku.getWidth() / 2}`,
40 | y: middle - danmaku.getHeight() / 2,
41 | };
42 | },
43 | });
44 | ```
45 |
46 | ## Fixing Danmaku on the Left
47 |
48 | ```ts {7,9-10}
49 | // This danmaku will stay `10px` from the left in the middle of the container for 5s.
50 | manager.pushFlexibleDanmaku('弹幕内容', {
51 | duration: 5000,
52 | direction: 'none',
53 | position(danmaku, container) {
54 | // Render in the 3rd track
55 | const { middle } = manager.getTrack(2).location;
56 | return {
57 | x: 10,
58 | y: `50% - ${danmaku.getHeight() / 2}`,
59 | };
60 | },
61 | });
62 | ```
63 |
--------------------------------------------------------------------------------
/docs/en/cases/image.md:
--------------------------------------------------------------------------------
1 | # Send Danmaku with Images
2 |
3 | ## Description
4 |
5 | To allow danmaku to carry images, similar to the implementation of the [**like/dislike**](./like) feature, you need to add custom content inside the danmaku node. In fact, it's not just images; you can **add any content** inside the danmaku node.
6 |
7 | > [!NOTE] Hint
8 | > The components in this section are demonstrated using **React**.
9 |
10 | ## Developing the Danmaku Component
11 |
12 | ```tsx {4-5}
13 | export function Danmaku({ danmaku }) {
14 | return (
15 |
16 |
17 | {danmaku.data}
18 |
19 | );
20 | }
21 | ```
22 |
23 | ## Render Danmaku
24 |
25 | ```tsx title="init.tsx" {9}
26 | import ReactDOM from 'react-dom/client';
27 | import { create } from 'danmu';
28 | import { Danmaku } from './Danmaku';
29 |
30 | const manager = create({
31 | plugin: {
32 | // Render the component onto the built-in node of the danmaku
33 | $createNode(danmaku) {
34 | ReactDOM.createRoot(danmaku.node).render( );
35 | },
36 | },
37 | });
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/en/cases/like.md:
--------------------------------------------------------------------------------
1 | # Like/Dislike
2 |
3 | ## Description
4 |
5 | When we hover over a danmaku, we might need to perform some actions. This section will guide you through implementing a toolbar that pops up when the mouse hovers over the danmaku, featuring **like** and **dislike** functionalities.
6 |
7 | > [!NOTE] Hint
8 | > The components in this section are demonstrated using **React**.
9 |
10 | ## Developing the Danmaku Component
11 |
12 | ```tsx {23-24}
13 | import { useState } from 'react';
14 | import { Tool } from './Tool';
15 |
16 | export function Danmaku({ danmaku }) {
17 | const [visible, setVisible] = useState(false);
18 |
19 | return (
20 | {
23 | danmaku.pause();
24 | setVisible(true);
25 | }}
26 | // Resume the danmaku's movement when the mouse leaves
27 | onMouseLeave={() => {
28 | // When in a frozen state, do not resume movement
29 | // tip: (but this depends on your business requirements)
30 | if (manager.isFreeze()) return;
31 | danmaku.resume();
32 | setVisible(false);
33 | }}
34 | >
35 | {danmaku.data}
36 | {visible ? : null}
37 |
38 | );
39 | }
40 | ```
41 |
42 | ## Developing the Toolbar Component
43 |
44 | ```tsx {11-12}
45 | export function Tool() {
46 | // Send `like/dislike` request and store the result in the database
47 | const send = (type: string) => {
48 | fetch('http://abc.com/like', {
49 | method: 'POST',
50 | body: JSON.stringify({ type }),
51 | });
52 | };
53 | return (
54 |
55 | send('good')}>like
56 | send('not-good')}>dislike
57 |
58 | );
59 | }
60 | ```
61 |
62 | ## Render Danmaku
63 |
64 | ```tsx {9}
65 | import ReactDOM from 'react-dom/client';
66 | import { create } from 'danmu';
67 | import { Danmaku } from './Danmaku';
68 |
69 | const manager = create({
70 | plugin: {
71 | // Render the component onto the built-in node of the danmaku
72 | $createNode(danmaku) {
73 | ReactDOM.createRoot(danmaku.node).render( );
74 | },
75 | },
76 | });
77 | ```
78 |
--------------------------------------------------------------------------------
/docs/en/cases/loop.md:
--------------------------------------------------------------------------------
1 | # Send Looping Danmaku
2 |
3 | ## Description
4 |
5 | This section will provide examples on how to send a looping danmaku. There are two different modes for looping facile danmaku.
6 |
7 | > [!NOTE] Hint
8 | >
9 | > 1. Implemented through [**`setloop`**](../reference/danmaku-api/#danmaku-setloop). In this mode, the danmaku **will not participate in collision detection** during the looping playback, except for the first time.
10 | > 2. Implemented recursively through the [**`destroy`**](../reference/danmaku-hooks/#hooks-destroy) hook. This method will allow the looping danmaku to **participate in collision detection**, but the motion time of the looping playback may be inconsistent.
11 |
12 | ### Implementation via `setloop()`
13 |
14 | Adding a global hook will affect all danmaku.
15 |
16 | ```ts {5,11}
17 | const manager = create({
18 | plugin: {
19 | $beforeMove(danmaku) {
20 | // Set loop
21 | danmaku.setloop();
22 | },
23 |
24 | $moved(danmaku) {
25 | // Stop loop playback after 3 looping
26 | if (danmaku.loops >= 3) {
27 | danmaku.unloop();
28 | }
29 | },
30 | },
31 | });
32 | ```
33 |
34 | By adding a plugin to the danmaku itself, you can make it effective for only a specific danmaku.
35 |
36 | > You can copy the following code and paste it into the console of the online [**demo**](https://imtaotao.github.io/danmu/) to see the effect.
37 |
38 | ```ts {5,11}
39 | manager.push('content', {
40 | plugin: {
41 | beforeMove(danmaku) {
42 | // Set loop
43 | danmaku.setloop();
44 | },
45 |
46 | moved(danmaku) {
47 | // Stop loop playback after 3 looping
48 | if (danmaku.loops >= 3) {
49 | danmaku.unloop();
50 | }
51 | },
52 | },
53 | });
54 | ```
55 |
56 | ### Implementing Loop Playback via Recursion
57 |
58 | The above implementation leverages the official API, but you can also implement it recursively yourself.
59 |
60 | > [!NOTE] Hint
61 | > **Flexible danmaku will not participate in collision detection, so if you are dealing with flexible danmaku, do not use this method. Instead, use `setloop`.**
62 | >
63 | > > You can copy the following code and paste it into the console of the online [**demo**](https://imtaotao.github.io/danmu/) to see the effect.
64 |
65 | ```ts {7,11,15-16}
66 | let loops = 0;
67 |
68 | manager.push('content', {
69 | plugin: {
70 | destroyed(danmaku, mark) {
71 | // Stop loop playback after 3 looping
72 | if (++loops >= 3) return;
73 |
74 | // If you are triggering the hook by manually calling the destroy method
75 | // You can pass a mark via `danmaku.destroy('mark')` to make a judgment
76 | if (mark === 'mark') return;
77 |
78 | // If you have limits on memory and view, it may cause the send to fail
79 | // You can call `manager.canPush('facile')` to check
80 | danmaku.loops = 0;
81 | manager.unshift(danmaku);
82 | },
83 | },
84 | });
85 | ```
86 |
--------------------------------------------------------------------------------
/docs/en/cases/recommend.md:
--------------------------------------------------------------------------------
1 | # Live Streaming and Video
2 |
3 | ## Description
4 |
5 | In live streaming and video streaming, the real-time requirement for danmaku is relatively high. The default [**collision algorithm configuration**](../reference/manager-configuration/#config-mode) is `strict`, which delays rendering until the rendering conditions are met. **Therefore, you should set it to `adaptive`**. This will make the engine attempt collision detection first, and if the conditions are not met, it will ignore the collision algorithm and render immediately.
6 |
7 | ## Example
8 |
9 | ```ts {4,8}
10 | // Set it during initialization
11 | const manager = create({
12 | // .
13 | mode: 'adaptive',
14 | });
15 |
16 | // Or use the `setMode()` API
17 | manager.setMode('adaptive');
18 | ```
19 |
20 | If you want to set the minimum spacing between danmaku within a single track (only effective when danmaku hit collision detection)
21 |
22 | ```ts
23 | The minimum spacing between danmaku within the same track is `10px`
24 | manager.setGap(10);
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/en/cases/simplified-mode.md:
--------------------------------------------------------------------------------
1 | # Simplified Mode
2 |
3 | ## Description
4 |
5 | In video and live streaming scenarios, due to the need for real-time rendering, rendering too many danmaku can cause page lag. Here are two methods to address this:
6 |
7 | - Implement a simplified danmaku mode.
8 | - Set [**`limits.view`**](../reference/manager-configuration/#config-limits) to limit the number of rendered danmaku.
9 |
10 | > [!NOTE] Hint
11 | > Similar to implementing the danmaku cooldown feature, the simplified danmaku mode is also achieved by relying on the [**`willRender`**](../reference/manager-hooks/#hooks-willrender) hook.
12 |
13 | ## Implementation Using Simplified Danmaku Mode
14 |
15 | ```ts {10}
16 | import { random } from 'aidly';
17 | import { create } from 'danmu';
18 |
19 | const manager = create({
20 | plugin: {
21 | // Compared to implementing danmaku cooldown, as an alternative implementation,
22 | // directly insert the default plugin during initialization
23 | willRender(ref) {
24 | // We filter out 50% of the danmaku
25 | if (random(0, 100) < 50) {
26 | ref.prevent = true;
27 | }
28 | return ref;
29 | },
30 | },
31 | });
32 | ```
33 |
34 | ## Implementation by Setting `limits.view`
35 |
36 | ```ts {7}
37 | import { create } from 'danmu';
38 |
39 | const manager = create({
40 | limits: {
41 | // In the page view container,
42 | // the maximum number of items that can be simultaneously rendered is `100`.
43 | view: 100,
44 | },
45 | });
46 | ```
47 |
--------------------------------------------------------------------------------
/docs/en/cases/track-settings.md:
--------------------------------------------------------------------------------
1 | # Track Settings
2 |
3 | ## Description
4 |
5 | This section teaches you how to control tracks. We can control the number of tracks to render a specific quantity.
6 |
7 | > [!NOTE] Hint
8 | > You can copy and paste the following code into the console of the online [**demo**](https://imtaotao.github.io/danmu/) to see the effect.
9 |
10 | ## Limiting to a Few Consecutive Tracks
11 |
12 | **Limit to the top 3 tracks:**
13 |
14 | ```ts {2,8-10}
15 | // If we want the track height to be `50px`.
16 | manager.setTrackHeight('100% / 3');
17 |
18 | // If the rendering area is not set, the track height will be determined by the default `container.height / 3`,
19 | // which may result in a track height that is not what you want
20 | manager.setArea({
21 | y: {
22 | start: 0,
23 | // The total height of 3 tracks is `150px`
24 | end: 150,
25 | },
26 | });
27 | ```
28 |
29 | **Limit to the middle 3 tracks:**
30 |
31 | ```ts {1,5-6}
32 | manager.setTrackHeight('100% / 3');
33 |
34 | manager.setArea({
35 | y: {
36 | start: `50%`,
37 | end: `50% + 150`,
38 | },
39 | });
40 | ```
41 |
42 | ## Limiting to a Few Non-Consecutive Tracks
43 |
44 | To limit to a few non-consecutive tracks, in addition to the operations for consecutive tracks, you also need to leverage the [**`willRender`**](../reference/manager-hooks/#hooks-willrender) hook.
45 |
46 | ```ts {2,7-9,16,20-23}
47 | // If we want the track height to be `50px` and render tracks `0, 2, and 4`
48 | manager.setTrackHeight('100% / 6');
49 |
50 | // Set the rendering area of the container
51 | manager.setArea({
52 | y: {
53 | start: 0,
54 | // The total height of 6 tracks is 300px
55 | end: 300,
56 | },
57 | });
58 |
59 | manager.use({
60 | willRender(ref) {
61 | // Flexible danmaku is not strongly related to tracks and does not have the `trackIndex` attribute
62 | if (ref.trackIndex === null) return ref;
63 |
64 | // If it is tracks `1, 3, and 5`,
65 | // prevent rendering and re-add them to wait for the next rendering
66 | if (ref.trackIndex % 2 === 1) {
67 | ref.prevent = true;
68 | manager.unshift(ref.danmaku);
69 | }
70 | return ref;
71 | },
72 | });
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/en/cases/uniform-speed.md:
--------------------------------------------------------------------------------
1 | # Uniform Speed
2 |
3 | ## Description
4 |
5 | Since the default behavior of this danmaku library does not ensure uniform speed for danmaku movements, users with uniform motion requirements can achieve this by using the following methods.
6 |
7 | > [!NOTE] Important Notes
8 | >
9 | > 1. When you set the `speed` for danmaku, the configurations `config.timeRange` and `config.duration` will become invalid.
10 | > 2. In uniform motion mode, this library only ensures that the speed of danmaku is consistent, but it does not guarantee that the spacing between danmaku is uniform. You can set the `gap` configuration, but this only ensures that the spacing between danmaku is not less than the value of `gap`, not that it is equal to it.
11 | > 3. If you strongly require uniform spacing, you can reduce the speed to achieve an approximate result. See the explanation in this [issue](https://github.com/imtaotao/danmu/issues/34).
12 |
13 | ### Setting Uniform Speed
14 |
15 | There are two ways to enable uniform speed mode.
16 |
17 | > [!NOTE] Principle
18 | > When the speed of danmaku is set, the motion duration of danmaku is calculated as follows, you can adjust the launching speed according to the desired duration:
19 | >
20 | > 1. $FacileDuration = (containter.width + danmaku.width) / v$
21 | > 2. $FlexibleDuration = (danmaku.position.x + danmaku.width) / v$
22 |
23 | ```ts {3-4}
24 | // 1. Configure speed during initialization
25 | const manager = create({
26 | speed: 0.1,
27 | speed: '100% / 1000', // The `duration` is approximately 1000ms
28 | });
29 | ```
30 |
31 | ```ts {2-3}
32 | // 2. Use the `setSpeed` API to set speed
33 | manager.setSpeed(0.1);
34 | manager.setSpeed('100% / 2000'); // The `duration` is approximately 2000ms
35 | ```
36 |
37 | ### Cancel Uniform Speed
38 |
39 | Set speed to `null` to cancel uniform speed mode and revert to the default behavior.
40 |
41 | ```ts
42 | manager.setSpeed(null);
43 | ```
44 |
45 | ### Excluding Specific Danmaku from Uniform Speed Mode
46 |
47 | If you want most danmaku to move at a uniform speed but exclude certain danmaku from this mode, you can specify the `speed` configuration as `null` when sending those danmaku. Alternatively, you can assign a speed value different from the global `speed`.
48 |
49 | ```ts {3}
50 | // 1. Facile danmaku
51 | manager.push('content', {
52 | speed: null,
53 | });
54 | ```
55 |
56 | ```ts {3}
57 | // 2. Flexible danmaku
58 | manager.pushFlexibleDanmaku('content', {
59 | speed: null,
60 | });
61 | ```
62 |
63 | ### Relationship with [**`config.mode`**](../reference/manager-configuration.md#config-mode)
64 |
65 | hen speed is set for danmaku, it will still be subject to the restrictions of `config.mode`.
66 |
67 | - **`node`** No collision detection, danmaku will render immediately.
68 | - **`strict`** Strict collision detection, rendering will be delayed if conditions are not met.
69 | - **`adaptive`** Attempts collision detection while ensuring immediate rendering.
70 |
--------------------------------------------------------------------------------
/docs/en/guide/create-plugin.md:
--------------------------------------------------------------------------------
1 | # Writing Plugins
2 |
3 | Writing a plugin is very simple, but with the hooks and APIs exposed by the kernel, you can easily achieve powerful and customized requirements.
4 |
5 | ## Description
6 |
7 | Since the kernel does not expose the functionality to **filter danmaku based on conditions**, the reason being that the kernel does not know the data structure of the danmaku content, which is highly related to business requirements, we will demonstrate the **simplified danmaku** functionality through a plugin.
8 |
9 | ## 💻 Writing a Plugin
10 |
11 | > [!NOTE] Tip
12 | >
13 | > - Your plugin should have a `name` for debugging and troubleshooting purposes (make sure it does not conflict with other plugins).
14 | > - A plugin can optionally declare a `version`, which is useful if you publish your plugin as a standalone package on `npm`.
15 |
16 | ```ts {11,15}
17 | export function filter({ userIds, keywords }) {
18 | return (manager) => {
19 | return {
20 | name: 'filter-keywords-or-user',
21 | version: '1.0.0', // The `version` field is not mandatory.
22 | willRender(ref) {
23 | const { userId, content } = ref.danmaku.data.value;
24 | // `ref.type` is used to distinguish between facile danmaku and flexible danmaku.
25 | console.log(ref.type);
26 |
27 | if (userIds && userIds.includes(userId)) {
28 | ref.prevent = true;
29 | } else if (keywords) {
30 | for (const word of keywords) {
31 | if (content.includes(word)) {
32 | ref.prevent = true;
33 | break;
34 | }
35 | }
36 | }
37 | return ref;
38 | },
39 | };
40 | };
41 | }
42 | ```
43 |
44 | ## 🛠️ Register Plugin
45 |
46 | You need to register the plugin using `manager.use()`.
47 |
48 | ```ts {9-12}
49 | import { create } from 'danmu';
50 |
51 | const manager = create<{
52 | userId: number;
53 | content: string;
54 | }>();
55 |
56 | manager.use(
57 | filter({
58 | userIds: [1],
59 | keywords: ['bad'],
60 | }),
61 | );
62 | ```
63 |
64 | ## 💬 Send Danmaku
65 |
66 | - ❌ **Will** be blocked from rendering by the plugin
67 |
68 | ```ts {2}
69 | manager.push({
70 | userId: 1,
71 | content: '',
72 | });
73 | ```
74 |
75 | - ❌ **Will** be blocked from rendering by the plugin
76 |
77 | ```ts {3}
78 | manager.push({
79 | userId: 2,
80 | content: "You're really bad",
81 | });
82 | ```
83 |
84 | - ✔️ **Will not** be blocked from rendering by the plugin
85 |
86 | ```ts {2}
87 | manager.push({
88 | userId: 2,
89 | content: '',
90 | });
91 | ```
92 |
93 | - ✔️ **Will not** be blocked from rendering by the plugin
94 |
95 | ```ts {3}
96 | manager.push({
97 | userId: 2,
98 | content: "You're awesome",
99 | });
100 | ```
101 |
--------------------------------------------------------------------------------
/docs/en/guide/typescript-interface.md:
--------------------------------------------------------------------------------
1 | # Typescript Interface
2 |
3 | ## Description
4 |
5 | The `TypeScript` type declarations for danmaku are very comprehensive, so when you use it in `TypeScript`, you will get excellent type hints. This is very friendly for a plugin-based system and can even be considered indispensable.
6 |
7 | ## Declare Danmaku Content Type
8 |
9 | When you get the `danmaku` instance type in various hooks, the type of its **content** defaults to `unknown`. However, you can pass a generic type during initialization to constrain it.
10 |
11 | ```ts
12 | import { create } from 'danmu';
13 |
14 | const manager = create<{ content: string; img: string }>({
15 | $beforeMove(danmaku) {
16 | // You can see that the `data` type is `{ content: string, img: string }`
17 | danmaku.data;
18 | },
19 | });
20 | ```
21 |
22 | ## Pass a type to `statuses`
23 |
24 | Since `manager.statuses` does not perform any work within the kernel, it simply provides a plain object for users to record states. Therefore, its default type is `Record`. You can also pass a generic type to change it. For a specific example, refer to our [**demo**](https://github.com/imtaotao/danmu/blob/master/demo/src/manager.tsx#L9).
25 |
26 | ```ts
27 | import { create } from 'danmu';
28 |
29 | const manager = create();
30 |
31 | // You can see that the `statuses` type is `{ background: string }`
32 | manager.statuses;
33 | ```
34 |
35 | ## # Default Exported Type Declarations
36 |
37 | Below are the types we export by default, which can assist you in writing business code or plugins.
38 |
39 | ```ts
40 | // You can use all of these types
41 | import type {
42 | Track,
43 | Manager,
44 | HookOn,
45 | HooksOn,
46 | Plugin,
47 | HookType,
48 | Mode,
49 | Speed,
50 | StyleKey,
51 | Position,
52 | PushOptions,
53 | PushFlexOptions,
54 | Location,
55 | ValueType,
56 | Direction,
57 | Danmaku,
58 | DanmakuType,
59 | DanmakuPlugin,
60 | ManagerPlugin,
61 | ManagerOptions,
62 | CreateOption,
63 | } from 'danmu';
64 | ```
65 |
--------------------------------------------------------------------------------
/docs/en/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: 'Danmaku'
7 | text: 'Powerful and Extensible Danmaku Library'
8 | tagline: Danmu is a library for managing, rendering, and sending danmaku. It covers many business scenarios and provides an easy-to-use extension mechanism.
9 | image: /logo.svg
10 | actions:
11 | - theme: brand
12 | text: Getting Started ->
13 | link: /en/guide/getting-started
14 | - theme: alt
15 | text: Online Examples
16 | link: https://imtaotao.github.io/danmu/
17 |
18 | features:
19 | - title: Multiple Rendering Algorithms
20 | details: We provide strict and progressive, as well as ordinary full real-time rendering algorithms.
21 | icon: 🔍
22 | - title: Rich API
23 | details: Provides a variety of APIs needed for different business scenarios, greatly simplifying the development process.
24 | icon: 🌟
25 | - title: Customizable Styles
26 | details: The rendering layer of danmaku is provided by CSS, allowing you to draw any danmaku that fits DOM requirements. The style rules fully reuse CSS, so there is no additional learning curve.
27 | icon: 🧩
28 | - title: Plugin System
29 | details: We provide a rich set of hooks that allow you to easily develop plugins to meet more customized needs. This is a very powerful capability.
30 | icon: 🔌
31 | ---
32 |
--------------------------------------------------------------------------------
/docs/en/reference/container-api.md:
--------------------------------------------------------------------------------
1 | # Container API
2 |
3 | The danmaku container instance has the following properties and methods. You can refer to this section when you get a container instance in certain hooks. You can retrieve the instance via [**`manager.container`**](./manager-properties.md#manager-container).
4 |
5 | > [!NOTE] Notes
6 | > If you need to change the dimensions of the container, it's recommended to use the `manager.setArea()` method instead of changing it through `manager.container.setStyle()`. Otherwise, you will need to manually call `manager.format()`.
7 |
8 | ```ts
9 | declare class Container {
10 | width: number;
11 | height: number;
12 | node: HTMLDivElement;
13 | parentNode: HTMLElement | null;
14 | setStyle(key: T, val: CSSStyleDeclaration[T]): void;
15 | }
16 | ```
17 |
18 | ## container.width
19 |
20 | **Type: `number`**
21 | **Default Value: `0`**
22 |
23 | The width of the container, this value may change after calling `manager.format()`.
24 |
25 | ## container.height
26 |
27 | **Type: `number`**
28 | **Default Value: `0`**
29 |
30 | The height of the container, this value may change after calling `manager.format()`.
31 |
32 | ## container.node
33 |
34 | **Type: `HTMLDivElement`**
35 | **Default Value: `div`**
36 |
37 | The DOM node of the container.
38 |
39 | ## container.parentNode
40 |
41 | **Type: `HTMLElement | null`**
42 | **Default Value: `null`**
43 |
44 | The parent node of the container, which can be accessed through this property after setting with `manager.mount()`.
45 |
46 | ## container.setStyle()
47 |
48 | **Type: `setStyle(key: T, val: CSSStyleDeclaration[T]): void`**
49 |
50 | This method allows you to set the style of the container node.
51 |
52 | ```ts
53 | // So you can set some styles on the container like this
54 | manager.container.setStyle('background', 'red');
55 | ```
56 |
--------------------------------------------------------------------------------
/docs/en/reference/danmaku-api.md:
--------------------------------------------------------------------------------
1 | # Danmaku API
2 |
3 | Danmaku instances have several methods available for you to use. You can use them to perform actions or retrieve some state data of the current danmaku. Here is an example:
4 |
5 | ```tsx {6,9,17-19}
6 | // Destroy the danmaku when it is clicked
7 | function DanmakuComponent(props: { danmaku: Danmaku }) {
8 | return (
9 | {
11 | props.danmaku.destroy();
12 | }}
13 | >
14 | {props.danmaku.data.value}
15 |
16 | );
17 | }
18 |
19 | // Render the component onto the built-in node of the danmaku
20 | manager.use({
21 | $createNode(danmaku) {
22 | ReactDOM.createRoot(danmaku.node).render(
23 | ,
24 | );
25 | },
26 | });
27 | ```
28 |
29 | ## `danmaku.hide()`
30 |
31 | **Type: `() => void`**
32 |
33 | Set the current danmaku instance to a hidden state, which essentially sets `visibility: hidden` and triggers the `hide` hook.
34 |
35 | ## `danmaku.show()`
36 |
37 | **Type: `() => void`**
38 |
39 | Restore the current danmaku instance from a hidden state to visible, which will trigger the `show` hook.
40 |
41 | ## `danmaku.pause()`
42 |
43 | **Type: `() => void`**
44 |
45 | Pause the current moving danmaku instance, which will trigger the `pause` hook.
46 |
47 | ## `danmaku.resume()`
48 |
49 | **Type: `() => void`**
50 |
51 | Resume the current paused danmaku instance, which will trigger the `resume` hook.
52 |
53 | ## `danmaku.setloop()`
54 |
55 | **Type: `() => void`**
56 |
57 | Set the danmaku instance to loop playback mode.
58 |
59 | ## `danmaku.unloop()`
60 |
61 | **Type: `() => void`**
62 |
63 | Cancel the loop playback mode for the danmaku instance.
64 |
65 | ## `danmaku.destroy()`
66 |
67 | **Type: `(mark?: unknown) => Promise`**
68 |
69 | Destroy the current danmaku instance from the container and remove it from memory, which will trigger the `beforeDestroy` and `destroyed` hook. You can also try passing a `mark` identifier. The built-in destroy behavior of the engine does not pass an identifier.
70 |
71 | ```ts {4,8,14}
72 | const manager = create({
73 | plugin: {
74 | $moved(danmaku) {
75 | danmaku.destroy('mark');
76 | },
77 |
78 | $beforeDestroy(danmaku, mark) {
79 | if (mark === 'mark') {
80 | // .
81 | }
82 | },
83 |
84 | $destroyed(danmaku, mark) {
85 | if (mark === 'mark') {
86 | // .
87 | }
88 | },
89 | },
90 | });
91 | ```
92 |
93 | ## `danmaku.setStyle()`
94 |
95 | **Type: `(key: StyleKey, val: CSSStyleDeclaration[StyleKey]) => void`**
96 |
97 | Set the `CSS` styles of the built-in node for the current danmaku instance.
98 |
99 | ```ts
100 | // Change the background color of the built-in danmaku node to red
101 | danmaku.setStyle('background', 'red');
102 | ```
103 |
104 | ## `danmaku.getWidth()`
105 |
106 | **Type: `() => number`**
107 |
108 | `getWidth()` will return the width of the danmaku instance itself, which can also be very useful when sending advanced danmaku.
109 |
110 | ## `danmaku.getHeight()`
111 |
112 | **Type: `() => number`**
113 |
114 | `getHeight()` will return the height of the danmaku instance itself, which can be very useful when calculating the `position` for advanced danmaku.
115 |
116 | ## `danmaku.remove()`
117 |
118 | **Type: `(pluginName: string) => void`**
119 |
120 | Remove a specific plugin from the current danmaku instance, but you must specify the plugin name.
121 |
122 | ## `danmaku.use()`
123 |
124 | **Type: `(plugin: DanmakuPlugin | ((d: this) => DanmakuPlugin)) => DanmakuPlugin`**
125 |
126 | Register a plugin for the current danmaku instance and return the plugin instance. If you need to remove the plugin later, you can save the plugin's `name`. If not provided, a `uuid`-formatted `name` will be generated by default.
127 |
128 | **If `name` is provided:**
129 |
130 | ```ts
131 | const plugin = danmaku.use({
132 | name: 'test-plugin',
133 | // .
134 | });
135 | console.log(plugin.name); // 'test-plugin'
136 | ```
137 |
138 | **If `name` is not provided:**
139 |
140 | ```ts
141 | const plugin = danmaku.use({
142 | // .
143 | });
144 | console.log(plugin.name); // uuid
145 | ```
146 |
--------------------------------------------------------------------------------
/docs/en/reference/danmaku-hooks.md:
--------------------------------------------------------------------------------
1 | # Danmaku Hooks
2 |
3 | Danmaku hooks are only triggered when the behavior of the danmaku itself changes.
4 |
5 | **1. Register through `manager`**
6 |
7 | ```ts
8 | // When registering through `manager`, you need to add the `$` prefix
9 | import { create } from 'danmu';
10 |
11 | const manager = create({
12 | plugin: {
13 | $hide(danmaku) {},
14 | $show(danmaku) {},
15 | },
16 | });
17 | ```
18 |
19 | **2. Register through danmaku instance**
20 |
21 | If you obtain a danmaku instance in other global hooks, you can register it this way. This can be very useful when writing plugins.
22 |
23 | ```ts
24 | danmaku.use({
25 | hide(danmaku) {},
26 | show(danmaku) {},
27 | });
28 | ```
29 |
30 | ## `hooks.hide`
31 |
32 | **Type: `SyncHook<[Danmaku]>`**
33 |
34 | The `hide` hook is triggered when the danmaku is hidden.
35 |
36 | ## `hooks.show`
37 |
38 | **Type: `SyncHook<[Danmaku]>`**
39 |
40 | The `show` hook is triggered when the danmaku changes from hidden to visible.
41 |
42 | ## `hooks.pause`
43 |
44 | **Type: `SyncHook<[Danmaku]>`**
45 |
46 | The `pause` hook is triggered when the danmaku is paused.
47 |
48 | ## `hooks.resume`
49 |
50 | **Type: `SyncHook<[Danmaku]>`**
51 |
52 | The `resume` hook is triggered when the danmaku resumes from being paused.
53 |
54 | ## `hooks.beforeMove`
55 |
56 | **Type: `SyncHook<[Danmaku]>`**
57 |
58 | The `beforeMove` hook is triggered just before the danmaku starts moving. You can make some style changes to the danmaku at this time.
59 |
60 | ## `hooks.moved`
61 |
62 | **Type: `SyncHook<[Danmaku]>`**
63 |
64 | The `moved` hook is triggered when the danmaku finishes moving. Finishing the movement does not mean it will be destroyed immediately. For performance reasons, the kernel will batch collect and destroy them together.
65 |
66 | ## `hooks.appendNode`
67 |
68 | **Type: `SyncHook<[Danmaku, HTMLElement]>`**
69 |
70 | The `appendNode` hook is triggered when the danmaku node is added to the container. It occurs after the `createNode` hook.
71 |
72 | ## `hooks.removeNode`
73 |
74 | **Type: `SyncHook<[Danmaku, HTMLElement]>`**
75 |
76 | The `removeNode` hook will be triggered when the danmaku is removed from the container.
77 |
78 | ## `hooks.createNode`
79 |
80 | **Type: `SyncHook<[Danmaku, HTMLElement]>`**
81 |
82 | The `createNode` hook will be triggered after the built-in HTML node of the danmaku is created. You can use `danmaku.node` to get this node within this hook and **perform some styling and rendering operations on the node. This is a very important step for the extensibility of this framework **.
83 |
84 | **Example:**
85 |
86 | ```tsx {8-10}
87 | function DanmakuComponent(props: { danmaku: Danmaku }) {
88 | return {props.danmaku.data.value}
;
89 | }
90 |
91 | manager.use({
92 | $createNode(danmaku) {
93 | // Render the component onto the built-in node of the danmaku
94 | ReactDOM.createRoot(danmaku.node).render(
95 | ,
96 | );
97 | },
98 | });
99 | ```
100 |
101 | ## `hooks.beforeDestroy`
102 |
103 | **Type: `AsyncHook<[Danmaku, unknown]>`**
104 |
105 | The `beforeDestroy` hook is triggered before the danmaku is destroyed. This hook allows returning a `promise`. If you need to manually call the [**`danmaku.destroy`**](../reference/danmaku-api/#danmaku-destroy) method, you can try passing a `mark`.
106 |
107 | **Example:**
108 |
109 | ```ts{6}
110 | import { sleep } from 'aidly';
111 |
112 | manager.use({
113 | async $beforeDestroy(danmaku, mark) {
114 | // Will block for 1s, then destroy the current danmaku
115 | await sleep(1000);
116 | },
117 | });
118 | ```
119 |
120 | ## `hooks.destroyed`
121 |
122 | **Type: `SyncHook<[Danmaku, unknown]>`**
123 |
124 | The `destroyed` hook is triggered when the danmaku is destroyed. If you need to manually call the [**`danmaku.destroy`**](../reference/danmaku-api/#danmaku-destroy) method, you can try passing a `mark`.
125 |
126 | **Example:**
127 |
128 | ```ts{3}
129 | manager.use({
130 | $destroyed(danmaku, mark) {
131 | if (mark) {
132 | // .
133 | }
134 | },
135 | });
136 | ```
137 |
--------------------------------------------------------------------------------
/docs/en/reference/danmaku-properties.md:
--------------------------------------------------------------------------------
1 | # Danmaku Properties
2 |
3 | Danmaku instances have many properties that record the current state of the danmaku. You can use them to make decisions and achieve your own business objectives.
4 |
5 | ## `danmaku.data`
6 |
7 | **Type: `PushData`**
8 |
9 | `data` is the data you passed in when sending the danmaku.
10 |
11 | ```ts
12 | manager.use({
13 | $createNode(danmaku) {
14 | console.log(danmaku.data); // { value: 1 }
15 | },
16 | });
17 |
18 | manager.push({ value: 1 });
19 | ```
20 |
21 | ## `danmaku.type`
22 |
23 | **Type: `facile' | 'flexible'`**
24 |
25 | The type of danmaku can be either `facile` or `flexible`. You can use this property to make different decisions.
26 |
27 | ## `danmaku.node`
28 |
29 | **Type: `HTMLElement`**
30 |
31 | The built-in node of the danmaku, where you can render the actual content of the danmaku.
32 |
33 | ## `danmaku.loops`
34 |
35 | **Type: `number`**
36 | **Default: `0`**
37 |
38 | The playback count of the danmaku, which automatically increments by `+1` after the danmaku finishes moving. You may need this property if you have looping danmaku requirements.
39 |
40 | ## `danmaku.duration`
41 |
42 | **Type: `number`**
43 |
44 | The duration of the danmaku's movement.
45 |
46 | ## `danmaku.direction`
47 |
48 | **Type: `'right' | 'left' | 'none'`**
49 |
50 | The direction of the danmaku's movement.
51 |
52 | ## `danmaku.isFixedDuration`
53 |
54 | **Type: `boolean`**
55 |
56 | Used to determine whether the movement duration of the current danmaku instance has been adjusted.
57 |
58 | ## `danmaku.pluginSystem`
59 |
60 | **Type: `PluginSystem`**
61 |
62 | The plugin system instance for the danmaku. For its API, refer to the **hooks-plugin** documentation.
63 |
64 | https://github.com/imtaotao/hooks-plugin?tab=readme-ov-file#apis
65 |
--------------------------------------------------------------------------------
/docs/en/reference/manager-properties.md:
--------------------------------------------------------------------------------
1 | # Manager Properties
2 |
3 | > [!NOTE] Unit Hint
4 | > All units involved in the calculation can be computed through expressions, similar to CSS `calc`.
5 | >
6 | > 1. **`number`**: The default unit is `px`.
7 | > 2. **`string`**: Expression calculation. Supports mathematical operations (`+`, `-`, `*`, `/`), and only `%` and `px` units are supported.
8 | >
9 | > ```ts
10 | > manager.setGap('(100% - 10px) / 5');
11 | > ```
12 |
13 | ## `manager.version`
14 |
15 | **Type: `string`**
16 |
17 | The current version of the `danmu` library.
18 |
19 | ## `manager.options`
20 |
21 | **Type: `ManagerOptions`**
22 |
23 | [**`manager.options`**](./manager-configuration), you can get some initial values from here and use them.
24 |
25 | ```ts
26 | console.log(manager.options.durationRange); // [number, number]
27 | ```
28 |
29 | ## `manager.statuses`
30 |
31 | **Type: `Record`**
32 |
33 | A property for recording states, not used by the kernel. It is mainly provided for business purposes to record some states. The default type is `Record`, but you can pass a generic type when creating the `manager`.
34 |
35 | ```ts {3}
36 | import { create } from 'danmu';
37 |
38 | const manager = create();
39 |
40 | manager.statuses; // Type is `{ background: string }`
41 | ```
42 |
43 | ## `manager.trackCount`
44 |
45 | **Type: `number`**
46 |
47 | The current number of tracks inside the container. When the container size changes and after `format` (either by manually calling the `format()` method or by calling the `setArea()` method), the `trackCount` will also change accordingly.
48 |
49 | ## `manager.container`
50 |
51 | **Type: `Container`**
52 |
53 | See the [**`Container API`**](./container-api) section.
54 |
55 | ## `manager.pluginSystem`
56 |
57 | **Type: `PluginSystem`**
58 |
59 | The plugin system instance of `manager`, its API can be found in the **hooks-plugin** documentation.
60 |
61 | https://github.com/imtaotao/hooks-plugin?tab=readme-ov-file#apis
62 |
--------------------------------------------------------------------------------
/docs/en/reference/track-api.md:
--------------------------------------------------------------------------------
1 | # Track API
2 |
3 | The Track class represents the API related to tracks. You can obtain an instance of a track by using [**`manager.getTrack(i)`**](./manager-api#manager-gettrack).
4 |
5 | ```ts
6 | interface Location {
7 | top: number;
8 | middle: number;
9 | bottom: number;
10 | }
11 |
12 | declare class Track {
13 | width: number;
14 | height: number;
15 | index: number;
16 | isLock: boolean;
17 | location: Location;
18 | list: Array>;
19 | lock(): void;
20 | unlock(): void;
21 | clear(): void;
22 | each(fn: (danmaku: FacileDanmaku) => unknown | boolean): void;
23 | }
24 | ```
25 |
26 | ## track.width
27 |
28 | **Type: `number`**
29 |
30 | The width of the track, which may change after you call `manager.format()`.
31 |
32 | ## track.height
33 |
34 | **Type: `number`**
35 |
36 | The height of the track, which may change after you call `manager.format()`.
37 |
38 | ## track.index
39 |
40 | **Type: `number`**
41 |
42 | The position of this track in the track list of the current container. For example, if index is 1, it represents the second track in the list.
43 |
44 | ## track.isLock
45 |
46 | **Type: `boolean`**
47 | **Default Value: `false`**
48 |
49 | Used to determine if the current track is locked.
50 |
51 | ## track.list
52 |
53 | **Type: `Array`**
54 |
55 | The list of facile danmaku within this track. If you need to iterate over the list, it is recommended to use the `track.each()` method.
56 |
57 | ## track.location
58 |
59 | **Type: `Location`**
60 |
61 | This attribute represents the height-related position information of the track, measured in `px`. It is calculated based on the container, and changes when the container is formatted. This property can be especially useful when you need to send a flexible danmaku to a specific track.
62 |
63 | > [!NOTE] Tips
64 | >
65 | > 1. If you can determine the height of the danmaku without needing to calculate it, you don’t need to use the `getHeight()` method.
66 | > 2. Ensure that the track you are accessing exists, otherwise it will throw an error. You can check how many tracks are available using `manager.trackCount`.
67 | > 3. When rendering flexible danmaku as shown in the following example, the danmaku itself is not constrained by the track; it is merely rendered at a track's location. Therefore, calling `track.lock()` on a track will not affect the flexible danmaku.
68 | > 4. You can try this code in our online [**demo**](https://imtaotao.github.io/danmu/) by opening your browser's console and entering the code to see the effects.
69 |
70 | ```ts {9,12}
71 | // Send a flexible danmaku
72 | manager.pushFlexibleDanmaku(
73 | { content: 'content' },
74 | {
75 | duration: 5000,
76 | direction: 'none',
77 | position(danmaku, container) {
78 | // Render in the fourth track
79 | const { middle } = manager.getTrack(3).location;
80 | return {
81 | x: (container.width - danmaku.getWidth()) * 0.5,
82 | y: middle - danmaku.getHeight() / 2,
83 | };
84 | },
85 | },
86 | );
87 | ```
88 |
89 | ## track.lock()
90 |
91 | **Type: `() => void`**
92 |
93 | Used to lock the current track. Once the track is locked, no new danmaku will be sent on this track.
94 |
95 | ## track.unlock()
96 |
97 | **Type: `() => void`**
98 |
99 | Unlocks the current track.
100 |
101 | ## track.clear()
102 |
103 | **Type: `() => void`**
104 |
105 | Clears all danmaku within the current track, but does not prevent subsequent danmaku from being sent.
106 |
107 | ```ts
108 | // If you need to clear the track and prevent further danmaku
109 | const track = manager.getTrack(0);
110 |
111 | track.clear();
112 | track.lock();
113 | ```
114 |
115 | ## track.each()
116 |
117 | **Type: `(fn: (danmaku: FacileDanmaku) => unknown | boolean) => void`**
118 |
119 | Iterates over `track.list`. It is advised to use this method when you need to destroy individual danmaku. Returning `false` from the callback function will prevent further iterations.
120 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "private": "true",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vitepress dev",
7 | "build": "vitepress build",
8 | "preview": "vitepress preview"
9 | },
10 | "dependencies": {
11 | "vitepress": "^1.3.3",
12 | "vitepress-plugin-mermaid": "^2.0.16",
13 | "markdown-it-mathjax3": "^4.3.2",
14 | "typescript": "^5.8.3"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/docs/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/zh/cases/anti-occlusion.md:
--------------------------------------------------------------------------------
1 | # 防遮挡功能的实现
2 |
3 | ## 描述
4 |
5 | 本章节将学习如何实现防遮挡功能,由于防遮挡功能需要定义被遮挡的范围,一般情况下,都是通过定义一个 `svg` 图片设置范围,然后通过 `CSS` 的 [**`maskImage`**](https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image) 属性来实现,主要是有以下两个步骤需要你来实现。
6 |
7 | > [!NOTE] 提示
8 | >
9 | > 1. 轮询获取需要防止被覆盖的 svg 图片,一般是通过 **AI** 来生成,不过也视业务属性来确定。
10 | > 2. 调用 danmu 库提供的 [**`manager.updateOccludedUrl`**](../reference/manager-api/#manager-updateoccludedurl) 来设置 `CSS` 属性。
11 |
12 | ## 示例
13 |
14 | ```ts {6,9}
15 | (async function update() {
16 | const { url } = await fetch('https://abc.com/svg').then((res) => res.json());
17 |
18 | // 1. 更新蒙层(如果不传第二个参数,默认设置到 manager.container.node 上)
19 | // 2. url 也可以是 base64 图片,这可能会对你有帮助
20 | manager.updateOccludedUrl(url, '#id');
21 |
22 | // 轮询请求
23 | setTimeout(() => update(), 1000);
24 | })();
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/zh/cases/cooldown.md:
--------------------------------------------------------------------------------
1 | # 弹幕冷却时间
2 |
3 | ## 描述
4 |
5 | 在直播的场景中,当用户在短时间内高频率的发送弹幕时,会有刷屏的嫌疑,本章节会对这种场景给出一个简单的实现,你可以参考其原理和思想自行根据你的业务场景进行扩展。
6 |
7 | > [!NOTE] 提示
8 | > 我们主要依赖 [**`willRender`**](../reference/manager-hooks/#hooks-willrender) 这个钩子来实现。
9 |
10 | ## 根据弹幕内容来实现
11 |
12 | ```ts {13-23}
13 | import { create } from 'danmu';
14 |
15 | const cd = 3000;
16 | const map = Object.create(null);
17 |
18 | // 创建 manager,定义发送弹幕的类型为 string
19 | const manager = create({ ... });
20 |
21 | // 写一个插件来专门处理弹幕 cd 的事情
22 | manager.use({
23 | name: 'cd',
24 | willRender(ref) {
25 | const now = Date.now();
26 | const content = ref.danmaku.data;
27 | const prevTime = map[content];
28 |
29 | if (prevTime && now - prevTime < cd) {
30 | ref.prevent = true;
31 | console.warn(`"${content}" is blocked.`);
32 | } else {
33 | map[content] = now;
34 | }
35 | return ref;
36 | },
37 | });
38 |
39 | // ✔️ 成功
40 | manager.push('弹幕内容'); // [!code hl]
41 |
42 | // ❌ 被阻止
43 | manager.push('弹幕内容'); // [!code error]
44 |
45 | // ✔️ 成功
46 | setTimeout(() => {
47 | manager.push('弹幕内容'); // [!code hl]
48 | }, 3000)
49 | ```
50 |
51 | ## 根据用户 ID 来实现
52 |
53 | ```ts {12-22}
54 | import { create } from 'danmu';
55 |
56 | // 创建 manager,定义发送弹幕的类型为 { userId: number, content: string }
57 | const manager = create<{ userId: number, content: string }>({ ... });
58 |
59 | const cd = 3000;
60 | const map = Object.create(null);
61 |
62 | manager.use({
63 | name: 'cd',
64 | willRender(ref) {
65 | const now = Date.now();
66 | const { userId } = ref.danmaku.data;
67 | const prevTime = map[userId];
68 |
69 | if (prevTime && now - prevTime < cd) {
70 | ref.prevent = true;
71 | console.warn(`"${userId}" is blocked.`);
72 | } else {
73 | map[userId] = now;
74 | }
75 | return ref;
76 | },
77 | });
78 |
79 | // ✔️ 成功
80 | manager.push({ useId: 1, content: 'content1' }); // [!code hl]
81 |
82 | // ❌ 被阻止
83 | manager.push({ useId: 1, content: 'content2' }); // [!code error]
84 |
85 | // ✔️ 成功
86 | setTimeout(() => {
87 | manager.push({ useId: 1, content: 'content3' }); // [!code hl]
88 | }, 3000)
89 | ```
90 |
--------------------------------------------------------------------------------
/docs/zh/cases/custom-container.md:
--------------------------------------------------------------------------------
1 | # 自定义容器样式
2 |
3 | ## 描述
4 |
5 | 我们主要是通过 [**`manager.container.setStyle`**](../reference/manager-properties/#manager-container-setstyle) 这个 api 来实现。
6 |
7 | > [!NOTE] 提示
8 | > 通过官方提供的 api 设置的样式,只会作用于容器的根节点上,也就是 [**`manager.container.node`**](../reference/manager-properties/#manager-container-node)。
9 |
10 | ## 示例
11 |
12 | ```ts {14,24}
13 | import { create } from 'danmu';
14 |
15 | // 需要添加的样式
16 | const styles = {
17 | background: 'red',
18 | // .
19 | };
20 |
21 | const manager = create({
22 | plugin: {
23 | // 你可以在初始化的时候添加钩子处理
24 | init(manager) {
25 | for (const key in styles) {
26 | manager.container.setStyle(key, styles[key]);
27 | }
28 | // 你也可以在这里给容器 DOM 添加 className
29 | manager.container.node.classList.add('className');
30 | },
31 | },
32 | });
33 |
34 | // 或者直接调用 api
35 | for (const key in styles) {
36 | manager.container.setStyle(key, styles[key]);
37 | }
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/zh/cases/custom-danmaku.md:
--------------------------------------------------------------------------------
1 | # 自定义弹幕样式
2 |
3 | ## 描述
4 |
5 | 由于我们可以拿到弹幕的 DOM 节点,所以可以很方便的自定义 CSS 样式,主要是通过 [**`manager.setStyle`**](../reference/manager-api/#manager-setstyle) 和 [**`danmaku.setStyle`**](../reference/danmaku-api/#danmaku-setstyle) 这两个 api 来实现。
6 |
7 | > [!NOTE] 提示
8 | > 通过官方提供的 api 设置的样式,只会作用于弹幕的根节点上,也就是 [**`danmaku.node`**](../reference/danmaku-props/#danmaku-node)。
9 |
10 | ## 通过 `manager.setStyle` 来设置
11 |
12 | ```ts {14}
13 | import { create } from 'danmu';
14 |
15 | // 需要添加的样式
16 | const styles = {
17 | color: 'red',
18 | fontSize: '15px',
19 | // .
20 | };
21 |
22 | const manager = create();
23 |
24 | // 后续渲染的弹幕和当前已经渲染的弹幕会设置上这些样式。
25 | for (const key in styles) {
26 | manager.setStyle(key, styles[key]);
27 | }
28 | ```
29 |
30 | ## 通过 `danmaku.setStyle` 来设置
31 |
32 | 在这种实现中,真实的业务场景里面你可能需要借用 [**`manager.statuses`**](../reference/manager-properties/#manager-statuses) 来简化实现。
33 |
34 | ```ts {15,26}
35 | import { create } from 'danmu';
36 |
37 | // 需要添加的样式
38 | const styles = {
39 | color: 'red',
40 | fontSize: '15px',
41 | // .
42 | };
43 |
44 | // 初始化的时候添加钩子处理,这样当有新的弹幕渲染时会自动添加上这些样式
45 | const manager = create({
46 | plugin: {
47 | $beforeMove(danmaku) {
48 | for (const key in styles) {
49 | danmaku.setStyle(key, styles[key]);
50 | }
51 | // 你也可以在这里给弹幕 DOM 添加 className
52 | danmaku.node.classList.add('className');
53 | },
54 | },
55 | });
56 |
57 | // 对当前正在渲染的弹幕添加样式
58 | manager.asyncEach((danmaku) => {
59 | for (const key in styles) {
60 | danmaku.setStyle(key, styles[key]);
61 | }
62 | });
63 | ```
64 |
--------------------------------------------------------------------------------
/docs/zh/cases/filter-keyword.md:
--------------------------------------------------------------------------------
1 | # 过滤关键字
2 |
3 | ## 描述
4 |
5 | 过滤关键字的功能实现我们在[**编写插件**](../guide/create-plugin)那一章节已经学习过。
6 |
7 | > [!NOTE] 提示
8 | > 过滤关键字的实现也是依赖 [**`willRender`**](../reference/manager-hooks/#hooks-willrender) 这个钩子来实现。
9 |
10 | ## 示例
11 |
12 | ```ts {4,12}
13 | import { create } from 'danmu';
14 |
15 | // 定义关键字列表
16 | const keywords = ['a', 'c', 'e'];
17 |
18 | // 创建 manager,定义发送弹幕的类型为 string
19 | const manager = create({
20 | plugin: {
21 | willRender(ref) {
22 | for (const word of keywords) {
23 | if (ref.danmaku.data.includes(word)) {
24 | ref.prevent = true;
25 | break;
26 | }
27 | }
28 | return ref;
29 | },
30 | },
31 | });
32 |
33 | // ❌ 会被过滤
34 | manager.push('ab'); // [!code error]
35 |
36 | // ✔️ 不会被过滤
37 | manager.push('bd'); // [!code hl]
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/zh/cases/fixed.md:
--------------------------------------------------------------------------------
1 | # 固定弹幕在容器顶部
2 |
3 | ## 描述
4 |
5 | 本章节将介绍如何将弹幕固定在某一位置,以 **`top`** 和 **`left`** 这两个位置举例。由于我们需要自定义位置,所以我们需要使用高级弹幕的能力。
6 |
7 | > [!NOTE] 提示
8 | > 以下这些代码你都可以复制,然后粘贴在在线 [**demo**](https://imtaotao.github.io/danmu/) 的控制台上查看效果。
9 |
10 | ## 将弹幕固定在顶部
11 |
12 | **1. 固定在最顶部的位置:**
13 |
14 | ```ts {7-8}
15 | // 这条弹幕将会居中距离顶部 10px 的位置悬停 5s
16 | manager.pushFlexibleDanmaku('弹幕内容', {
17 | duration: 5000,
18 | direction: 'none',
19 | position(danmaku, container) {
20 | return {
21 | x: `50% - ${danmaku.getWidth() / 2}`,
22 | y: 10, // 离容器顶部的距离为 10px
23 | };
24 | },
25 | });
26 | ```
27 |
28 | **2. 固定在顶部第 2 条轨道上:**
29 |
30 | ```ts {9-10}
31 | // 这条弹幕将会在第二条轨道居中的位置悬停 5s
32 | manager.pushFlexibleDanmaku('弹幕内容', {
33 | duration: 5000,
34 | direction: 'none',
35 | position(danmaku, container) {
36 | // 渲染在第 3 条轨道中
37 | const { middle } = manager.getTrack(2).location;
38 | return {
39 | x: `50% - ${danmaku.getWidth() / 2}`,
40 | y: middle - danmaku.getHeight() / 2,
41 | };
42 | },
43 | });
44 | ```
45 |
46 | ## 将弹幕固定在左边
47 |
48 | ```ts {7,9-10}
49 | // 这条弹幕将会在容器中间距离左边 10px 的地方停留 5s
50 | manager.pushFlexibleDanmaku('弹幕内容', {
51 | duration: 5000,
52 | direction: 'none',
53 | position(danmaku, container) {
54 | // 渲染在第 3 条轨道中
55 | const { middle } = manager.getTrack(2).location;
56 | return {
57 | x: 10,
58 | y: `50% - ${danmaku.getHeight() / 2}`,
59 | };
60 | },
61 | });
62 | ```
63 |
--------------------------------------------------------------------------------
/docs/zh/cases/image.md:
--------------------------------------------------------------------------------
1 | # 发送带图片的弹幕
2 |
3 | ## 描述
4 |
5 | 要让弹幕里面能够携带图片,这和前面的实现[**点赞/点踩**](./like)的功能类似,都是要在弹幕的节点内部添加自定义的内容,实际上不止图片,你可以往弹幕的节点里面**添加任何的内容。**
6 |
7 | > [!NOTE] 提示
8 | > 本章节的组件以 **React** 来实现演示。
9 |
10 | ## 开发弹幕组件
11 |
12 | ```tsx {4-5}
13 | export function Danmaku({ danmaku }) {
14 | return (
15 |
16 |
17 | {danmaku.data}
18 |
19 | );
20 | }
21 | ```
22 |
23 | ## 渲染弹幕
24 |
25 | ```tsx title="init.tsx" {9}
26 | import ReactDOM from 'react-dom/client';
27 | import { create } from 'danmu';
28 | import { Danmaku } from './Danmaku';
29 |
30 | const manager = create({
31 | plugin: {
32 | // 将组件渲染到弹幕的内置节点上
33 | $createNode(danmaku) {
34 | ReactDOM.createRoot(danmaku.node).render( );
35 | },
36 | },
37 | });
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/zh/cases/like.md:
--------------------------------------------------------------------------------
1 | # 点赞/点踩
2 |
3 | ## 描述
4 |
5 | 当我们停留在弹幕上时,可能会需要做一些操作,本章节会带你实现一个鼠标停留在弹幕上时弹出一个工具栏,拥有**点赞**和**点踩**这两个功能。
6 |
7 | > [!NOTE] 提示
8 | > 本章节的组件以 **React** 来实现演示。
9 |
10 | ## 开发弹幕组件
11 |
12 | ```tsx {22-23}
13 | import { useState } from 'react';
14 | import { Tool } from './Tool';
15 |
16 | export function Danmaku({ danmaku }) {
17 | const [visible, setVisible] = useState(false);
18 |
19 | return (
20 | {
23 | danmaku.pause();
24 | setVisible(true);
25 | }}
26 | // 鼠标离开弹幕时,恢复运动
27 | onMouseLeave={() => {
28 | // 当处于冻结状态时,不要给恢复运动了(不过也视你的业务情况而定)
29 | if (manager.isFreeze()) return;
30 | danmaku.resume();
31 | setVisible(false);
32 | }}
33 | >
34 | {danmaku.data}
35 | {visible ? : null}
36 |
37 | );
38 | }
39 | ```
40 |
41 | ## 开发工具栏组件
42 |
43 | ```tsx {11-12}
44 | export function Tool() {
45 | // 发送 `点赞/点踩` 的请求,将结果存储在数据库
46 | const send = (type: string) => {
47 | fetch('http://abc.com/like', {
48 | method: 'POST',
49 | body: JSON.stringify({ type }),
50 | });
51 | };
52 | return (
53 |
54 | send('good')}>点赞
55 | send('not-good')}>点踩
56 |
57 | );
58 | }
59 | ```
60 |
61 | ## 渲染弹幕
62 |
63 | ```tsx {9}
64 | import ReactDOM from 'react-dom/client';
65 | import { create } from 'danmu';
66 | import { Danmaku } from './Danmaku';
67 |
68 | const manager = create({
69 | plugin: {
70 | // 将组件渲染到弹幕的内置节点上
71 | $createNode(danmaku) {
72 | ReactDOM.createRoot(danmaku.node).render( );
73 | },
74 | },
75 | });
76 | ```
77 |
--------------------------------------------------------------------------------
/docs/zh/cases/loop.md:
--------------------------------------------------------------------------------
1 | # 发送循环弹幕
2 |
3 | ## 描述
4 |
5 | 本章节将举例介绍如何发送一个循环播放的弹幕。普通弹幕的循环有两种不同的模式。
6 |
7 | > [!NOTE] 提示
8 | >
9 | > 1. 通过 [**`setloop`**](../reference/danmaku-api/#danmaku-setloop) 来实现,这种模式在除第一次之外的循环播放次数中,**不会参与碰撞计算。**
10 | > 2. 通过 [**`destroy`**](../reference/danmaku-hooks/#hooks-destroy) 钩子来递归实现,这种方式会让弹幕的循环播放**参与碰撞计算,但是循环播放的运动时间可能会不一致。**
11 |
12 | ### 通过 `setloop()` 来实现
13 |
14 | 添加全局钩子这会对所有的弹幕生效
15 |
16 | ```ts {5,11}
17 | const manager = create({
18 | plugin: {
19 | $beforeMove(danmaku) {
20 | // 设置循环
21 | danmaku.setloop();
22 | },
23 |
24 | $moved(danmaku) {
25 | // 循环播放 3 次后,终止循环播放
26 | if (danmaku.loops >= 3) {
27 | danmaku.unloop();
28 | }
29 | },
30 | },
31 | });
32 | ```
33 |
34 | 通过添加弹幕自身的插件可以只对某一个弹幕生效。
35 |
36 | > 你可以复制下面这段代码,然后粘贴在在线 [**demo**](https://imtaotao.github.io/danmu/) 的控制台上查看效果。
37 |
38 | ```ts {5,11}
39 | manager.push('弹幕内容', {
40 | plugin: {
41 | beforeMove(danmaku) {
42 | // 设置循环
43 | danmaku.setloop();
44 | },
45 |
46 | moved(danmaku) {
47 | // 循环播放 3 次后,终止循环播放
48 | if (danmaku.loops >= 3) {
49 | danmaku.unloop();
50 | }
51 | },
52 | },
53 | });
54 | ```
55 |
56 | ### 通过递归来实现循环播放
57 |
58 | 上面一种实现方式是借助官方提供的 api 来实现的,不过你也可以自己来递归实现。
59 |
60 | > [!NOTE] 提示
61 | > **高级弹幕不会参与碰撞计算,所以如果是高级弹幕的场景,不要通过这种方式来实现,使用 `setloop` 即可。**
62 | >
63 | > > 你可以复制下面这段代码,然后粘贴在在线 [**demo**](https://imtaotao.github.io/danmu/) 的控制台上查看效果。
64 |
65 | ```ts {7,11,15-16}
66 | let loops = 0;
67 |
68 | manager.push('弹幕内容', {
69 | plugin: {
70 | destroyed(danmaku, mark) {
71 | // 循环播放 3 次后,终止循环播放
72 | if (++loops >= 3) return;
73 |
74 | // 如果你是通过手动调用 destroy 方法来触发的钩子
75 | // 可以通过 danmaku.destroy('mark') 传递 mark 来判断一下
76 | if (mark === 'mark') return;
77 |
78 | // 如果你有对内存和视图做限,可能会导致发送失败
79 | // 你可以调用 manager.canPush('facile') 来判断
80 | danmaku.loops = 0;
81 | manager.unshift(danmaku);
82 | },
83 | },
84 | });
85 | ```
86 |
--------------------------------------------------------------------------------
/docs/zh/cases/recommend.md:
--------------------------------------------------------------------------------
1 | # 直播和视频场景的建议
2 |
3 | ## 描述
4 |
5 | 由于在直播和视频直播中,弹幕的实时性要求比较高,而默认的[**碰撞算法配置为**](../reference/manager-configuration/#config-mode) `strict`,当不满足渲染条件的时候会延迟到满足渲染条件后才渲染,**所以你应当设置为 `adaptive`**,这会让引擎会先尝试进行碰撞检测,如果不满足条件会忽略碰撞算法而立即渲染。
6 |
7 | ## 示例
8 |
9 | ```ts {4,8}
10 | // 在初始化的时候设置
11 | const manager = create({
12 | // .
13 | mode: 'adaptive',
14 | });
15 |
16 | // 或者使用 `setMode()` api
17 | manager.setMode('adaptive');
18 | ```
19 |
20 | 如果你想设置一条轨道内弹幕之间最小间距(仅当弹幕命中了碰撞检测的时候才会生效)
21 |
22 | ```ts
23 | // 同一条轨道内弹幕的间距最小为 10px
24 | manager.setGap(10);
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/zh/cases/simplified-mode.md:
--------------------------------------------------------------------------------
1 | # 弹幕精简模式
2 |
3 | ## 描述
4 |
5 | 当在视频和直播等场景中,由于有实时渲染的需求,当渲染的弹幕数量过多时,会造成页面卡顿,这里有两种方法来实现:
6 |
7 | - 实现弹幕精简模式。
8 | - 设置 [**`limits.view`**](../reference/manager-configuration/#config-limits) 来限制渲染的弹幕数量。
9 |
10 | > [!NOTE] 提示
11 | > 和实现弹幕冷却时间的功能一样,弹幕精简模式的实现也是依赖 [**`willRender`**](../reference/manager-hooks/#hooks-willrender) 这个钩子来实现。
12 |
13 | ## 以弹幕精简模式来实现
14 |
15 | ```ts {10}
16 | import { random } from 'aidly';
17 | import { create } from 'danmu';
18 |
19 | const manager = create({
20 | plugin: {
21 | // 这里相比实现弹幕 cd,作为另一种实现对比,直接在初始化的时候插入默认插件来实现
22 | willRender(ref) {
23 | // 我们过滤 50% 的弹幕
24 | if (random(0, 100) < 50) {
25 | ref.prevent = true;
26 | }
27 | return ref;
28 | },
29 | },
30 | });
31 | ```
32 |
33 | ## 设置 `limits.view` 来实现
34 |
35 | ```ts {6}
36 | import { create } from 'danmu';
37 |
38 | const manager = create({
39 | limits: {
40 | // 在页面视图容器中能够同时渲染的最大值为 100 个
41 | view: 100,
42 | },
43 | });
44 | ```
45 |
--------------------------------------------------------------------------------
/docs/zh/cases/track-settings.md:
--------------------------------------------------------------------------------
1 | # 轨道设置
2 |
3 | ## 描述
4 |
5 | 本章节学习如何控制轨道,我们可以将轨道控制在具体的数量来渲染。
6 |
7 | > [!NOTE] 提示
8 | > 以下这些代码你都可以复制,然后粘贴在在线 [**demo**](https://imtaotao.github.io/danmu/) 的控制台上查看效果。
9 |
10 | ## 限制为几条连续的轨道
11 |
12 | **限制为顶部 3 条弹幕:**
13 |
14 | ```ts {2,8-10}
15 | // 如果我们希望轨道高度为 50px
16 | manager.setTrackHeight('100% / 3');
17 |
18 | // 如果不设置渲染区域,轨道的高度会根据默认的 container.height / 3 得到,
19 | // 这可能导致轨道高度不是你想要的
20 | manager.setArea({
21 | y: {
22 | start: 0,
23 | // 3 条轨道的总高度为 150px
24 | end: 150,
25 | },
26 | });
27 | ```
28 |
29 | **限制为中间 3 条弹幕:**
30 |
31 | ```ts {1,5-6}
32 | manager.setTrackHeight('100% / 3');
33 |
34 | manager.setArea({
35 | y: {
36 | start: `50%`,
37 | end: `50% + 150`,
38 | },
39 | });
40 | ```
41 |
42 | ## 限制为几条不连续的轨道
43 |
44 | 限制为几条不连续的轨道,除了要做和连续轨道的操作之外,还需要借助 [**`willRender`**](../reference/manager-hooks/#hooks-willrender) 这个钩子来实现。
45 |
46 | ```ts {2,7-9,16,19-22}
47 | // 如果我们希望轨道高度为 50px,并渲染 0,2,4 这几条轨道
48 | manager.setTrackHeight('100% / 6');
49 |
50 | // 设置容器的渲染区域
51 | manager.setArea({
52 | y: {
53 | start: 0,
54 | // 6 条轨道的总高度为 300px
55 | end: 300,
56 | },
57 | });
58 |
59 | manager.use({
60 | willRender(ref) {
61 | // 高级弹幕和轨道不强相关,没有 trackIndex 这个属性
62 | if (ref.trackIndex === null) return ref;
63 |
64 | // 如果为 1,3,5 这几条轨道就阻止渲染,并重新添加等待下次渲染
65 | if (ref.trackIndex % 2 === 1) {
66 | ref.prevent = true;
67 | manager.unshift(ref.danmaku);
68 | }
69 | return ref;
70 | },
71 | });
72 | ```
73 |
--------------------------------------------------------------------------------
/docs/zh/cases/uniform-speed.md:
--------------------------------------------------------------------------------
1 | # 匀速运动
2 |
3 | ## 描述
4 |
5 | 由于本弹幕库对弹幕的运动速度控制默认不是匀速的,所以对于有匀速需求的用户来说,可以通过以下的方式来实现。
6 |
7 | > [!NOTE] 注意事项
8 | >
9 | > 1. 当为弹幕设置速度后,`config.timeRange` 和 `config.duration` 这俩配置会失效。
10 | > 2. 本弹幕库在匀速模式下只会保证弹幕的速度是一致的,但是弹幕之间的间距是不保证是一样的,虽然可以设置 `gap` 配置,但是这只是让弹幕之间的间距不小于 `gap` 设置的值,而不是等于。
11 | > 3. 如果你有强诉求保证间距是一样的,你可以减小速度来达到近似的值,原理见此 [issue](https://github.com/imtaotao/danmu/issues/34)。
12 |
13 | ### 设置匀速运动
14 |
15 | 有以下两种方式来设置为匀速运动模式。
16 |
17 | > [!NOTE] 原理
18 | > 当设置弹幕速度后,弹幕的运动时间计算如下,你可以根据想要的时间推导出速度:
19 | >
20 | > 1. $FacileDuration = (containter.width + danmaku.width) / v$
21 | > 2. $FlexibleDuration = (danmaku.position.x + danmaku.width) / v$
22 |
23 | ```ts {3-4}
24 | // 1. 在初始化的时候设置速度
25 | const manager = create({
26 | speed: 0.1,
27 | speed: '100% / 1000', // duration 约等于 1000ms
28 | });
29 | ```
30 |
31 | ```ts {2-3}
32 | // 2. 通过 `setSpeed` api 来设置
33 | manager.setSpeed(0.1);
34 | manager.setSpeed('100% / 2000'); // duration 约等于 2000ms
35 | ```
36 |
37 | ### 取消匀速运动
38 |
39 | 设置为 `null` 则取消匀速运动,恢复为默认的行为。
40 |
41 | ```ts
42 | manager.setSpeed(null);
43 | ```
44 |
45 | ### 单个弹幕不跟随匀速模式
46 |
47 | 如果你需要让大部分弹幕是匀速运动,但是一些特殊的弹幕不是匀速运动,可以在发送这些弹幕的时候传递 `speed` 配置为 `null`。当然你可以给一个不同于全局 `speed` 的值。
48 |
49 | ```ts {3}
50 | // 1. 普通弹幕
51 | manager.push('弹幕内容', {
52 | speed: null,
53 | });
54 | ```
55 |
56 | ```ts {3}
57 | // 2. 高级弹幕
58 | manager.pushFlexibleDanmaku('弹幕内容', {
59 | speed: null,
60 | });
61 | ```
62 |
63 | ### 和 [**`config.mode`**](../reference/manager-configuration.md#config-mode) 的关系
64 |
65 | 当设置弹幕的速度后,还是会受到 `config.mode` 的限制。
66 |
67 | - **`node`** 弹幕的发送不会有任何碰撞检测,弹幕会立即渲染。
68 | - **`strict`** 会进行严格的碰撞检测,如果不满足条件则会推迟渲染。
69 | - **`adaptive`** 在满足立即渲染的前提下,会尽力进行碰撞检测。
70 |
--------------------------------------------------------------------------------
/docs/zh/guide/create-plugin.md:
--------------------------------------------------------------------------------
1 | # 编写插件
2 |
3 | 编写一个插件是很简单的,但是借助内核暴露出来的`钩子`和 `API`,你可以很轻松的实现强大且定制化的需求。
4 |
5 | ## 描述
6 |
7 | 由于内核没有暴露出来**根据条件来实现过滤弹幕**的功能,原因在于内核不知道弹幕内容的数据结构,这和业务的诉求强相关,所以我们在此通过插件来实现**精简弹幕**的功能用来演示。
8 |
9 | ## 💻 编写一个插件
10 |
11 | > [!NOTE] 提示
12 | >
13 | > - 你编写的插件应当取一个 `name`,以便于调试定位问题(注意不要和其他插件冲突了)。
14 | > - 插件可以选择性的声明一个 `version`,这在你的插件作为独立包发到 `npm` 上时很有用。
15 |
16 | ```ts {10,14}
17 | export function filter({ userIds, keywords }) {
18 | return (manager) => {
19 | return {
20 | name: 'filter-keywords-or-user',
21 | version: '1.0.0', // version 字段不是必须的
22 | willRender(ref) {
23 | const { userId, content } = ref.danmaku.data.value;
24 | console.log(ref.type); // 可以根据此字段来区分是普通弹幕还是高级弹幕
25 |
26 | if (userIds && userIds.includes(userId)) {
27 | ref.prevent = true;
28 | } else if (keywords) {
29 | for (const word of keywords) {
30 | if (content.includes(word)) {
31 | ref.prevent = true;
32 | break;
33 | }
34 | }
35 | }
36 | return ref;
37 | },
38 | };
39 | };
40 | }
41 | ```
42 |
43 | ## 🛠️ 注册插件
44 |
45 | 你需要通过 `manager.use()` 来注册插件。
46 |
47 | ```ts {9-12}
48 | import { create } from 'danmu';
49 |
50 | const manager = create<{
51 | userId: number;
52 | content: string;
53 | }>();
54 |
55 | manager.use(
56 | filter({
57 | userIds: [1],
58 | keywords: ['菜'],
59 | }),
60 | );
61 | ```
62 |
63 | ## 💬 发送弹幕
64 |
65 | - ❌ **会**被插件阻止渲染
66 |
67 | ```ts {2}
68 | manager.push({
69 | userId: 1,
70 | content: '',
71 | });
72 | ```
73 |
74 | - ❌ **会**被插件阻止渲染
75 |
76 | ```ts {3}
77 | manager.push({
78 | userId: 2,
79 | content: '你真菜',
80 | });
81 | ```
82 |
83 | - ✔️ **不会**被插件阻止渲染
84 |
85 | ```ts {2}
86 | manager.push({
87 | userId: 2,
88 | content: '',
89 | });
90 | ```
91 |
92 | - ✔️ **不会**被插件阻止渲染
93 |
94 | ```ts {3}
95 | manager.push({
96 | userId: 2,
97 | content: '你真棒',
98 | });
99 | ```
100 |
--------------------------------------------------------------------------------
/docs/zh/guide/getting-started.md:
--------------------------------------------------------------------------------
1 | # 快速开始
2 |
3 | > [!NOTE] 注意
4 | > danmu 目前还没有到 v1.0 版本,不要使用未公开的 API,如果您发现任何错误或意外情况,请在 GitHub 上创建 issues 。
5 |
6 | `danmu` 是一个**高度可扩展,功能丰富齐全**的弹幕库,为开发者提供便捷接入和编写自定义插件的能力,满足复杂的需求同时也允许极致的定制化。我们建立了一个**示例站点**,你可以在此看到一些效果。
7 |
8 | > [!NOTE] 提示
9 | > 你可以在 demo 站点中打开控制台,通过 `window.manager` 来获取到 manager 实例,用来实时调试查看效果。
10 |
11 | https://imtaotao.github.io/danmu
12 |
13 | ## 🎯 为什么选择 `danmu` ?
14 |
15 | 现代的视频网站或多或少的都有添加弹幕功能,弹幕可以带来一系列不同的观影体验,实现一个好用,功能齐全的弹幕库并不是一件容易的事情,市面上有很多不同的弹幕库选择,但是大多数都依赖都是基于 `Canvas` 来实现的,这导致样式的绘制会很局限,并且没有什么手段进行扩展,这对后续的迭代来说是致命的,因为换一个库来实现,成本很高。
16 |
17 | `danmu` 基于 `CSS + DOM` 来绘制弹幕,这意味着,弹幕的运动可以基于浏览器原生的动画能力,并且 `CSS + DOM` 可以做的事情格外的多,这让不同形式的弹幕都得以存在(想象一下一个弹幕嵌入一个网页的场景)。并且我们提供的弹幕的碰撞计算,弹幕的运动速率可以不是固定的但是也能不会互相碰撞。
18 |
19 | ## 🚀 快速开始
20 |
21 | ### 1. 安装依赖
22 |
23 | 你可以使用您喜欢的包管理器在项目的依赖项中安装 `danmu`,从而将 Danmaku 添加到您现有的项目当中:
24 |
25 | ::: code-group
26 |
27 | ```sh [npm]
28 | $ npm install danmu
29 | ```
30 |
31 | ```sh [pnpm]
32 | $ pnpm install danmu
33 | ```
34 |
35 | ```sh [yarn]
36 | $ yarn add danmu
37 | ```
38 |
39 | :::
40 |
41 | 我们也提供了 `CDN` 供你来开发调试,**不要在生产环境使用此 `CDN`**:
42 |
43 | ```html
44 |
45 | ```
46 |
47 | ### 2. 创建 `manager`
48 |
49 | `danmu` 核心包只暴露了一个 `create` 方法,用来创建一个 `manager` 实例,是的,我们所有的实现都是多实例的方式实现的,创建的时候传入的配置可以看[**配置**](../reference/manager-configuration)这一章节的介绍。
50 |
51 | ```ts {8}
52 | import { create } from 'danmu';
53 |
54 | // 在此处创建一个 manager 实例,如果不传递则会使用默认的配置
55 | const manager = create({
56 | trackHeight: '20%',
57 | plugin: {
58 | $createNode(danmaku) {
59 | danmaku.node.textContent = danmaku.data;
60 | },
61 |
62 | willRender(ref) {
63 | console.log(ref.type); // 即将要渲染的弹幕类型
64 | console.log(ref.danmaku); // 即将要渲染的弹幕实例
65 | ref.prevent = true; // 设置为 true 将阻止渲染,可以在这里做弹幕过滤工作
66 | return ref;
67 | },
68 | },
69 | // .
70 | });
71 | ```
72 |
73 | ### 3. 挂载并渲染
74 |
75 | 当我们创建好了一个 `manager` 的时候就可以挂载到某个具体的节点上并渲染,实际上 `manager` 内部会启动一个定时器来轮询将内存区的弹幕来渲染出来,而轮询的时间是交由你来控制的(如果没有通过配置传递,会有一个默认的值 **`500ms`**,详见配置章节)。
76 |
77 | ```ts
78 | const container = document.getElementById('container');
79 |
80 | // 挂载,然后开始渲染
81 | manager.mount(container);
82 | manager.startPlaying();
83 | ```
84 |
85 | ### 4. 发送普通弹幕
86 |
87 | 当前面的一些工作准备完成之后,就可以发送弹幕了。
88 |
89 | ```ts
90 | // 发送弹幕的内容类型可以在创建 manager 的时候通过范型来约定,可以看后面的章节
91 | manager.push('弹幕内容');
92 | ```
93 |
94 | 但是通过 [**`manager.push`**](../reference/manager-api/#manager-push) 方法来发送的弹幕可能会受到弹幕算法的影响,不会立即渲染,想象一些往一个数组里面 push 一个数据,但是消费是从数组最前端拿出数据消费的。我们可以换一个 [**`manager.unshift`**](../reference/manager-api/#manager-unshift) 方法来发送弹幕。
95 |
96 | ```ts
97 | // 这会在下一次渲染轮询中,立即渲染
98 | manager.unshift('弹幕内容');
99 | ```
100 |
101 | 我们在初始化 `manager` 的时候,可以通过 `plugin` 属性来传递默认全局插件,他的作用域是对所有的弹幕都生效,而且包含[**全局**](../reference/manager-hooks)和[**弹幕**](../reference/danmaku-hooks)两种类型的钩子。
102 |
103 | 不过我们在发送弹幕的时候,也可以传递弹幕自己的插件,他不包含全局钩子,**作用域只对当前渲染的弹幕生效**,如果你需要的话,这会让你更好的来控制当前要渲染的这个弹幕。
104 |
105 | ```ts
106 | manager.push('弹幕内容', {
107 | plugin: {
108 | beforeMove(danmaku) {
109 | // beforeMove 钩子会在弹幕即将开始运动之前触发,你可以在这里更改弹幕的样式
110 | danmaku.setStyle(cssKey, cssValue);
111 | },
112 | },
113 | // .
114 | });
115 | ```
116 |
117 | ### 5. 发送高级弹幕
118 |
119 | 普通弹幕会受到碰撞渲染算法的限制,对于那些需要特殊处理的弹幕,例如顶部弹幕,特殊位置的弹幕,则需要通过 [**`manager.pushFlexibleDanmaku`**](../reference/manager-api/#manager-pushflexibledanmaku) 这个 `API` 发送高级弹幕来渲染,高级弹幕不会受到碰撞算法的限制。
120 |
121 | ```ts
122 | manager.pushFlexibleDanmaku('弹幕内容', {
123 | duration: 1000, // 默认从 manager.options.durationRange 中随机取一个值
124 | direction: 'none', // 默认取 manager.options.direction 的值
125 | position: (danmaku, container) => {
126 | // 这会让弹幕在容器居中的位置出现,因为 direction 为 none,所以会静止播放 1s
127 | return {
128 | x: `50% - ${danmaku.getWidth() / 2}`, // [!code ++]
129 | y: `50% - ${danmaku.getHeight() / 2}`, // [!code ++]
130 | };
131 | },
132 | plugin: {
133 | // plugin 参数是可选的,具体可以参考普通弹幕的钩子,这里是一样的
134 | },
135 | });
136 | ```
137 |
--------------------------------------------------------------------------------
/docs/zh/guide/typescript-interface.md:
--------------------------------------------------------------------------------
1 | # Typescript 类型
2 |
3 | ## 描述
4 |
5 | Danmaku 的 `TypeScript` 类型声明很齐全,所以当你在 `TypeScript` 中使用时,你会得到很好的类型提示,这对于插件化系统来说会非常的友好,甚至可以说是不可或缺的。
6 |
7 | ## 声明弹幕内容类型
8 |
9 | 当你在各种钩子里面拿到的 `danmaku` 实例类型时,其**弹幕内容**的类型默认为 `unknown`,但是你可以在初始化的时候传入范型来约束。
10 |
11 | ```ts
12 | import { create } from 'danmu';
13 |
14 | const manager = create<{ content: string; img: string }>({
15 | $beforeMove(danmaku) {
16 | // 你可以看到 data 类型为 { content: string, img: string }
17 | danmaku.data;
18 | },
19 | });
20 | ```
21 |
22 | ## 给 `statuses` 传递类型
23 |
24 | 由于 `manager.statuses` 不会在内核中有任何工作,他仅仅是提供一个普通对象给用户记录状态使用的,所以默认类型为 `Record`,你也可以传递范型来改变他,具体的实例可以参考我们的 [**demo**](https://github.com/imtaotao/danmu/blob/master/demo/src/manager.tsx#L9)。
25 |
26 | ```ts
27 | import { create } from 'danmu';
28 |
29 | const manager = create();
30 |
31 | // 你可以看到 statuses 类型为 { background: string }
32 | manager.statuses;
33 | ```
34 |
35 | ## 默认导出的类型声明
36 |
37 | 以下是我们默认导出的类型,可以帮助你在业务代码编写中或者插件编写中提供一些帮助。
38 |
39 | ```ts
40 | // 以下这些类型你都可以使用
41 | import type {
42 | Track,
43 | Manager,
44 | HookOn,
45 | HooksOn,
46 | Plugin,
47 | HookType,
48 | Mode,
49 | Speed,
50 | StyleKey,
51 | Position,
52 | PushOptions,
53 | PushFlexOptions,
54 | Location,
55 | ValueType,
56 | Direction,
57 | Danmaku,
58 | DanmakuType,
59 | DanmakuPlugin,
60 | ManagerPlugin,
61 | ManagerOptions,
62 | CreateOption,
63 | } from 'danmu';
64 | ```
65 |
--------------------------------------------------------------------------------
/docs/zh/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: 'Danmaku'
7 | text: '强大可扩展的弹幕库'
8 | tagline: Danmu 是一个用来管理,渲染,发送弹幕的库,覆盖了很多业务场景并且提供了好用的扩展方式。
9 | image: /logo.svg
10 | actions:
11 | - theme: brand
12 | text: 快速开始 ->
13 | link: /zh/guide/getting-started
14 | - theme: alt
15 | text: 在线示例
16 | link: https://imtaotao.github.io/danmu/
17 |
18 | features:
19 | - title: 多种渲染算法
20 | details: 我们提供了严格和渐进式的,以及普通的全量实时渲染算法。
21 | icon: 🔍
22 | - title: 丰富的 api
23 | details: 提供了很多业务场景需要的 api,最大程度的简化开发方式。
24 | icon: 🌟
25 | - title: 可自定义样式
26 | details: 弹幕的渲染层由 CSS 提供,可以绘制任何适用 DOM 的弹幕需求,样式规则完全复用 CSS,没有额外的学习成本。
27 | icon: 🧩
28 | - title: 插件化
29 | details: 我们提供了丰富的钩子,这允许你方便的开发插件来满足更加定制化的需求,这是非常强大的能力。
30 | icon: 🔌
31 | ---
32 |
--------------------------------------------------------------------------------
/docs/zh/reference/container-api.md:
--------------------------------------------------------------------------------
1 | # 容器 API
2 |
3 | 弹幕容器实例上面有以下一些属性和方法,当你在一些钩子里面获取到 container 实例时,可以参考本小节的知识。你可以通过 [**`manager.container`**](./manager-properties.md#manager-container) 获取实例。
4 |
5 | > [!NOTE] 注意事项
6 | > 如果你需要对容器的宽高做更改,建议使用 `manager.setArea()` 方法,而不要通过 `manager.container.setStyle()` 来更改,否则你需要手动调用 `manager.format()`。
7 |
8 | ```ts
9 | declare class Container {
10 | width: number;
11 | height: number;
12 | node: HTMLDivElement;
13 | parentNode: HTMLElement | null;
14 | setStyle(key: T, val: CSSStyleDeclaration[T]): void;
15 | }
16 | ```
17 |
18 | ## container.width
19 |
20 | **类型:`number`**
21 | **默认值:`0`**
22 |
23 | 容器的宽度,当你调用 `manager.format()` 后,这个值可能会有变化。
24 |
25 | ## container.height
26 |
27 | **类型:`number`**
28 | **默认值:`0`**
29 |
30 | 容器的高度,当你调用 `manager.format()` 后,这个值可能会有变化。
31 |
32 | ## container.node
33 |
34 | **类型:`HTMLDivElement`**
35 | **默认值:`div`**
36 |
37 | 容器的 DOM 节点。
38 |
39 | ## container.parentNode
40 |
41 | **类型:`HTMLElement | null`**
42 | **默认值:`null`**
43 |
44 | 容器的父节点,通过 `manager.mount()` 设置后,可以通过此属性拿到。
45 |
46 | ## container.setStyle()
47 |
48 | **类型:`setStyle(key: T, val: CSSStyleDeclaration[T]): void`**
49 |
50 | 这个方法可以设置容器节点的样式。
51 |
52 | ```ts
53 | // 所以你可以以下方式来给容器设置一些样式
54 | manager.container.setStyle('background', 'red');
55 | ```
56 |
--------------------------------------------------------------------------------
/docs/zh/reference/danmaku-api.md:
--------------------------------------------------------------------------------
1 | # 弹幕方法
2 |
3 | 弹幕实例有一些方法供你使用,你可用用他们来做一些行为,或者获取当前弹幕的一些状态数据。下面是一个示例:
4 |
5 | ```tsx {6,9,17-19}
6 | // 点击弹幕的时候会销毁弹幕
7 | function DanmakuComponent(props: { danmaku: Danmaku }) {
8 | return (
9 | {
11 | props.danmaku.destroy();
12 | }}
13 | >
14 | {props.danmaku.data.value}
15 |
16 | );
17 | }
18 |
19 | // 将组件渲染到弹幕的内置节点上
20 | manager.use({
21 | $createNode(danmaku) {
22 | ReactDOM.createRoot(danmaku.node).render(
23 | ,
24 | );
25 | },
26 | });
27 | ```
28 |
29 | ## `danmaku.hide()`
30 |
31 | **类型:`() => void`**
32 |
33 | 将当前弹幕实例设置为隐藏状态,实际上是设置 `visibility: hidden`,会调用 `hide` 钩子。
34 |
35 | ## `danmaku.show()`
36 |
37 | **类型:`() => void`**
38 |
39 | 将当前弹幕实例从隐藏状态恢复显示,会调用 `show` 钩子。
40 |
41 | ## `danmaku.pause()`
42 |
43 | **类型:`() => void`**
44 |
45 | 将当前正在运动状态的弹幕实例暂停,会调用 `pause` 钩子。
46 |
47 | ## `danmaku.resume()`
48 |
49 | **类型:`() => void`**
50 |
51 | 将当前正在暂停状态的弹幕实例恢复运动,会调用 `resume` 钩子。
52 |
53 | ## `danmaku.setloop()`
54 |
55 | **类型:`() => void`**
56 |
57 | 将弹幕实例设置为循环播放状态。
58 |
59 | ## `danmaku.unloop()`
60 |
61 | **类型:`() => void`**
62 |
63 | 将弹幕实例从循环播放状态取消。
64 |
65 | ## `danmaku.destroy()`
66 |
67 | **类型:`(mark?: unknown) => Promise`**
68 |
69 | 将当前弹幕实例从容器中销毁,并从内存中移除,会调用 `beforeDestroy` 和 `destroyed` 钩子,你也可以尝试传递 `mark` 标识符,引擎内置的 destroy 行为是不会传递标识符的。
70 |
71 | ```ts {4,8,14}
72 | const manager = create({
73 | plugin: {
74 | $moved(danmaku) {
75 | danmaku.destroy('mark');
76 | },
77 |
78 | $beforeDestroy(danmaku, mark) {
79 | if (mark === 'mark') {
80 | // .
81 | }
82 | },
83 |
84 | $destroyed(danmaku, mark) {
85 | if (mark === 'mark') {
86 | // .
87 | }
88 | },
89 | },
90 | });
91 | ```
92 |
93 | ## `danmaku.setStyle()`
94 |
95 | **类型:`(key: StyleKey, val: CSSStyleDeclaration[StyleKey]) => void`**
96 |
97 | 设置当前弹幕实例内置节点的 `CSS` 样式。
98 |
99 | ```ts
100 | // 将内置弹幕节点的背景色改为红色
101 | danmaku.setStyle('background', 'red');
102 | ```
103 |
104 | ## `danmaku.getWidth()`
105 |
106 | **类型:`() => number`**
107 |
108 | `getWidth()` 将返回弹幕实例自身的宽度,同样的在你发送高级弹幕的时候会很有用。
109 |
110 | ## `danmaku.getHeight()`
111 |
112 | **类型:`() => number`**
113 |
114 | `getHeight()` 将返回弹幕实例自身的高度,这在你发送高级弹幕的时候计算 `position` 的时候会很有用。
115 |
116 | ## `danmaku.remove()`
117 |
118 | **类型:`(pluginName: string) => void`**
119 |
120 | 移除当前弹幕实例的某个插件,但是必须指定插件名字。
121 |
122 | ## `danmaku.use()`
123 |
124 | **类型:`(plugin: DanmakuPlugin | ((d: this) => DanmakuPlugin)) => DanmakuPlugin`**
125 |
126 | 给当前弹幕实例注册一个插件,返回插件实例,如果你在后续需要移除插件,可以保存插件的 `name`,如果不传会默认分别一个 `uuid` 形式的 `name`。
127 |
128 | **如果传了 `name`:**
129 |
130 | ```ts
131 | const plugin = danmaku.use({
132 | name: 'test-plugin',
133 | // .
134 | });
135 | console.log(plugin.name); // 'test-plugin'
136 | ```
137 |
138 | **如果不传 `name`:**
139 |
140 | ```ts
141 | const plugin = danmaku.use({
142 | // .
143 | });
144 | console.log(plugin.name); // uuid
145 | ```
146 |
--------------------------------------------------------------------------------
/docs/zh/reference/danmaku-hooks.md:
--------------------------------------------------------------------------------
1 | # 弹幕钩子
2 |
3 | 弹幕的钩子只会在弹幕自身的行为发生改变的时候进行触发。
4 |
5 | **1. 通过 `manager` 注册**
6 |
7 | ```ts
8 | // 通过 `manager` 来注册需要加上 `$` 前缀
9 | import { create } from 'danmu';
10 |
11 | const manager = create({
12 | plugin: {
13 | $hide(danmaku) {},
14 | $show(danmaku) {},
15 | },
16 | });
17 | ```
18 |
19 | **2. 通过弹幕实例来注册**
20 |
21 | 如果你在其他全局钩子里面拿到了弹幕实例,则可以通过下面这种方式注册,这在插件的编写中会很有用。
22 |
23 | ```ts
24 | danmaku.use({
25 | hide(danmaku) {},
26 | show(danmaku) {},
27 | });
28 | ```
29 |
30 | ## `hooks.hide`
31 |
32 | **类型:`SyncHook<[Danmaku]>`**
33 |
34 | `hide` 钩子会在弹幕隐藏的时候触发。
35 |
36 | ## `hooks.show`
37 |
38 | **类型:`SyncHook<[Danmaku]>`**
39 |
40 | `show` 钩子会在弹幕从隐藏到显示的时候触发。
41 |
42 | ## `hooks.pause`
43 |
44 | **类型:`SyncHook<[Danmaku]>`**
45 |
46 | `pause` 钩子会在弹幕暂停的时候触发。
47 |
48 | ## `hooks.resume`
49 |
50 | **类型:`SyncHook<[Danmaku]>`**
51 |
52 | `resume` 钩子会在弹幕从暂停恢复的时候触发。
53 |
54 | ## `hooks.beforeMove`
55 |
56 | **类型:`SyncHook<[Danmaku]>`**
57 |
58 | `beforeMove` 钩子会在弹幕即将运动的时候触发,你可以在此时对弹幕做一些样式变更操作。
59 |
60 | ## `hooks.moved`
61 |
62 | **类型:`SyncHook<[Danmaku]>`**
63 |
64 | `moved` 钩子会在弹幕运动结束的时候触发,运动结束不代表会立即销毁,为了性能考虑,内核引擎会批量收集统一销毁。
65 |
66 | ## `hooks.appendNode`
67 |
68 | **类型:`SyncHook<[Danmaku, HTMLElement]>`**
69 |
70 | `appendNode` 钩子会在弹幕的节点添加到容器时候触发,他在 `createNode` 节点之后。
71 |
72 | ## `hooks.removeNode`
73 |
74 | **类型:`SyncHook<[Danmaku, HTMLElement]>`**
75 |
76 | `removeNode` 钩子会在弹幕从容器中移除的时候触发。
77 |
78 | ## `hooks.createNode`
79 |
80 | **类型:`SyncHook<[Danmaku, HTMLElement]>`**
81 |
82 | `createNode` 钩子会在弹幕的内置 HTML 节点创建后时候触发,你可以在这个钩子里面通过 `danmaku.node` 拿到这个节点,**进行一些样式和节点的渲染操作,这是本框架扩展性很重要的一步操作,很重要**。
83 |
84 | **示例:**
85 |
86 | ```tsx {8-10}
87 | function DanmakuComponent(props: { danmaku: Danmaku }) {
88 | return {props.danmaku.data.value}
;
89 | }
90 |
91 | manager.use({
92 | $createNode(danmaku) {
93 | // 将组件渲染到弹幕的内置节点上
94 | ReactDOM.createRoot(danmaku.node).render(
95 | ,
96 | );
97 | },
98 | });
99 | ```
100 |
101 | ## `hooks.beforeDestroy`
102 |
103 | **类型:`AsyncHook<[Danmaku, unknown]>`**
104 |
105 | `beforeDestroy` 钩子会在弹幕销毁之前触发,这个钩子允许返回一个 `promise`,如果你需要手动调用 [**`danmaku.destroy`**](../reference/danmaku-api/#danmaku-destroy) 方法,可以尝试传递 `mark`。
106 |
107 | **示例:**
108 |
109 | ```ts{6}
110 | import { sleep } from 'aidly';
111 |
112 | manager.use({
113 | async $beforeDestroy(danmaku, mark) {
114 | // 将会阻止 1s,然后销毁当前弹幕
115 | await sleep(1000);
116 | },
117 | });
118 | ```
119 |
120 | ## `hooks.destroyed`
121 |
122 | **类型:`SyncHook<[Danmaku, unknown]>`**
123 |
124 | `destroyed` 钩子会在弹幕销毁后触发,如果你需要手动调用 [**`danmaku.destroy`**](../reference/danmaku-api/#danmaku-destroy) 方法,可以尝试传递 `mark`。
125 |
126 | **示例:**
127 |
128 | ```ts {3}
129 | manager.use({
130 | $destroyed(danmaku, mark) {
131 | if (mark) {
132 | // .
133 | }
134 | },
135 | });
136 | ```
137 |
--------------------------------------------------------------------------------
/docs/zh/reference/danmaku-properties.md:
--------------------------------------------------------------------------------
1 | # 弹幕属性
2 |
3 | 弹幕实例里面有很多用来记录当前弹幕状态的属性,你可以用他们来做一些判断,实现自己的业务目的。
4 |
5 | ## `danmaku.data`
6 |
7 | **类型:`PushData`**
8 |
9 | `data` 是你在发送数据时传入的数据。
10 |
11 | ```ts
12 | manager.use({
13 | $createNode(danmaku) {
14 | console.log(danmaku.data); // { value: 1 }
15 | },
16 | });
17 |
18 | manager.push({ value: 1 });
19 | ```
20 |
21 | ## `danmaku.type`
22 |
23 | **类型:`facile' | 'flexible'`**
24 |
25 | 弹幕的类型,分为普通弹幕和高级弹幕,普通弹幕的类型为 `facile`,你可以根据此属性来判断做不同的处理。
26 |
27 | ## `danmaku.node`
28 |
29 | **类型:`HTMLElement`**
30 |
31 | 弹幕的内置节点,你可以往这个节点里面渲染真正的弹幕内容。
32 |
33 | ## `danmaku.loops`
34 |
35 | **类型:`number`**
36 | **默认值:`0`**
37 |
38 | 弹幕的播放次数,弹幕运动结束后会自动 `+1`,如果你有循环弹幕的需求,可能需要此属性。
39 |
40 | ## `danmaku.duration`
41 |
42 | **类型:`number`**
43 |
44 | 弹幕的运动时长。
45 |
46 | ## `danmaku.direction`
47 |
48 | **类型:`'right' | 'left' | 'none'`**
49 |
50 | 弹幕的运动方向。
51 |
52 | ## `danmaku.isFixedDuration`
53 |
54 | **类型:`boolean`**
55 |
56 | 用于判断当前弹幕实例是否被修正过运动时间。
57 |
58 | ## `danmaku.pluginSystem`
59 |
60 | **类型:`PluginSystem`**
61 |
62 | 弹幕的插件系统实例,其 api 可以见 **hooks-plugin** 的文档。
63 |
64 | https://github.com/imtaotao/hooks-plugin?tab=readme-ov-file#apis
65 |
--------------------------------------------------------------------------------
/docs/zh/reference/manager-configuration.md:
--------------------------------------------------------------------------------
1 | # manager 配置
2 |
3 | 在初始化 `manager` 的时候,允许传递全局 options,替换默认的配置。以下所有的配置都是**可选的**。
4 |
5 | > [!NOTE] 注意
6 | > 这里的是全局属性,在发送弹幕的时候如果一些可选属性没有传递,**会默认取这里的全局属性**。
7 |
8 | **示例:**
9 |
10 | ```ts {17}
11 | interface CreateOption {
12 | rate?: number;
13 | interval?: number;
14 | gap?: number | string;
15 | speed?: number | string | null; // 优先级比 `durationRange` 高
16 | durationRange?: [number, number];
17 | trackHeight?: number | string;
18 | plugin?: ManagerPlugin;
19 | limits?: {
20 | view?: number;
21 | stash?: number;
22 | };
23 | direction?: 'right' | 'left';
24 | mode?: 'none' | 'strict' | 'adaptive';
25 | }
26 |
27 | export declare function create(options?: CreateOption): Manager;
28 | ```
29 |
30 | 除了 `plugin`,所有的参数都可以通过在**创建 `manager` 的时候设置**或者**通过 api 来更改**。
31 |
32 | **示例:**
33 |
34 | ```ts
35 | import { create } from 'danmu';
36 |
37 | // 1. 在初始化的时候设置
38 | const manager = create({ rate: 1 });
39 |
40 | // 2. 通过 api 来更改
41 | manager.setRate(1);
42 | ```
43 |
44 | ## `config.mode`
45 |
46 | **类型:`'none' | 'strict' | 'adaptive'`**
47 | **默认值:`'strict'`**
48 |
49 | > [!NOTE] 注意事项
50 | >
51 | > 当弹幕设置了具体的速度时(匀速模式)也会遵守以下行为。
52 |
53 | 用来确定内核的**碰撞检测算法**。**如果你的业务场景是直播或者视频播放,你应该设置为 `adaptive`,这样在满足实时渲染的前提下,尽可能的不发生弹幕碰撞。**
54 |
55 | - **`none`** 不会有任何碰撞检测,弹幕会立即渲染。
56 | - **`strict`** 会进行严格的碰撞检测,如果不满足条件则会推迟渲染。
57 | - **`adaptive`** 在满足立即渲染的前提下,会尽力进行碰撞检测(推荐)。
58 |
59 | ## `config.rate`
60 |
61 | **类型:`number`**
62 | **默认值:`1`**
63 |
64 | 用来设置设置弹幕的运动速率,弹幕的原始运动速度会乘以 `rate` 这个系数。
65 |
66 | ## `config.speed`
67 |
68 | **类型:`string | number | null | undefined`**
69 | **默认值:`undefined`**
70 |
71 | > [!NOTE] 说明
72 | >
73 | > 1. 当所有弹幕都以固定的速度运动时则会体现出**匀速运动**的特性。
74 | > 2. 和 `config.rate` 的区别是,`config.speed` 是运动的速度,而 `config.rate` 是速度的倍数,例如:
75 | >
76 | > ```js
77 | > rate = 1.5;
78 | > speed = 0.1;
79 | > 最终运动速度为 = `0.1 * 1.5`;
80 | > ```
81 |
82 | 弹幕运动的速度,这个配置会和 `durationRange` 冲突,优先级会更高,如果设置了此配置则 `durationRange` 会失效。如果发送弹幕的时候有自己的 `speed`,则会取弹幕自身的配置。
83 |
84 | ## `config.gap`
85 |
86 | **类型:`number | string`**
87 | **默认值:`0`**
88 |
89 | 同一条轨道在碰撞检测的啥情况下,后一条弹幕与前一条弹幕最小相隔的距离。**不能设置一个小于 `0` 的值,如果传递的是 `string` 类型则代表是百分比,但是必须是 `10%` 这种语法**, 仅当弹幕命中了碰撞检测的时候才会生效。
90 |
91 | **示例:**
92 |
93 | ```ts
94 | manager.setGap(100); // 最小间距为 100px
95 | manager.setGap('10%'); // 最小间距为容器宽度的 10%
96 | ```
97 |
98 | ## `config.interval`
99 |
100 | **类型:`number`**
101 | **默认值:`500`**
102 |
103 | 内核轮询渲染的频率,默认为 `500ms` 一次,你可以根据业务现实情况调整为一个合适的值。其实就是 `setTimeout` 的时间。
104 |
105 | ## `config.direction`
106 |
107 | **类型:`string`**
108 | **默认值:`'right'`**
109 |
110 | 弹幕的运动方向,默认从右往左运动,**普通弹幕没有 `none` 值,高级弹幕可以设置 `none` 值**。如果发送弹幕的时候有自己的 `direction`,则会取弹幕自身的配置。
111 |
112 | ## `config.trackHeight`
113 |
114 | **类型:`number | string`**
115 | **默认值:`'20%'`**
116 |
117 | 轨道高度,如果传入的值为 `number` 类型,则默认是 `px`,如果传递是 `string` 类型,必须是 `10%` 这种语法,代表基于容器的百分比高度。
118 |
119 | **示例:**
120 |
121 | ```ts
122 | manager.setTrackHeight(100); // 高度为 100px
123 | manager.setTrackHeight('33%'); // 高度为容器高度的 33%
124 | ```
125 |
126 | ## `config.durationRange`
127 |
128 | **类型:`[number, number]`**
129 | **默认值:`[4000, 6000]`**
130 |
131 | 普通弹幕的运动时间,这是一个范围值,**普通弹幕会在这个范围内随机选择一个时间作为运动时间**,如果你希望所有的弹幕运动时间都一致(注意不是匀速),你可以将两个数设置为同样的数。如果发送弹幕的时候有自己的 `duration`,则会取弹幕自身的配置。
132 |
133 | ## `config.limits`
134 |
135 | **类型:`{ view?: number; stash?: number }`**
136 | **默认值:`{ stash: Infinity }`**
137 |
138 | > [!NOTE] 提示
139 | > 这个参数会限制弹幕渲染的数量,内存和视图的**默认值都是不限制**。
140 |
141 | - `view` 限制渲染在容器中的弹幕数量,如果超过了此限制,普通弹幕会放在内存中,等待合适的时机渲染,高级弹幕会直接丢弃。
142 | - `stash` 限制存放在内存中的弹幕数量,如果超过此限制则会被丢弃,并触发告警或调用插件钩子,你可以适当调整此参数。
143 |
144 | ## `config.plugin`
145 |
146 | **类型:`ManagerPlugin | Array>`**
147 | **默认值:`undefined`**
148 |
149 | 创建 `manager` 的时候默认的 `managerPlugin`,如果你需要注册新插件可以使用 `manager.use` 方法。详情可见 [**manager 钩子**](./manager-hooks) 和 [**编写插件**](../guide/create-plugin) 这两章。
150 |
151 | **示例:**
152 |
153 | ```ts
154 | import { create } from 'danmu';
155 |
156 | const manager = create({
157 | plugin: {
158 | // .
159 | start() {},
160 | stop() {},
161 | },
162 | });
163 | ```
164 |
165 | **以数组类型传递:**
166 |
167 | ```ts
168 | // 你也可以传递为一个数组,方便你添加其他插件时控制顺序
169 | const manager = create({
170 | plugin: [
171 | // .
172 | {
173 | // .
174 | start() {},
175 | stop() {},
176 | },
177 | ],
178 | });
179 | ```
180 |
--------------------------------------------------------------------------------
/docs/zh/reference/manager-hooks.md:
--------------------------------------------------------------------------------
1 | # manager 钩子
2 |
3 | manager 的钩子包含 `manager` 自身的钩子和 `弹幕` 的钩子。
4 |
5 | > [!NOTE] 注意
6 | >
7 | > 1. 弹幕钩子和全局钩子以 **`$`** 前缀作为区分。
8 | > 2. 这里只会介绍 `manager` **自身的钩子**,因为弹幕钩子会在[**弹幕钩子章节**](./danmaku-hooks)介绍。
9 |
10 | ## 1. 创建 `manager` 时注册
11 |
12 | **示例:**
13 |
14 | ```ts
15 | import { create } from 'danmu';
16 |
17 | const manager = create({
18 | plugin: {
19 | start() {},
20 | stop() {},
21 | },
22 | });
23 | ```
24 |
25 | ## 2. 通过 `manager.use` 来注册
26 |
27 | [**`manager.use()`**](./manager-api/#manager-use) 是用来注册插件的 api,如果你有外部插件或者自己编写的插件可用,也可以通过此方法来注册。
28 |
29 | **示例:**
30 |
31 | ```ts
32 | manager.use({
33 | start() {},
34 | stop() {},
35 | });
36 | ```
37 |
38 | ## `hooks.init`
39 |
40 | **类型:`SyncHook<[manager: Manager]>`**
41 |
42 | `init` 钩子会在创建 `manager` 的时候触发,一般情况下只有当你通过 `create` 方法创建 `manager` 的时候才会用到,这只是一个语法糖让你方便拿到 `manager` 实例。
43 |
44 | ```ts {3}
45 | const manager = create({
46 | plugin: {
47 | init(manager) {
48 | // .
49 | },
50 | },
51 | });
52 | ```
53 |
54 | ## `hooks.push`
55 |
56 | **Type: `SyncHook<[PushData | Danmaku, DanmakuType, boolean]>`**
57 |
58 | `push` 钩子会在发送弹幕的时候触发,调用 `manager.push()`,`manager.unshift()`,和 `manager.pushFlexibleDanmaku()` 的时候都会触发,你可以在此钩子拿到将要发送的弹幕数据,弹幕类型,是否是 `unshift` 等。
59 |
60 | ```ts {6}
61 | const manager = create({
62 | plugin: {
63 | push(data, type, isUnshift) {
64 | if (manager.isDanmaku(data)) return;
65 | // 处理高级弹幕
66 | if (type === 'flexible') {
67 | // .
68 | } else {
69 | // .
70 | }
71 | },
72 | },
73 | });
74 | ```
75 |
76 | ## `hooks.start`
77 |
78 | **类型:`SyncHook`**
79 |
80 | `start` 钩子会在渲染引擎启动的时候触发,调用 `manager.startPlaying()` 的时候才会触发。
81 |
82 | ## `hooks.stop`
83 |
84 | **类型:`SyncHook`**
85 |
86 | `stop` 钩子会在渲染引擎关闭的时候触发,调用 `manager.stopPlaying()` 的时候才会触发。
87 |
88 | ## `hooks.show`
89 |
90 | **类型:`SyncHook`**
91 |
92 | `show` 钩子会在批量将弹幕从隐藏状态改为显示状态的时候触发,调用 `manager.show()` 的时候才会触发。
93 |
94 | ## `hooks.hide`
95 |
96 | **类型:`SyncHook`**
97 |
98 | `hide` 钩子会在批量将弹幕隐藏的时候触发,调用 `manager.hide()` 的时候才会触发。
99 |
100 | ## `hooks.clear`
101 |
102 | **类型:`SyncHook<['facile' | 'flexible' | null | undefined]>`**
103 |
104 | `clear` 钩子会在清空当前渲染和内存中的弹幕和弹幕数据的时候触发,调用 `manager.clear()` 的时候。
105 |
106 | ## `hooks.mount`
107 |
108 | **类型:`SyncHook<[HTMLElement]>`**
109 |
110 | `mount` 钩子会在将容器挂载到指定 `DOM` 节点的时候触发,你可以在钩子里面拿到挂载的节点,调用 `manager.mount()` 的时候才会触发。
111 |
112 | ## `hooks.unmount`
113 |
114 | **类型:`SyncHook<[HTMLElement | null]>`**
115 |
116 | `unmount` 钩子会在将容器从当前挂载的节点卸载的时候触发,你可以在钩子里面拿到将要卸载的节点,如果有的话,调用 `manager.unmount()` 的时候才会触发。
117 |
118 | ## `hooks.freeze`
119 |
120 | **类型:`SyncHook`**
121 |
122 | `freeze` 钩子会在冻结当前渲染的弹幕的时候触发,调用 `manager.freeze()` 的时候才会触发。
123 |
124 | ## `hooks.unfreeze`
125 |
126 | **类型:`SyncHook`**
127 |
128 | `unfreeze` 钩子会在当前弹幕从冻结解除时候触发,调用 `manager.unfreeze()` 的时候才会触发。
129 |
130 | ## `hooks.format`
131 |
132 | **类型:`SyncHook`**
133 |
134 | `format` 钩子会在容器格式化的时候触发,调用 `manager.format()` 手动格式化的时候才会触发。
135 |
136 | ## `hooks.render`
137 |
138 | **类型:`SyncHook<[DanmakuType]>`**
139 |
140 | `render` 钩子会在每一次轮询渲染的时候触发。
141 |
142 | ## `hooks.willRender`
143 |
144 | **类型:`SyncWaterfallHook`**
145 |
146 | ```ts
147 | interface RenderData {
148 | type: DanmakuType;
149 | prevent: boolean;
150 | danmaku: Danmaku;
151 | trackIndex: null | number;
152 | }
153 | ```
154 |
155 | `willRender` 钩子会在弹幕即将要进入渲染队列的时候触发,你可以在此钩子阻止弹幕渲染,这对于通过**关键字**或者**轨道 index** 来过滤弹幕的功能实现很有用。
156 |
157 | ## `hooks.finished`
158 |
159 | **类型:`SyncHook`**
160 |
161 | `finished` 钩子会在当前内存中的所有弹幕渲染完毕的时候触发,但是不代表以后就不会渲染弹幕了,因为你还可能继续在发送新的弹幕。
162 |
163 | ## `hooks.limitWarning`
164 |
165 | **类型:`SyncHook<[DanmakuType, number]>`**
166 |
167 | `limitWarning` 钩子会在超过内存弹幕容量阈值的时候触发,如果你没有设置此钩子,则会在控制台抛出告警,如果你想取消控制台告警,可以自行添加此钩子处理。
168 |
169 | ## `hooks.updateOptions`
170 |
171 | **类型:`SyncHook<[Partial]>`**
172 |
173 | `updateOptions` 钩子会在更新配置的时候触发,调用 `manager.updateOptions()` 的时候才会触发,你可以在此钩子拿到新的配置。
174 |
175 | > [!NOTE] 注意事项
176 | > 实际上很多更改配置的方法,例如 `manager.setRate` 等,底层都是调用的 `manager.updateOptions()` 方法,也是会触发此钩子的。
177 |
--------------------------------------------------------------------------------
/docs/zh/reference/manager-properties.md:
--------------------------------------------------------------------------------
1 | # manager 属性
2 |
3 | > [!NOTE] 单位提示
4 | > 所有参与计算的单位都允许通过表达式来计算,类似 CSS 的 `calc`。
5 | >
6 | > 1. **`number`**:默认单位为 `px`。
7 | > 2. **`string`**:表达式计算。支持(`+`, `-`, `*`, `/`)数学计算,只支持 `%` 和 `px` 两种单位。
8 | >
9 | > ```ts
10 | > manager.setGap('(100% - 10px) / 5');
11 | > ```
12 |
13 | ## `manager.version`
14 |
15 | **类型:`string`**
16 |
17 | 当前 `danmu` 库的版本。
18 |
19 | ## `manager.options`
20 |
21 | **类型:`ManagerOptions`**
22 |
23 | [**`manager.options`**](./manager-configuration),你可以从这里取到一些初始值并使用。
24 |
25 | ```ts
26 | console.log(manager.options.durationRange); // [number, number]
27 | ```
28 |
29 | ## `manager.statuses`
30 |
31 | **类型:`Record`**
32 |
33 | 一个记录状态的属性,在内核中没有使用,主要是提供给业务方记录一些状态使用的。默认类型为一个 `Record`,不过你可以在创建 `manager` 的时候传递范型。
34 |
35 | ```ts {3}
36 | import { create } from 'danmu';
37 |
38 | const manager = create();
39 |
40 | manager.statuses; // 类型为 { background: string }
41 | ```
42 |
43 | ## `manager.trackCount`
44 |
45 | **类型:`number`**
46 |
47 | 当前容器内部轨道的数量。当容器的大小改变后,并且 `format` 之后(手动调用 `format()` 方法或者调用 `setArea()` 方法也会 `format`),`trackCount` 也会随之改变。
48 |
49 | ## `manager.container`
50 |
51 | **类型:`Container`**
52 |
53 | 见 [**`容器 API`**](./container-api) 小节。
54 |
55 | ## `manager.pluginSystem`
56 |
57 | **类型:`PluginSystem`**
58 |
59 | `manager` 的插件系统实例,其 api 可以见 **hooks-plugin**的文档。
60 |
61 | https://github.com/imtaotao/hooks-plugin?tab=readme-ov-file#apis
62 |
--------------------------------------------------------------------------------
/docs/zh/reference/track-api.md:
--------------------------------------------------------------------------------
1 | # 轨道 API
2 |
3 | Track 类是轨道相关的 api,你可以通过 [**`manager.getTrack(i)`**](./manager-api#manager-gettrack) 来获取某一条轨道实例。
4 |
5 | ```ts
6 | interface Location {
7 | top: number;
8 | middle: number;
9 | bottom: number;
10 | }
11 |
12 | declare class Track {
13 | width: number;
14 | height: number;
15 | index: number;
16 | isLock: boolean;
17 | location: Location;
18 | list: Array>;
19 | lock(): void;
20 | unlock(): void;
21 | clear(): void;
22 | each(fn: (danmaku: FacileDanmaku) => unknown | boolean): void;
23 | }
24 | ```
25 |
26 | ## track.width
27 |
28 | **类型:`number`**
29 |
30 | 轨道的宽度,当你调用 `manager.format()` 后,这个值可能会有变化。
31 |
32 | ## track.height
33 |
34 | **类型:`number`**
35 |
36 | 轨道的高度,当你调用 `manager.format()` 后,这个值可能会有变化。
37 |
38 | ## track.index
39 |
40 | **类型:`number`**
41 |
42 | 再当前容器的轨道列表中,当前这条轨道所在的坐标,例如 index 为 1,则代表是当前容器轨道列表中的第 2 条轨道。
43 |
44 | ## track.isLock
45 |
46 | **类型:`boolean`**
47 | **默认值:`false`**
48 |
49 | 用来判断当前这条轨道是否被锁定。
50 |
51 | ## track.list
52 |
53 | **类型:`Array`**
54 |
55 | 当前这条轨道内部的弹幕列表,如果你要对列表进行循环,建议使用 track.each() 方法。
56 |
57 | ## track.location
58 |
59 | **类型:`Location`**
60 |
61 | 当前这条轨道高度相关位置信息,**单位为 `px`**, 他是基于容器来计算的,当容器 format 之后,此熟悉也会跟着变化,当你发送高级弹幕的时候如果需要将其发送到某条轨道上,此属性可能会很有用。
62 |
63 | > [!NOTE] 提示
64 | >
65 | > 1. 对于弹幕的高度,如果你不需要通过计算就可以得到,则不需要通过 `getHeight()` 方法。
66 | > 2. 你要确保获取的轨道存在,否则会报错,可以通过 `manager.trackCount` 来判断当前总共有多少条轨道。
67 | > 3. 通过以下示例的方式渲染高级弹幕,实际上弹幕不会受到轨道的约束,只是渲染的位置在某条轨道上,所以当你对某条轨道调用了 `track.lock()`,并不会影响高级弹幕。
68 | > 4. 你可以在我们的在线 [**demo**](https://imtaotao.github.io/danmu/) 打开浏览器控制台输入这段代码查看效果。
69 |
70 | ```ts {9,12}
71 | // 发送一个高级弹幕
72 | manager.pushFlexibleDanmaku(
73 | { content: '弹幕内容' },
74 | {
75 | duration: 5000,
76 | direction: 'none',
77 | position(danmaku, container) {
78 | // 渲染在第 4 条轨道中
79 | const { middle } = manager.getTrack(3).location;
80 | return {
81 | x: (container.width - danmaku.getWidth()) * 0.5,
82 | y: middle - danmaku.getHeight() / 2,
83 | };
84 | },
85 | },
86 | );
87 | ```
88 |
89 | ## track.lock()
90 |
91 | **类型:`() => void`**
92 |
93 | 用于锁定当前轨道,当轨道被锁定之后,当前这条轨道将不会发送新的弹幕。
94 |
95 | ## track.unlock()
96 |
97 | **类型:`() => void`**
98 |
99 | 解锁当前这条轨道
100 |
101 | ## track.clear()
102 |
103 | **类型:`() => void`**
104 |
105 | 清空当前这条轨道内部所有的弹幕,但是不会阻止后续的弹幕发送。
106 |
107 | ```ts
108 | // 如果你需要清空并阻止后续的弹幕发送
109 | const track = manager.getTrack(0);
110 |
111 | track.clear();
112 | track.lock();
113 | ```
114 |
115 | ## track.each()
116 |
117 | **类型:`(fn: (danmaku: FacileDanmaku) => unknown | boolean) => void`**
118 |
119 | 对 `track.list` 进行遍历,当你有对弹幕进行销毁的行为时,最好是使用此方法,回调函数返回 `false` 会阻止后续的遍历。
120 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | clearMocks: true,
6 | rootDir: __dirname,
7 | preset: 'ts-jest',
8 | testMatch: ['/src/__tests__/**/*spec.[jt]s?(x)'],
9 | coverageProvider: 'v8',
10 | coverageDirectory: 'coverage',
11 | transform: { '\\.ts$': 'ts-jest' },
12 | globals: {
13 | __TEST__: 'true',
14 | __VERSION__: '"unknown"',
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "danmu",
3 | "version": "0.16.0",
4 | "description": "Flexible, cross-platform, powerful danmu library.",
5 | "main": "./dist/danmu.cjs.js",
6 | "unpkg": "./dist/danmu.umd.js",
7 | "module": "./dist/danmu.esm-bundler.js",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "import": {
12 | "types": "./dist/index.d.ts",
13 | "node": "./dist/danmu.esm-bundler.mjs",
14 | "default": "./dist/danmu.esm-bundler.js"
15 | },
16 | "require": {
17 | "types": "./dist/index.d.ts",
18 | "default": "./dist/danmu.cjs.js"
19 | }
20 | }
21 | },
22 | "files": [
23 | "dist"
24 | ],
25 | "scripts": {
26 | "test": "jest",
27 | "prepare": "husky",
28 | "release": "tsx ./release.ts",
29 | "preview:docs": "cd docs && pnpm run preview",
30 | "dev:docs": "cd docs && pnpm run dev",
31 | "dev:demo": "cd demo && pnpm run dev",
32 | "dev:core": "rollup --config --environment BUILD:development",
33 | "build:docs": "rimraf ./docs/.vitepress/dist && cd docs && pnpm run build",
34 | "build:demo": "rimraf ./demo/dist && cd demo && pnpm run build",
35 | "build:core": "rimraf ./dist && rollup --config && rimraf ./dist/__tests__ && pnpm run format:es",
36 | "format": "pnpm run format:es && pnpm run format:docs",
37 | "format:docs": "prettier --write ./docs",
38 | "format:es": "prettier --write --ignore-path=.prettierignore --parser typescript \"(src|demo|dist)/**/*.((m)?js|ts?(x))\""
39 | },
40 | "lint-staged": {
41 | "*.js": [
42 | "prettier --write"
43 | ],
44 | "*.ts?(x)": [
45 | "prettier --parser=typescript --write"
46 | ]
47 | },
48 | "author": "imtaotao",
49 | "keywords": [
50 | "danmu",
51 | "danmaku",
52 | "danmuku",
53 | "弹幕",
54 | "弹幕库"
55 | ],
56 | "license": "MIT",
57 | "publishConfig": {
58 | "registry": "https://registry.npmjs.org"
59 | },
60 | "repository": {
61 | "type": "git",
62 | "url": "git+https://github.com/imtaotao/danmu.git"
63 | },
64 | "bugs": {
65 | "url": "https://github.com/imtaotao/danmu/issues"
66 | },
67 | "packageManager": "pnpm@9.1.3",
68 | "devDependencies": {
69 | "@jsdevtools/version-bump-prompt": "^6.1.0",
70 | "@rollup/plugin-commonjs": "^26.0.1",
71 | "@rollup/plugin-json": "^6.1.0",
72 | "@rollup/plugin-node-resolve": "^15.2.3",
73 | "@rollup/plugin-replace": "^5.0.7",
74 | "@types/jest": "^29.5.12",
75 | "@types/node": "^22.5.0",
76 | "chalk": "^5.3.0",
77 | "execa": "^9.3.1",
78 | "husky": "^9.0.11",
79 | "jest": "^29.7.0",
80 | "lint-staged": "^15.2.5",
81 | "minimist": "^1.2.8",
82 | "prettier": "^3.3.3",
83 | "rimraf": "^5.0.7",
84 | "rollup": "^4.18.0",
85 | "rollup-plugin-cleanup": "^3.2.1",
86 | "rollup-plugin-typescript2": "^0.36.0",
87 | "semver": "^7.6.3",
88 | "ts-jest": "^29.1.4",
89 | "tsx": "^4.17.0",
90 | "typescript": "^5.8.3"
91 | },
92 | "dependencies": {
93 | "aidly": "^1.20.0",
94 | "hooks-plugin": "^1.3.0"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "demo"
3 | - "docs"
--------------------------------------------------------------------------------
/release.ts:
--------------------------------------------------------------------------------
1 | // pnpm release --patch -> 1.0.1 (default)
2 | // pnpm release --minor -> 1.1.0
3 | // pnpm release --major -> 2.0.0
4 | // pnpm release --beta -> 1.0.1-beta-1645598740512.0
5 |
6 | import fs from 'node:fs';
7 | import chalk from 'chalk';
8 | import semver from 'semver';
9 | import minimist from 'minimist';
10 | import bumpPrompt from '@jsdevtools/version-bump-prompt';
11 |
12 | type ReleaseType = 'prerelease' | 'patch' | 'minor' | 'major';
13 |
14 | const args = minimist(process.argv.slice(2));
15 |
16 | const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
17 |
18 | const log = (msg: string) => console.log(chalk.cyan(msg));
19 |
20 | let type: ReleaseType = 'patch';
21 |
22 | async function run(bin: string, args: Array, opts = {}) {
23 | const { execa } = await import('execa');
24 | const result = await execa(bin, args, {
25 | stdio: 'pipe',
26 | cwd: process.cwd(),
27 | ...opts,
28 | });
29 | if (result.exitCode) {
30 | log(`${result.stdout}\n${result.stderr}\n`);
31 | console.log(
32 | chalk.red(`\nCommand execution failed, Code "${result.exitCode}"`),
33 | );
34 | process.exit(1);
35 | }
36 | return result;
37 | }
38 |
39 | async function release(releaseType: ReleaseType) {
40 | if (!pkg.version) return;
41 | log('\nBuilding...');
42 | await build();
43 |
44 | const data = await bumpVersion(releaseType);
45 |
46 | log('\nCommitting changes...');
47 | await commit(data.newVersion);
48 |
49 | log('\nPublish...');
50 | await publish(data.newVersion);
51 |
52 | log('\nPush to GitHub...');
53 | await run('git', ['push']);
54 | await run('git', ['push', '--tags']);
55 |
56 | log(`\nUpdated: "${data.oldVersion}" -> "${data.newVersion}"`);
57 | }
58 |
59 | function build() {
60 | return run('pnpm', ['run', 'build:core']);
61 | }
62 |
63 | async function publish(version: string) {
64 | let publishArgs = ['publish', '--access', 'public', '--no-git-checks'];
65 |
66 | if (version) {
67 | let releaseTag = 'latest';
68 | if (version.includes('alpha')) {
69 | releaseTag = 'alpha';
70 | } else if (version.includes('beta')) {
71 | releaseTag = 'beta';
72 | } else if (version.includes('rc')) {
73 | releaseTag = 'rc';
74 | }
75 | publishArgs = publishArgs.concat(['--tag', releaseTag]);
76 | }
77 | return run('pnpm', publishArgs);
78 | }
79 |
80 | async function commit(version: string) {
81 | await run('git', ['add', '--all']);
82 | await run('git', ['commit', '-m', `release: v${version}`]);
83 | await run('git', ['tag', `v${version}`]);
84 | }
85 |
86 | function bumpVersion(releaseType: ReleaseType) {
87 | if (releaseType === 'prerelease') {
88 | const identifier = `beta-${Number(new Date())}`;
89 | releaseType = semver.inc(pkg.version, releaseType, identifier) as any;
90 | }
91 | return bumpPrompt({
92 | tag: false,
93 | push: false,
94 | release: releaseType,
95 | files: ['./package.json'],
96 | });
97 | }
98 |
99 | if (args.beta) {
100 | type = 'prerelease';
101 | } else if (args.minor) {
102 | type = 'minor';
103 | } else if (args.major) {
104 | type = 'major';
105 | }
106 |
107 | release(type);
108 |
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import ts from "typescript";
3 | import json from "@rollup/plugin-json";
4 | import cleanup from "rollup-plugin-cleanup";
5 | import replace from "@rollup/plugin-replace";
6 | import commonjs from "@rollup/plugin-commonjs";
7 | import typescript from "rollup-plugin-typescript2";
8 | import { nodeResolve } from "@rollup/plugin-node-resolve";
9 | import pkg from "./package.json" with { type: "json" };
10 |
11 | const { dirname: __dirname } = import.meta;
12 |
13 | const banner =
14 | "/*!\n" +
15 | ` * ${pkg.name}.js\n` +
16 | ` * (c) 2024-${new Date().getFullYear()} Imtaotao\n` +
17 | " * Released under the MIT License.\n" +
18 | " */";
19 |
20 | const outputConfigs = {
21 | cjs: {
22 | format: "cjs",
23 | file: path.resolve(__dirname, "dist/danmu.cjs.js"),
24 | },
25 | "esm-bundler-js": {
26 | format: "es",
27 | file: path.resolve(__dirname, "dist/danmu.esm-bundler.js"),
28 | },
29 | "esm-bundler-mjs": {
30 | format: "es",
31 | file: path.resolve(__dirname, "dist/danmu.esm-bundler.mjs"),
32 | },
33 | umd: {
34 | format: "umd",
35 | file: path.resolve(__dirname, "dist/danmu.umd.js"),
36 | },
37 | };
38 |
39 | if (process.env.BUILD === 'development') {
40 | Object.keys(outputConfigs).forEach(key => {
41 | if (key !== 'esm-bundler-js') {
42 | delete outputConfigs[key];
43 | }
44 | })
45 | }
46 |
47 | const packageConfigs = Object.keys(outputConfigs).map((format) =>
48 | createConfig(format, outputConfigs[format]),
49 | );
50 |
51 | function createConfig(format, output) {
52 | let nodePlugins = [];
53 | const isUmdBuild = /umd/.test(format);
54 | const input = path.resolve(__dirname, "./src/index.ts");
55 | const external =
56 | isUmdBuild || !pkg.dependencies ? [] : Object.keys(pkg.dependencies);
57 |
58 | output.banner = banner;
59 | output.externalLiveBindings = true;
60 | if (isUmdBuild) output.name = "Danmu";
61 |
62 | if (format !== "cjs") {
63 | nodePlugins = [
64 | nodeResolve({ browser: isUmdBuild }),
65 | commonjs({ sourceMap: false }),
66 | ];
67 | }
68 |
69 | return {
70 | input,
71 | output,
72 | external,
73 | plugins: [
74 | cleanup(),
75 | json({
76 | namedExports: false,
77 | }),
78 | typescript({
79 | clean: true, // no cache
80 | typescript: ts,
81 | tsconfig: path.resolve(__dirname, "./tsconfig.json"),
82 | }),
83 | replace({
84 | __TEST__: `false`,
85 | __VERSION__: `'${pkg.version}'`,
86 | preventAssignment: true,
87 | }),
88 | ...nodePlugins,
89 | ],
90 | };
91 | }
92 |
93 | export default packageConfigs;
94 |
--------------------------------------------------------------------------------
/src/__tests__/index.spec.ts:
--------------------------------------------------------------------------------
1 | describe('test', () => {
2 | it('a', async () => {});
3 | });
4 |
--------------------------------------------------------------------------------
/src/container.ts:
--------------------------------------------------------------------------------
1 | import { map, assert } from 'aidly';
2 | import { ids, toNumber } from './utils';
3 | import type { StyleKey, SizeArea, AreaOptions } from './types';
4 |
5 | export class Container {
6 | public width = 0;
7 | public height = 0;
8 | public node: HTMLDivElement;
9 | public parentNode: HTMLElement | null = null;
10 | private _parentWidth = 0;
11 | private _parentHeight = 0;
12 | private _size = {
13 | x: { start: 0, end: '100%' } as SizeArea,
14 | y: { start: 0, end: '100%' } as SizeArea,
15 | };
16 |
17 | public constructor() {
18 | this.node = document.createElement('div');
19 | this.node.setAttribute('data-danmu-container', String(ids.container++));
20 | this.setStyle('overflow', 'hidden');
21 | this.setStyle('position', 'relative');
22 | this.setStyle('top', '0');
23 | this.setStyle('left', '0');
24 | }
25 |
26 | /**
27 | * @internal
28 | */
29 | private _sizeToNumber() {
30 | const size = Object.create(null) as {
31 | x: SizeArea;
32 | y: SizeArea;
33 | };
34 | const transform = (v: string | number, all: number) => {
35 | return typeof v === 'string' ? (v ? toNumber(v, all) : 0) : v;
36 | };
37 | size.x = map(this._size.x, (v) =>
38 | transform(v, this._parentWidth),
39 | ) as SizeArea;
40 | size.y = map(this._size.y, (v) =>
41 | transform(v, this._parentHeight),
42 | ) as SizeArea;
43 | return size;
44 | }
45 |
46 | /**
47 | * @internal
48 | */
49 | public _mount(node: HTMLElement) {
50 | this._unmount();
51 | this.parentNode = node;
52 | this._format();
53 | this.parentNode.appendChild(this.node);
54 | }
55 |
56 | /**
57 | * @internal
58 | */
59 | public _unmount() {
60 | this.parentNode = null;
61 | if (this.node.parentNode) {
62 | this.node.parentNode.removeChild(this.node);
63 | }
64 | }
65 |
66 | /**
67 | * @internal
68 | */
69 | public _updateSize({ x, y }: AreaOptions) {
70 | const isLegal = (v: unknown): v is string | number => {
71 | return typeof v === 'string' || typeof v === 'number';
72 | };
73 | if (x) {
74 | if (isLegal(x.end)) this._size.x.end = x.end;
75 | if (isLegal(x.start)) this._size.x.start = x.start;
76 | }
77 | if (y) {
78 | if (isLegal(y.end)) this._size.y.end = y.end;
79 | if (isLegal(y.start)) this._size.y.start = y.start;
80 | }
81 | }
82 |
83 | /**
84 | * @internal
85 | */
86 | public _toNumber(p: 'height' | 'width', val: number | string) {
87 | let n = typeof val === 'number' ? val : toNumber(val, this[p]);
88 | if (n > this[p]) n = this[p];
89 | assert(!Number.isNaN(n), `${val} is not a number`);
90 | return n;
91 | }
92 |
93 | /**
94 | * @internal
95 | */
96 | public _format() {
97 | if (this.parentNode) {
98 | const styles = getComputedStyle(this.parentNode);
99 | this._parentWidth = Number(styles.width.replace('px', ''));
100 | this._parentHeight = Number(styles.height.replace('px', ''));
101 | }
102 | const { x, y } = this._sizeToNumber();
103 | this.width = x.end - x.start;
104 | this.height = y.end - y.start;
105 | this.setStyle('left', `${x.start}px`);
106 | this.setStyle('top', `${y.start}px`);
107 | this.setStyle('width', `${this.width}px`);
108 | this.setStyle('height', `${this.height}px`);
109 | }
110 |
111 | public setStyle(key: T, val: CSSStyleDeclaration[T]) {
112 | this.node.style[key] = val;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare const __TEST__: boolean;
2 | declare const __VERSION__: string;
3 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'aidly';
2 | import { Manager } from './manager';
3 | import type { CreateOption } from './types';
4 |
5 | const formatOptions = (options?: CreateOption) => {
6 | const newOptions = Object.assign(
7 | {
8 | gap: 0,
9 | rate: 1,
10 | limits: {},
11 | interval: 500,
12 | mode: 'strict',
13 | direction: 'right',
14 | trackHeight: '20%',
15 | durationRange: [4000, 6000],
16 | },
17 | options,
18 | );
19 | assert(newOptions.gap >= 0, 'Gap must be greater than or equal to 0');
20 | if (typeof newOptions.limits.stash !== 'number') {
21 | newOptions.limits.stash = Infinity;
22 | }
23 | return newOptions;
24 | };
25 |
26 | export function create<
27 | T extends unknown,
28 | S extends Record = Record,
29 | >(options?: CreateOption) {
30 | const opts = formatOptions(options);
31 | const manager = new Manager(opts);
32 | if (opts.plugin) {
33 | const plugins = Array.isArray(opts.plugin) ? opts.plugin : [opts.plugin];
34 | for (const plugin of plugins) {
35 | manager.use(plugin);
36 | }
37 | manager.pluginSystem.lifecycle.init.emit(manager);
38 | }
39 | return manager;
40 | }
41 |
42 | export type { Track } from './track';
43 | export type { Manager } from './manager';
44 | export type { HookOn, HooksOn, Plugin, HookType } from 'hooks-plugin';
45 | export type {
46 | Mode,
47 | Speed,
48 | StyleKey,
49 | Position,
50 | PushOptions,
51 | PushFlexOptions,
52 | Location,
53 | ValueType,
54 | Direction,
55 | Danmaku,
56 | DanmakuType,
57 | DanmakuPlugin,
58 | ManagerPlugin,
59 | ManagerOptions,
60 | CreateOption,
61 | } from './types';
62 |
--------------------------------------------------------------------------------
/src/lifeCycle.ts:
--------------------------------------------------------------------------------
1 | import type { Nullable } from 'aidly';
2 | import {
3 | SyncHook,
4 | AsyncHook,
5 | SyncWaterfallHook,
6 | PluginSystem,
7 | } from 'hooks-plugin';
8 | import { ids } from './utils';
9 | import type { Manager } from './manager';
10 | import type {
11 | Danmaku,
12 | DanmakuType,
13 | DanmakuPlugin,
14 | ManagerOptions,
15 | } from './types';
16 |
17 | export function createDanmakuLifeCycle>() {
18 | return new PluginSystem({
19 | hide: new SyncHook<[T]>(),
20 | show: new SyncHook<[T]>(),
21 | pause: new SyncHook<[T]>(),
22 | resume: new SyncHook<[T]>(),
23 | beforeMove: new SyncHook<[T]>(),
24 | moved: new SyncHook<[T]>(),
25 | createNode: new SyncHook<[T, HTMLElement]>(),
26 | appendNode: new SyncHook<[T, HTMLElement]>(),
27 | removeNode: new SyncHook<[T, HTMLElement]>(),
28 | beforeDestroy: new AsyncHook<[T, unknown]>(),
29 | destroyed: new SyncHook<[T, unknown]>(),
30 | });
31 | }
32 |
33 | export function createManagerLifeCycle() {
34 | const { lifecycle } = createDanmakuLifeCycle>();
35 | return new PluginSystem({
36 | // Danmaku hooks
37 | $show: lifecycle.show,
38 | $hide: lifecycle.hide,
39 | $pause: lifecycle.pause,
40 | $resume: lifecycle.resume,
41 | $beforeMove: lifecycle.beforeMove,
42 | $moved: lifecycle.moved,
43 | $createNode: lifecycle.createNode,
44 | $appendNode: lifecycle.appendNode,
45 | $removeNode: lifecycle.removeNode,
46 | $beforeDestroy: lifecycle.beforeDestroy,
47 | $destroyed: lifecycle.destroyed,
48 | // Global hooks
49 | format: new SyncHook<[]>(),
50 | start: new SyncHook<[]>(),
51 | stop: new SyncHook<[]>(),
52 | show: new SyncHook<[]>(),
53 | hide: new SyncHook<[]>(),
54 | freeze: new SyncHook<[]>(),
55 | unfreeze: new SyncHook<[]>(),
56 | finished: new SyncHook<[]>(),
57 | clear: new SyncHook<[Nullable]>(),
58 | mount: new SyncHook<[HTMLElement]>(),
59 | unmount: new SyncHook<[HTMLElement | null]>(),
60 | init: new SyncHook<[manager: Manager]>(),
61 | limitWarning: new SyncHook<[DanmakuType, number]>(),
62 | push: new SyncHook<[T | Danmaku, DanmakuType, boolean]>(),
63 | render: new SyncHook<[DanmakuType]>(),
64 | updateOptions: new SyncHook<
65 | [Partial, Nullable]
66 | >(),
67 | willRender: new SyncWaterfallHook<{
68 | type: DanmakuType;
69 | prevent: boolean;
70 | danmaku: Danmaku;
71 | trackIndex: null | number;
72 | }>(),
73 | });
74 | }
75 |
76 | const scope = '$';
77 | const cache = [] as Array<[string, string]>;
78 |
79 | export function createDanmakuPlugin(
80 | plSys: Manager['pluginSystem'],
81 | ): DanmakuPlugin {
82 | const plugin = {
83 | name: `__danmaku_plugin_${ids.bridge++}__`,
84 | } as Record;
85 |
86 | if (cache.length) {
87 | for (const [k, nk] of cache) {
88 | plugin[nk] = (...args: Array) => {
89 | return (plSys.lifecycle as any)[k].emit(...args);
90 | };
91 | }
92 | } else {
93 | const keys = Object.keys(plSys.lifecycle);
94 | for (const k of keys) {
95 | if (k.startsWith(scope)) {
96 | const nk = k.replace(scope, '');
97 | cache.push([k, nk]);
98 | plugin[nk] = (...args: Array) => {
99 | return (plSys.lifecycle as any)[k].emit(...args);
100 | };
101 | }
102 | }
103 | }
104 | return plugin;
105 | }
106 |
--------------------------------------------------------------------------------
/src/track.ts:
--------------------------------------------------------------------------------
1 | import { remove } from 'aidly';
2 | import type { Location } from './types';
3 | import type { Container } from './container';
4 | import type { FacileDanmaku } from './danmaku/facile';
5 |
6 | export interface TrackOptions {
7 | index: number;
8 | location: Location;
9 | container: Container;
10 | list: Array>;
11 | }
12 |
13 | export class Track {
14 | private _container: Container;
15 | public isLock = false;
16 | public index: number;
17 | public location: Location;
18 | public list: Array>;
19 |
20 | public constructor({ index, list, location, container }: TrackOptions) {
21 | this.list = list;
22 | this.index = index;
23 | this.location = location;
24 | this._container = container;
25 | }
26 |
27 | public get width() {
28 | return this._container.width;
29 | }
30 |
31 | public get height() {
32 | return this.location.bottom - this.location.top;
33 | }
34 |
35 | // We have to make a copy.
36 | // During the loop, there are too many factors that change danmaku,
37 | // which makes it impossible to guarantee the stability of the list.
38 | public each(fn: (danmaku: FacileDanmaku) => unknown | boolean) {
39 | for (const dm of Array.from(this.list)) {
40 | if (fn(dm) === false) break;
41 | }
42 | }
43 |
44 | public lock() {
45 | this.isLock = true;
46 | }
47 |
48 | public unlock() {
49 | this.isLock = false;
50 | }
51 |
52 | public clear() {
53 | this.each((dm) => dm.destroy());
54 | }
55 |
56 | /**
57 | * @internal
58 | */
59 | public _add(dm: FacileDanmaku) {
60 | this.list.push(dm);
61 | }
62 |
63 | /**
64 | * @internal
65 | */
66 | public _remove(dm: FacileDanmaku) {
67 | remove(this.list, dm);
68 | }
69 |
70 | /**
71 | * @internal
72 | */
73 | public _updateLocation(location: Location) {
74 | this.location = location;
75 | }
76 |
77 | /**
78 | * @internal
79 | */
80 | public _last(li: number) {
81 | for (let i = this.list.length - 1; i >= 0; i--) {
82 | const dm = this.list[i - li];
83 | if (dm && !dm.paused && dm.loops === 0 && dm.type === 'facile') {
84 | return dm;
85 | }
86 | }
87 | return null;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { Nullable } from 'aidly';
2 | import type { HooksOn, RefinePlugin } from 'hooks-plugin';
3 | import type { Manager } from './manager';
4 | import type { Container } from './container';
5 | import type { FacileDanmaku } from './danmaku/facile';
6 | import type { FlexibleDanmaku } from './danmaku/flexible';
7 |
8 | export type DanmakuType = 'facile' | 'flexible';
9 |
10 | export type Direction = 'left' | 'right' | 'none';
11 |
12 | export type Mode = 'none' | 'strict' | 'adaptive';
13 |
14 | export type Speed = Nullable;
15 |
16 | export type Layer = StashData | FacileDanmaku;
17 |
18 | export type Danmaku = FacileDanmaku | FlexibleDanmaku;
19 |
20 | export type StyleKey = keyof Omit;
21 |
22 | export type FilterCallback = EachCallback;
23 |
24 | export type EachCallback = (
25 | danmaku: FacileDanmaku | FlexibleDanmaku,
26 | ) => boolean | void;
27 |
28 | export type ValueType> = Exclude<
29 | Parameters[0],
30 | FacileDanmaku
31 | >;
32 |
33 | export type ManagerPlugin = RefinePlugin<
34 | Manager['pluginSystem']['lifecycle']
35 | >;
36 |
37 | export type DanmakuPlugin = RefinePlugin<
38 | FacileDanmaku['pluginSystem']['lifecycle']
39 | >;
40 |
41 | export type InternalStatuses = {
42 | freeze: boolean;
43 | viewStatus: 'hide' | 'show';
44 | styles: Record;
45 | };
46 |
47 | export type PushFlexOptions = Omit, 'direction'> & {
48 | direction?: Direction;
49 | position:
50 | | Position
51 | | ((
52 | danmaku: Danmaku,
53 | container: Container,
54 | ) => Position);
55 | };
56 |
57 | export interface PushOptions {
58 | speed?: Speed;
59 | duration?: number;
60 | plugin?: DanmakuPlugin;
61 | rate?: ManagerOptions['rate'];
62 | direction?: ManagerOptions['direction'];
63 | }
64 |
65 | export interface EngineOptions {
66 | mode: Mode;
67 | rate: number;
68 | gap: number | string;
69 | speed?: Speed;
70 | trackHeight: number | string;
71 | durationRange: [number, number];
72 | direction: Exclude;
73 | limits: {
74 | view?: number;
75 | stash: number;
76 | };
77 | }
78 |
79 | export interface ManagerOptions extends EngineOptions {
80 | interval: number;
81 | }
82 |
83 | export interface Location {
84 | top: number;
85 | middle: number;
86 | bottom: number;
87 | }
88 |
89 | export interface Position {
90 | x: T;
91 | y: T;
92 | }
93 |
94 | export interface MoveTimer {
95 | cb: () => void;
96 | clear: () => void;
97 | }
98 |
99 | export interface SizeArea {
100 | start: T;
101 | end: T;
102 | }
103 |
104 | export interface AreaOptions {
105 | x?: Partial>;
106 | y?: Partial>;
107 | }
108 |
109 | export interface FreezeOptions {
110 | preventEvents?: Array<'pause' | 'stop' | 'resume' | 'start' | (string & {})>;
111 | }
112 |
113 | export interface StashData {
114 | data: T;
115 | options: Required>;
116 | }
117 |
118 | export interface InfoRecord {
119 | pauseTime: number;
120 | startTime: number;
121 | prevPauseTime: number;
122 | }
123 |
124 | export interface RenderOptions {
125 | statuses: InternalStatuses;
126 | danmakuPlugin: DanmakuPlugin;
127 | hooks: HooksOn<
128 | Manager['pluginSystem'],
129 | ['render', 'finished', 'willRender']
130 | >;
131 | }
132 |
133 | export interface CreateOption extends Partial {
134 | plugin?: ManagerPlugin | Array>;
135 | }
136 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { raf, once, mathExprEvaluate } from 'aidly';
2 |
3 | export const INTERNAL_FLAG = Symbol();
4 |
5 | export const ids = {
6 | danmu: 1,
7 | bridge: 1,
8 | runtime: 1,
9 | container: 1,
10 | };
11 |
12 | export const nextFrame = (fn: FrameRequestCallback) => raf(() => raf(fn));
13 |
14 | export const randomIdx = (founds: Set, rows: number): number => {
15 | const n = Math.floor(Math.random() * rows);
16 | return founds.has(n) ? randomIdx(founds, rows) : n;
17 | };
18 |
19 | export const toNumber = (val: string, all: number) => {
20 | return mathExprEvaluate(val, {
21 | units: {
22 | px: (n) => n,
23 | '%': (n) => (Number(n) / 100) * all,
24 | },
25 | });
26 | };
27 |
28 | export const whenTransitionEnds = (node: HTMLElement) => {
29 | return new Promise((resolve) => {
30 | const onEnd = once(() => {
31 | node.removeEventListener('transitionend', onEnd);
32 | resolve();
33 | });
34 | node.addEventListener('transitionend', onEnd);
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "outDir": "dist",
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "strict": true,
8 | "allowJs": true,
9 | "sourceMap": false,
10 | "declaration": true,
11 | "noImplicitAny": true,
12 | "esModuleInterop": true,
13 | "lib": ["DOM", "ESNext"],
14 | "moduleResolution": "Node"
15 | },
16 | "include": ["src"]
17 | }
18 |
--------------------------------------------------------------------------------