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 <noreply@anthropic.com>
This commit is contained in:
@@ -150,7 +150,7 @@ export default function CourseContentPage() {
|
|||||||
<>
|
<>
|
||||||
{/* 知识图谱视图 */}
|
{/* 知识图谱视图 */}
|
||||||
{viewMode === 'graph' && (
|
{viewMode === 'graph' && (
|
||||||
<div className="w-full h-[calc(100vh-250px)] min-h-[600px] lg:min-h-[700px] rounded-lg border border-white/10 overflow-hidden">
|
<div className="w-full h-[calc(100vh-250px)] min-h-[600px] lg:min-h-[700px] rounded-lg border border-border/40 overflow-hidden bg-slate-50">
|
||||||
<KnowledgeGraph bookStructure={bookStructure} />
|
<KnowledgeGraph bookStructure={bookStructure} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,15 +40,15 @@ interface KnowledgeGraphProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CHAPTER_COLORS = [
|
const CHAPTER_COLORS = [
|
||||||
'#a78bfa',
|
'#6366f1',
|
||||||
'#22d3ee',
|
'#0ea5e9',
|
||||||
'#fbbf24',
|
'#8b5cf6',
|
||||||
'#34d399',
|
'#14b8a6',
|
||||||
'#f472b6',
|
'#ec4899',
|
||||||
'#60a5fa',
|
'#f59e0b',
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROOT_COLOR = '#c4b5fd';
|
const ROOT_COLOR = '#4f46e5';
|
||||||
|
|
||||||
export default function KnowledgeGraph({ bookStructure, onNodeClick }: KnowledgeGraphProps) {
|
export default function KnowledgeGraph({ bookStructure, onNodeClick }: KnowledgeGraphProps) {
|
||||||
const graphRef = useRef<any>(null);
|
const graphRef = useRef<any>(null);
|
||||||
@@ -162,7 +162,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
return { nodes, links };
|
return { nodes, links };
|
||||||
}, [bookStructure]);
|
}, [bookStructure]);
|
||||||
|
|
||||||
// p5-style ambient particle background
|
// Soft ambient background matching the site's light academic theme
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = particleCanvasRef.current;
|
const canvas = particleCanvasRef.current;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
@@ -180,16 +180,15 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
ctx.scale(dpr, dpr);
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
const particleCount = 40;
|
const particleCount = 30;
|
||||||
const connectDist = 130;
|
const connectDist = 150;
|
||||||
const particles = Array.from({ length: particleCount }, () => ({
|
const particles = Array.from({ length: particleCount }, () => ({
|
||||||
x: Math.random() * W,
|
x: Math.random() * W,
|
||||||
y: Math.random() * H,
|
y: Math.random() * H,
|
||||||
vx: (Math.random() - 0.5) * 0.2,
|
vx: (Math.random() - 0.5) * 0.15,
|
||||||
vy: (Math.random() - 0.5) * 0.2,
|
vy: (Math.random() - 0.5) * 0.15,
|
||||||
size: Math.random() * 1.5 + 0.5,
|
size: Math.random() * 1.2 + 0.4,
|
||||||
hue: 210 + Math.random() * 60,
|
alpha: Math.random() * 0.2 + 0.05,
|
||||||
alpha: Math.random() * 0.35 + 0.1,
|
|
||||||
phase: Math.random() * Math.PI * 2,
|
phase: Math.random() * Math.PI * 2,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -197,21 +196,22 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
let time = 0;
|
let time = 0;
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
time += 0.01;
|
time += 0.008;
|
||||||
ctx.fillStyle = '#0a0a1a';
|
// Light background matching site theme
|
||||||
|
ctx.fillStyle = '#f8fafc';
|
||||||
ctx.fillRect(0, 0, W, H);
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
// update positions
|
// update positions
|
||||||
for (const p of particles) {
|
for (const p of particles) {
|
||||||
p.x += p.vx + Math.sin(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.05;
|
p.y += p.vy + Math.cos(time + p.phase) * 0.03;
|
||||||
if (p.x < 0 || p.x > W) p.vx *= -1;
|
if (p.x < 0 || p.x > W) p.vx *= -1;
|
||||||
if (p.y < 0 || p.y > H) p.vy *= -1;
|
if (p.y < 0 || p.y > H) p.vy *= -1;
|
||||||
p.x = Math.max(0, Math.min(W, p.x));
|
p.x = Math.max(0, Math.min(W, p.x));
|
||||||
p.y = Math.max(0, Math.min(H, p.y));
|
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 i = 0; i < particles.length; i++) {
|
||||||
for (let j = i + 1; j < particles.length; j++) {
|
for (let j = i + 1; j < particles.length; j++) {
|
||||||
const dx = particles[i].x - particles[j].x;
|
const dx = particles[i].x - particles[j].x;
|
||||||
@@ -222,28 +222,18 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(particles[i].x, particles[i].y);
|
ctx.moveTo(particles[i].x, particles[i].y);
|
||||||
ctx.lineTo(particles[j].x, particles[j].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.lineWidth = 0.5;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw particles
|
// draw particles — soft indigo dots
|
||||||
for (const p of particles) {
|
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
|
ctx.fillStyle = `rgba(99, 102, 241, ${p.alpha * pulse})`;
|
||||||
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.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
@@ -301,7 +291,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
<ForceGraph2D
|
<ForceGraph2D
|
||||||
ref={graphRef}
|
ref={graphRef}
|
||||||
graphData={graphData}
|
graphData={graphData}
|
||||||
backgroundColor="rgba(10, 10, 26, 0.88)"
|
backgroundColor="rgba(248, 250, 252, 0.85)"
|
||||||
nodeLabel={(node: any) => node._fullLabel || node.name}
|
nodeLabel={(node: any) => node._fullLabel || node.name}
|
||||||
nodeVal={(node: any) => node.val || 8}
|
nodeVal={(node: any) => node.val || 8}
|
||||||
nodeRelSize={6}
|
nodeRelSize={6}
|
||||||
@@ -329,51 +319,35 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
||||||
if (!isFinite(node.x) || !isFinite(node.y)) return;
|
if (!isFinite(node.x) || !isFinite(node.y)) return;
|
||||||
const label = node.name;
|
const label = node.name;
|
||||||
const color = node.color || '#a78bfa';
|
const color = node.color || '#6366f1';
|
||||||
const nodeRadius = Math.max(3, (node.val || 8) * 0.6);
|
const nodeRadius = Math.max(3, (node.val || 8) * 0.6);
|
||||||
|
|
||||||
// 1. outer glow halo
|
// 1. subtle shadow
|
||||||
const glowR = nodeRadius * 8;
|
ctx.fillStyle = 'rgba(99, 102, 241, 0.08)';
|
||||||
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;
|
|
||||||
ctx.beginPath();
|
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();
|
ctx.fill();
|
||||||
|
|
||||||
// 2. inner glow
|
// 2. soft outer ring
|
||||||
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;
|
|
||||||
ctx.beginPath();
|
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();
|
ctx.fill();
|
||||||
|
|
||||||
// 3. core
|
// 3. core circle
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(node.x, node.y, nodeRadius, 0, Math.PI * 2);
|
ctx.arc(node.x, node.y, nodeRadius, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// 4. bright center spot
|
// 4. white border for definition
|
||||||
ctx.beginPath();
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
||||||
ctx.arc(node.x, node.y, nodeRadius * 0.3, 0, Math.PI * 2);
|
ctx.lineWidth = Math.max(0.5, 1.2 / globalScale);
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.75)';
|
ctx.stroke();
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 5. label
|
// 5. label
|
||||||
const fontSize = Math.max(9, 13 / globalScale);
|
const fontSize = Math.max(9, 12 / globalScale);
|
||||||
ctx.font = `600 ${fontSize}px "Noto Serif SC", Georgia, serif`;
|
ctx.font = `500 ${fontSize}px "Noto Serif SC", Georgia, serif`;
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
@@ -381,16 +355,16 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
const pad = 4 / globalScale;
|
const pad = 4 / globalScale;
|
||||||
const labelY = node.y + nodeRadius + fontSize / 2 + pad * 3;
|
const labelY = node.y + nodeRadius + fontSize / 2 + pad * 3;
|
||||||
|
|
||||||
// label background pill
|
// label background pill — light style
|
||||||
const bw = tw + pad * 3;
|
const bw = tw + pad * 4;
|
||||||
const bh = fontSize + pad * 2;
|
const bh = fontSize + pad * 2.5;
|
||||||
const r = Math.max(2, 3 / globalScale);
|
const r = Math.max(2, 4 / globalScale);
|
||||||
const lx = node.x - bw / 2;
|
const lx = node.x - bw / 2;
|
||||||
const ly = labelY - bh / 2;
|
const ly = labelY - bh / 2;
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(10, 10, 26, 0.85)';
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.92)';
|
||||||
ctx.strokeStyle = color + '40';
|
ctx.strokeStyle = color + '30';
|
||||||
ctx.lineWidth = Math.max(0.5, 0.8 / globalScale);
|
ctx.lineWidth = Math.max(0.3, 0.6 / globalScale);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(lx + r, ly);
|
ctx.moveTo(lx + r, ly);
|
||||||
@@ -406,8 +380,8 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// label text
|
// label text — dark for light background
|
||||||
ctx.fillStyle = '#e2e8f0';
|
ctx.fillStyle = '#1e293b';
|
||||||
ctx.fillText(label, node.x, labelY);
|
ctx.fillText(label, node.x, labelY);
|
||||||
}}
|
}}
|
||||||
linkCanvasObjectMode={() => 'replace'}
|
linkCanvasObjectMode={() => 'replace'}
|
||||||
@@ -416,42 +390,28 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
const tgt = link.target;
|
const tgt = link.target;
|
||||||
if (!isFinite(src.x) || !isFinite(src.y) || !isFinite(tgt.x) || !isFinite(tgt.y)) return;
|
if (!isFinite(src.x) || !isFinite(src.y) || !isFinite(tgt.x) || !isFinite(tgt.y)) return;
|
||||||
|
|
||||||
const srcColor = src.color || '#a78bfa';
|
const srcColor = src.color || '#6366f1';
|
||||||
const tgtColor = tgt.color || '#a78bfa';
|
const tgtColor = tgt.color || '#6366f1';
|
||||||
|
|
||||||
const dx = tgt.x - src.x;
|
const dx = tgt.x - src.x;
|
||||||
const dy = tgt.y - src.y;
|
const dy = tgt.y - src.y;
|
||||||
const len = Math.sqrt(dx * dx + dy * dy) || 1;
|
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 cpX = (src.x + tgt.x) / 2 + (-dy / len) * len * curvature;
|
||||||
const cpY = (src.y + tgt.y) / 2 + (dx / 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);
|
const gradient = ctx.createLinearGradient(src.x, src.y, tgt.x, tgt.y);
|
||||||
gradient.addColorStop(0, srcColor + '45');
|
gradient.addColorStop(0, srcColor + '35');
|
||||||
gradient.addColorStop(1, tgtColor + '45');
|
gradient.addColorStop(1, tgtColor + '35');
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(src.x, src.y);
|
ctx.moveTo(src.x, src.y);
|
||||||
ctx.quadraticCurveTo(cpX, cpY, tgt.x, tgt.y);
|
ctx.quadraticCurveTo(cpX, cpY, tgt.x, tgt.y);
|
||||||
ctx.strokeStyle = gradient;
|
ctx.strokeStyle = gradient;
|
||||||
ctx.lineWidth = Math.max(
|
ctx.lineWidth = Math.max(
|
||||||
0.5,
|
0.4,
|
||||||
(link.type === 'root-chapter' ? 1.8 : 0.8) / Math.max(0.3, globalScale)
|
(link.type === 'root-chapter' ? 1.5 : 0.7) / 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)
|
|
||||||
);
|
);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}}
|
}}
|
||||||
@@ -466,7 +426,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleZoomIn}
|
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="放大"
|
title="放大"
|
||||||
>
|
>
|
||||||
<ZoomIn className="w-4 h-4" />
|
<ZoomIn className="w-4 h-4" />
|
||||||
@@ -475,7 +435,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleZoomOut}
|
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="缩小"
|
title="缩小"
|
||||||
>
|
>
|
||||||
<ZoomOut className="w-4 h-4" />
|
<ZoomOut className="w-4 h-4" />
|
||||||
@@ -484,7 +444,7 @@ export default function KnowledgeGraph({ bookStructure, onNodeClick }: Knowledge
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleResetView}
|
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="重置视图"
|
title="重置视图"
|
||||||
>
|
>
|
||||||
<RotateCcw className="w-4 h-4" />
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
|||||||
Reference in New Issue
Block a user