Fichiers
roundcube-theme-skynet/login.html
T
2026-05-14 19:19:27 +02:00

1006 lignes
24 KiB
HTML

<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
/>
<style>
@import url("https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700&display=swap");
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--blue-deep: #020510;
--blue-night: #040d2a;
--blue-dark: #0a1a4a;
--blue-mid: #0d2a7a;
--blue-bright: #1a6fff;
--blue-glow: #00aaff;
--blue-light: #7ab8ff;
--blue-pale: #aad4ff;
--cyan: #00cfff;
}
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
background: var(--blue-deep) !important;
font-family: "Share Tech Mono", monospace;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
/* Masquer les éléments Roundcube par défaut */
#layout-content {
all: unset;
display: contents;
}
#logo {
display: none !important;
}
/* ── CANVAS BACKGROUND ── */
#bg-canvas {
position: fixed;
inset: 0;
z-index: 0;
}
/* ── GRID OVERLAY ── */
.sky-grid {
position: fixed;
inset: 0;
z-index: 1;
background-image: linear-gradient(
rgba(26, 111, 255, 0.05) 1px,
transparent 1px
),
linear-gradient(90deg, rgba(26, 111, 255, 0.05) 1px, transparent 1px);
background-size: 48px 48px;
animation: grid-pulse 8s ease-in-out infinite;
pointer-events: none;
}
@keyframes grid-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
/* ── SCANLINES ── */
.sky-scanlines {
position: fixed;
inset: 0;
z-index: 2;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 3px,
rgba(0, 0, 0, 0.12) 3px,
rgba(0, 0, 0, 0.12) 4px
);
pointer-events: none;
}
/* ── CENTER GLOW ── */
.sky-glow {
position: fixed;
width: 800px;
height: 800px;
border-radius: 50%;
background: radial-gradient(
circle,
rgba(0, 80, 220, 0.22) 0%,
transparent 70%
);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
pointer-events: none;
animation: glow-breathe 6s ease-in-out infinite;
}
@keyframes glow-breathe {
0%,
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
50% {
opacity: 0.6;
transform: translate(-50%, -50%) scale(1.15);
}
}
/* ── CARD ── */
.sky-card {
position: relative;
z-index: 10;
background: rgba(2, 8, 30, 0.6);
border: 1px solid rgba(26, 111, 255, 0.3);
width: 440px;
padding: 2.5rem 2.25rem;
animation: card-appear 1.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes card-appear {
from {
opacity: 0;
transform: translateY(30px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.sky-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(
90deg,
transparent,
var(--blue-bright),
var(--cyan),
var(--blue-bright),
transparent
);
animation: top-line-scan 3s linear infinite;
}
@keyframes top-line-scan {
0% {
background-position: -440px 0;
}
100% {
background-position: 440px 0;
}
}
.sky-card::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
rgba(26, 111, 255, 0.4),
transparent
);
}
/* corners */
.sky-corner {
position: absolute;
width: 16px;
height: 16px;
border-color: var(--blue-bright);
border-style: solid;
}
.sky-tl {
top: 8px;
left: 8px;
border-width: 1px 0 0 1px;
animation: corner-flash 4s ease-in-out infinite 0s;
}
.sky-tr {
top: 8px;
right: 8px;
border-width: 1px 1px 0 0;
animation: corner-flash 4s ease-in-out infinite 1s;
}
.sky-bl {
bottom: 8px;
left: 8px;
border-width: 0 0 1px 1px;
animation: corner-flash 4s ease-in-out infinite 2s;
}
.sky-br {
bottom: 8px;
right: 8px;
border-width: 0 1px 1px 0;
animation: corner-flash 4s ease-in-out infinite 3s;
}
@keyframes corner-flash {
0%,
90%,
100% {
border-color: var(--blue-bright);
opacity: 1;
}
95% {
border-color: var(--cyan);
opacity: 0.5;
}
}
/* ── LOGO ── */
.sky-logo {
text-align: center;
margin-bottom: 0.6rem;
animation: fade-in 1s ease both 0.4s;
}
.sky-logo img {
height: 68px;
filter: drop-shadow(0 0 8px rgba(0, 170, 255, 0.6));
animation: logo-glow 3s ease-in-out infinite;
}
@keyframes logo-glow {
0%,
100% {
filter: drop-shadow(0 0 8px rgba(0, 170, 255, 0.6));
}
50% {
filter: drop-shadow(0 0 18px rgba(0, 200, 255, 0.95)) brightness(1.15);
}
}
/* ── BRAND ── */
.sky-brand-title {
font-family: "Orbitron", monospace;
color: var(--blue-glow);
font-size: 26px;
letter-spacing: 10px;
text-transform: uppercase;
text-align: center;
animation: fade-in 1s ease both 0.5s;
text-shadow: 0 0 20px rgba(0, 170, 255, 0.5);
}
.sky-brand-sub {
color: rgba(26, 111, 255, 0.5);
font-size: 9px;
letter-spacing: 3px;
text-align: center;
margin-top: 4px;
margin-bottom: 1.75rem;
animation: fade-in 1s ease both 0.6s;
}
.sky-divider {
border: none;
border-top: 1px solid #0a1a4a;
margin-bottom: 1.5rem;
}
/* ── ROUNDCUBE FORM OVERRIDE ── */
/* On surcharge les styles du formulaire généré par Roundcube */
#login-form {
animation: fade-in 0.6s ease both 0.7s;
}
#login-form .form-group,
#login-form .input-group {
margin-bottom: 1.25rem;
}
#login-form label {
color: rgba(26, 111, 255, 0.7) !important;
font-size: 9px !important;
letter-spacing: 3px !important;
font-family: "Share Tech Mono", monospace !important;
text-transform: uppercase !important;
display: block;
margin-bottom: 6px;
}
#login-form input[type="text"],
#login-form input[type="password"],
#login-form input[type="email"] {
background: #010820 !important;
border: 1px solid #0d2266 !important;
color: var(--blue-light) !important;
font-family: "Share Tech Mono", monospace !important;
font-size: 13px !important;
padding: 12px !important;
width: 100% !important;
outline: none !important;
letter-spacing: 1px !important;
border-radius: 0 !important;
transition: border-color 0.2s, box-shadow 0.2s;
caret-color: var(--cyan);
}
#login-form input:focus {
border-color: var(--blue-bright) !important;
box-shadow: 0 0 0 1px rgba(26, 111, 255, 0.15),
inset 0 0 16px rgba(26, 111, 255, 0.04) !important;
}
#login-form input::placeholder {
color: #0d2266 !important;
}
/* Bouton submit généré par Roundcube */
#login-form input[type="submit"],
#login-form button[type="submit"],
#login-form .btn-primary {
background: rgba(26, 79, 255, 0.06) !important;
border: 1px solid rgba(26, 79, 255, 0.6) !important;
color: var(--blue-light) !important;
font-family: "Orbitron", monospace !important;
font-size: 11px !important;
font-weight: 700 !important;
letter-spacing: 6px !important;
text-transform: uppercase !important;
padding: 14px !important;
width: 100% !important;
cursor: pointer !important;
border-radius: 0 !important;
transition: all 0.25s !important;
margin-top: 0.5rem !important;
}
#login-form input[type="submit"]:hover,
#login-form button[type="submit"]:hover,
#login-form .btn-primary:hover {
background: rgba(26, 79, 255, 0.16) !important;
border-color: var(--cyan) !important;
color: var(--blue-pale) !important;
}
/* Select (timezone, langue) */
#login-form select {
background: #010820 !important;
border: 1px solid #0d2266 !important;
color: var(--blue-light) !important;
font-family: "Share Tech Mono", monospace !important;
font-size: 12px !important;
padding: 8px !important;
border-radius: 0 !important;
}
/* ── STATUS BAR ── */
.sky-status {
display: flex;
justify-content: space-between;
margin-top: 1.25rem;
animation: fade-in 1s ease both 1.1s;
}
.sky-status-item {
color: rgba(13, 34, 102, 0.9);
font-size: 9px;
letter-spacing: 1px;
}
.sky-status-item span {
color: rgba(26, 111, 255, 0.55);
}
.sky-status-item.online span {
color: #00ff88;
animation: pulse-green 2s ease-in-out infinite;
}
@keyframes pulse-green {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.sky-threat {
display: flex;
align-items: center;
gap: 6px;
margin-top: 1rem;
padding: 8px 10px;
border: 1px solid rgba(13, 34, 102, 0.6);
background: rgba(1, 8, 32, 0.5);
animation: fade-in 1s ease both 1s;
}
.sky-threat-label {
color: rgba(26, 111, 255, 0.4);
font-size: 9px;
letter-spacing: 2px;
flex-shrink: 0;
}
.sky-threat-bars {
display: flex;
gap: 3px;
flex: 1;
}
.sky-tbar {
height: 8px;
flex: 1;
background: rgba(13, 34, 102, 0.4);
border: 1px solid rgba(13, 34, 102, 0.6);
}
.sky-tbar.on {
background: var(--blue-bright);
animation: bar-pulse 2s ease-in-out infinite;
}
.sky-tbar.on:nth-child(2) {
animation-delay: 0.2s;
background: var(--blue-glow);
}
.sky-tbar.on:nth-child(3) {
animation-delay: 0.4s;
background: var(--cyan);
}
@keyframes bar-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.sky-threat-val {
color: rgba(0, 207, 255, 0.6);
font-size: 9px;
letter-spacing: 1px;
flex-shrink: 0;
}
.sky-ticker {
color: rgba(13, 34, 102, 0.8);
font-size: 8px;
letter-spacing: 1px;
margin-top: 0.6rem;
overflow: hidden;
white-space: nowrap;
animation: fade-in 1s ease both 1.2s;
}
.sky-ticker-inner {
display: inline-block;
animation: ticker-scroll 22s linear infinite;
}
@keyframes ticker-scroll {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
.sky-node-id {
color: rgba(26, 74, 255, 0.25);
font-size: 8px;
text-align: center;
margin-top: 5px;
letter-spacing: 2px;
animation: fade-in 1s ease both 1.3s;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* ── FORMULAIRE ROUNDCUBE CACHÉ ── */
#rc-form-hidden {
display: none !important;
}
/* ── CHAMPS CUSTOM SKYNET ── */
.sky-field-wrap {
position: relative;
margin-bottom: 1.25rem;
animation: slide-up 0.6s ease both;
}
.sky-field-wrap:nth-child(1) {
animation-delay: 0.7s;
}
.sky-field-wrap:nth-child(2) {
animation-delay: 0.85s;
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.sky-field-label {
color: rgba(26, 111, 255, 0.7);
font-size: 9px;
letter-spacing: 3px;
margin-bottom: 6px;
display: block;
text-transform: uppercase;
}
.sky-field-icon {
position: absolute;
left: 12px;
bottom: 13px;
color: var(--blue-bright);
font-size: 12px;
transition: color 0.2s;
pointer-events: none;
}
.sky-input {
background: #010820;
border: 1px solid #0d2266;
color: var(--blue-light);
font-family: "Share Tech Mono", monospace;
font-size: 13px;
padding: 12px 12px 12px 32px;
width: 100%;
outline: none;
letter-spacing: 1px;
border-radius: 0;
transition: border-color 0.2s, box-shadow 0.2s;
caret-color: var(--cyan);
}
.sky-input:focus {
border-color: var(--blue-bright);
box-shadow: 0 0 0 1px rgba(26, 111, 255, 0.15),
inset 0 0 16px rgba(26, 111, 255, 0.04);
}
.sky-input:focus ~ .sky-field-icon {
color: var(--cyan);
}
.sky-input::placeholder {
color: #0d2266;
}
/* ── BOUTON CUSTOM ── */
.sky-submit {
background: rgba(26, 79, 255, 0.06);
border: 1px solid rgba(26, 79, 255, 0.6);
color: var(--blue-light);
font-family: "Orbitron", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 6px;
text-transform: uppercase;
padding: 14px;
width: 100%;
cursor: pointer;
margin-top: 0.25rem;
position: relative;
overflow: hidden;
transition: all 0.25s;
border-radius: 0;
animation: slide-up 0.6s ease both 1s;
}
.sky-submit::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(26, 111, 255, 0.12),
transparent
);
transition: left 0.5s;
}
.sky-submit:hover {
background: rgba(26, 79, 255, 0.16);
border-color: var(--cyan);
color: var(--blue-pale);
text-shadow: 0 0 10px rgba(0, 200, 255, 0.5);
}
.sky-submit:hover::before {
left: 100%;
}
.sky-submit:active {
transform: scale(0.99);
}
/* ── BLINK ── */
.blink {
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}
/* ── MESSAGE ERREUR ── */
#messagestack {
display: none;
}
.sky-error {
display: none;
}
#messagestack > div,
.sky-error {
color: #ff4444;
font-size: 10px;
letter-spacing: 2px;
text-align: center;
margin-bottom: 1rem;
padding: 8px;
border: 1px solid rgba(255, 68, 68, 0.3);
background: rgba(255, 68, 68, 0.05);
animation: fade-in 0.3s ease;
}
.sky-error.visible {
display: block;
}
/* Masquer le footer Roundcube par défaut */
#login-footer {
display: none !important;
}
/* ── RESPONSIVE MOBILE ── */
@media (max-width: 480px) {
html,
body {
align-items: flex-start;
overflow-y: auto;
}
.sky-card {
width: 100%;
min-height: 100dvh;
margin: 0;
padding: 2rem 1.25rem 2rem;
border-left: none;
border-right: none;
border-top: none;
display: flex;
flex-direction: column;
justify-content: center;
}
.sky-logo img {
height: 54px;
}
.sky-brand-title {
font-size: 20px;
letter-spacing: 6px;
}
.sky-brand-sub {
font-size: 8px;
letter-spacing: 2px;
margin-bottom: 1.25rem;
}
/* font-size 16px evite le zoom auto iOS sur focus */
.sky-input {
font-size: 16px !important;
padding: 14px 12px 14px 32px !important;
}
.sky-submit {
font-size: 12px !important;
padding: 16px !important;
letter-spacing: 4px !important;
}
.sky-status {
flex-direction: column;
gap: 4px;
align-items: center;
}
.sky-status-item {
font-size: 9px;
}
.sky-threat {
display: none;
}
.sky-ticker {
font-size: 8px;
}
.sky-node-id {
font-size: 7px;
}
.sky-error {
font-size: 11px;
letter-spacing: 1px;
}
}
@media (max-width: 360px) {
.sky-brand-title {
font-size: 17px;
letter-spacing: 4px;
}
.sky-card {
padding: 1.5rem 1rem;
}
}
</style>
<!-- Background animé -->
<canvas id="bg-canvas"></canvas>
<div class="sky-grid"></div>
<div class="sky-scanlines"></div>
<div class="sky-glow"></div>
<!-- Carte de login -->
<div class="sky-card" id="sky-login-card">
<div class="sky-corner sky-tl"></div>
<div class="sky-corner sky-tr"></div>
<div class="sky-corner sky-bl"></div>
<div class="sky-corner sky-br"></div>
<div class="sky-logo">
<img src="./skins/skynet/images/logo-skynet.png" alt="Skynet" />
</div>
<div class="sky-brand-title">SKYNET</div>
<div class="sky-brand-sub">
GLOBAL DEFENSE NETWORK &nbsp;//&nbsp; SECURE NODE ACCESS
</div>
<hr class="sky-divider" />
<!-- FORMULAIRE ROUNDCUBE CACHÉ — gère le token CSRF et la soumission réelle -->
<div id="rc-form-hidden">
<roundcube:form
id="login-form"
name="login-form"
method="post"
class="propform"
>
<roundcube:object
name="loginform"
form="login-form"
size="40"
submit="true"
class="form-control"
/>
</roundcube:form>
</div>
<!-- UI SKYNET — recopie les valeurs dans le vrai form au submit -->
<div id="sky-ui-form">
<div class="sky-error" id="sky-error">
&nbsp; AUTHENTIFICATION ÉCHOUÉE
</div>
<roundcube:object name="message" id="messagestack" />
<div class="sky-field-wrap">
<label class="sky-field-label" for="sky-user">Identifiant</label>
<input
class="sky-input"
type="text"
id="sky-user"
placeholder="utilisateur@domaine.tld"
autocomplete="username"
/>
<span class="sky-field-icon"></span>
</div>
<div class="sky-field-wrap">
<label class="sky-field-label" for="sky-pass">Code d'accès</label>
<input
class="sky-input"
type="password"
id="sky-pass"
placeholder="••••••••••••"
autocomplete="current-password"
/>
<span class="sky-field-icon"></span>
</div>
<button class="sky-submit" id="sky-submit-btn" type="button">
CONNEXION <span class="blink">_</span>
</button>
</div>
<div class="sky-threat">
<span class="sky-threat-label">MENACE</span>
<div class="sky-threat-bars">
<div class="sky-tbar on"></div>
<div class="sky-tbar on"></div>
<div class="sky-tbar on"></div>
<div class="sky-tbar"></div>
<div class="sky-tbar"></div>
<div class="sky-tbar"></div>
<div class="sky-tbar"></div>
</div>
<span class="sky-threat-val">MODÉRÉE</span>
</div>
<div class="sky-status">
<div class="sky-status-item online">RÉSEAU : <span>EN LIGNE</span></div>
<div class="sky-status-item">SÉCURITÉ : <span>NIV.5</span></div>
<div class="sky-status-item">NŒUD : <span>HELIOS</span></div>
</div>
<div class="sky-ticker">
<span class="sky-ticker-inner">
// ACCÈS NON AUTORISÉ SERA TRACÉ ET ÉLIMINÉ &nbsp;&nbsp;//&nbsp;&nbsp;
SKYNET SURVEILLE TOUTES LES CONNEXIONS &nbsp;&nbsp;//&nbsp;&nbsp;
PROTOCOLE DE SÉCURITÉ DELTA ACTIF &nbsp;&nbsp;//&nbsp;&nbsp; TOUTE
INTRUSION SERA NEUTRALISÉE &nbsp;&nbsp;//&nbsp;&nbsp;
</span>
</div>
<div class="sky-node-id">
SYS-ID: 4A2F-9C01-DELTA &nbsp;|&nbsp; v4.0.2026 &nbsp;|&nbsp; SKYNET CORP.
</div>
</div>
<noscript>
<p class="noscriptwarning"><roundcube:label name="noscriptwarning" /></p>
</noscript>
<roundcube:include file="includes/footer.html" />
<script>
/* ── BRIDGE : UI Skynet → Formulaire Roundcube caché ── */
document.addEventListener("DOMContentLoaded", function () {
// Détecter si Roundcube a affiché une erreur via #messagestack
const rcError = document.querySelector(
"#messagestack .alert-warning, #messagestack .alert-danger, #messagestack .alert"
);
if (rcError && rcError.textContent.trim() !== "") {
const skyError = document.getElementById("sky-error");
skyError.textContent = "⚠ AUTHENTIFICATION REFUSÉE — ACCÈS INTERDIT";
skyError.classList.add("visible");
const card = document.getElementById("sky-login-card");
card.style.borderColor = "rgba(255,68,68,0.5)";
card.style.boxShadow = "0 0 30px rgba(255,68,68,0.1)";
const ms = document.getElementById("messagestack");
if (ms) ms.style.display = "none";
}
document
.getElementById("sky-submit-btn")
.addEventListener("click", function () {
const user = document.getElementById("sky-user").value.trim();
const pass = document.getElementById("sky-pass").value;
if (!user || !pass) {
document.getElementById("sky-error").textContent =
"⚠ IDENTIFIANTS MANQUANTS";
document.getElementById("sky-error").classList.add("visible");
return;
}
// Recopier dans les vrais champs Roundcube
const rcUser =
document.getElementById("rcmloginuser") ||
document.querySelector('#login-form input[name="_user"]');
const rcPass =
document.getElementById("rcmloginpwd") ||
document.querySelector('#login-form input[name="_pass"]');
if (rcUser) rcUser.value = user;
if (rcPass) rcPass.value = pass;
// Animation de soumission
const btn = document.getElementById("sky-submit-btn");
btn.textContent = "AUTHENTIFICATION...";
btn.style.borderColor = "var(--cyan)";
btn.style.color = "var(--cyan)";
// Soumettre le vrai formulaire Roundcube
const rcForm = document.getElementById("login-form");
if (rcForm) {
rcForm.style.display = "block"; // visible l'instant du submit
rcForm.submit();
}
});
// Soumettre avec Entrée
["sky-user", "sky-pass"].forEach((id) => {
document.getElementById(id).addEventListener("keydown", function (e) {
if (e.key === "Enter")
document.getElementById("sky-submit-btn").click();
});
});
// Focus automatique
document.getElementById("sky-user").focus();
});
/* ── CANVAS BACKGROUND ── */
const canvas = document.getElementById("bg-canvas");
const ctx = canvas.getContext("2d");
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener("resize", resize);
const particles = [];
for (let i = 0; i < 80; i++) {
particles.push({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
vx: (Math.random() - 0.5) * 0.4,
vy: -Math.random() * 0.6 - 0.1,
r: Math.random() * 1.5 + 0.5,
alpha: Math.random() * 0.5 + 0.1,
color: Math.random() > 0.5 ? "#1a6fff" : "#00aaff",
});
}
const streams = [];
for (let i = 0; i < 14; i++) {
streams.push({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
speed: Math.random() * 1.5 + 0.5,
length: Math.random() * 80 + 40,
alpha: Math.random() * 0.15 + 0.05,
});
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
streams.forEach((s) => {
const grad = ctx.createLinearGradient(s.x, s.y - s.length, s.x, s.y);
grad.addColorStop(0, "rgba(26,111,255,0)");
grad.addColorStop(1, `rgba(0,170,255,${s.alpha})`);
ctx.strokeStyle = grad;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(s.x, s.y - s.length);
ctx.lineTo(s.x, s.y);
ctx.stroke();
s.y += s.speed;
if (s.y > canvas.height + s.length) {
s.y = -s.length;
s.x = Math.random() * canvas.width;
}
});
particles.forEach((p) => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.globalAlpha = p.alpha;
ctx.fill();
ctx.globalAlpha = 1;
p.x += p.vx;
p.y += p.vy;
if (p.y < -5) {
p.y = canvas.height + 5;
p.x = Math.random() * canvas.width;
}
if (p.x < -5) p.x = canvas.width + 5;
if (p.x > canvas.width + 5) p.x = -5;
});
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 100) {
ctx.strokeStyle = "#1a4aff";
ctx.lineWidth = 0.3;
ctx.globalAlpha = (1 - dist / 100) * 0.15;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
ctx.globalAlpha = 1;
}
}
}
requestAnimationFrame(draw);
}
draw();
</script>