zurück zu about

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 update
28 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 refs
52 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 points
79 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 only
102 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 // Particles
127 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 <div
168 {...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 <div
177 ref={hexStreamRef}
178 aria-hidden
179 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 <span
189 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 <span
203 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 <button
213 type="button"
214 onClick={entropy.reset}
215 className="text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
216 >
217 zurücksetzen
218 </button>
219 <button
220 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