SIMON Master Console Rebuild
True command center for SIMON + WEB360
This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing
console.php
and become the stronger replacement path.
Dashboard
Tree
Viewer
System Map
File Registry
Notes
Files
15,026
Folders
377
Scanned Size
3.79 GB
PHP Files
791
Editable Text Files
12,310
File viewer
guarded to /htdocs
/simon_ai/file-scanner.php
<?php /** * SIMON Nebula File Scanner * Single-file PHP application. * PHP 8.0+ * * Features: * - Local file upload/import * - Optional FTP/SFTP-style credential form placeholder UI * - Keyword scanner * - Keyword frequency chart * - Image/video/audio/text/PDF preview handling * - Alerts ticker * - Idle nebula screensaver * - Lightweight scan history stored in JSON * * Security notes: * - Do not expose this publicly without authentication. * - Keep uploads outside public web root if possible. */ declare(strict_types=1); session_start(); const APP_NAME = 'SIMON Nebula File Scanner'; const APP_VERSION = '1.0.0'; const MAX_UPLOAD_BYTES = 80_000_000; const DATA_DIR = __DIR__ . '/_scanner_data'; const UPLOAD_DIR = DATA_DIR . '/uploads'; const HISTORY_FILE = DATA_DIR . '/scan_history.json'; $defaultKeywords = [ 'password','passwd','secret','token','api_key','apikey','bearer','private_key','ssh-rsa', 'eval(','base64_decode','shell_exec','exec(','system(','passthru','proc_open','popen', 'iframe','script','onerror','onclick','document.cookie','localStorage','DROP TABLE', 'SELECT *','INSERT INTO','UPDATE users','admin','root','chmod','curl','wget', 'malware','virus','payload','backdoor','webshell','phar','phtml','cmd=' ]; function ensure_dirs(): void { foreach ([DATA_DIR, UPLOAD_DIR] as $dir) { if (!is_dir($dir)) { mkdir($dir, 0755, true); } } $deny = DATA_DIR . '/.htaccess'; if (!file_exists($deny)) { file_put_contents($deny, "Options -Indexes\n<FilesMatch \"\\.(php|phtml|phar|cgi|pl|py|sh)$\">\nRequire all denied\n</FilesMatch>\n"); } } function h(?string $value): string { return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function human_bytes(int $bytes): string { $units = ['B','KB','MB','GB','TB']; $i = 0; $size = (float)$bytes; while ($size >= 1024 && $i < count($units) - 1) { $size /= 1024; $i++; } return ($i === 0 ? (string)$bytes : number_format($size, 2)) . ' ' . $units[$i]; } function read_history(): array { if (!file_exists(HISTORY_FILE)) return []; $json = file_get_contents(HISTORY_FILE); $data = json_decode($json ?: '[]', true); return is_array($data) ? $data : []; } function save_history(array $history): void { $history = array_slice($history, 0, 30); file_put_contents(HISTORY_FILE, json_encode($history, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } function safe_upload_name(string $name): string { $name = preg_replace('/[^a-zA-Z0-9._-]+/', '_', $name) ?: 'upload.bin'; return date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '_' . $name; } function guess_mime(string $path): string { if (function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME_TYPE); if ($finfo) { $mime = finfo_file($finfo, $path); finfo_close($finfo); if (is_string($mime) && $mime !== '') return $mime; } } return 'application/octet-stream'; } function is_text_like(string $mime, string $ext): bool { $ext = strtolower($ext); if (str_starts_with($mime, 'text/')) return true; return in_array($ext, ['txt','log','md','json','xml','html','htm','css','js','php','swift','py','rb','go','java','c','cpp','h','sql','csv','ini','env','yaml','yml'], true); } function extract_text_sample(string $path, string $mime, string $ext): string { if (!is_text_like($mime, $ext)) return ''; $fh = fopen($path, 'rb'); if (!$fh) return ''; $sample = fread($fh, 1_200_000); fclose($fh); $sample = (string)$sample; $sample = preg_replace('/[^\P{C}\t\n\r]+/u', '', $sample) ?? $sample; return $sample; } function scan_file(string $path, string $originalName, array $keywords): array { $size = filesize($path) ?: 0; $mime = guess_mime($path); $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); $sha = hash_file('sha256', $path) ?: ''; $text = extract_text_sample($path, $mime, $ext); $counts = []; $matches = []; $totalHits = 0; foreach ($keywords as $kw) { $kw = trim((string)$kw); if ($kw === '') continue; $count = 0; if ($text !== '') { $count = substr_count(strtolower($text), strtolower($kw)); } if ($count > 0) { $counts[$kw] = $count; $totalHits += $count; $pos = stripos($text, $kw); $context = ''; if ($pos !== false) { $start = max(0, $pos - 80); $context = substr($text, $start, 220); } $matches[] = ['keyword' => $kw, 'count' => $count, 'context' => $context]; } } $risk = 0; $alerts = []; if ($totalHits > 0) { $risk += min(70, $totalHits * 7); $alerts[] = 'Keyword hits detected: ' . $totalHits; } if (preg_match('/\.(php|phtml|phar|exe|dll|sh|bat|cmd|jar)$/i', $originalName)) { $risk += 20; $alerts[] = 'Executable or server-side file extension detected.'; } if ($size > 50_000_000) { $risk += 8; $alerts[] = 'Large file requires manual review.'; } if ($text === '' && !str_starts_with($mime, 'image/') && !str_starts_with($mime, 'video/') && !str_starts_with($mime, 'audio/')) { $risk += 5; $alerts[] = 'Binary or unreadable file content.'; } $risk = min(100, $risk); return [ 'id' => bin2hex(random_bytes(8)), 'app' => APP_NAME, 'version' => APP_VERSION, 'name' => $originalName, 'stored' => basename($path), 'path' => $path, 'url' => basename(DATA_DIR) . '/uploads/' . rawurlencode(basename($path)), 'size' => $size, 'sizeHuman' => human_bytes($size), 'mime' => $mime, 'ext' => $ext, 'sha256' => $sha, 'createdAt' => date('c'), 'keywordCounts' => $counts, 'matches' => $matches, 'totalHits' => $totalHits, 'risk' => $risk, 'verdict' => $risk >= 70 ? 'FLAGGED' : ($risk >= 30 ? 'REVIEW' : 'CLEAN'), 'alerts' => $alerts ?: ['No keyword alerts found.'], 'textSample' => $text !== '' ? mb_substr($text, 0, 12000) : '' ]; } ensure_dirs(); $error = ''; $result = null; $history = read_history(); $keywordsText = $_POST['keywords'] ?? implode("\n", $defaultKeywords); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? 'scan_upload'; $keywords = preg_split('/[\r\n,]+/', (string)$keywordsText) ?: []; $keywords = array_values(array_unique(array_filter(array_map('trim', $keywords)))); if ($action === 'ftp_login') { $error = 'FTP login UI is ready. Server-side FTP browsing is intentionally disabled until you add credentials handling and authentication.'; } elseif (!isset($_FILES['scan_file']) || !is_array($_FILES['scan_file'])) { $error = 'Choose a file to scan.'; } else { $file = $_FILES['scan_file']; if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $error = 'Upload failed. Error code: ' . (int)$file['error']; } elseif (($file['size'] ?? 0) > MAX_UPLOAD_BYTES) { $error = 'File is too large. Max size is ' . human_bytes(MAX_UPLOAD_BYTES) . '.'; } else { $original = (string)($file['name'] ?? 'upload.bin'); $safe = safe_upload_name($original); $target = UPLOAD_DIR . '/' . $safe; if (!move_uploaded_file((string)$file['tmp_name'], $target)) { $error = 'Could not move uploaded file.'; } else { $result = scan_file($target, $original, $keywords); array_unshift($history, $result); save_history($history); } } } } $active = $result ?: ($history[0] ?? null); $chartLabels = $active ? array_keys($active['keywordCounts'] ?? []) : []; $chartValues = $active ? array_values($active['keywordCounts'] ?? []) : []; $alerts = $active['alerts'] ?? ['SIMON scanner ready.', 'Upload a file or connect source.', 'Nebula frame online.']; ?> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <title><?=h(APP_NAME)?></title> <meta name="theme-color" content="#070914"> <style> :root{ --bg:#050711;--panel:rgba(12,16,34,.78);--panel2:rgba(20,25,52,.62);--line:rgba(158,190,255,.22); --text:#edf4ff;--muted:#9fb1d9;--cyan:#68e9ff;--indigo:#7d6bff;--pink:#ff4fd8;--gold:#ffd36e; --danger:#ff6969;--warn:#ffb86b;--ok:#50e59b;--r:24px;--shadow:0 28px 90px rgba(0,0,0,.48) } *{box-sizing:border-box}html,body{height:100%;margin:0;background:#02030a;color:var(--text);font:14px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;overflow:hidden} body:before{content:"";position:fixed;inset:-20%;background:radial-gradient(circle at 20% 15%,rgba(255,79,216,.28),transparent 30%),radial-gradient(circle at 75% 25%,rgba(104,233,255,.24),transparent 28%),radial-gradient(circle at 45% 80%,rgba(125,107,255,.28),transparent 32%),linear-gradient(135deg,#02030a,#071025 45%,#0d0516);filter:saturate(1.3);animation:nebula 18s ease-in-out infinite alternate;z-index:-3} body:after{content:"";position:fixed;inset:0;background-image:radial-gradient(circle,rgba(255,255,255,.55) 0 1px,transparent 1.5px);background-size:55px 55px;opacity:.13;z-index:-2;animation:stars 30s linear infinite} @keyframes nebula{from{transform:scale(1) rotate(0deg)}to{transform:scale(1.12) rotate(4deg)}}@keyframes stars{to{background-position:300px 600px}} .app{position:fixed;inset:20px;border:1px solid rgba(162,195,255,.32);border-radius:32px;overflow:hidden;background:linear-gradient(145deg,rgba(8,10,22,.92),rgba(11,15,32,.82));box-shadow:0 0 0 1px rgba(255,255,255,.05) inset,0 0 80px rgba(104,233,255,.14),0 0 120px rgba(255,79,216,.09),var(--shadow)} .frameGlow{position:absolute;inset:0;pointer-events:none;border-radius:32px;background:linear-gradient(90deg,transparent,rgba(104,233,255,.2),transparent,rgba(255,79,216,.18),transparent);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);padding:1px;animation:rim 8s linear infinite}@keyframes rim{to{filter:hue-rotate(360deg)}} .top{height:64px;display:flex;align-items:center;gap:16px;padding:0 20px;border-bottom:1px solid var(--line);background:rgba(3,5,13,.55);backdrop-filter:blur(16px)} .logo{display:flex;align-items:center;gap:11px;font-weight:900;letter-spacing:.02em}.orb{width:34px;height:34px;border-radius:50%;background:radial-gradient(circle at 30% 25%,#fff,var(--cyan) 18%,var(--indigo) 50%,#13051e 80%);box-shadow:0 0 24px var(--cyan),0 0 40px rgba(255,79,216,.35);animation:pulse 2.4s ease-in-out infinite}@keyframes pulse{50%{transform:scale(1.08);filter:saturate(1.8)}} .ticker{flex:1;min-width:0;height:38px;border:1px solid var(--line);border-radius:999px;overflow:hidden;background:rgba(255,255,255,.05);display:flex;align-items:center}.tickerTrack{white-space:nowrap;animation:ticker 22s linear infinite;color:#dbe8ff}.ticker b{color:var(--gold);margin:0 12px}@keyframes ticker{from{transform:translateX(100%)}to{transform:translateX(-130%)}} .grid{height:calc(100% - 64px);display:grid;grid-template-columns:330px minmax(360px,1fr) 360px;gap:1px;background:var(--line)}.pane{background:rgba(5,8,19,.82);padding:18px;overflow:auto}.pane::-webkit-scrollbar{width:10px}.pane::-webkit-scrollbar-thumb{background:linear-gradient(var(--cyan),var(--pink));border-radius:999px}.pane::-webkit-scrollbar-track{background:rgba(255,255,255,.04)} h2,h3{margin:0 0 14px}h2{font-size:20px}h3{font-size:15px;color:#e8f0ff}.card{border:1px solid var(--line);border-radius:var(--r);background:linear-gradient(145deg,var(--panel),var(--panel2));padding:16px;margin-bottom:14px;box-shadow:0 18px 45px rgba(0,0,0,.22)} label{display:block;margin:10px 0 7px;color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.12em}input,textarea,select{width:100%;border:1px solid rgba(160,190,255,.26);border-radius:14px;background:rgba(0,0,0,.34);color:var(--text);padding:12px;outline:none}textarea{min-height:170px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.btn{appearance:none;border:0;border-radius:15px;padding:12px 14px;background:linear-gradient(135deg,var(--cyan),var(--indigo));color:white;font-weight:800;cursor:pointer;box-shadow:0 12px 28px rgba(104,233,255,.18)}.btn.alt{background:rgba(255,255,255,.08);border:1px solid var(--line)}.row{display:flex;gap:10px;align-items:center}.row>*{flex:1}.small{font-size:12px;color:var(--muted)} .preview{min-height:360px;border:1px solid var(--line);border-radius:26px;background:rgba(0,0,0,.28);display:grid;place-items:center;overflow:hidden;position:relative}.preview img,.preview video,.preview audio,.preview iframe{max-width:100%;max-height:62vh;width:auto;border:0}.preview video{width:100%}.textPreview{width:100%;height:100%;max-height:60vh;overflow:auto;padding:16px;white-space:pre-wrap;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#d7e6ff}.empty{color:var(--muted);text-align:center;padding:30px}.meta{display:grid;grid-template-columns:130px 1fr;gap:8px;border-top:1px solid var(--line);padding-top:14px;margin-top:14px}.meta div:nth-child(odd){color:var(--muted)} .verdict{display:inline-flex;align-items:center;gap:8px;border-radius:999px;padding:8px 12px;font-weight:900;letter-spacing:.08em}.CLEAN{background:rgba(80,229,155,.14);color:var(--ok);border:1px solid rgba(80,229,155,.35)}.REVIEW{background:rgba(255,184,107,.14);color:var(--warn);border:1px solid rgba(255,184,107,.35)}.FLAGGED{background:rgba(255,105,105,.14);color:var(--danger);border:1px solid rgba(255,105,105,.4)} .meter{height:10px;border-radius:999px;background:rgba(255,255,255,.1);overflow:hidden;margin:10px 0}.meter span{display:block;height:100%;background:linear-gradient(90deg,var(--ok),var(--warn),var(--danger));width:0}.match{border-top:1px solid var(--line);padding:12px 0}.match b{color:var(--cyan)}.context{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:#cbd9f7;background:rgba(0,0,0,.22);border-radius:12px;padding:10px;margin-top:8px;white-space:pre-wrap;font-size:12px}.hist{display:flex;align-items:center;gap:10px;padding:10px;border-radius:14px;border:1px solid rgba(255,255,255,.08);margin-bottom:8px;background:rgba(255,255,255,.04)}.hist strong{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tag{font-size:11px;padding:4px 8px;border-radius:999px;background:rgba(104,233,255,.12);color:var(--cyan);border:1px solid rgba(104,233,255,.25)} canvas#chart{width:100%;height:220px;border-radius:18px;background:rgba(0,0,0,.2);border:1px solid rgba(255,255,255,.08)} .saver{position:fixed;inset:0;background:#02030a;z-index:30;display:none;place-items:center;overflow:hidden}.saver.active{display:grid}.saver canvas{position:absolute;inset:0;width:100%;height:100%}.saverText{position:relative;text-align:center}.saverText h1{font-size:clamp(34px,7vw,96px);margin:0;text-shadow:0 0 30px var(--cyan),0 0 65px var(--pink)}.saverText p{color:var(--muted);letter-spacing:.24em;text-transform:uppercase} .error{border:1px solid rgba(255,105,105,.45);background:rgba(255,105,105,.1);color:#ffd0d0;border-radius:16px;padding:12px;margin-bottom:14px} @media(max-width:1100px){.app{inset:0;border-radius:0}.grid{grid-template-columns:1fr;height:auto;overflow:auto}.pane{overflow:visible}.top{position:sticky;top:0;z-index:5}.ticker{display:none}body{overflow:auto}.app{position:relative;min-height:100vh}} </style> </head> <body> <div class="app" id="app"> <div class="frameGlow"></div> <header class="top"> <div class="logo"><span class="orb"></span><span>SIMON Operator Scanner</span></div> <div class="ticker" aria-label="alerts"><div class="tickerTrack"> <?php foreach ($alerts as $a): ?><b>ALERT</b><?=h($a)?><?php endforeach; ?> <b>STATUS</b>Idle screensaver armed after 45 seconds. </div></div> <button class="btn alt" type="button" onclick="showSaver(true)">Screensaver</button> </header> <main class="grid"> <section class="pane"> <h2>Source</h2> <?php if ($error): ?><div class="error"><?=h($error)?></div><?php endif; ?> <form class="card" method="post" enctype="multipart/form-data"> <input type="hidden" name="action" value="scan_upload"> <label>Select / Upload File</label> <input type="file" name="scan_file" required> <label>Keywords / signals</label> <textarea name="keywords" spellcheck="false"><?=h((string)$keywordsText)?></textarea> <button class="btn" style="margin-top:12px;width:100%">Scan File</button> <p class="small">Max upload: <?=h(human_bytes(MAX_UPLOAD_BYTES))?>. For public deployment, add login protection.</p> </form> <form class="card" method="post"> <input type="hidden" name="action" value="ftp_login"> <h3>FTP / Remote Login</h3> <label>Host</label><input name="ftp_host" placeholder="ftp.example.com"> <div class="row"><div><label>User</label><input name="ftp_user"></div><div><label>Port</label><input name="ftp_port" value="21"></div></div> <label>Password</label><input name="ftp_pass" type="password"> <button class="btn alt" style="margin-top:12px;width:100%">Connect Source</button> <p class="small">UI is included. Add authenticated FTP browsing only after protecting this admin page.</p> </form> <div class="card"> <h3>Recent Scans</h3> <?php if (!$history): ?><p class="small">No scan history yet.</p><?php endif; ?> <?php foreach (array_slice($history, 0, 8) as $item): ?> <div class="hist"> <span class="tag"><?=h($item['verdict'] ?? 'SCAN')?></span> <div style="min-width:0"><strong><?=h($item['name'] ?? 'file')?></strong><span class="small"><?=h($item['sizeHuman'] ?? '')?> · Risk <?=h((string)($item['risk'] ?? 0))?>/100</span></div> </div> <?php endforeach; ?> </div> </section> <section class="pane"> <h2>Preview</h2> <div class="card"> <div class="preview"> <?php if (!$active): ?> <div class="empty">Upload a file to preview images, video, audio, PDFs, text, code, and scan details.</div> <?php else: $mime = (string)($active['mime'] ?? ''); $url = (string)($active['url'] ?? ''); if (str_starts_with($mime, 'image/')): ?> <img src="<?=h($url)?>" alt="File preview"> <?php elseif (str_starts_with($mime, 'video/')): ?> <video src="<?=h($url)?>" controls playsinline></video> <?php elseif (str_starts_with($mime, 'audio/')): ?> <audio src="<?=h($url)?>" controls></audio> <?php elseif ($mime === 'application/pdf'): ?> <iframe src="<?=h($url)?>" title="PDF preview" style="width:100%;height:65vh"></iframe> <?php elseif (($active['textSample'] ?? '') !== ''): ?> <div class="textPreview"><?=h((string)$active['textSample'])?></div> <?php else: ?> <div class="empty">No visual preview available for this file type.<br><?=h($mime)?></div> <?php endif; endif; ?> </div> <?php if ($active): ?> <div class="meta"> <div>Name</div><div><?=h($active['name'])?></div> <div>MIME</div><div><?=h($active['mime'])?></div> <div>Size</div><div><?=h($active['sizeHuman'])?></div> <div>SHA-256</div><div style="word-break:break-all"><?=h($active['sha256'])?></div> <div>Scanned</div><div><?=h($active['createdAt'])?></div> </div> <?php endif; ?> </div> </section> <aside class="pane"> <h2>Analysis</h2> <div class="card"> <?php if ($active): ?> <span class="verdict <?=h($active['verdict'])?>"><?=h($active['verdict'])?></span> <h3 style="margin-top:16px">Threat Score: <?=h((string)$active['risk'])?>/100</h3> <div class="meter"><span style="width:<?=h((string)$active['risk'])?>%"></span></div> <p class="small">Keyword hits: <?=h((string)$active['totalHits'])?> · Matches: <?=h((string)count($active['matches']))?></p> <?php else: ?> <p class="small">Scanner waiting for file input.</p> <?php endif; ?> </div> <div class="card"> <h3>Keyword Graph</h3> <canvas id="chart" width="720" height="360"></canvas> </div> <div class="card"> <h3>Detailed Signals</h3> <?php if (!$active || empty($active['matches'])): ?> <p class="small">No keyword matches to display.</p> <?php else: foreach ($active['matches'] as $m): ?> <div class="match"><b><?=h($m['keyword'])?></b> <span class="tag"><?=h((string)$m['count'])?></span> <?php if (!empty($m['context'])): ?><div class="context"><?=h($m['context'])?></div><?php endif; ?> </div> <?php endforeach; endif; ?> </div> </aside> </main> </div> <div class="saver" id="saver" onclick="showSaver(false)"> <canvas id="saverCanvas"></canvas> <div class="saverText"><h1>SIMON</h1><p>Nebula scanner standing by</p></div> </div> <script> const labels = <?=json_encode($chartLabels, JSON_UNESCAPED_SLASHES)?>; const values = <?=json_encode($chartValues, JSON_UNESCAPED_SLASHES)?>; const chart = document.getElementById('chart'); const ctx = chart.getContext('2d'); function drawChart(){ ctx.clearRect(0,0,chart.width,chart.height); ctx.fillStyle='rgba(255,255,255,.06)';ctx.fillRect(0,0,chart.width,chart.height); if(!labels.length){ctx.fillStyle='rgba(220,232,255,.7)';ctx.font='24px system-ui';ctx.fillText('No keyword hits yet',28,70);return;} const max=Math.max(...values,1), pad=42, gap=10, w=(chart.width-pad*2)/labels.length-gap; labels.forEach((label,i)=>{ const h=(chart.height-95)*(values[i]/max), x=pad+i*(w+gap), y=chart.height-50-h; const g=ctx.createLinearGradient(0,y,0,chart.height-50);g.addColorStop(0,'#68e9ff');g.addColorStop(.55,'#7d6bff');g.addColorStop(1,'#ff4fd8'); ctx.fillStyle=g;roundRect(ctx,x,y,Math.max(12,w),h,10);ctx.fill(); ctx.fillStyle='#edf4ff';ctx.font='18px system-ui';ctx.fillText(values[i],x,y-8); ctx.save();ctx.translate(x+8,chart.height-24);ctx.rotate(-.45);ctx.fillStyle='rgba(220,232,255,.82)';ctx.font='15px system-ui';ctx.fillText(label.slice(0,18),0,0);ctx.restore(); }); } function roundRect(ctx,x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath()} drawChart(); const saver=document.getElementById('saver'), sc=document.getElementById('saverCanvas'), sx=sc.getContext('2d');let idleTimer, particles=[]; function resizeSaver(){sc.width=innerWidth;sc.height=innerHeight}addEventListener('resize',resizeSaver);resizeSaver(); function seed(){particles=Array.from({length:180},()=>({x:Math.random()*sc.width,y:Math.random()*sc.height,z:Math.random()*2+0.2,v:Math.random()*0.8+0.1,a:Math.random()*Math.PI*2}))}seed(); function animateSaver(){ sx.fillStyle='rgba(2,3,10,.18)';sx.fillRect(0,0,sc.width,sc.height); particles.forEach(p=>{p.a+=0.004*p.z;p.x+=Math.cos(p.a)*p.v;p.y+=Math.sin(p.a)*p.v;if(p.x<0)p.x=sc.width;if(p.x>sc.width)p.x=0;if(p.y<0)p.y=sc.height;if(p.y>sc.height)p.y=0;const r=p.z*1.8;const g=sx.createRadialGradient(p.x,p.y,0,p.x,p.y,r*6);g.addColorStop(0,'rgba(104,233,255,.9)');g.addColorStop(.45,'rgba(255,79,216,.28)');g.addColorStop(1,'transparent');sx.fillStyle=g;sx.beginPath();sx.arc(p.x,p.y,r*6,0,Math.PI*2);sx.fill();}); requestAnimationFrame(animateSaver); }animateSaver(); function resetIdle(){clearTimeout(idleTimer);idleTimer=setTimeout(()=>showSaver(true),45000)} ['mousemove','mousedown','keydown','touchstart','scroll'].forEach(e=>addEventListener(e,()=>{if(saver.classList.contains('active'))showSaver(false);else resetIdle();},{passive:true}));resetIdle(); </script> </body> </html>
Save file
Quick jump
open a path
Open