'use client'; import React, { useEffect, useRef, useState, useCallback } from 'react'; interface DanmakuData { time: number; text: string; [key: string]: any; } interface CustomHeatmapProps { danmakuList: DanmakuData[]; duration: number; currentTime: number; enabled: boolean; onSeek?: (time: number) => void; className?: string; } const CustomHeatmap: React.FC = ({ danmakuList, duration, currentTime, enabled, onSeek, className = '', }) => { const canvasRef = useRef(null); const containerRef = useRef(null); const [heatmapData, setHeatmapData] = useState([]); const [isHovering, setIsHovering] = useState(false); const [hoverTime, setHoverTime] = useState(0); // 计算热力图数据 const calculateHeatmapData = useCallback(() => { if (!duration || duration <= 0 || danmakuList.length === 0) { return []; } // 将视频时长分成若干个时间段(每秒一个) const segments = Math.ceil(duration); const heatData = new Array(segments).fill(0); // 统计每个时间段的弹幕数量 danmakuList.forEach((danmaku) => { const segmentIndex = Math.floor(danmaku.time); if (segmentIndex >= 0 && segmentIndex < segments) { heatData[segmentIndex]++; } }); // 归一化数据到 0-1 范围 const maxCount = Math.max(...heatData, 1); return heatData.map((count) => count / maxCount); }, [danmakuList, duration]); // 当弹幕列表或时长变化时重新计算热力图数据 useEffect(() => { const data = calculateHeatmapData(); setHeatmapData(data); }, [calculateHeatmapData]); // 绘制热力图 useEffect(() => { const canvas = canvasRef.current; if (!canvas || heatmapData.length === 0) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const width = canvas.width; const height = canvas.height; // 清空画布 ctx.clearRect(0, 0, width, height); // 计算每个柱子的宽度 const barWidth = width / heatmapData.length; const progressRatio = duration > 0 ? currentTime / duration : 0; // 绘制热力图柱状图 heatmapData.forEach((value, index) => { const x = index * barWidth; const barHeight = value * height; const y = height - barHeight; // 判断是否已播放 const isPlayed = (index / heatmapData.length) <= progressRatio; // 使用灰色透明,已播放的部分深色一点 const opacity = isPlayed ? 0.5 + value * 0.3 : 0.2 + value * 0.3; const color = `rgba(128, 128, 128, ${opacity})`; ctx.fillStyle = color; ctx.fillRect(x, y, Math.ceil(barWidth) + 1, barHeight); }); // 绘制当前播放位置指示器 if (duration > 0) { const progressX = (currentTime / duration) * width; ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; ctx.fillRect(progressX - 1, 0, 2, height); } }, [heatmapData, currentTime, duration]); // 处理鼠标移动 const handleMouseMove = (e: React.MouseEvent) => { const container = containerRef.current; if (!container || !duration) return; const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const percentage = x / rect.width; const time = percentage * duration; setHoverTime(time); setIsHovering(true); }; // 处理鼠标离开 const handleMouseLeave = () => { setIsHovering(false); }; // 处理点击跳转 const handleClick = (e: React.MouseEvent) => { const container = containerRef.current; if (!container || !duration || !onSeek) return; const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const percentage = x / rect.width; const time = percentage * duration; onSeek(time); }; // 格式化时间显示 const formatTime = (seconds: number): string => { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) { return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } return `${m}:${s.toString().padStart(2, '0')}`; }; // 获取悬停位置的弹幕密度 const getHoverDensity = (): string => { if (!isHovering || heatmapData.length === 0) return ''; const segmentIndex = Math.floor(hoverTime); if (segmentIndex >= 0 && segmentIndex < heatmapData.length) { const density = heatmapData[segmentIndex]; if (density < 0.2) return '低'; if (density < 0.5) return '中'; if (density < 0.8) return '高'; return '极高'; } return ''; }; if (!enabled) { return null; } return (
{/* 悬停提示 */} {isHovering && (
{formatTime(hoverTime)} - 弹幕密度: {getHoverDensity()}
)}
); }; export default CustomHeatmap;