// ===== charts.jsx ===== // Custom SVG charts: Gauge, Radar, ModelBars, Hero data viz. const { useState: useS, useEffect: useE, useRef: useR } = React; // ---------- Gauge (large circular AIGC %) ---------- function Gauge({ value = 67, size = 240, label = 'AIGC 概率', sublabel = '', strokeWidth = 18 }) { const r = (size - strokeWidth) / 2; const c = 2 * Math.PI * r; const [drawn, setDrawn] = useS(0); useE(() => { let raf, start; const dur = 1400; const step = (ts) => { if (!start) start = ts; const t = Math.min(1, (ts - start) / dur); const e = 1 - Math.pow(1 - t, 3); setDrawn(value * e); if (t < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [value]); // color stops by value const color = drawn < 30 ? '#10B981' : drawn < 55 ? '#F59E0B' : drawn < 75 ? '#EF4444' : '#DC2626'; const trackColor = '#E2E8F0'; // 270° sweep from -135° to +135° (leave 90° gap at bottom) const SWEEP = 0.78; // 280deg portion of full circle const offset = c * (1 - (drawn / 100) * SWEEP); return (
{/* track */} {/* value */} {/* tick marks */} {Array.from({ length: 11 }).map((_, i) => { const a = (-90 + (SWEEP * 360) * (i / 10)) * (Math.PI / 180); const ix = size/2 + Math.cos(a) * (r - strokeWidth/2 - 6); const iy = size/2 + Math.sin(a) * (r - strokeWidth/2 - 6); const ox = size/2 + Math.cos(a) * (r - strokeWidth/2 - 12); const oy = size/2 + Math.sin(a) * (r - strokeWidth/2 - 12); return ; })}
{label}
{Math.round(drawn)}%
{sublabel &&
{sublabel}
}
); } // ---------- Radar (5-axis polygon) ---------- function Radar({ dims, size = 360 }) { const cx = size / 2, cy = size / 2; const radius = size * 0.36; const axes = MOCK.DIMENSIONS; const N = axes.length; const [t, setT] = useS(0); useE(() => { let raf, start; const dur = 1200; const step = (ts) => { if (!start) start = ts; const p = Math.min(1, (ts - start) / dur); setT(1 - Math.pow(1 - p, 3)); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, []); const pt = (i, v) => { const angle = (-Math.PI / 2) + (i * 2 * Math.PI / N); const r = radius * (v / 100); return [cx + Math.cos(angle) * r, cy + Math.sin(angle) * r]; }; const labelPt = (i) => { const angle = (-Math.PI / 2) + (i * 2 * Math.PI / N); const r = radius + 26; return [cx + Math.cos(angle) * r, cy + Math.sin(angle) * r, angle]; }; const poly = axes.map((a, i) => pt(i, dims[a.key] * t).join(',')).join(' '); return ( {/* rings */} {[20, 40, 60, 80, 100].map((g, i) => ( pt(j, g).join(',')).join(' ')} fill="none" stroke={i === 4 ? '#CBD5E1' : '#E2E8F0'} strokeDasharray={i === 4 ? '0' : '0'} /> ))} {/* axes */} {axes.map((_, i) => { const [x, y] = pt(i, 100); return ; })} {/* polygon */} {/* dots */} {axes.map((a, i) => { const [x, y] = pt(i, dims[a.key] * t); return ; })} {/* labels */} {axes.map((a, i) => { const [lx, ly, angle] = labelPt(i); const align = Math.cos(angle) > 0.3 ? 'start' : Math.cos(angle) < -0.3 ? 'end' : 'middle'; return ( {a.label} {dims[a.key]} ); })} ); } // ---------- Model match bars ---------- function ModelBars({ models }) { return (
{models.map((m, i) => { const intense = m.match >= 60; const mid = m.match >= 35 && m.match < 60; const barColor = intense ? '#EF4444' : mid ? '#F59E0B' : '#10B981'; return (
{m.name}
{m.match}%
); })}
); } // ---------- Hero data viz (decorative SVG with a 'live' probability curve) ---------- function HeroViz() { const [phase, setPhase] = useS(0); useE(() => { const id = setInterval(() => setPhase(p => (p + 1) % 1000), 50); return () => clearInterval(id); }, []); // Generate probability density curve const points = []; for (let i = 0; i <= 60; i++) { const x = i / 60; // Mixed bimodal: human (low) + AI (high) const human = 0.42 * Math.exp(-Math.pow((x - 0.28) / 0.13, 2)); const ai = 0.58 * Math.exp(-Math.pow((x - 0.72) / 0.10, 2)); points.push({ x, y: human + ai, human, ai }); } const W = 520, H = 360; const toX = (x) => 40 + x * (W - 80); const toY = (y) => H - 60 - y * (H - 100); const pathFor = (key) => points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${toX(p.x).toFixed(1)} ${toY(p[key]).toFixed(1)}`).join(' '); const areaFor = (key, color) => { const top = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${toX(p.x).toFixed(1)} ${toY(p[key]).toFixed(1)}`).join(' '); return `${top} L ${toX(1).toFixed(1)} ${toY(0).toFixed(1)} L ${toX(0).toFixed(1)} ${toY(0).toFixed(1)} Z`; }; // Animated scan ratio const scanX = 0.05 + (phase % 200) / 200 * 0.9; const localProb = points.reduce((acc, p) => acc + Math.exp(-Math.pow((p.x - scanX) / 0.05, 2)) * p.y, 0) / 8; return (
实时统计示意
{/* background */} {/* axes */} {[0, 25, 50, 75, 100].map((tick) => ( {tick}% ))} AIGC 概率分布 · Perplexity × Burstiness {/* curves */} {/* live scan */} {/* annotations */} 人类写作 AI 生成 {/* corner readouts */} PERPLEXITY 68.4 BURSTINESS σ 12.7
); } // ---------- Process step indicator ---------- function StepNumber({ n }) { return (
{n}
); } Object.assign(window, { Gauge, Radar, ModelBars, HeroViz, StepNumber });