diff --git a/.env.example b/.env.example
index 2d60775..7ac7db7 100644
--- a/.env.example
+++ b/.env.example
@@ -17,4 +17,11 @@ META_DESCRIPTION=Welcome to DrawPlace! Collaborate with art enthusiasts to creat
DRAW_DELAY_MS=5000
DRAW_MAX_POINTS=24
CANVAS_WIDTH=1534
-CANVAS_HEIGHT=1009
\ No newline at end of file
+CANVAS_HEIGHT=1009
+
+# Danmaku
+DANMAKU_ACTIVITY_ID=your_activity
+DANMAKU_TOKEN_NAME=your_token_name
+DANMAKU_TOKEN=your_custom_token
+DANMAKU_ROOT_PATH=https://danmaku.geekpie.club
+DANMAKU_SOCKET_PATH=/socket.io
diff --git a/components/Board.tsx b/components/Board.tsx
index ed62373..199488b 100644
--- a/components/Board.tsx
+++ b/components/Board.tsx
@@ -30,6 +30,7 @@ import { Button } from "./ui/button";
import { cn } from "@/lib/utils";
import AnnounceModal from "./AnnounceModal";
import { parseInitData } from "@/lib/binary-parser";
+import DanmakuPlayer from "./Danmaku";
const Board = () => {
const { config } = useRuntimeConfigContext();
@@ -496,118 +497,120 @@ const Board = () => {
{/* Canvas Area - Flexible */}
-
-
setRatio(e.state.scale)}
- doubleClick={{ disabled: true }}
- >
-
- 1 ? "pixelated" : "auto",
+
+
+
setRatio(e.state.scale)}
+ doubleClick={{ disabled: true }}
+ >
+
+ 1 ? "pixelated" : "auto",
+ }}
+ >
+
+
+
+
setShowLoginModal(false)}
+ />
+ {
+ updateSettingsConfig({ showGuideOnLoad: false });
+ setShowGuideModal(false);
+ }}
+ onClose={() => {
+ updateSettingsConfig({ showGuideOnLoad: false });
+ setShowGuideModal(false);
+ }}
+ />
+ {
+ updateSettingsConfig({
+ announcementVersion: process.env.NEXT_PUBLIC_APP_VERSION || "",
+ });
+ setShowAnnounce(false);
+ }}
+ onClose={() => {
+ updateSettingsConfig({
+ announcementVersion: process.env.NEXT_PUBLIC_APP_VERSION || "",
+ });
+ setShowAnnounce(false);
+ }}
+ />
+
+
-
-
- setShowLoginModal(false)}
- />
- {
- updateSettingsConfig({ showGuideOnLoad: false });
- setShowGuideModal(false);
- }}
- onClose={() => {
- updateSettingsConfig({ showGuideOnLoad: false });
- setShowGuideModal(false);
- }}
- />
- {
- updateSettingsConfig({
- announcementVersion: process.env.NEXT_PUBLIC_APP_VERSION || "",
- });
- setShowAnnounce(false);
- }}
- onClose={() => {
- updateSettingsConfig({
- announcementVersion: process.env.NEXT_PUBLIC_APP_VERSION || "",
- });
- setShowAnnounce(false);
- }}
- />
-
-
+ {editable ? (
+ 绘制模式
+ ) : (
+ 浏览模式
+ )}
+
+
-
+
{/* Bottom Control Area */}
diff --git a/components/Danmaku.tsx b/components/Danmaku.tsx
new file mode 100644
index 0000000..31725b2
--- /dev/null
+++ b/components/Danmaku.tsx
@@ -0,0 +1,167 @@
+"use client";
+
+import React, { useEffect, useRef } from 'react';
+import { CommentManager } from '@wiidede/comment-core-library';
+import io, { Socket } from 'socket.io-client';
+import 'comment-core-library/dist/css/style.css';
+import { useSettingsConfigContext } from "./SettingsProvider";
+import { useRuntimeConfigContext } from "./RuntimeConfigProvider";
+import { toast } from 'sonner';
+
+export interface RawDanmakuData {
+ id?: number;
+ text: string;
+ mode: number;
+ size: number;
+ color: number;
+ time: number;
+ dur?: number;
+ addons?: Record
;
+}
+
+interface DanmakuPlayerProps {
+ children: React.ReactNode;
+ activityId?: string;
+ tokenName?: string;
+ token?: string;
+ rootPath?: string;
+ socketPath?: string;
+}
+
+const DanmakuPlayer = ({
+ children,
+}: { children: React.ReactNode }) => {
+ const stageRef = useRef(null);
+ const cmRef = useRef(null);
+ const socketRef = useRef(null);
+ const { config, updateStatus } = useSettingsConfigContext();
+ const { config: runtimeConfig } = useRuntimeConfigContext();
+
+ const activityId = runtimeConfig.DANMAKU_ACTIVITY_ID;
+ const tokenName = runtimeConfig.DANMAKU_TOKEN_NAME;
+ const token = runtimeConfig.DANMAKU_TOKEN;
+ const rootPath = runtimeConfig.DANMAKU_ROOT_PATH;
+ const socketPath = runtimeConfig.DANMAKU_SOCKET_PATH;
+
+ useEffect(() => {
+ if (!stageRef.current) return;
+
+ const cm = new CommentManager(stageRef.current);
+ cm.init();
+ cm.start();
+
+ // 动态调整弹幕缩放比例,基于屏幕宽度
+ // cm.options.scroll.scale = Math.round(document.body.clientWidth / 500);
+
+ cm.setBounds();
+ cmRef.current = cm;
+
+ const handleResize = () => cm.setBounds();
+ window.addEventListener('resize', handleResize);
+
+ return () => {
+ cm.stop();
+ window.removeEventListener('resize', handleResize);
+ };
+ }, []);
+
+ // Socket connection
+ useEffect(() => {
+ if (!activityId || !rootPath) {
+ return;
+ }
+
+ // 注意:Namespace 是 /danmaku
+ const socket = io(`${rootPath}/danmaku`, {
+ path: socketPath,
+ query: {
+ activity: activityId,
+ tokenName: tokenName,
+ token: token,
+ },
+ transports: ['websocket', 'polling']
+ });
+
+ socket.on('connect', () => {
+ console.log(`已连接到弹幕服务器 (Activity: ${activityId})`);
+ // toast.info("已连接到弹幕服务器");
+ updateStatus({ isDanmakuConnected: true });
+ });
+
+ socket.on('disconnect', () => {
+ console.log('与弹幕服务器断开连接');
+ toast.warning("与弹幕服务器断开连接");
+ updateStatus({ isDanmakuConnected: false });
+ });
+
+ socket.on('danmaku', (data: RawDanmakuData) => {
+ console.log('收到弹幕:', data);
+ if (cmRef.current) {
+ cmRef.current.send({
+ text: data.text,
+ mode: data.mode,
+ size: data.size,
+ color: data.color,
+ dur: data.dur * 2,
+ stime: data.time,
+ shadow: data.color === 0xffffff,
+ ...data.addons
+ });
+ }
+ });
+
+ socketRef.current = socket;
+
+ return () => {
+ if (socketRef.current) {
+ socketRef.current.disconnect();
+ updateStatus({ isDanmakuConnected: false });
+ }
+ };
+ }, [activityId, tokenName, token, rootPath, socketPath]);
+
+ const sendTestComment = () => {
+ if (cmRef.current) {
+ cmRef.current.send({
+ text: "这是一条测试弹幕 " + Math.random().toFixed(2),
+ mode: 1, // 滚动模式
+ size: 25,
+ color: 0xffffff,
+ dur: 4000
+ });
+ }
+ };
+
+ return (
+
+
+ {children}
+ {/*
*/}
+
+ );
+};
+
+export default DanmakuPlayer;
diff --git a/components/Dock.tsx b/components/Dock.tsx
index afd86ed..7738779 100644
--- a/components/Dock.tsx
+++ b/components/Dock.tsx
@@ -12,6 +12,8 @@ import {
PaintRoller,
Palette,
Settings,
+ MessageSquare,
+ MessageSquareOff,
} from "lucide-react";
import { Input } from "./ui/input";
import {
@@ -50,8 +52,12 @@ const Dock = ({
handleDraw,
}) => {
const { config } = useRuntimeConfigContext();
- const { status: statusConfig, updateStatus: updateStatusConfig } =
- useSettingsConfigContext();
+ const {
+ config: settingsConfig,
+ updateConfig: updateSettingsConfig,
+ status: statusConfig,
+ updateStatus: updateStatusConfig,
+ } = useSettingsConfigContext();
if (!dataSource) return null;
const [token, setToken] = useState("");
const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -142,6 +148,25 @@ const Dock = ({
)}
+
+