Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 418f5f9322 | |||
| b9f9370ddd | |||
| 0430ee62b0 | |||
| 090959e524 | |||
| 1b2cc5150b | |||
| b0edb79a44 | |||
| cf2e30fafa | |||
| 0e3bdd0686 | |||
| 54ff05f260 | |||
| ef0a239641 | |||
| 936a72fd8c | |||
| 78bafef395 | |||
| a68c07c94c | |||
| 7468520f08 | |||
| adf670a3ce | |||
| fcee0028b9 | |||
| 99cbf9c75d | |||
| 8c60e2c815 | |||
| 8af4da543c | |||
| a31a6aac26 | |||
| aab7636b97 | |||
| f9618590c6 | |||
| fcf7aad80a | |||
| fbe9517588 | |||
| 998c4888c7 | |||
| e6514ca53c | |||
| 9b19a182bf | |||
| 349f9a06cf | |||
| 7a2646203b | |||
| 3257e1e778 | |||
| 366661f38f | |||
| 2b5cd19012 | |||
| 50852d6970 | |||
| 7e8b0a2955 |
+1138
File diff suppressed because it is too large
Load Diff
+1218
File diff suppressed because it is too large
Load Diff
@@ -5,14 +5,41 @@ $error = '';
|
||||
$rawOutput = '';
|
||||
|
||||
if ($host !== '') {
|
||||
$sanitizedHost = preg_replace('/[^A-Za-z0-9\-\.:]/', '', $host);
|
||||
if ($sanitizedHost === '') {
|
||||
// Erweiterte Validierung für IPv4, IPv6 und Hostnamen
|
||||
$sanitizedHost = trim($host);
|
||||
|
||||
// Prüfe ob es eine gültige IP oder Hostname ist
|
||||
$isValidIPv4 = filter_var($sanitizedHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
|
||||
$isValidIPv6 = filter_var($sanitizedHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
$isValidHostname = preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/', $sanitizedHost);
|
||||
|
||||
if (!$isValidIPv4 && !$isValidIPv6 && !$isValidHostname) {
|
||||
$error = 'Bitte geben Sie einen gültigen Hostnamen oder eine IP-Adresse ein.';
|
||||
} else {
|
||||
$command = 'traceroute -n ' . escapeshellarg($sanitizedHost) . ' 2>&1';
|
||||
// Prüfe ob traceroute verfügbar ist
|
||||
$tracerouteCmd = $isValidIPv6 ? 'traceroute6' : 'traceroute';
|
||||
$checkCmd = 'command -v ' . $tracerouteCmd . ' > /dev/null 2>&1';
|
||||
exec($checkCmd, $output, $returnCode);
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
// Fallback zu traceroute wenn traceroute6 nicht verfügbar
|
||||
if ($isValidIPv6) {
|
||||
$tracerouteCmd = 'traceroute';
|
||||
exec('command -v traceroute > /dev/null 2>&1', $output, $returnCode);
|
||||
}
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
$error = 'Traceroute ist auf diesem System nicht verfügbar.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($error === '') {
|
||||
// Führe traceroute mit Timeout aus (max 30 Sekunden)
|
||||
$command = 'timeout 30 ' . $tracerouteCmd . ' -n -m 20 -w 2 ' . escapeshellarg($sanitizedHost) . ' 2>&1';
|
||||
$rawOutput = shell_exec($command);
|
||||
if ($rawOutput === null) {
|
||||
$error = 'Traceroute konnte nicht ausgeführt werden. Ist das Kommando verfügbar?';
|
||||
|
||||
if ($rawOutput === null || trim($rawOutput) === '') {
|
||||
$error = 'Traceroute konnte nicht ausgeführt werden oder lieferte keine Ausgabe.';
|
||||
} else {
|
||||
$traceData = parseTraceroute($rawOutput);
|
||||
if (empty($traceData)) {
|
||||
@@ -21,6 +48,7 @@ if ($host !== '') {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($traceData)) {
|
||||
$traceData = getSampleTrace();
|
||||
@@ -38,21 +66,37 @@ function parseTraceroute(string $raw): array
|
||||
|
||||
$hops = [];
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^\s*\d+\s+/', $line) !== 1) {
|
||||
// Überspringe Header-Zeilen und leere Zeilen
|
||||
if (!preg_match('/^\s*\d+\s+/', $line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
preg_match_all('/(\d+\.\d+)\s+ms/', $line, $latencyMatches);
|
||||
// Extrahiere die Hop-Nummer
|
||||
if (!preg_match('/^\s*(\d+)\s+/', $line, $hopMatch)) {
|
||||
continue;
|
||||
}
|
||||
$hopNumber = (int) $hopMatch[1];
|
||||
|
||||
// Extrahiere Latenz-Werte (unterstützt verschiedene Formate)
|
||||
preg_match_all('/(\d+(?:\.\d+)?)\s*ms/', $line, $latencyMatches);
|
||||
$latencies = array_map('floatval', $latencyMatches[1] ?? []);
|
||||
$avgLatency = !empty($latencies) ? array_sum($latencies) / count($latencies) : null;
|
||||
$avgLatency = !empty($latencies) ? round(array_sum($latencies) / count($latencies), 2) : null;
|
||||
|
||||
if (preg_match('/^\s*(\d+)\s+([0-9\.\*]+)/', $line, $parts) !== 1) {
|
||||
continue;
|
||||
// Extrahiere IP-Adresse (IPv4, IPv6 oder *)
|
||||
// Pattern für IPv4: xxx.xxx.xxx.xxx
|
||||
// Pattern für IPv6: xxxx:xxxx:... oder komprimiert ::
|
||||
$ip = 'Zeitüberschreitung';
|
||||
|
||||
// Versuche IPv4 zu finden
|
||||
if (preg_match('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/', $line, $ipMatch)) {
|
||||
$ip = $ipMatch[1];
|
||||
}
|
||||
|
||||
$hopNumber = (int) $parts[1];
|
||||
$ip = $parts[2];
|
||||
if ($ip === '*') {
|
||||
// Versuche IPv6 zu finden (verschiedene Formate)
|
||||
elseif (preg_match('/([0-9a-fA-F]{1,4}:+[0-9a-fA-F:]+)/', $line, $ipMatch)) {
|
||||
$ip = $ipMatch[1];
|
||||
}
|
||||
// Prüfe auf * (Timeout)
|
||||
elseif (preg_match('/\s+\*\s+/', $line)) {
|
||||
$ip = 'Zeitüberschreitung';
|
||||
}
|
||||
|
||||
|
||||
+790
@@ -0,0 +1,790 @@
|
||||
<?php
|
||||
$host = $_GET['host'] ?? '';
|
||||
$traceData = [];
|
||||
$error = '';
|
||||
$rawOutput = '';
|
||||
|
||||
if ($host !== '') {
|
||||
// Erweiterte Validierung für IPv4, IPv6 und Hostnamen
|
||||
$sanitizedHost = trim($host);
|
||||
|
||||
// Prüfe ob es eine gültige IP oder Hostname ist
|
||||
$isValidIPv4 = filter_var($sanitizedHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
|
||||
$isValidIPv6 = filter_var($sanitizedHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
$isValidHostname = preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/', $sanitizedHost);
|
||||
|
||||
if (!$isValidIPv4 && !$isValidIPv6 && !$isValidHostname) {
|
||||
$error = 'Bitte geben Sie einen gültigen Hostnamen oder eine IP-Adresse ein.';
|
||||
} else {
|
||||
// Prüfe ob traceroute verfügbar ist
|
||||
$tracerouteCmd = $isValidIPv6 ? 'traceroute6' : 'traceroute';
|
||||
$checkCmd = 'command -v ' . $tracerouteCmd . ' > /dev/null 2>&1';
|
||||
exec($checkCmd, $output, $returnCode);
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
// Fallback zu traceroute wenn traceroute6 nicht verfügbar
|
||||
if ($isValidIPv6) {
|
||||
$tracerouteCmd = 'traceroute';
|
||||
exec('command -v traceroute > /dev/null 2>&1', $output, $returnCode);
|
||||
}
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
$error = 'Traceroute ist auf diesem System nicht verfügbar.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($error === '') {
|
||||
// Führe traceroute mit Timeout aus (max 30 Sekunden)
|
||||
$command = 'timeout 30 ' . $tracerouteCmd . ' -n -m 20 -w 2 ' . escapeshellarg($sanitizedHost) . ' 2>&1';
|
||||
$rawOutput = shell_exec($command);
|
||||
|
||||
if ($rawOutput === null || trim($rawOutput) === '') {
|
||||
$error = 'Traceroute konnte nicht ausgeführt werden oder lieferte keine Ausgabe.';
|
||||
} else {
|
||||
$traceData = parseTraceroute($rawOutput);
|
||||
if (empty($traceData)) {
|
||||
$error = 'Keine Hops gefunden. Prüfen Sie den Hostnamen oder versuchen Sie es später erneut.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($traceData)) {
|
||||
$traceData = getSampleTrace();
|
||||
if ($error === '') {
|
||||
$error = 'Es werden Beispieldaten angezeigt. Starten Sie eine Abfrage, um echte Traceroute-Daten zu sehen.';
|
||||
}
|
||||
}
|
||||
|
||||
$positions = generateGalaxyLayout(count($traceData));
|
||||
$traceWithPositions = array_map(function ($hop, $index) use ($positions) {
|
||||
return $hop + [
|
||||
'position' => $positions[$index] ?? ['x' => 0, 'y' => 0, 'z' => 0],
|
||||
];
|
||||
}, $traceData, array_keys($traceData));
|
||||
|
||||
function parseTraceroute(string $raw): array
|
||||
{
|
||||
$lines = preg_split('/\r?\n/', trim($raw));
|
||||
if (!$lines) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$hops = [];
|
||||
foreach ($lines as $line) {
|
||||
// Überspringe Header-Zeilen und leere Zeilen
|
||||
if (!preg_match('/^\s*\d+\s+/', $line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extrahiere die Hop-Nummer
|
||||
if (!preg_match('/^\s*(\d+)\s+/', $line, $hopMatch)) {
|
||||
continue;
|
||||
}
|
||||
$hopNumber = (int) $hopMatch[1];
|
||||
|
||||
// Extrahiere Latenz-Werte (unterstützt verschiedene Formate)
|
||||
preg_match_all('/(\d+(?:\.\d+)?)\s*ms/', $line, $latencyMatches);
|
||||
$latencies = array_map('floatval', $latencyMatches[1] ?? []);
|
||||
$avgLatency = !empty($latencies) ? round(array_sum($latencies) / count($latencies), 2) : null;
|
||||
|
||||
// Extrahiere IP-Adresse (IPv4, IPv6 oder *)
|
||||
$ip = 'Zeitüberschreitung';
|
||||
|
||||
// Versuche IPv4 zu finden
|
||||
if (preg_match('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/', $line, $ipMatch)) {
|
||||
$ip = $ipMatch[1];
|
||||
}
|
||||
// Versuche IPv6 zu finden (verschiedene Formate)
|
||||
elseif (preg_match('/([0-9a-fA-F]{1,4}:+[0-9a-fA-F:]+)/', $line, $ipMatch)) {
|
||||
$ip = $ipMatch[1];
|
||||
}
|
||||
// Prüfe auf * (Timeout)
|
||||
elseif (preg_match('/\s+\*\s+/', $line)) {
|
||||
$ip = 'Zeitüberschreitung';
|
||||
}
|
||||
|
||||
$hops[] = [
|
||||
'hop' => $hopNumber,
|
||||
'ip' => $ip,
|
||||
'avgLatency' => $avgLatency,
|
||||
'raw' => trim($line),
|
||||
];
|
||||
}
|
||||
|
||||
return $hops;
|
||||
}
|
||||
|
||||
function getSampleTrace(): array
|
||||
{
|
||||
return [
|
||||
['hop' => 1, 'ip' => '192.168.0.1', 'avgLatency' => 1.23, 'raw' => '1 192.168.0.1 1.12 ms 1.20 ms 1.38 ms'],
|
||||
['hop' => 2, 'ip' => '10.12.34.1', 'avgLatency' => 9.87, 'raw' => '2 10.12.34.1 9.44 ms 9.90 ms 10.29 ms'],
|
||||
['hop' => 3, 'ip' => '172.16.5.4', 'avgLatency' => 18.54, 'raw' => '3 172.16.5.4 18.12 ms 18.45 ms 18.71 ms'],
|
||||
['hop' => 4, 'ip' => '198.51.100.12', 'avgLatency' => 27.42, 'raw' => '4 198.51.100.12 27.03 ms 27.54 ms 27.68 ms'],
|
||||
['hop' => 5, 'ip' => '203.0.113.5', 'avgLatency' => 36.18, 'raw' => '5 203.0.113.5 35.82 ms 36.19 ms 36.52 ms'],
|
||||
['hop' => 6, 'ip' => '93.184.216.34', 'avgLatency' => 48.91, 'raw' => '6 93.184.216.34 48.44 ms 48.90 ms 49.39 ms'],
|
||||
];
|
||||
}
|
||||
|
||||
function generateGalaxyLayout(int $count): array
|
||||
{
|
||||
$positions = [];
|
||||
$radiusStep = 28;
|
||||
$angleOffset = pi() * (3 - sqrt(5)); // Golden angle
|
||||
$heightStep = 12;
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$radius = ($i + 1) * 4 + ($i % 2 === 0 ? $radiusStep : $radiusStep * 0.6);
|
||||
$angle = $i * $angleOffset;
|
||||
$y = ($i - $count / 2) * $heightStep;
|
||||
|
||||
$positions[] = [
|
||||
'x' => cos($angle) * $radius,
|
||||
'y' => $y,
|
||||
'z' => sin($angle) * $radius,
|
||||
];
|
||||
}
|
||||
|
||||
return $positions;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HyperTracer 3D</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600&family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg-gradient: radial-gradient(circle at 20% 20%, rgba(56, 189, 248, 0.2), transparent 55%),
|
||||
radial-gradient(circle at 80% 30%, rgba(129, 140, 248, 0.25), transparent 50%),
|
||||
#020617;
|
||||
--panel-bg: rgba(15, 23, 42, 0.86);
|
||||
--accent: #22d3ee;
|
||||
--accent-strong: #38bdf8;
|
||||
--warning: #f87171;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
font-family: 'Roboto', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg-gradient);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
padding: 1.5rem clamp(2rem, 3vw, 4rem);
|
||||
background: linear-gradient(135deg, rgba(15, 118, 110, 0.9), rgba(8, 47, 73, 0.85));
|
||||
backdrop-filter: blur(6px);
|
||||
border-bottom: 1px solid rgba(45, 212, 191, 0.35);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: clamp(1.6rem, 2vw + 1rem, 2.6rem);
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
header form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header input[type="text"] {
|
||||
width: min(320px, 50vw);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(56, 189, 248, 0.5);
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
color: inherit;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
header input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.15);
|
||||
}
|
||||
|
||||
header button {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
background: linear-gradient(135deg, var(--accent-strong), #2563eb);
|
||||
color: #0f172a;
|
||||
cursor: pointer;
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
header button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 24px rgba(37, 99, 235, 0.35);
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 400px) 1fr;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
aside {
|
||||
padding: clamp(1.5rem, 2.5vw, 2.75rem);
|
||||
border-right: 1px solid rgba(148, 163, 184, 0.2);
|
||||
background: var(--panel-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
aside h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(148, 197, 255, 0.95);
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(37, 99, 235, 0.15);
|
||||
border: 1px solid rgba(37, 99, 235, 0.35);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: rgba(248, 113, 113, 0.14);
|
||||
border-color: rgba(248, 113, 113, 0.45);
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.hop-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.hop-card {
|
||||
padding: 1rem 1.1rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(30, 64, 175, 0.2);
|
||||
border: 1px solid rgba(56, 189, 248, 0.35);
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.hop-card:hover, .hop-card.active {
|
||||
transform: translateX(4px);
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 10px 24px rgba(14, 116, 144, 0.35);
|
||||
}
|
||||
|
||||
.hop-card span.label {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.hop-card strong {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 1.2rem 1.35rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.legend-item::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, var(--accent), rgba(37, 99, 235, 0.8));
|
||||
box-shadow: 0 0 12px rgba(45, 212, 191, 0.8);
|
||||
}
|
||||
|
||||
.legend-item:nth-child(2)::before {
|
||||
background: linear-gradient(135deg, rgba(248, 113, 113, 0.9), rgba(185, 28, 28, 0.6));
|
||||
box-shadow: 0 0 12px rgba(248, 113, 113, 0.75);
|
||||
}
|
||||
|
||||
.legend-item:nth-child(3)::before {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(90deg, rgba(56, 189, 248, 0), rgba(56, 189, 248, 0.8), rgba(56, 189, 248, 0));
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.control-deck {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1.2rem 1.35rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
border: 1px solid rgba(56, 189, 248, 0.25);
|
||||
}
|
||||
|
||||
.control-deck label {
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.control-deck input[type="range"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scene-wrapper {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#scene-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.overlay-hud {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.hud-info {
|
||||
align-self: end;
|
||||
justify-self: end;
|
||||
margin: clamp(1.5rem, 3vw, 3rem);
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
border: 1px solid rgba(56, 189, 248, 0.35);
|
||||
font-size: 0.9rem;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.hud-info strong {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
}
|
||||
|
||||
.webgl-warning {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
backdrop-filter: blur(6px);
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.webgl-warning.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
aside {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
.scene-wrapper {
|
||||
min-height: 420px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>HyperTracer 3D</h1>
|
||||
<form method="get">
|
||||
<label for="host" class="visually-hidden">Host</label>
|
||||
<input type="text" id="host" name="host" placeholder="Hostname oder IP" value="<?php echo htmlspecialchars($host, ENT_QUOTES); ?>">
|
||||
<button type="submit">Traceroute starten</button>
|
||||
</form>
|
||||
</header>
|
||||
<main>
|
||||
<aside>
|
||||
<section>
|
||||
<h2>Status</h2>
|
||||
<div class="status <?php echo $error !== '' ? 'error' : ''; ?>">
|
||||
<?php echo htmlspecialchars($error !== '' ? $error : 'Bereit für Hypertracing.'); ?>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Hops</h2>
|
||||
<div class="hop-list" id="hop-list">
|
||||
<?php foreach ($traceWithPositions as $hop): ?>
|
||||
<article class="hop-card" data-hop="<?php echo (int) $hop['hop']; ?>">
|
||||
<span class="label">Hop <?php echo (int) $hop['hop']; ?></span>
|
||||
<strong><?php echo htmlspecialchars($hop['ip']); ?></strong>
|
||||
<span><?php echo $hop['avgLatency'] !== null ? htmlspecialchars($hop['avgLatency'] . ' ms') : 'Latenz unbekannt'; ?></span>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<section class="legend">
|
||||
<h2>Legende</h2>
|
||||
<div class="legend-item">Aktueller Hop</div>
|
||||
<div class="legend-item">Paketverlust / Zeitüberschreitung</div>
|
||||
<div class="legend-item">Pfadintensität ~ Latenz</div>
|
||||
</section>
|
||||
<section class="control-deck">
|
||||
<h2>Steuerung</h2>
|
||||
<label for="timeline">Zeitleiste</label>
|
||||
<input type="range" min="0" max="<?php echo max(count($traceWithPositions) - 1, 0); ?>" value="0" id="timeline">
|
||||
<label for="speed">Kamerageschwindigkeit</label>
|
||||
<input type="range" min="1" max="50" value="14" id="speed">
|
||||
</section>
|
||||
</aside>
|
||||
<section class="scene-wrapper">
|
||||
<canvas id="scene-canvas"></canvas>
|
||||
<div class="overlay-hud">
|
||||
<div class="hud-info" id="hud-info">Hop <strong>1</strong>: <span id="hud-ip"></span></div>
|
||||
</div>
|
||||
<div class="webgl-warning" id="webgl-warning">
|
||||
WebGL konnte nicht initialisiert werden. Bitte verwenden Sie einen modernen Browser oder aktivieren Sie WebGL.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module">
|
||||
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js';
|
||||
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.160/examples/jsm/controls/OrbitControls.js';
|
||||
import { Line2 } from 'https://cdn.jsdelivr.net/npm/three@0.160/examples/jsm/lines/Line2.js';
|
||||
import { LineGeometry } from 'https://cdn.jsdelivr.net/npm/three@0.160/examples/jsm/lines/LineGeometry.js';
|
||||
import { LineMaterial } from 'https://cdn.jsdelivr.net/npm/three@0.160/examples/jsm/lines/LineMaterial.js';
|
||||
|
||||
const rawTrace = <?php echo json_encode($traceWithPositions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
const canvas = document.getElementById('scene-canvas');
|
||||
const hudInfo = document.getElementById('hud-info');
|
||||
const hudIp = document.getElementById('hud-ip');
|
||||
const timeline = document.getElementById('timeline');
|
||||
const speed = document.getElementById('speed');
|
||||
const hopList = document.getElementById('hop-list');
|
||||
const webglWarning = document.getElementById('webgl-warning');
|
||||
|
||||
let renderer, scene, camera, controls;
|
||||
let routeCurve, routeMesh, glowMaterial;
|
||||
let hopMeshes = [];
|
||||
let activeIndex = 0;
|
||||
let animationClock = new THREE.Clock();
|
||||
let timelineProgress = 0;
|
||||
|
||||
try {
|
||||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||
} catch (err) {
|
||||
webglWarning.classList.add('show');
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!renderer.capabilities.isWebGL2 && !renderer.capabilities.isWebGL) {
|
||||
webglWarning.classList.add('show');
|
||||
}
|
||||
|
||||
const DPR = Math.min(window.devicePixelRatio || 1, 2.5);
|
||||
renderer.setPixelRatio(DPR);
|
||||
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
|
||||
renderer.setClearColor(0x020617, 1);
|
||||
|
||||
scene = new THREE.Scene();
|
||||
camera = new THREE.PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 0.1, 5000);
|
||||
camera.position.set(60, 120, 160);
|
||||
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.08;
|
||||
controls.maxDistance = 800;
|
||||
controls.minDistance = 20;
|
||||
controls.autoRotate = true;
|
||||
controls.autoRotateSpeed = 0.5;
|
||||
|
||||
const ambient = new THREE.AmbientLight(0x67e8f9, 0.4);
|
||||
scene.add(ambient);
|
||||
|
||||
const keyLight = new THREE.SpotLight(0x60a5fa, 1.6, 0, Math.PI / 8, 0.25, 1.5);
|
||||
keyLight.position.set(120, 260, 80);
|
||||
scene.add(keyLight);
|
||||
|
||||
const fillLight = new THREE.DirectionalLight(0x22d3ee, 0.6);
|
||||
fillLight.position.set(-120, -50, -140);
|
||||
scene.add(fillLight);
|
||||
|
||||
const fogColor = new THREE.Color('#0b1120');
|
||||
scene.fog = new THREE.FogExp2(fogColor, 0.0012);
|
||||
|
||||
// Starfield backdrop
|
||||
const starGeometry = new THREE.BufferGeometry();
|
||||
const starCount = 3200;
|
||||
const starPositions = new Float32Array(starCount * 3);
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
const radius = THREE.MathUtils.randFloat(260, 2200);
|
||||
const theta = THREE.MathUtils.randFloatSpread(360);
|
||||
const phi = THREE.MathUtils.randFloatSpread(360);
|
||||
const x = radius * Math.sin(theta) * Math.cos(phi);
|
||||
const y = radius * Math.sin(theta) * Math.sin(phi);
|
||||
const z = radius * Math.cos(theta);
|
||||
starPositions.set([x, y, z], i * 3);
|
||||
}
|
||||
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
|
||||
const starMaterial = new THREE.PointsMaterial({ color: 0x38bdf8, size: 2, sizeAttenuation: true, transparent: true, opacity: 0.65 });
|
||||
const starField = new THREE.Points(starGeometry, starMaterial);
|
||||
scene.add(starField);
|
||||
|
||||
// Lade Glow Texture mit Fehlerbehandlung
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
let glowTexture = null;
|
||||
|
||||
textureLoader.load(
|
||||
'https://cdn.jsdelivr.net/gh/ykob/sketch-threejs@master/example/img/glow.png',
|
||||
(texture) => {
|
||||
glowTexture = texture;
|
||||
// Aktualisiere Sprites mit geladenem Texture
|
||||
hopMeshes.forEach(mesh => {
|
||||
const sprite = mesh.children.find(child => child instanceof THREE.Sprite);
|
||||
if (sprite && sprite.material) {
|
||||
sprite.material.map = glowTexture;
|
||||
sprite.material.needsUpdate = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
(error) => {
|
||||
console.warn('Konnte Glow-Texture nicht laden, verwende Fallback:', error);
|
||||
// Fallback: Verwende einfache Farbe ohne Texture
|
||||
}
|
||||
);
|
||||
|
||||
const hopPositions = rawTrace.map(hop => new THREE.Vector3(hop.position.x, hop.position.y, hop.position.z));
|
||||
routeCurve = new THREE.CatmullRomCurve3(hopPositions, false, 'catmullrom', 0.1);
|
||||
|
||||
const points = routeCurve.getPoints(1024);
|
||||
const linePositions = [];
|
||||
const colors = [];
|
||||
|
||||
const latencyRange = (() => {
|
||||
const latencies = rawTrace.map(h => h.avgLatency ?? 0);
|
||||
return { min: Math.min(...latencies), max: Math.max(...latencies) };
|
||||
})();
|
||||
|
||||
points.forEach((point, index) => {
|
||||
linePositions.push(point.x, point.y, point.z);
|
||||
const progress = index / points.length;
|
||||
const color = new THREE.Color().setHSL(THREE.MathUtils.lerp(0.55, 0.08, progress), 0.9, 0.55);
|
||||
colors.push(color.r, color.g, color.b);
|
||||
});
|
||||
|
||||
const lineGeometry = new LineGeometry();
|
||||
lineGeometry.setPositions(linePositions);
|
||||
lineGeometry.setColors(colors);
|
||||
|
||||
const lineMaterial = new LineMaterial({
|
||||
color: 0xffffff,
|
||||
linewidth: 0.003,
|
||||
vertexColors: true,
|
||||
dashed: false,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
depthWrite: false
|
||||
});
|
||||
|
||||
lineMaterial.resolution.set(canvas.clientWidth, canvas.clientHeight);
|
||||
|
||||
routeMesh = new Line2(lineGeometry, lineMaterial);
|
||||
scene.add(routeMesh);
|
||||
|
||||
const hopGeometry = new THREE.SphereGeometry(3.2, 32, 32);
|
||||
const hopMaterial = new THREE.MeshStandardMaterial({ color: 0x38bdf8, emissive: 0x164e63, metalness: 0.5, roughness: 0.35 });
|
||||
|
||||
// Sprite-Material ohne Texture erstellen (wird später aktualisiert wenn Texture geladen ist)
|
||||
const spriteMaterial = new THREE.SpriteMaterial({ map: null, color: 0x38bdf8, transparent: true, opacity: 0.6, depthWrite: false });
|
||||
|
||||
rawTrace.forEach((hop, index) => {
|
||||
const mesh = new THREE.Mesh(hopGeometry, hopMaterial.clone());
|
||||
mesh.position.copy(hopPositions[index]);
|
||||
mesh.userData = { hop, index };
|
||||
|
||||
const sprite = new THREE.Sprite(spriteMaterial.clone());
|
||||
sprite.scale.set(16, 16, 1);
|
||||
mesh.add(sprite);
|
||||
|
||||
scene.add(mesh);
|
||||
hopMeshes.push(mesh);
|
||||
});
|
||||
|
||||
const pulseGeometry = new THREE.SphereGeometry(2, 24, 24);
|
||||
glowMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uColor: { value: new THREE.Color('#22d3ee') }
|
||||
},
|
||||
vertexShader: `varying float vIntensity;\nvoid main() {\n vec3 transformed = position;\n float radius = length(transformed);\n vIntensity = smoothstep(0.0, 1.0, radius / 2.0);\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}`,
|
||||
fragmentShader: `uniform float uTime;\nuniform vec3 uColor;\nvarying float vIntensity;\nvoid main() {\n float alpha = smoothstep(0.9, 0.0, vIntensity + sin(uTime * 4.0) * 0.2);\n gl_FragColor = vec4(uColor, alpha);\n}`,
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false
|
||||
});
|
||||
|
||||
const pulseMesh = new THREE.Mesh(pulseGeometry, glowMaterial);
|
||||
scene.add(pulseMesh);
|
||||
|
||||
const gridHelper = new THREE.PolarGridHelper(280, 16, 8, 64, 0x0ea5e9, 0x1e40af);
|
||||
gridHelper.material.opacity = 0.3;
|
||||
gridHelper.material.transparent = true;
|
||||
gridHelper.rotation.x = Math.PI / 2;
|
||||
scene.add(gridHelper);
|
||||
|
||||
function resizeRendererToDisplaySize() {
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight;
|
||||
const needResize = canvas.width !== width * DPR || canvas.height !== height * DPR;
|
||||
if (needResize) {
|
||||
renderer.setSize(width, height, false);
|
||||
camera.aspect = width / height || 1;
|
||||
camera.updateProjectionMatrix();
|
||||
lineMaterial.resolution.set(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveHop(index, centerCamera = false) {
|
||||
activeIndex = THREE.MathUtils.clamp(index, 0, hopMeshes.length - 1);
|
||||
hopMeshes.forEach((mesh, idx) => {
|
||||
const isActive = idx === activeIndex;
|
||||
mesh.material.emissiveIntensity = isActive ? 1.5 : 0.4;
|
||||
mesh.scale.setScalar(isActive ? 1.6 : 1);
|
||||
});
|
||||
|
||||
const hop = rawTrace[activeIndex];
|
||||
hudInfo.querySelector('strong').textContent = hop.hop;
|
||||
hudIp.textContent = `${hop.ip} • ${hop.avgLatency !== null ? hop.avgLatency + ' ms' : 'Latenz unbekannt'}`;
|
||||
|
||||
document.querySelectorAll('.hop-card').forEach(card => {
|
||||
card.classList.toggle('active', Number(card.dataset.hop) === hop.hop);
|
||||
});
|
||||
|
||||
if (centerCamera) {
|
||||
const target = hopPositions[activeIndex];
|
||||
controls.target.copy(target);
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
resizeRendererToDisplaySize();
|
||||
|
||||
const delta = animationClock.getDelta();
|
||||
const elapsed = animationClock.getElapsedTime();
|
||||
|
||||
controls.update();
|
||||
|
||||
starField.rotation.y += delta * 0.01;
|
||||
starField.rotation.x += delta * 0.005;
|
||||
|
||||
glowMaterial.uniforms.uTime.value = elapsed;
|
||||
|
||||
const speedValue = Number(speed.value) / 120;
|
||||
timelineProgress += delta * speedValue;
|
||||
const nextIndex = Math.floor(timelineProgress) % hopMeshes.length;
|
||||
if (nextIndex !== activeIndex) {
|
||||
setActiveHop(nextIndex);
|
||||
timeline.value = nextIndex;
|
||||
}
|
||||
|
||||
const currentPoint = routeCurve.getPointAt((timelineProgress % hopMeshes.length) / hopMeshes.length);
|
||||
if (currentPoint) {
|
||||
pulseMesh.position.copy(currentPoint);
|
||||
pulseMesh.scale.setScalar(1 + Math.sin(elapsed * 4) * 0.5 + 0.8);
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
setActiveHop(0, true);
|
||||
animate();
|
||||
|
||||
timeline.addEventListener('input', (event) => {
|
||||
timelineProgress = Number(event.target.value);
|
||||
setActiveHop(Number(event.target.value), true);
|
||||
});
|
||||
|
||||
speed.addEventListener('input', () => {
|
||||
// speed adjusts automatically via animate loop
|
||||
});
|
||||
|
||||
hopList.addEventListener('click', (event) => {
|
||||
const card = event.target.closest('.hop-card');
|
||||
if (!card) return;
|
||||
const hopNumber = Number(card.dataset.hop);
|
||||
const index = rawTrace.findIndex(h => h.hop === hopNumber);
|
||||
if (index >= 0) {
|
||||
timeline.value = index;
|
||||
timelineProgress = index;
|
||||
setActiveHop(index, true);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
|
||||
camera.aspect = canvas.clientWidth / canvas.clientHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
lineMaterial.resolution.set(canvas.clientWidth, canvas.clientHeight);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+718
@@ -0,0 +1,718 @@
|
||||
<?php
|
||||
$host = $_GET['host'] ?? '';
|
||||
$traceData = [];
|
||||
$error = '';
|
||||
$rawOutput = '';
|
||||
|
||||
if ($host !== '') {
|
||||
// Erweiterte Validierung für IPv4, IPv6 und Hostnamen
|
||||
$sanitizedHost = trim($host);
|
||||
|
||||
// Prüfe ob es eine gültige IP oder Hostname ist
|
||||
$isValidIPv4 = filter_var($sanitizedHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
|
||||
$isValidIPv6 = filter_var($sanitizedHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
$isValidHostname = preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/', $sanitizedHost);
|
||||
|
||||
if (!$isValidIPv4 && !$isValidIPv6 && !$isValidHostname) {
|
||||
$error = 'Bitte geben Sie einen gültigen Hostnamen oder eine IP-Adresse ein.';
|
||||
} else {
|
||||
// Prüfe ob traceroute verfügbar ist
|
||||
$tracerouteCmd = $isValidIPv6 ? 'traceroute6' : 'traceroute';
|
||||
$checkCmd = 'command -v ' . $tracerouteCmd . ' > /dev/null 2>&1';
|
||||
exec($checkCmd, $output, $returnCode);
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
// Fallback zu traceroute wenn traceroute6 nicht verfügbar
|
||||
if ($isValidIPv6) {
|
||||
$tracerouteCmd = 'traceroute';
|
||||
exec('command -v traceroute > /dev/null 2>&1', $output, $returnCode);
|
||||
}
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
$error = 'Traceroute ist auf diesem System nicht verfügbar.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($error === '') {
|
||||
// Führe traceroute mit Timeout aus (max 30 Sekunden)
|
||||
$command = 'timeout 30 ' . $tracerouteCmd . ' -n -m 20 -w 2 ' . escapeshellarg($sanitizedHost) . ' 2>&1';
|
||||
$rawOutput = shell_exec($command);
|
||||
|
||||
if ($rawOutput === null || trim($rawOutput) === '') {
|
||||
$error = 'Traceroute konnte nicht ausgeführt werden oder lieferte keine Ausgabe.';
|
||||
} else {
|
||||
$traceData = parseTraceroute($rawOutput);
|
||||
if (empty($traceData)) {
|
||||
$error = 'Keine Hops gefunden. Prüfen Sie den Hostnamen oder versuchen Sie es später erneut.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($traceData)) {
|
||||
$traceData = getSampleTrace();
|
||||
if ($error === '') {
|
||||
$error = 'Es werden Beispieldaten angezeigt. Starten Sie eine Abfrage, um echte Traceroute-Daten zu sehen.';
|
||||
}
|
||||
}
|
||||
|
||||
$positions = generateHelixLayout(count($traceData));
|
||||
$traceWithPositions = array_map(function ($hop, $index) use ($positions) {
|
||||
return $hop + [
|
||||
'position' => $positions[$index] ?? ['x' => 0, 'y' => 0, 'z' => 0],
|
||||
];
|
||||
}, $traceData, array_keys($traceData));
|
||||
|
||||
function parseTraceroute(string $raw): array
|
||||
{
|
||||
$lines = preg_split('/\r?\n/', trim($raw));
|
||||
if (!$lines) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$hops = [];
|
||||
foreach ($lines as $line) {
|
||||
// Überspringe Header-Zeilen und leere Zeilen
|
||||
if (!preg_match('/^\s*\d+\s+/', $line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extrahiere die Hop-Nummer
|
||||
if (!preg_match('/^\s*(\d+)\s+/', $line, $hopMatch)) {
|
||||
continue;
|
||||
}
|
||||
$hopNumber = (int) $hopMatch[1];
|
||||
|
||||
// Extrahiere Latenz-Werte (unterstützt verschiedene Formate)
|
||||
preg_match_all('/(\d+(?:\.\d+)?)\s*ms/', $line, $latencyMatches);
|
||||
$latencies = array_map('floatval', $latencyMatches[1] ?? []);
|
||||
$avgLatency = !empty($latencies) ? round(array_sum($latencies) / count($latencies), 2) : null;
|
||||
|
||||
// Extrahiere IP-Adresse (IPv4, IPv6 oder *)
|
||||
$ip = 'Zeitüberschreitung';
|
||||
|
||||
// Versuche IPv4 zu finden
|
||||
if (preg_match('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/', $line, $ipMatch)) {
|
||||
$ip = $ipMatch[1];
|
||||
}
|
||||
// Versuche IPv6 zu finden (verschiedene Formate)
|
||||
elseif (preg_match('/([0-9a-fA-F]{1,4}:+[0-9a-fA-F:]+)/', $line, $ipMatch)) {
|
||||
$ip = $ipMatch[1];
|
||||
}
|
||||
// Prüfe auf * (Timeout)
|
||||
elseif (preg_match('/\s+\*\s+/', $line)) {
|
||||
$ip = 'Zeitüberschreitung';
|
||||
}
|
||||
|
||||
$hops[] = [
|
||||
'hop' => $hopNumber,
|
||||
'ip' => $ip,
|
||||
'avgLatency' => $avgLatency,
|
||||
'raw' => trim($line),
|
||||
];
|
||||
}
|
||||
|
||||
return $hops;
|
||||
}
|
||||
|
||||
function getSampleTrace(): array
|
||||
{
|
||||
return [
|
||||
['hop' => 1, 'ip' => '192.168.0.1', 'avgLatency' => 1.23, 'raw' => '1 192.168.0.1 1.12 ms 1.20 ms 1.38 ms'],
|
||||
['hop' => 2, 'ip' => '10.12.34.1', 'avgLatency' => 9.87, 'raw' => '2 10.12.34.1 9.44 ms 9.90 ms 10.29 ms'],
|
||||
['hop' => 3, 'ip' => '172.16.5.4', 'avgLatency' => 18.54, 'raw' => '3 172.16.5.4 18.12 ms 18.45 ms 18.71 ms'],
|
||||
['hop' => 4, 'ip' => '198.51.100.12', 'avgLatency' => 27.42, 'raw' => '4 198.51.100.12 27.03 ms 27.54 ms 27.68 ms'],
|
||||
['hop' => 5, 'ip' => '203.0.113.5', 'avgLatency' => 36.18, 'raw' => '5 203.0.113.5 35.82 ms 36.19 ms 36.52 ms'],
|
||||
['hop' => 6, 'ip' => '93.184.216.34', 'avgLatency' => 48.91, 'raw' => '6 93.184.216.34 48.44 ms 48.90 ms 49.39 ms'],
|
||||
];
|
||||
}
|
||||
|
||||
function generateHelixLayout(int $count): array
|
||||
{
|
||||
$positions = [];
|
||||
$radius = 160;
|
||||
$heightStep = 70;
|
||||
$angleStep = pi() / 3;
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$angle = $i * $angleStep;
|
||||
$y = ($i - ($count - 1) / 2) * $heightStep;
|
||||
$positions[] = [
|
||||
'x' => cos($angle) * $radius,
|
||||
'y' => $y,
|
||||
'z' => sin($angle) * $radius,
|
||||
];
|
||||
}
|
||||
|
||||
return $positions;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HyperTracer Canvas Edition</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500&family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.22), transparent 55%),
|
||||
radial-gradient(circle at 80% 25%, rgba(147, 51, 234, 0.28), transparent 50%),
|
||||
#020617;
|
||||
--panel-bg: rgba(15, 23, 42, 0.88);
|
||||
--accent: #38bdf8;
|
||||
--accent-strong: #f472b6;
|
||||
--text: #e2e8f0;
|
||||
--muted: #94a3b8;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 28vw) 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
background: var(--bg);
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
grid-column: 1 / -1;
|
||||
padding: 1.5rem clamp(2rem, 4vw, 5rem);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(2, 132, 199, 0.75), rgba(30, 64, 175, 0.7));
|
||||
border-bottom: 1px solid rgba(125, 211, 252, 0.35);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
margin: 0;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
font-size: clamp(1.5rem, 1vw + 1.8rem, 2.6rem);
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
form button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(135deg, #38bdf8, #3b82f6);
|
||||
color: #0f172a;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
form button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 30px rgba(14, 165, 233, 0.35);
|
||||
}
|
||||
|
||||
.panel {
|
||||
grid-row: 2 / -1;
|
||||
padding: clamp(1.5rem, 2vw, 2.5rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
background: var(--panel-bg);
|
||||
border-right: 1px solid rgba(96, 165, 250, 0.2);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(56, 189, 248, 0.08);
|
||||
border: 1px solid rgba(56, 189, 248, 0.22);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.error {
|
||||
border-color: rgba(248, 113, 113, 0.45);
|
||||
color: #fecaca;
|
||||
background: rgba(248, 113, 113, 0.08);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hop-list {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.hop-item {
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
border: 1px solid transparent;
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.hop-item:hover {
|
||||
border-color: rgba(56, 189, 248, 0.35);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.hop-item.active {
|
||||
border-color: rgba(250, 204, 21, 0.75);
|
||||
box-shadow: 0 12px 20px rgba(250, 204, 21, 0.18);
|
||||
}
|
||||
|
||||
.hop-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.hop-ip {
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 1rem;
|
||||
color: #bae6fd;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.overlay .message {
|
||||
padding: 1.5rem 2.5rem;
|
||||
border-radius: 1.5rem;
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
text-align: center;
|
||||
max-width: 420px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.legend span::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
body {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
.panel {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2 / 3;
|
||||
max-height: 45vh;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-row: 3 / 4;
|
||||
min-height: 55vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>HyperTracer — Canvas Flight Deck</h1>
|
||||
<form method="get">
|
||||
<input type="text" name="host" value="<?= htmlspecialchars($host, ENT_QUOTES) ?>" placeholder="Zielhost oder IP-Adresse" aria-label="Traceroute Host">
|
||||
<button type="submit">Traceroute starten</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<aside class="panel">
|
||||
<div class="status<?= $error ? ' error' : '' ?>">
|
||||
<?= htmlspecialchars($error ?: 'Bereit für den Start. Nutzen Sie die Steuerung, um die Route zu erforschen.') ?>
|
||||
</div>
|
||||
|
||||
<section class="controls">
|
||||
<div class="control-group">
|
||||
<label for="yaw">Rotation um Y-Achse</label>
|
||||
<input type="range" id="yaw" min="-180" max="180" value="0">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="pitch">Blickwinkel</label>
|
||||
<input type="range" id="pitch" min="-60" max="60" value="-15">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="speed">Animationsgeschwindigkeit</label>
|
||||
<input type="range" id="speed" min="0" max="3" step="0.1" value="1.2">
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span style="color:#facc15">Aktiver Hop</span>
|
||||
<span style="color:#38bdf8">Reguläre Hops</span>
|
||||
<span style="color:#a855f7">Startrail</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ol class="hop-list" id="hopList">
|
||||
<?php foreach ($traceWithPositions as $hop): ?>
|
||||
<li class="hop-item" data-hop="<?= (int) $hop['hop'] ?>">
|
||||
<div class="hop-meta">
|
||||
<span>Hop <?= (int) $hop['hop'] ?></span>
|
||||
<span><?= $hop['avgLatency'] !== null ? $hop['avgLatency'] . ' ms' : '—' ?></span>
|
||||
</div>
|
||||
<div class="hop-ip"><?= htmlspecialchars($hop['ip']) ?></div>
|
||||
<div class="hop-raw"><?= htmlspecialchars($hop['raw']) ?></div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<canvas id="galaxyCanvas"></canvas>
|
||||
<div class="overlay" id="compatOverlay" hidden>
|
||||
<div class="message">
|
||||
<h2>Kein WebGL? Kein Problem.</h2>
|
||||
<p>Diese Version nutzt ein Canvas-Emulator-Setup, das auch ohne WebGL läuft. Sollte das Canvas dennoch nicht unterstützt sein, aktualisieren Sie Ihren Browser.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const data = <?= json_encode($traceWithPositions, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) ?>;
|
||||
const canvas = document.getElementById('galaxyCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const overlay = document.getElementById('compatOverlay');
|
||||
|
||||
if (!ctx) {
|
||||
overlay.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
|
||||
const state = {
|
||||
yaw: 0,
|
||||
pitch: -15 * Math.PI / 180,
|
||||
rotationVelocity: 0.15,
|
||||
autoRotate: true,
|
||||
selectedHop: data.length ? data[0].hop : null,
|
||||
time: 0,
|
||||
speedFactor: 1.2,
|
||||
};
|
||||
|
||||
const stars = new Array(240).fill(null).map(() => ({
|
||||
x: (Math.random() - 0.5) * 1000,
|
||||
y: (Math.random() - 0.5) * 800,
|
||||
z: Math.random() * 900 + 200,
|
||||
speed: Math.random() * 0.4 + 0.1,
|
||||
}));
|
||||
|
||||
const yawControl = document.getElementById('yaw');
|
||||
const pitchControl = document.getElementById('pitch');
|
||||
const speedControl = document.getElementById('speed');
|
||||
const hopList = document.getElementById('hopList');
|
||||
|
||||
yawControl.addEventListener('input', () => {
|
||||
state.yaw = yawControl.value * Math.PI / 180;
|
||||
state.autoRotate = false;
|
||||
});
|
||||
|
||||
pitchControl.addEventListener('input', () => {
|
||||
state.pitch = pitchControl.value * Math.PI / 180;
|
||||
});
|
||||
|
||||
speedControl.addEventListener('input', () => {
|
||||
state.speedFactor = Number(speedControl.value) || 0.1;
|
||||
});
|
||||
|
||||
hopList.addEventListener('click', (event) => {
|
||||
const item = event.target.closest('.hop-item');
|
||||
if (!item) return;
|
||||
state.selectedHop = Number(item.dataset.hop);
|
||||
state.autoRotate = false;
|
||||
highlightListItem();
|
||||
});
|
||||
|
||||
function highlightListItem() {
|
||||
hopList.querySelectorAll('.hop-item').forEach((item) => {
|
||||
const isActive = Number(item.dataset.hop) === state.selectedHop;
|
||||
item.classList.toggle('active', isActive);
|
||||
if (isActive) {
|
||||
item.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
width = rect.width * dpr;
|
||||
height = rect.height * dpr;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
highlightListItem();
|
||||
|
||||
function rotatePoint(point, yaw, pitch) {
|
||||
const cosY = Math.cos(yaw);
|
||||
const sinY = Math.sin(yaw);
|
||||
const cosX = Math.cos(pitch);
|
||||
const sinX = Math.sin(pitch);
|
||||
|
||||
// Rotation around Y axis
|
||||
const x1 = point.x * cosY - point.z * sinY;
|
||||
const z1 = point.x * sinY + point.z * cosY;
|
||||
|
||||
// Rotation around X axis (pitch)
|
||||
const y2 = point.y * cosX - z1 * sinX;
|
||||
const z2 = point.y * sinX + z1 * cosX;
|
||||
|
||||
return { x: x1, y: y2, z: z2 };
|
||||
}
|
||||
|
||||
function project(point) {
|
||||
const distance = 900;
|
||||
const scale = distance / (distance + point.z);
|
||||
return {
|
||||
x: point.x * scale,
|
||||
y: point.y * scale,
|
||||
scale,
|
||||
};
|
||||
}
|
||||
|
||||
function renderBackground(delta) {
|
||||
ctx.fillStyle = 'rgba(4, 6, 24, 0.75)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.save();
|
||||
ctx.translate(canvas.width / 2, canvas.height / 2);
|
||||
|
||||
stars.forEach((star) => {
|
||||
star.z -= delta * state.speedFactor * star.speed * 40;
|
||||
if (star.z < 60) {
|
||||
star.z = 900;
|
||||
}
|
||||
const rotated = rotatePoint(star, state.yaw * 0.3, state.pitch * 0.3);
|
||||
const { x, y, scale } = project(rotated);
|
||||
const alpha = Math.min(1, 0.2 + (1 - scale) * 1.2);
|
||||
const size = Math.max(0.5, 1.5 * (1 - scale));
|
||||
ctx.fillStyle = `rgba(148, 163, 184, ${alpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function renderConnections(points) {
|
||||
ctx.save();
|
||||
ctx.translate(canvas.width / 2, canvas.height / 2);
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const current = points[i];
|
||||
const next = points[i + 1];
|
||||
ctx.moveTo(current.projected.x, current.projected.y);
|
||||
ctx.lineTo(next.projected.x, next.projected.y);
|
||||
}
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.strokeStyle = 'rgba(56, 189, 248, 0.35)';
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0].projected.x, points[0].projected.y);
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const p = points[i];
|
||||
ctx.lineTo(p.projected.x, p.projected.y);
|
||||
}
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = 'rgba(124, 58, 237, 0.25)';
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function renderNodes(points, delta) {
|
||||
ctx.save();
|
||||
ctx.translate(canvas.width / 2, canvas.height / 2);
|
||||
|
||||
points.forEach((point) => {
|
||||
const isActive = point.data.hop === state.selectedHop;
|
||||
const baseRadius = isActive ? 14 : 8;
|
||||
const pulse = Math.sin(state.time * (isActive ? 6 : 3) + point.data.hop) * 0.3 + 1;
|
||||
const radius = baseRadius * point.projected.scale * 1.3 * pulse;
|
||||
const gradient = ctx.createRadialGradient(
|
||||
point.projected.x,
|
||||
point.projected.y,
|
||||
radius * 0.2,
|
||||
point.projected.x,
|
||||
point.projected.y,
|
||||
radius
|
||||
);
|
||||
if (isActive) {
|
||||
gradient.addColorStop(0, 'rgba(250, 204, 21, 0.9)');
|
||||
gradient.addColorStop(1, 'rgba(250, 204, 21, 0.05)');
|
||||
} else {
|
||||
gradient.addColorStop(0, 'rgba(56, 189, 248, 0.9)');
|
||||
gradient.addColorStop(1, 'rgba(56, 189, 248, 0.05)');
|
||||
}
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(point.projected.x, point.projected.y, Math.max(radius, 4), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
const label = `#${point.data.hop}`;
|
||||
ctx.font = '12px Orbitron';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = isActive ? 'rgba(250, 204, 21, 0.9)' : 'rgba(191, 219, 254, 0.8)';
|
||||
ctx.fillText(label, point.projected.x, point.projected.y - radius - 6);
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function render(delta) {
|
||||
state.time += delta * state.speedFactor * 0.001;
|
||||
if (state.autoRotate) {
|
||||
state.yaw += delta * state.rotationVelocity * 0.0002 * state.speedFactor;
|
||||
yawControl.value = ((state.yaw * 180 / Math.PI + 540) % 360) - 180;
|
||||
}
|
||||
|
||||
renderBackground(delta);
|
||||
|
||||
const preparedPoints = data.map((hop) => {
|
||||
const rotated = rotatePoint(hop.position, state.yaw, state.pitch);
|
||||
const projected = project(rotated);
|
||||
return { data: hop, rotated, projected };
|
||||
}).sort((a, b) => a.rotated.z - b.rotated.z);
|
||||
|
||||
renderConnections(preparedPoints);
|
||||
renderNodes(preparedPoints, delta);
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
let lastTime = performance.now();
|
||||
function step(now) {
|
||||
const delta = now - lastTime;
|
||||
lastTime = now;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
render(delta);
|
||||
}
|
||||
|
||||
canvas.addEventListener('click', (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = (event.clientX - rect.left) * dpr - canvas.width / 2;
|
||||
const y = (event.clientY - rect.top) * dpr - canvas.height / 2;
|
||||
const threshold = 18 * dpr;
|
||||
|
||||
let closest = null;
|
||||
let minDist = Infinity;
|
||||
data.forEach((hop) => {
|
||||
const rotated = rotatePoint(hop.position, state.yaw, state.pitch);
|
||||
const projected = project(rotated);
|
||||
const dx = projected.x * dpr - x;
|
||||
const dy = projected.y * dpr - y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < minDist && dist < threshold) {
|
||||
closest = hop;
|
||||
minDist = dist;
|
||||
}
|
||||
});
|
||||
|
||||
if (closest) {
|
||||
state.selectedHop = closest.hop;
|
||||
state.autoRotate = false;
|
||||
highlightListItem();
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(step);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user