zurück zu aboutSource.
Source.
Jede Zeile, transparent.
Das hier ist der echte, ausgelieferte Code dieser App — direkt aus dem Bundle gelesen. Keine kuratierte Demo-Version. Wähle links eine Datei, lies die Erklärung darüber, dann den Code darunter.
20 Dateien · MIT-Lizenz · .zip mit Verzeichnisstruktur
src/components/entropy-collector.tsx
231 Zeilen · 7.9 KB
Visualisierung des Entropie-Sammlers. Canvas + RAF-Loop mit Partikel-System, ohne React-Re-Renders bei Mausbewegung — daher flüssig auch auf langsamen Geräten.
src/components/entropy-collector.tsx
1import { useEffect, useRef } from "react";2import type { EntropyState } from "@/hooks/use-mouse-entropy";3 4const HEX = "0123456789ABCDEF";5 6type Props = {7 entropy: EntropyState & {8 bind: {9 onMouseMove: (e: React.MouseEvent) => void;10 onTouchMove: (e: React.TouchEvent) => void;11 };12 };13};14 15type Particle = { x: number; y: number; vx: number; vy: number; life: number; hex: string };16 17export function EntropyCollector({ entropy }: Props) {18 const canvasRef = useRef<HTMLCanvasElement | null>(null);19 const particlesRef = useRef<Particle[]>([]);20 const lastTrailLen = useRef(0);21 const trailRef = useRef(entropy.trail);22 const hexStreamRef = useRef<HTMLDivElement | null>(null);23 24 // Keep trail in ref (no re-render needed for canvas)25 trailRef.current = entropy.trail;26 27 // Falling hex stream — light DOM update28 useEffect(() => {29 const el = hexStreamRef.current;30 if (!el) return;31 let timer = 0;32 const cols = 14;33 const rows = 10;34 const tick = () => {35 let html = "";36 for (let r = 0; r < rows; r++) {37 for (let c = 0; c < cols; c++) {38 const ch = HEX[Math.floor(Math.random() * 16)];39 const op = (Math.random() * 0.5 + 0.05).toFixed(2);40 html += `<span style="opacity:${op}">${ch}</span>`;41 }42 html += "<br/>";43 }44 el.innerHTML = html;45 timer = window.setTimeout(tick, 220);46 };47 tick();48 return () => clearTimeout(timer);49 }, []);50 51 // Canvas RAF loop — mounts ONCE, reads from refs52 useEffect(() => {53 const canvas = canvasRef.current;54 if (!canvas) return;55 const ctx = canvas.getContext("2d");56 if (!ctx) return;57 58 const dpr = Math.min(window.devicePixelRatio || 1, 2);59 let rect = canvas.getBoundingClientRect();60 const setSize = () => {61 rect = canvas.getBoundingClientRect();62 canvas.width = rect.width * dpr;63 canvas.height = rect.height * dpr;64 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);65 };66 setSize();67 const ro = new ResizeObserver(setSize);68 ro.observe(canvas);69 70 let raf = 0;71 const draw = () => {72 const dark = document.documentElement.classList.contains("dark");73 const stroke = dark ? "255,255,255" : "20,20,20";74 ctx.clearRect(0, 0, rect.width, rect.height);75 76 const trail = trailRef.current;77 78 // spawn particles for new trail points79 if (trail.length !== lastTrailLen.current) {80 const start = Math.max(lastTrailLen.current, 0);81 for (let i = start; i < trail.length; i++) {82 const tp = trail[i];83 particlesRef.current.push({84 x: tp.x,85 y: tp.y,86 vx: (Math.random() - 0.5) * 1.2,87 vy: (Math.random() - 0.5) * 1.2,88 life: 1,89 hex: HEX[Math.floor(Math.random() * 16)],90 });91 }92 if (particlesRef.current.length > 120) {93 particlesRef.current.splice(0, particlesRef.current.length - 120);94 }95 lastTrailLen.current = trail.length;96 }97 98 if (trail.length > 1) {99 ctx.lineCap = "round";100 ctx.lineJoin = "round";101 // draw last ~80 segments only102 const start = Math.max(1, trail.length - 80);103 for (let i = start; i < trail.length; i++) {104 const a = trail[i - 1];105 const b = trail[i];106 const t = (i - start) / (trail.length - start);107 ctx.strokeStyle = `rgba(${stroke}, ${t * 0.55})`;108 ctx.lineWidth = 0.8 + t * 1.4;109 ctx.beginPath();110 ctx.moveTo(a.x, a.y);111 ctx.lineTo(b.x, b.y);112 ctx.stroke();113 }114 const head = trail[trail.length - 1];115 const pulse = 4 + Math.sin(performance.now() / 180) * 2;116 ctx.fillStyle = `rgba(${stroke}, 0.12)`;117 ctx.beginPath();118 ctx.arc(head.x, head.y, pulse + 8, 0, Math.PI * 2);119 ctx.fill();120 ctx.fillStyle = `rgba(${stroke}, 0.9)`;121 ctx.beginPath();122 ctx.arc(head.x, head.y, 2.5, 0, Math.PI * 2);123 ctx.fill();124 }125 126 // Particles127 ctx.font = "10px ui-monospace, SF Mono, monospace";128 ctx.textBaseline = "middle";129 ctx.textAlign = "center";130 const ps = particlesRef.current;131 for (let i = ps.length - 1; i >= 0; i--) {132 const p = ps[i];133 p.x += p.vx;134 p.y += p.vy;135 p.vy += 0.01;136 p.life -= 0.014;137 if (p.life <= 0) {138 ps.splice(i, 1);139 continue;140 }141 ctx.fillStyle = `rgba(${stroke}, ${p.life * 0.7})`;142 ctx.fillText(p.hex, p.x, p.y);143 }144 145 raf = requestAnimationFrame(draw);146 };147 raf = requestAnimationFrame(draw);148 return () => {149 cancelAnimationFrame(raf);150 ro.disconnect();151 };152 }, []);153 154 const pct = Math.round(entropy.progress * 100);155 const segments = 32;156 const filled = Math.round((pct / 100) * segments);157 158 return (159 <div className="space-y-3">160 <div className="flex items-center justify-between font-mono text-[11px]">161 <span className="uppercase tracking-[0.18em] text-muted-foreground">Entropie-Pool</span>162 <span className={entropy.ready ? "text-foreground" : "text-muted-foreground"}>163 {entropy.bits} / 256 bits · {pct}%164 </span>165 </div>166 167 <div168 {...entropy.bind}169 className="group relative h-60 cursor-crosshair overflow-hidden rounded-2xl border border-border bg-gradient-to-br from-muted/60 via-background to-muted/30 shadow-inner"170 >171 <span className="pointer-events-none absolute left-2 top-2 h-3 w-3 border-l border-t border-foreground/30" />172 <span className="pointer-events-none absolute right-2 top-2 h-3 w-3 border-r border-t border-foreground/30" />173 <span className="pointer-events-none absolute left-2 bottom-2 h-3 w-3 border-l border-b border-foreground/30" />174 <span className="pointer-events-none absolute right-2 bottom-2 h-3 w-3 border-r border-b border-foreground/30" />175 176 <div177 ref={hexStreamRef}178 aria-hidden179 className="pointer-events-none absolute inset-0 select-none whitespace-pre p-3 font-mono text-[10px] leading-[1.35] tracking-[0.25em] text-foreground/30"180 />181 182 <span className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-foreground/40 to-transparent animate-[scan_3.5s_linear_infinite]" />183 184 <canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />185 186 <div className="pointer-events-none absolute inset-x-0 bottom-3 flex justify-center">187 <div className="flex items-center gap-2 rounded-full border border-border bg-background/80 px-3 py-1 backdrop-blur-md">188 <span189 className={`inline-block h-1.5 w-1.5 rounded-full ${190 entropy.ready ? "bg-foreground animate-pulse" : "bg-muted-foreground animate-ping"191 }`}192 />193 <p className="font-mono text-[10px] uppercase tracking-[0.2em] text-muted-foreground">194 {entropy.ready ? "Schlüssel bereit" : "Bewege Maus / Finger"}195 </p>196 </div>197 </div>198 </div>199 200 <div className="flex items-center gap-[3px]">201 {Array.from({ length: segments }).map((_, i) => (202 <span203 key={i}204 className={`h-1.5 flex-1 rounded-sm transition-all duration-300 ${205 i < filled ? "bg-foreground" : "bg-foreground/10"206 }`}207 />208 ))}209 </div>210 211 <div className="flex items-center justify-between text-[11px]">212 <button213 type="button"214 onClick={entropy.reset}215 className="text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"216 >217 zurücksetzen218 </button>219 <button220 type="button"221 onClick={entropy.forceComplete}222 className="text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"223 title="Verwendet stattdessen den Crypto-RNG des Browsers"224 >225 Crypto-RNG verwenden →226 </button>227 </div>228 </div>229 );230}231 