From 53a9380d7ae5e5625943b69a19ffc06e95902eb4 Mon Sep 17 00:00:00 2001 From: xiaopeng <1509442308@qq.com> Date: Fri, 22 May 2026 14:25:53 +0800 Subject: [PATCH] style: redesign knowledge graph to light academic theme Switch from dark p5-style background to light slate theme with softer indigo particles, white label pills, and reduced glow effects for better readability in academic context. Co-Authored-By: Claude Opus 4.7 --- web/src/app/(main)/course-content/page.tsx | 2 +- .../course-content/knowledge-graph.tsx | 156 +++++++----------- 2 files changed, 59 insertions(+), 99 deletions(-) diff --git a/web/src/app/(main)/course-content/page.tsx b/web/src/app/(main)/course-content/page.tsx index 0b92cfc..7117267 100644 --- a/web/src/app/(main)/course-content/page.tsx +++ b/web/src/app/(main)/course-content/page.tsx @@ -150,7 +150,7 @@ export default function CourseContentPage() { <> {/* 知识图谱视图 */} {viewMode === 'graph' && ( -
+
)} diff --git a/web/src/components/course-content/knowledge-graph.tsx b/web/src/components/course-content/knowledge-graph.tsx index 2db3428..872c9d1 100644 --- a/web/src/components/course-content/knowledge-graph.tsx +++ b/web/src/components/course-content/knowledge-graph.tsx @@ -40,15 +40,15 @@ interface KnowledgeGraphProps { } const CHAPTER_COLORS = [ - '#a78bfa', - '#22d3ee', - '#fbbf24', - '#34d399', - '#f472b6', - '#60a5fa', + '#6366f1', + '#0ea5e9', + '#8b5cf6', + '#14b8a6', + '#ec4899', + '#f59e0b', ]; -const ROOT_COLOR = '#c4b5fd'; +const ROOT_COLOR = '#4f46e5'; export default function KnowledgeGraph({ bookStructure, onNodeClick }: KnowledgeGraphProps) { const graphRef = useRef(null); @@ -162,7 +162,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge return { nodes, links }; }, [bookStructure]); - // p5-style ambient particle background + // Soft ambient background matching the site's light academic theme useEffect(() => { const canvas = particleCanvasRef.current; if (!canvas) return; @@ -180,16 +180,15 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge if (!ctx) return; ctx.scale(dpr, dpr); - const particleCount = 40; - const connectDist = 130; + const particleCount = 30; + const connectDist = 150; const particles = Array.from({ length: particleCount }, () => ({ x: Math.random() * W, y: Math.random() * H, - vx: (Math.random() - 0.5) * 0.2, - vy: (Math.random() - 0.5) * 0.2, - size: Math.random() * 1.5 + 0.5, - hue: 210 + Math.random() * 60, - alpha: Math.random() * 0.35 + 0.1, + vx: (Math.random() - 0.5) * 0.15, + vy: (Math.random() - 0.5) * 0.15, + size: Math.random() * 1.2 + 0.4, + alpha: Math.random() * 0.2 + 0.05, phase: Math.random() * Math.PI * 2, })); @@ -197,21 +196,22 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge let time = 0; const animate = () => { - time += 0.01; - ctx.fillStyle = '#0a0a1a'; + time += 0.008; + // Light background matching site theme + ctx.fillStyle = '#f8fafc'; ctx.fillRect(0, 0, W, H); // update positions for (const p of particles) { - p.x += p.vx + Math.sin(time + p.phase) * 0.05; - p.y += p.vy + Math.cos(time + p.phase) * 0.05; + p.x += p.vx + Math.sin(time + p.phase) * 0.03; + p.y += p.vy + Math.cos(time + p.phase) * 0.03; if (p.x < 0 || p.x > W) p.vx *= -1; if (p.y < 0 || p.y > H) p.vy *= -1; p.x = Math.max(0, Math.min(W, p.x)); p.y = Math.max(0, Math.min(H, p.y)); } - // draw connections + // draw connections — subtle indigo lines for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; @@ -222,28 +222,18 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); - ctx.strokeStyle = `rgba(120, 140, 220, ${a})`; + ctx.strokeStyle = `rgba(99, 102, 241, ${a})`; ctx.lineWidth = 0.5; ctx.stroke(); } } } - // draw particles + // draw particles — soft indigo dots for (const p of particles) { - const pulse = Math.sin(time * 2 + p.phase) * 0.3 + 0.7; + const pulse = Math.sin(time * 1.5 + p.phase) * 0.2 + 0.8; - // glow - const grd = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 6); - grd.addColorStop(0, `hsla(${p.hue}, 60%, 60%, ${p.alpha * 0.3 * pulse})`); - grd.addColorStop(1, `hsla(${p.hue}, 60%, 60%, 0)`); - ctx.fillStyle = grd; - ctx.beginPath(); - ctx.arc(p.x, p.y, p.size * 6, 0, Math.PI * 2); - ctx.fill(); - - // core - ctx.fillStyle = `hsla(${p.hue}, 60%, 70%, ${p.alpha * pulse})`; + ctx.fillStyle = `rgba(99, 102, 241, ${p.alpha * pulse})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); @@ -301,7 +291,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge node._fullLabel || node.name} nodeVal={(node: any) => node.val || 8} nodeRelSize={6} @@ -329,51 +319,35 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => { if (!isFinite(node.x) || !isFinite(node.y)) return; const label = node.name; - const color = node.color || '#a78bfa'; + const color = node.color || '#6366f1'; const nodeRadius = Math.max(3, (node.val || 8) * 0.6); - // 1. outer glow halo - const glowR = nodeRadius * 8; - const grd = ctx.createRadialGradient( - node.x, node.y, 0, - node.x, node.y, glowR - ); - grd.addColorStop(0, color + '25'); - grd.addColorStop(0.4, color + '0c'); - grd.addColorStop(1, color + '00'); - ctx.fillStyle = grd; + // 1. subtle shadow + ctx.fillStyle = 'rgba(99, 102, 241, 0.08)'; ctx.beginPath(); - ctx.arc(node.x, node.y, glowR, 0, Math.PI * 2); + ctx.arc(node.x, node.y, nodeRadius * 2.5, 0, Math.PI * 2); ctx.fill(); - // 2. inner glow - const innerR = nodeRadius * 3; - const grd2 = ctx.createRadialGradient( - node.x, node.y, 0, - node.x, node.y, innerR - ); - grd2.addColorStop(0, color + '70'); - grd2.addColorStop(1, color + '00'); - ctx.fillStyle = grd2; + // 2. soft outer ring ctx.beginPath(); - ctx.arc(node.x, node.y, innerR, 0, Math.PI * 2); + ctx.arc(node.x, node.y, nodeRadius * 1.4, 0, Math.PI * 2); + ctx.fillStyle = color + '18'; ctx.fill(); - // 3. core + // 3. core circle ctx.beginPath(); ctx.arc(node.x, node.y, nodeRadius, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); - // 4. bright center spot - ctx.beginPath(); - ctx.arc(node.x, node.y, nodeRadius * 0.3, 0, Math.PI * 2); - ctx.fillStyle = 'rgba(255,255,255,0.75)'; - ctx.fill(); + // 4. white border for definition + ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.lineWidth = Math.max(0.5, 1.2 / globalScale); + ctx.stroke(); // 5. label - const fontSize = Math.max(9, 13 / globalScale); - ctx.font = `600 ${fontSize}px "Noto Serif SC", Georgia, serif`; + const fontSize = Math.max(9, 12 / globalScale); + ctx.font = `500 ${fontSize}px "Noto Serif SC", Georgia, serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; @@ -381,16 +355,16 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge const pad = 4 / globalScale; const labelY = node.y + nodeRadius + fontSize / 2 + pad * 3; - // label background pill - const bw = tw + pad * 3; - const bh = fontSize + pad * 2; - const r = Math.max(2, 3 / globalScale); + // label background pill — light style + const bw = tw + pad * 4; + const bh = fontSize + pad * 2.5; + const r = Math.max(2, 4 / globalScale); const lx = node.x - bw / 2; const ly = labelY - bh / 2; - ctx.fillStyle = 'rgba(10, 10, 26, 0.85)'; - ctx.strokeStyle = color + '40'; - ctx.lineWidth = Math.max(0.5, 0.8 / globalScale); + ctx.fillStyle = 'rgba(255, 255, 255, 0.92)'; + ctx.strokeStyle = color + '30'; + ctx.lineWidth = Math.max(0.3, 0.6 / globalScale); ctx.beginPath(); ctx.moveTo(lx + r, ly); @@ -406,8 +380,8 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge ctx.fill(); ctx.stroke(); - // label text - ctx.fillStyle = '#e2e8f0'; + // label text — dark for light background + ctx.fillStyle = '#1e293b'; ctx.fillText(label, node.x, labelY); }} linkCanvasObjectMode={() => 'replace'} @@ -416,42 +390,28 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge const tgt = link.target; if (!isFinite(src.x) || !isFinite(src.y) || !isFinite(tgt.x) || !isFinite(tgt.y)) return; - const srcColor = src.color || '#a78bfa'; - const tgtColor = tgt.color || '#a78bfa'; + const srcColor = src.color || '#6366f1'; + const tgtColor = tgt.color || '#6366f1'; const dx = tgt.x - src.x; const dy = tgt.y - src.y; const len = Math.sqrt(dx * dx + dy * dy) || 1; - const curvature = 0.12; + const curvature = 0.1; const cpX = (src.x + tgt.x) / 2 + (-dy / len) * len * curvature; const cpY = (src.y + tgt.y) / 2 + (dx / len) * len * curvature; const gradient = ctx.createLinearGradient(src.x, src.y, tgt.x, tgt.y); - gradient.addColorStop(0, srcColor + '45'); - gradient.addColorStop(1, tgtColor + '45'); + gradient.addColorStop(0, srcColor + '35'); + gradient.addColorStop(1, tgtColor + '35'); ctx.beginPath(); ctx.moveTo(src.x, src.y); ctx.quadraticCurveTo(cpX, cpY, tgt.x, tgt.y); ctx.strokeStyle = gradient; ctx.lineWidth = Math.max( - 0.5, - (link.type === 'root-chapter' ? 1.8 : 0.8) / Math.max(0.3, globalScale) - ); - ctx.stroke(); - - // subtle glow line - ctx.beginPath(); - ctx.moveTo(src.x, src.y); - ctx.quadraticCurveTo(cpX, cpY, tgt.x, tgt.y); - const glowGradient = ctx.createLinearGradient(src.x, src.y, tgt.x, tgt.y); - glowGradient.addColorStop(0, srcColor + '12'); - glowGradient.addColorStop(1, tgtColor + '12'); - ctx.strokeStyle = glowGradient; - ctx.lineWidth = Math.max( - 1.5, - (link.type === 'root-chapter' ? 5 : 3) / Math.max(0.3, globalScale) + 0.4, + (link.type === 'root-chapter' ? 1.5 : 0.7) / Math.max(0.3, globalScale) ); ctx.stroke(); }} @@ -466,7 +426,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge variant="outline" size="icon" onClick={handleZoomIn} - className="bg-black/40 backdrop-blur-sm border-white/10 text-white hover:bg-white/10 hover:text-white" + className="bg-white/80 backdrop-blur-sm border-border/40 text-foreground hover:bg-white hover:text-foreground" title="放大" > @@ -475,7 +435,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge variant="outline" size="icon" onClick={handleZoomOut} - className="bg-black/40 backdrop-blur-sm border-white/10 text-white hover:bg-white/10 hover:text-white" + className="bg-white/80 backdrop-blur-sm border-border/40 text-foreground hover:bg-white hover:text-foreground" title="缩小" > @@ -484,7 +444,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge variant="outline" size="icon" onClick={handleResetView} - className="bg-black/40 backdrop-blur-sm border-white/10 text-white hover:bg-white/10 hover:text-white" + className="bg-white/80 backdrop-blur-sm border-border/40 text-foreground hover:bg-white hover:text-foreground" title="重置视图" >