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]>
This commit is contained in:
esefandClaude Opus 5 committed 2026-10-18 15:00:56 +03:00
commit 9abf266444
31 files changed
+2602

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
/* ═══════════════════════════════════════════════════════════════════════════
Lightning Effect — canvas-based, matches ReVoltVPN Flutter LightningEffect
Random jagged yellow bolts with glow, 2-10 sec intervals, double-strike
═══════════════════════════════════════════════════════════════════════════ */
(function () {
const canvas = document.getElementById('bg-lightning');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
/* ── Configuration (mirrors LightningEffect defaults) ────────────────── */
const MIN_INTERVAL_SEC = 2;
const MAX_INTERVAL_SEC = 10;
const DOUBLE_CHANCE = 0.2;
/* ── State ──────────────────────────────────────────────────────────── */
let width = 0;
let height = 0;
let bolt = null; // current LightningBolt or null
let opacity = 0; // current opacity (driven by easing)
let startTime = 0; // performance.now() at strike start
const DURATION_MS = 400; // total animation duration
let animId = null;
let timerId = null;
/* ── LightningBolt model ──────────────────────────────────────────────── */
class LightningBolt {
/**
* @param {Array<{x:number, y:number}>} mainPath — zigzag from top to bottom
* @param {Array<Array<{x:number, y:number}>>} branches — fork paths
*/
constructor(mainPath, branches = []) {
this.mainPath = mainPath;
this.branches = branches;
}
/** Generate a random bolt — matches Flutter LightningBolt.random(). */
static random() {
const points = [];
// Start: top, slight horizontal offset
let x = 0.5 + (Math.random() - 0.5) * 0.15;
points.push({ x, y: 0.0 });
// Zigzag down — 6-10 segments
const segments = 6 + Math.floor(Math.random() * 5);
for (let i = 1; i < segments; i++) {
const y = i / segments;
x += (Math.random() - 0.5) * 0.25;
x = clamp(x, 0.05, 0.95);
points.push({ x, y });
}
// End: bottom
points.push({ x: clamp(x, 0.1, 0.9), y: 1.0 });
// Branches — 0-2 forks
const branches = [];
const branchCount = Math.floor(Math.random() * 3);
for (let b = 0; b < branchCount; b++) {
const branch = [];
const startIdx = 1 + Math.floor(Math.random() * (points.length - 2));
const start = points[startIdx];
branch.push({ ...start });
let bx = start.x;
let by = start.y;
const branchLen = 2 + Math.floor(Math.random() * 3);
for (let j = 0; j < branchLen; j++) {
by += 0.05 + Math.random() * 0.12;
bx += (Math.random() - 0.5) * 0.3;
bx = clamp(bx, 0.02, 0.98);
by = clamp(by, 0.0, 1.0);
branch.push({ x: bx, y: by });
}
branches.push(branch);
}
return new LightningBolt(points, branches);
}
}
/* ── Easing: sharp flash curve (fast on, fast off, no slow fade) ────── */
function flashOpacity(t) {
if (t < 0.08) return t / 0.08; // fast ramp 0→1 in 8% of time
return 1.0 - ((t - 0.08) / 0.92); // linear decay over rest of duration
}
/* ── Schedule next strike ────────────────────────────────────────────── */
function scheduleNext() {
const secs = MIN_INTERVAL_SEC + Math.floor(Math.random() * (MAX_INTERVAL_SEC - MIN_INTERVAL_SEC + 1));
timerId = setTimeout(strike, secs * 1000);
}
/* ── Strike ──────────────────────────────────────────────────────────── */
function strike() {
bolt = LightningBolt.random();
startTime = performance.now();
// Double strike?
if (Math.random() < DOUBLE_CHANCE) {
setTimeout(() => {
bolt = LightningBolt.random();
startTime = performance.now();
}, 120);
}
scheduleNext();
}
/* ── 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);
}
/* ── Draw a single path (main or branch) ─────────────────────────────── */
function drawPath(pts, px, pn) {
if (pts.length < 2) return;
ctx.beginPath();
ctx.moveTo(pts[0].x * width, pts[0].y * height);
for (let i = 1; i < pts.length; i++) {
ctx.lineTo(pts[i].x * width, pts[i].y * height);
}
ctx.stroke();
}
/* ── Render frame ────────────────────────────────────────────────────── */
function render() {
if (!bolt) { opacity = 0; return; }
const elapsed = performance.now() - startTime;
const t = Math.min(elapsed / DURATION_MS, 1.0);
opacity = flashOpacity(t);
if (opacity <= 0.02) { bolt = null; opacity = 0; return; }
}
/* ── Animation loop ──────────────────────────────────────────────────── */
function tick() {
animId = requestAnimationFrame(tick);
render();
ctx.clearRect(0, 0, width, height);
if (!bolt || opacity <= 0.02) return;
const baseAlpha = Math.round(opacity * 255);
const glowAlpha = Math.round(opacity * 0.35 * 255);
// Glow layer — wide, blurred
ctx.save();
ctx.strokeStyle = `rgba(255, 214, 0, ${(glowAlpha / 255).toFixed(3)})`;
ctx.lineWidth = 18;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.shadowColor = `rgba(255, 214, 0, ${(glowAlpha / 255).toFixed(3)})`;
ctx.shadowBlur = 8;
drawPath(bolt.mainPath);
for (const b of bolt.branches) drawPath(b);
ctx.restore();
// Core — medium
ctx.strokeStyle = `rgba(255, 214, 0, ${(baseAlpha / 255).toFixed(3)})`;
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
drawPath(bolt.mainPath);
for (const b of bolt.branches) drawPath(b);
// Hot center — white, only when bright
if (opacity > 0.6) {
const hotAlpha = Math.round(opacity * 180);
ctx.strokeStyle = `rgba(255, 255, 255, ${(hotAlpha / 255).toFixed(3)})`;
ctx.lineWidth = 2;
drawPath(bolt.mainPath);
for (const b of bolt.branches) drawPath(b);
}
}
/* ── Helpers ──────────────────────────────────────────────────────────── */
function clamp(v, min, max) { return v < min ? min : v > max ? max : v; }
/* ── Bootstrap ────────────────────────────────────────────────────────── */
resize();
window.addEventListener('resize', resize);
animId = requestAnimationFrame(tick);
scheduleNext();
})();
+104
View File
@@ -0,0 +1,104 @@
/* ═══════════════════════════════════════════════════════════════════════════
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);
})();
+104
View File
@@ -0,0 +1,104 @@
/* ═══════════════════════════════════════════════════════════════════════════
tabs.js — Tab system
- Toggles .active on .tab-pane sections already in the HTML
- Hash routing, back/forward
═══════════════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
var TABS = ['home', 'features', 'why', 'faq', 'contact', 'download', 'github', 'terms', 'privacy'];
var contentEl = document.getElementById('tab-content');
var navLinks = document.querySelectorAll('.nav__tab');
function switchTo(name) {
contentEl.querySelectorAll('.tab-pane').forEach(function (p) { p.classList.remove('active'); });
var target = document.getElementById('tab-' + name);
if (target) target.classList.add('active');
navLinks.forEach(function (link) {
link.classList.toggle('active', link.getAttribute('href') === '#' + name);
});
contentEl.scrollTop = 0;
if (window.location.hash !== '#' + name) history.pushState(null, '', '#' + name);
var titles = {
home: 'ReVoltVPN', features: 'Features — ReVoltVPN', why: 'Why ReVolt — ReVoltVPN',
faq: 'FAQ — ReVoltVPN', contact: 'Contact — ReVoltVPN', download: 'Download — ReVoltVPN', github: 'Source — ReVoltVPN',
terms: 'Terms of Service — ReVoltVPN', privacy: 'Privacy Policy — ReVoltVPN'
};
if (titles[name]) document.title = titles[name];
}
navLinks.forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
var name = this.getAttribute('href').replace('#', '');
if (TABS.indexOf(name) !== -1) {
switchTo(name);
closeMenu();
}
});
});
/* ── Hamburger menu ──────────────────────────────────────────────────── */
var navEl = document.getElementById('nav');
var hamburger = document.querySelector('.nav__hamburger');
function openMenu() {
navEl.classList.add('nav--open');
hamburger.setAttribute('aria-expanded', 'true');
}
function closeMenu() {
navEl.classList.remove('nav--open');
hamburger.setAttribute('aria-expanded', 'false');
}
if (hamburger) {
hamburger.addEventListener('click', function () {
navEl.classList.contains('nav--open') ? closeMenu() : openMenu();
});
/* Close when clicking outside */
document.addEventListener('click', function (e) {
if (!navEl.contains(e.target)) closeMenu();
});
/* Close on Escape */
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeMenu();
});
}
window.addEventListener('popstate', function () {
var hash = window.location.hash.replace('#', '');
switchTo(TABS.indexOf(hash) !== -1 ? hash : 'home');
});
var el = document.getElementById('home-headline');
if (el) {
var headlines = [
'A VPN that does not ask who you are.',
'Privacy is not a subscription.',
'Your ISP is not your friend.',
'Encryption should not require an account.',
'The internet, without the surveillance.',
'You are not the product.',
'A VPN that minds its own business.',
'Your data belongs to you. Not your ISP.',
'Open source. Closed mouths.',
'Browse like nobody is watching. Because nobody is.',
'The VPN that forgets you immediately.',
'Free speech deserves free tools.',
'You decide how you pay. Not your data.',
'Privacy first. Payment optional.'
];
el.textContent = headlines[Math.floor(Math.random() * headlines.length)];
}
var hash = window.location.hash.replace('#', '');
switchTo(TABS.indexOf(hash) !== -1 ? hash : 'home');
})();