Files
esefandClaude Opus 5 9abf266444 ReVoltVPN website
The landing page for ReVoltVPN, live at userevolt.app.

Vanilla HTML, CSS and JavaScript: no framework, no build step, no
dependencies. Every claim on the page is checked against the app it
describes, which is the reason this is public.

GPL-3.0, same as the app.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-18 15:00:56 +03:00

105 lines
4.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ═══════════════════════════════════════════════════════════════════════════
Rain Effect — canvas-based, matches ReVoltVPN Flutter RainEffect
~150 drops, white/light with slight leftward slant, semi-transparent
Recycles drops to the top when they fall off-screen.
═══════════════════════════════════════════════════════════════════════════ */
(function () {
const canvas = document.getElementById('bg-rain');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
/* ── Configuration (mirrors RainEffect defaults) ────────────────────── */
const DROP_COUNT = 150;
const COLOR = 'rgba(232, 234, 246, {opacity})'; // matches #E8EAF6
const MIN_LENGTH = 12;
const MAX_LENGTH = 44;
const WIND_DRIFT = 4; // slight leftward slant in px
const STROKE_W = 1.5;
/* ── State ──────────────────────────────────────────────────────────── */
const drops = [];
let width = 0;
let height = 0;
let lastTime = 0;
let animId = null;
/* ── Drop model ─────────────────────────────────────────────────────── */
class Drop {
constructor() { this.spawn(); }
/** Reset all properties to a new random drop. */
spawn() {
this.x = Math.random(); // 0–1 normalized
this.y = Math.random(); // 0–1 normalized
this.speed = 0.30 + Math.random() * 0.55; // matches Flutter range
this.length = MIN_LENGTH + Math.random() * (MAX_LENGTH - MIN_LENGTH);
this.opacity = 0.05 + Math.random() * 0.12; // matches Flutter range
}
}
/* ── Initialize drops ────────────────────────────────────────────────── */
for (let i = 0; i < DROP_COUNT; i++) {
drops.push(new Drop());
}
/* ── Resize handler ──────────────────────────────────────────────────── */
function resize() {
const dpr = window.devicePixelRatio || 1;
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
/* ── Animation loop ──────────────────────────────────────────────────── */
function tick(now) {
animId = requestAnimationFrame(tick);
/* Delta time in seconds, clamped */
let dt = 0.016; // ~60fps fallback
if (lastTime !== 0) {
dt = (now - lastTime) / 1000;
if (dt > 0.1) dt = 0.1;
}
lastTime = now;
/* Update drops */
for (const d of drops) {
d.y += d.speed * dt;
if (d.y > 1.08) {
d.spawn();
d.y = -0.06 - Math.random() * 0.08; // start slightly above viewport
}
}
/* Draw */
ctx.clearRect(0, 0, width, height);
ctx.lineCap = 'round';
ctx.lineWidth = STROKE_W;
for (const d of drops) {
const sx = d.x * width;
const sy = d.y * height;
const ex = sx - WIND_DRIFT; // slight leftward slant
const ey = sy + d.length;
ctx.strokeStyle = COLOR.replace('{opacity}', d.opacity.toFixed(3));
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(ex, ey);
ctx.stroke();
}
}
/* ── Bootstrap ───────────────────────────────────────────────────────── */
resize();
window.addEventListener('resize', resize);
animId = requestAnimationFrame(tick);
})();