import { useEffect, useRef } from 'react'; import styles from '../styles.module.css'; interface Props { realtime: any; overview: any; } interface Particle { x: number; y: number; progress: number; speed: number; pathIndex: number; } export default function EnergyFlowDiagram({ realtime, overview }: Props) { const canvasRef = useRef(null); const containerRef = useRef(null); const particlesRef = useRef([]); const rafRef = useRef(0); const gridPower = realtime?.grid_power ?? 0; const pvPower = realtime?.pv_power ?? 0; const totalPower = realtime?.total_power ?? 0; const hpPower = realtime?.heatpump_power ?? 0; useEffect(() => { const container = containerRef.current; const canvas = canvasRef.current; if (!container || !canvas) return; const resizeObserver = new ResizeObserver(() => { const rect = container.getBoundingClientRect(); canvas.width = rect.width * window.devicePixelRatio; canvas.height = rect.height * window.devicePixelRatio; canvas.style.width = rect.width + 'px'; canvas.style.height = rect.height + 'px'; }); resizeObserver.observe(container); // Initialize particles particlesRef.current = []; for (let i = 0; i < 60; i++) { particlesRef.current.push({ x: 0, y: 0, progress: Math.random(), speed: 0.002 + Math.random() * 0.003, pathIndex: Math.floor(Math.random() * 4), }); } const animate = () => { const ctx = canvas.getContext('2d'); if (!ctx) return; const dpr = window.devicePixelRatio; const w = canvas.width / dpr; const h = canvas.height / dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, w, h); // Node positions const nodes = { grid: { x: w * 0.12, y: h * 0.32, label: '电网', color: '#ff8c00' }, pv: { x: w * 0.12, y: h * 0.68, label: '光伏', color: '#00ff88' }, building: { x: w * 0.5, y: h * 0.5, label: '建筑负载', color: '#00d4ff' }, heatpump: { x: w * 0.85, y: h * 0.38, label: '热泵', color: '#00d4ff' }, heating: { x: w * 0.85, y: h * 0.68, label: '供暖', color: '#ff4757' }, }; // Define paths: [from, to] const paths = [ { from: nodes.grid, to: nodes.building, color: '#ff8c00', value: gridPower }, { from: nodes.pv, to: nodes.building, color: '#00ff88', value: pvPower }, { from: nodes.building, to: nodes.heatpump, color: '#00d4ff', value: hpPower }, { from: nodes.heatpump, to: nodes.heating, color: '#ff4757', value: hpPower * 3.5 }, ]; // Draw paths paths.forEach((path) => { ctx.beginPath(); ctx.moveTo(path.from.x, path.from.y); // Bezier curve const mx = (path.from.x + path.to.x) / 2; ctx.bezierCurveTo(mx, path.from.y, mx, path.to.y, path.to.x, path.to.y); ctx.strokeStyle = path.color + '30'; ctx.lineWidth = 3; ctx.stroke(); }); // Animate particles particlesRef.current.forEach((p) => { p.progress += p.speed; if (p.progress > 1) { p.progress = 0; p.pathIndex = Math.floor(Math.random() * paths.length); } const path = paths[p.pathIndex]; if (!path) return; const t = p.progress; const mx = (path.from.x + path.to.x) / 2; // Cubic bezier interpolation const u = 1 - t; const x = u * u * u * path.from.x + 3 * u * u * t * mx + 3 * u * t * t * mx + t * t * t * path.to.x; const y = u * u * u * path.from.y + 3 * u * u * t * path.from.y + 3 * u * t * t * path.to.y + t * t * t * path.to.y; const alpha = t < 0.1 ? t / 0.1 : t > 0.9 ? (1 - t) / 0.1 : 1; ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fillStyle = path.color; ctx.globalAlpha = alpha * 0.9; ctx.fill(); // Glow ctx.beginPath(); ctx.arc(x, y, 8, 0, Math.PI * 2); ctx.fillStyle = path.color; ctx.globalAlpha = alpha * 0.2; ctx.fill(); ctx.globalAlpha = 1; }); // Draw nodes Object.values(nodes).forEach((node) => { // Node bg ctx.beginPath(); const rw = 60, rh = 36, r = 8; const nx = node.x - rw, ny = node.y - rh; const nw = rw * 2, nh = rh * 2; ctx.moveTo(nx + r, ny); ctx.lineTo(nx + nw - r, ny); ctx.quadraticCurveTo(nx + nw, ny, nx + nw, ny + r); ctx.lineTo(nx + nw, ny + nh - r); ctx.quadraticCurveTo(nx + nw, ny + nh, nx + nw - r, ny + nh); ctx.lineTo(nx + r, ny + nh); ctx.quadraticCurveTo(nx, ny + nh, nx, ny + nh - r); ctx.lineTo(nx, ny + r); ctx.quadraticCurveTo(nx, ny, nx + r, ny); ctx.closePath(); ctx.fillStyle = 'rgba(6, 30, 62, 0.95)'; ctx.fill(); ctx.strokeStyle = node.color + '66'; ctx.lineWidth = 1.5; ctx.stroke(); // Shadow glow ctx.shadowColor = node.color; ctx.shadowBlur = 12; ctx.strokeStyle = node.color + '33'; ctx.stroke(); ctx.shadowBlur = 0; // Label ctx.fillStyle = 'rgba(224, 232, 240, 0.7)'; ctx.font = '12px system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(node.label, node.x, node.y - 8); // Value let val = '0 kW'; if (node.label === '电网') val = gridPower.toFixed(1) + ' kW'; else if (node.label === '光伏') val = pvPower.toFixed(1) + ' kW'; else if (node.label === '建筑负载') val = totalPower.toFixed(1) + ' kW'; else if (node.label === '热泵') val = hpPower.toFixed(1) + ' kW'; else if (node.label === '供暖') val = (hpPower * 3.5).toFixed(1) + ' kW'; ctx.fillStyle = node.color; ctx.font = 'bold 15px system-ui, sans-serif'; ctx.fillText(val, node.x, node.y + 12); }); rafRef.current = requestAnimationFrame(animate); }; rafRef.current = requestAnimationFrame(animate); return () => { cancelAnimationFrame(rafRef.current); resizeObserver.disconnect(); }; }, [gridPower, pvPower, totalPower, hpPower]); return (
); }