v2
Cette révision appartient à :
+518
@@ -0,0 +1,518 @@
|
||||
(function () {
|
||||
function init() {
|
||||
const cv = document.getElementById("skc");
|
||||
if (!cv) return;
|
||||
const ctx = cv.getContext("2d");
|
||||
|
||||
/* ── CANVAS SIZING ──
|
||||
Width = sky-login-card width
|
||||
Height = sky-login-card height
|
||||
Read after DOM paint via double requestAnimationFrame to get final dimensions.
|
||||
ResizeObserver keeps it in sync if card height changes later.
|
||||
*/
|
||||
function resizeCanvas() {
|
||||
const card = document.getElementById("sky-login-card");
|
||||
if (!card) return;
|
||||
const w = Math.round(card.offsetWidth) || 440;
|
||||
const h = Math.round(card.offsetHeight) || 600;
|
||||
if (cv.width !== w) cv.width = w;
|
||||
if (cv.height !== h) cv.height = h;
|
||||
}
|
||||
|
||||
/* Wait two frames so card.offsetHeight reflects final layout */
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
resizeCanvas();
|
||||
});
|
||||
});
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
|
||||
/* Track card height changes (font load, zoom, etc.) */
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
const ro = new ResizeObserver(resizeCanvas);
|
||||
const card = document.getElementById("sky-login-card");
|
||||
if (card) ro.observe(card);
|
||||
}
|
||||
|
||||
/* Dynamic accessors — always reflect current canvas size */
|
||||
const dim = {
|
||||
get W() {
|
||||
return cv.width;
|
||||
},
|
||||
get H() {
|
||||
return cv.height;
|
||||
},
|
||||
get CX() {
|
||||
return cv.width / 2;
|
||||
},
|
||||
get CY() {
|
||||
return cv.height / 2;
|
||||
},
|
||||
};
|
||||
|
||||
/* ── 3D GEOMETRY ──
|
||||
Raw coordinates in mm, centred on AX / OZ.
|
||||
Scale (SC) applied dynamically in proj() every frame.
|
||||
X range : 64.491..151.341 = 86.85 mm centre AX = 107.916
|
||||
Z range : 50.926..94.147 = 43.22 mm centre OZ = 72.536
|
||||
D = half-depth of extrusion (mm)
|
||||
*/
|
||||
const AX = 107.916,
|
||||
OZ = 72.536,
|
||||
D = 9;
|
||||
|
||||
/* Logo is rendered in the TOP THIRD of the canvas.
|
||||
CY_LOGO = vertical centre of the logo rendering area.
|
||||
Scale fits the logo into the top portion with margin. */
|
||||
const LOGO_ZONE = 0.42; /* logo uses top 42% of canvas height */
|
||||
|
||||
function getSC() {
|
||||
const availH = dim.H * LOGO_ZONE;
|
||||
const scX = dim.W / (86.85 + D * 2);
|
||||
const scZ = availH / (43.22 + D * 2);
|
||||
return Math.min(scX, scZ) * 0.86;
|
||||
}
|
||||
|
||||
/* Vertical centre of the logo zone */
|
||||
function getLogoY() {
|
||||
return (dim.H * LOGO_ZONE) / 2;
|
||||
}
|
||||
|
||||
function sv(x, z) {
|
||||
return [x - AX, z - OZ];
|
||||
}
|
||||
|
||||
/* Diamond (rotated square) */
|
||||
const LO = [
|
||||
sv(106.916, 51.0),
|
||||
sv(119.2, 61.8),
|
||||
sv(107.8, 74.5),
|
||||
sv(95.812, 62.842),
|
||||
];
|
||||
/* Right quadrilateral */
|
||||
const FD = [
|
||||
sv(121.188, 63.842),
|
||||
sv(151.341, 94.147),
|
||||
sv(109.186, 94.147),
|
||||
sv(109.186, 77.044),
|
||||
];
|
||||
/* Left quadrilateral (mirror of FD) */
|
||||
const FG = [
|
||||
sv(93.748, 64.742),
|
||||
sv(106.646, 77.044),
|
||||
sv(106.646, 94.147),
|
||||
sv(64.491, 94.147),
|
||||
];
|
||||
|
||||
/* Extrude a 2D polygon into a 3D prism.
|
||||
Returns {verts, edges}.
|
||||
Front faces: indices 0..n-1 (y = -D)
|
||||
Back faces: indices n..2n-1 (y = +D) */
|
||||
function extrude(pts2d) {
|
||||
const n = pts2d.length;
|
||||
const verts = [
|
||||
...pts2d.map(([x, z]) => [x, -D, z]),
|
||||
...pts2d.map(([x, z]) => [x, D, z]),
|
||||
];
|
||||
const edges = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
edges.push([i, j, "f"]); /* front edge */
|
||||
edges.push([i + n, j + n, "b"]); /* back edge */
|
||||
edges.push([i, i + n, "s"]); /* side edge */
|
||||
}
|
||||
return { verts, edges };
|
||||
}
|
||||
|
||||
const shapes = [extrude(LO), extrude(FD), extrude(FG)];
|
||||
|
||||
/* ── PERSPECTIVE PROJECTION ──
|
||||
SC applied here every frame so resizing is instant.
|
||||
Logo is centred on getLogoY() (top zone), not dim.CY. */
|
||||
function proj([x, y, z]) {
|
||||
const sc = getSC();
|
||||
const s = 1.0 / (1.0 + y / 500.0);
|
||||
return [dim.CX + x * sc * s, getLogoY() + z * sc * s, s, y];
|
||||
}
|
||||
|
||||
/* ── ROTATION HELPERS ── */
|
||||
function rx(p, a) {
|
||||
const c = Math.cos(a),
|
||||
s = Math.sin(a);
|
||||
return p.map(([x, y, z]) => [x, y * c - z * s, y * s + z * c]);
|
||||
}
|
||||
function ry(p, a) {
|
||||
const c = Math.cos(a),
|
||||
s = Math.sin(a);
|
||||
return p.map(([x, y, z]) => [x * c + z * s, y, -x * s + z * c]);
|
||||
}
|
||||
function rz(p, a) {
|
||||
const c = Math.cos(a),
|
||||
s = Math.sin(a);
|
||||
return p.map(([x, y, z]) => [x * c - y * s, x * s + y * c, z]);
|
||||
}
|
||||
|
||||
/* ── FLUORESCENT TUBE EFFECT ──
|
||||
4-pass rendering: wide glow → mid glow → tight glow → core wire */
|
||||
function tube(x1, y1, x2, y2, bright, al) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.strokeStyle = "rgba(100,180,255,0.05)";
|
||||
ctx.lineWidth = 20;
|
||||
ctx.globalAlpha = al * 0.55;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.strokeStyle = bright
|
||||
? "rgba(60,190,255,0.20)"
|
||||
: "rgba(15,80,200,0.08)";
|
||||
ctx.lineWidth = 8;
|
||||
ctx.globalAlpha = al * 0.88;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.strokeStyle = bright
|
||||
? "rgba(150,225,255,0.65)"
|
||||
: "rgba(45,120,255,0.22)";
|
||||
ctx.lineWidth = 2.4;
|
||||
ctx.globalAlpha = al;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.strokeStyle = bright
|
||||
? "rgba(225,246,255,0.98)"
|
||||
: "rgba(85,165,255,0.52)";
|
||||
ctx.lineWidth = 0.65;
|
||||
ctx.globalAlpha = al;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/* ── CRT ARTEFACT STATE ── */
|
||||
let t = 0,
|
||||
glT = 0,
|
||||
glL = [],
|
||||
scanY = 0,
|
||||
scanOn = false,
|
||||
scanTmr = 0;
|
||||
let flickAlpha = 1.0,
|
||||
flickTarget = 1.0,
|
||||
flickTimer = 0;
|
||||
|
||||
function spawnGlitch() {
|
||||
glL = [];
|
||||
const n = (2 + Math.random() * 5) | 0;
|
||||
for (let i = 0; i < n; i++)
|
||||
glL.push({
|
||||
y: (Math.random() * dim.H) | 0,
|
||||
h: (1 + Math.random() * 5) | 0,
|
||||
dx: (Math.random() - 0.5) * 26,
|
||||
a: 0.28 + Math.random() * 0.5,
|
||||
});
|
||||
glT = (3 + Math.random() * 8) | 0;
|
||||
}
|
||||
|
||||
/* ── MAIN RENDER LOOP ── */
|
||||
function draw() {
|
||||
const W = dim.W,
|
||||
H = dim.H,
|
||||
CX = dim.CX;
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
/* Global flicker (all shapes at once) */
|
||||
if (--flickTimer <= 0) {
|
||||
flickTarget = Math.random() < 0.06 ? 0.55 + Math.random() * 0.35 : 1.0;
|
||||
flickTimer = (2 + Math.random() * 12) | 0;
|
||||
}
|
||||
flickAlpha += (flickTarget - flickAlpha) * 0.18;
|
||||
|
||||
/* Slow tilted rotation */
|
||||
const ax = t * 0.0042 + 0.2,
|
||||
ay = t * 0.0078;
|
||||
//const ax = 0;
|
||||
//ay = 0;
|
||||
|
||||
const allEdges = [];
|
||||
|
||||
shapes.forEach(function ({ verts, edges }) {
|
||||
let pts = verts;
|
||||
pts = rx(pts, ax);
|
||||
pts = ry(pts, ay);
|
||||
pts = rz(pts, 0.07);
|
||||
const pr = pts.map(proj);
|
||||
edges.forEach(function ([a, b, tp]) {
|
||||
allEdges.push({ pr, a, b, tp, d: (pr[a][3] + pr[b][3]) * 0.5 });
|
||||
});
|
||||
});
|
||||
|
||||
/* Ambient glow centred on logo zone */
|
||||
const logoY = getLogoY();
|
||||
const g = ctx.createRadialGradient(CX, logoY, 5, CX, logoY, 100);
|
||||
g.addColorStop(0, "rgba(0,65,195," + 0.14 * flickAlpha + ")");
|
||||
g.addColorStop(1, "rgba(0,0,0,0)");
|
||||
ctx.fillStyle = g;
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
/* Painter's algorithm — back to front */
|
||||
allEdges.sort(function (u, v) {
|
||||
return v.d - u.d;
|
||||
});
|
||||
allEdges.forEach(function ({ pr, a, b, tp, d }) {
|
||||
const [ax2, ay2] = pr[a],
|
||||
[bx, by] = pr[b];
|
||||
const d01 = (d + D) / (2 * D);
|
||||
const bright = tp === "f";
|
||||
const base =
|
||||
tp === "f"
|
||||
? 0.52 + d01 * 0.46
|
||||
: tp === "b"
|
||||
? 0.07 + d01 * 0.06
|
||||
: 0.17 + d01 * 0.22;
|
||||
tube(ax2, ay2, bx, by, bright, base * flickAlpha);
|
||||
});
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
/* Glitch: horizontal band shifts + compression block artefacts */
|
||||
if (glT > 0) {
|
||||
glT--;
|
||||
glL.forEach(function (gl) {
|
||||
try {
|
||||
const sl = ctx.getImageData(0, gl.y, W, gl.h);
|
||||
ctx.globalAlpha = gl.a;
|
||||
ctx.putImageData(sl, gl.dx, gl.y);
|
||||
} catch (e) {}
|
||||
ctx.fillStyle = "rgba(0,150,255,0.04)";
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillRect(0, gl.y, W, gl.h);
|
||||
});
|
||||
if (Math.random() < 0.4) {
|
||||
ctx.globalAlpha = 0.08;
|
||||
ctx.fillStyle = "#00eeff";
|
||||
ctx.fillRect(0, (Math.random() * H) | 0, W, 1);
|
||||
}
|
||||
if (Math.random() < 0.25) {
|
||||
ctx.globalAlpha = 0.12;
|
||||
ctx.fillStyle = "rgba(0,80,220,0.5)";
|
||||
ctx.fillRect(
|
||||
(Math.random() * (W - 40)) | 0,
|
||||
(Math.random() * (H - 8)) | 0,
|
||||
(18 + Math.random() * 38) | 0,
|
||||
(2 + Math.random() * 7) | 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Rolling scan line */
|
||||
if (scanOn) {
|
||||
ctx.globalAlpha = 0.06;
|
||||
ctx.fillStyle = "rgba(80,200,255,1)";
|
||||
ctx.fillRect(0, scanY, W, 2);
|
||||
scanY = (scanY + 3) % H;
|
||||
if (--scanTmr <= 0) scanOn = false;
|
||||
}
|
||||
|
||||
/* Phosphor burn (persistence) */
|
||||
if (Math.random() < 0.016) {
|
||||
ctx.globalAlpha = 0.025;
|
||||
ctx.fillStyle = "rgba(0,120,255,1)";
|
||||
ctx.fillRect(
|
||||
(CX - 50 + Math.random() * 100) | 0,
|
||||
(logoY - 30 + Math.random() * 60) | 0,
|
||||
(Math.random() * 25) | 0,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
/* Random artefact triggers */
|
||||
if (Math.random() < 0.012) spawnGlitch();
|
||||
if (Math.random() < 0.006) {
|
||||
scanOn = true;
|
||||
scanY = 0;
|
||||
scanTmr = (20 + Math.random() * 30) | 0;
|
||||
}
|
||||
|
||||
t++;
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
draw();
|
||||
} /* end init() */
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
||||
/* ── BRIDGE: Skynet UI → hidden Roundcube form ── */
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
/* Detect Roundcube auth error and show Skynet error box */
|
||||
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";
|
||||
}
|
||||
|
||||
/* Submit handler: copy values then submit real RC form */
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const btn = document.getElementById("sky-submit-btn");
|
||||
btn.textContent = "AUTHENTIFICATION...";
|
||||
btn.style.borderColor = "var(--cyan)";
|
||||
btn.style.color = "var(--cyan)";
|
||||
|
||||
const rcForm = document.getElementById("login-form");
|
||||
if (rcForm) {
|
||||
rcForm.style.display = "block";
|
||||
rcForm.submit();
|
||||
}
|
||||
});
|
||||
|
||||
/* Submit on Enter key */
|
||||
["sky-user", "sky-pass"].forEach(function (id) {
|
||||
document.getElementById(id).addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") document.getElementById("sky-submit-btn").click();
|
||||
});
|
||||
});
|
||||
|
||||
/* Auto-focus username field */
|
||||
document.getElementById("sky-user").focus();
|
||||
});
|
||||
|
||||
/* ── FULLSCREEN PARTICLE 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 drawBg() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
/* Data streams */
|
||||
streams.forEach(function (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;
|
||||
}
|
||||
});
|
||||
|
||||
/* Floating particles */
|
||||
particles.forEach(function (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;
|
||||
});
|
||||
|
||||
/* Connection lines between nearby particles */
|
||||
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.globalAlpha = (1 - dist / 100) * 0.3;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.strokeStyle = "#00cfff";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(drawBg);
|
||||
}
|
||||
drawBg();
|
||||
Référencer dans un nouveau ticket
Bloquer un utilisateur