<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aurora Borealis</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000208;
height: 100vh;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let W = canvas.width = window.innerWidth;
let H = canvas.height = window.innerHeight;
window.addEventListener('resize', () => {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
initStars();
initNebula();
});
// ----- Mouse parallax -----
let mouseNorm = 0; // -1 to 1
window.addEventListener('mousemove', e => {
mouseNorm = (e.clientX / W - 0.5) * 2;
});
// ----- Stars -----
let stars = [];
function initStars() {
stars = [];
for (let i = 0; i < 420; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.75,
size: Math.random() * 2.0 + 0.2,
twinkle: Math.random() * Math.PI * 2,
twinkleSpeed: Math.random() * 0.025 + 0.008,
brightness: Math.random() * 0.5 + 0.4
});
}
}
initStars();
function drawStars() {
for (const s of stars) {
s.twinkle += s.twinkleSpeed;
const alpha = s.brightness + Math.sin(s.twinkle) * 0.35;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(220, 240, 255, ${Math.max(0, Math.min(1, alpha))})`;
ctx.fill();
}
}
// ----- Nebula background glows -----
let nebulae = [];
function initNebula() {
nebulae = [
{ x: W * 0.15, y: H * 0.2, r: W * 0.35, color: 'rgba(0,80,160,0.06)', speed: 0.0003 },
{ x: W * 0.75, y: H * 0.1, r: W * 0.30, color: 'rgba(80,0,160,0.05)', speed: 0.0004 },
{ x: W * 0.50, y: H * 0.35, r: W * 0.45, color: 'rgba(0,60,120,0.04)', speed: 0.0002 }
];
}
initNebula();
let nebulaT = 0;
function drawNebulae() {
nebulaT += 0.004;
for (const n of nebulae) {
const ox = Math.sin(nebulaT * n.speed * 1000) * 40;
const oy = Math.cos(nebulaT * n.speed * 1000 * 0.7) * 20;
const grad = ctx.createRadialGradient(n.x + ox, n.y + oy, 0, n.x + ox, n.y + oy, n.r);
grad.addColorStop(0, n.color);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
}
// ----- Aurora Bands -----
const AURORA_BANDS = [
{ yBase: 0.20, amplitude: 55, freqA: 0.0028, freqB: 0.0065, freqC: 0.013, speedA: 0.007, speedB: 0.011, speedC: 0.004, color: [0, 255, 140], alpha: 0.30, thickness: 0.12 },
{ yBase: 0.28, amplitude: 45, freqA: 0.0035, freqB: 0.0080, freqC: 0.016, speedA: 0.010, speedB: 0.015, speedC: 0.006, color: [0, 190, 255], alpha: 0.25, thickness: 0.10 },
{ yBase: 0.38, amplitude: 35, freqA: 0.0045, freqB: 0.0055, freqC: 0.010, speedA: 0.006, speedB: 0.009, speedC: 0.003, color: [130, 0, 255], alpha: 0.20, thickness: 0.08 },
{ yBase: 0.48, amplitude: 28, freqA: 0.0060, freqB: 0.0100, freqC: 0.020, speedA: 0.009, speedB: 0.013, speedC: 0.007, color: [255, 0, 140], alpha: 0.15, thickness: 0.07 },
];
let auroraTime = Array(AURORA_BANDS.length).fill(0);
function drawAurora() {
for (let b = 0; b < AURORA_BANDS.length; b++) {
const band = AURORA_BANDS[b];
auroraTime[b] += 1;
const t = auroraTime[b];
const T = t + mouseNorm * 18;
const pts = [];
const steps = 120;
for (let i = 0; i <= steps; i++) {
const x = (i / steps) * W;
const wave = Math.sin(x * band.freqA + T * band.speedA) * band.amplitude
+ Math.sin(x * band.freqB + T * band.speedB) * (band.amplitude * 0.55)
+ Math.sin(x * band.freqC + T * band.speedC) * (band.amplitude * 0.25);
pts.push({ x, y: H * band.yBase + wave });
}
const bandH = H * band.thickness;
const [r, g, bl] = band.color;
// Render upward sweep (sky glow)
ctx.beginPath();
ctx.moveTo(0, H * band.yBase - bandH * 0.5);
for (let i = 0; i <= steps; i++) {
if (i === 0) ctx.lineTo(pts[i].x, pts[i].y - bandH);
else {
const prev = pts[i - 1];
const cx = (prev.x + pts[i].x) / 2;
const cy = ((prev.y - bandH) + (pts[i].y - bandH)) / 2;
ctx.quadraticCurveTo(prev.x, prev.y - bandH, cx, cy);
}
}
for (let i = steps; i >= 0; i--) {
const prev = i < steps ? pts[i + 1] : pts[steps];
const cx = i < steps ? (prev.x + pts[i].x) / 2 : pts[steps].x;
const cy = i < steps ? (prev.y + pts[i].y) / 2 : pts[steps].y;
if (i === steps) ctx.lineTo(pts[i].x, pts[i].y);
else ctx.quadraticCurveTo(prev.x, prev.y, cx, cy);
}
ctx.closePath();
const grad = ctx.createLinearGradient(0, pts[0].y - bandH, 0, pts[0].y + bandH * 0.5);
grad.addColorStop(0, `rgba(${r},${g},${bl},0)`);
grad.addColorStop(0.3, `rgba(${r},${g},${bl},${band.alpha})`);
grad.addColorStop(0.6, `rgba(${r},${g},${bl},${band.alpha * 0.6})`);
grad.addColorStop(1, `rgba(${r},${g},${bl},0)`);
ctx.fillStyle = grad;
ctx.fill();
}
}
// ----- Shooting Stars -----
let shooters = [];
let nextShooter = 4000 + Math.random() * 5000;
let shooterTimer = 0;
function spawnShooter() {
shooters.push({
x: Math.random() * W * 0.6,
y: Math.random() * H * 0.3,
vx: 8 + Math.random() * 6,
vy: 3 + Math.random() * 4,
life: 1.0,
decay: 0.025 + Math.random() * 0.02
});
}
function drawShooters(dt) {
shooterTimer += dt;
if (shooterTimer > nextShooter) {
spawnShooter();
shooterTimer = 0;
nextShooter = 4000 + Math.random() * 6000;
}
shooters = shooters.filter(s => s.life > 0);
for (const s of shooters) {
const tailLen = 80 * s.life;
const grad = ctx.createLinearGradient(s.x - s.vx * 4, s.y - s.vy * 4, s.x, s.y);
grad.addColorStop(0, 'transparent');
grad.addColorStop(1, `rgba(255, 255, 255, ${s.life * 0.9})`);
ctx.beginPath();
ctx.moveTo(s.x - s.vx * (tailLen / s.vx), s.y - s.vy * (tailLen / s.vy));
ctx.lineTo(s.x, s.y);
ctx.strokeStyle = grad;
ctx.lineWidth = 1.5 * s.life;
ctx.stroke();
s.x += s.vx;
s.y += s.vy;
s.life -= s.decay;
}
}
let last = 0;
function animate(ts) {
const dt = ts - last;
last = ts;
ctx.fillStyle = '#000208';
ctx.fillRect(0, 0, W, H);
drawNebulae();
drawStars();
drawAurora();
drawShooters(dt);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
</script>
</body>
</html>
<canvas id="canvas"></canvas>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000208;
height: 100vh;
}
canvas {
display: block;
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let W = canvas.width = window.innerWidth;
let H = canvas.height = window.innerHeight;
window.addEventListener('resize', () => {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
initStars();
initNebula();
});
// ----- Mouse parallax -----
let mouseNorm = 0; // -1 to 1
window.addEventListener('mousemove', e => {
mouseNorm = (e.clientX / W - 0.5) * 2;
});
// ----- Stars -----
let stars = [];
function initStars() {
stars = [];
for (let i = 0; i < 420; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.75,
size: Math.random() * 2.0 + 0.2,
twinkle: Math.random() * Math.PI * 2,
twinkleSpeed: Math.random() * 0.025 + 0.008,
brightness: Math.random() * 0.5 + 0.4
});
}
}
initStars();
function drawStars() {
for (const s of stars) {
s.twinkle += s.twinkleSpeed;
const alpha = s.brightness + Math.sin(s.twinkle) * 0.35;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(220, 240, 255, ${Math.max(0, Math.min(1, alpha))})`;
ctx.fill();
}
}
// ----- Nebula background glows -----
let nebulae = [];
function initNebula() {
nebulae = [
{ x: W * 0.15, y: H * 0.2, r: W * 0.35, color: 'rgba(0,80,160,0.06)', speed: 0.0003 },
{ x: W * 0.75, y: H * 0.1, r: W * 0.30, color: 'rgba(80,0,160,0.05)', speed: 0.0004 },
{ x: W * 0.50, y: H * 0.35, r: W * 0.45, color: 'rgba(0,60,120,0.04)', speed: 0.0002 }
];
}
initNebula();
let nebulaT = 0;
function drawNebulae() {
nebulaT += 0.004;
for (const n of nebulae) {
const ox = Math.sin(nebulaT * n.speed * 1000) * 40;
const oy = Math.cos(nebulaT * n.speed * 1000 * 0.7) * 20;
const grad = ctx.createRadialGradient(n.x + ox, n.y + oy, 0, n.x + ox, n.y + oy, n.r);
grad.addColorStop(0, n.color);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
}
// ----- Aurora Bands -----
const AURORA_BANDS = [
{ yBase: 0.20, amplitude: 55, freqA: 0.0028, freqB: 0.0065, freqC: 0.013, speedA: 0.007, speedB: 0.011, speedC: 0.004, color: [0, 255, 140], alpha: 0.30, thickness: 0.12 },
{ yBase: 0.28, amplitude: 45, freqA: 0.0035, freqB: 0.0080, freqC: 0.016, speedA: 0.010, speedB: 0.015, speedC: 0.006, color: [0, 190, 255], alpha: 0.25, thickness: 0.10 },
{ yBase: 0.38, amplitude: 35, freqA: 0.0045, freqB: 0.0055, freqC: 0.010, speedA: 0.006, speedB: 0.009, speedC: 0.003, color: [130, 0, 255], alpha: 0.20, thickness: 0.08 },
{ yBase: 0.48, amplitude: 28, freqA: 0.0060, freqB: 0.0100, freqC: 0.020, speedA: 0.009, speedB: 0.013, speedC: 0.007, color: [255, 0, 140], alpha: 0.15, thickness: 0.07 },
];
let auroraTime = Array(AURORA_BANDS.length).fill(0);
function drawAurora() {
for (let b = 0; b < AURORA_BANDS.length; b++) {
const band = AURORA_BANDS[b];
auroraTime[b] += 1;
const t = auroraTime[b];
const T = t + mouseNorm * 18;
const pts = [];
const steps = 120;
for (let i = 0; i <= steps; i++) {
const x = (i / steps) * W;
const wave = Math.sin(x * band.freqA + T * band.speedA) * band.amplitude
+ Math.sin(x * band.freqB + T * band.speedB) * (band.amplitude * 0.55)
+ Math.sin(x * band.freqC + T * band.speedC) * (band.amplitude * 0.25);
pts.push({ x, y: H * band.yBase + wave });
}
const bandH = H * band.thickness;
const [r, g, bl] = band.color;
// Render upward sweep (sky glow)
ctx.beginPath();
ctx.moveTo(0, H * band.yBase - bandH * 0.5);
for (let i = 0; i <= steps; i++) {
if (i === 0) ctx.lineTo(pts[i].x, pts[i].y - bandH);
else {
const prev = pts[i - 1];
const cx = (prev.x + pts[i].x) / 2;
const cy = ((prev.y - bandH) + (pts[i].y - bandH)) / 2;
ctx.quadraticCurveTo(prev.x, prev.y - bandH, cx, cy);
}
}
for (let i = steps; i >= 0; i--) {
const prev = i < steps ? pts[i + 1] : pts[steps];
const cx = i < steps ? (prev.x + pts[i].x) / 2 : pts[steps].x;
const cy = i < steps ? (prev.y + pts[i].y) / 2 : pts[steps].y;
if (i === steps) ctx.lineTo(pts[i].x, pts[i].y);
else ctx.quadraticCurveTo(prev.x, prev.y, cx, cy);
}
ctx.closePath();
const grad = ctx.createLinearGradient(0, pts[0].y - bandH, 0, pts[0].y + bandH * 0.5);
grad.addColorStop(0, `rgba(${r},${g},${bl},0)`);
grad.addColorStop(0.3, `rgba(${r},${g},${bl},${band.alpha})`);
grad.addColorStop(0.6, `rgba(${r},${g},${bl},${band.alpha * 0.6})`);
grad.addColorStop(1, `rgba(${r},${g},${bl},0)`);
ctx.fillStyle = grad;
ctx.fill();
}
}
// ----- Shooting Stars -----
let shooters = [];
let nextShooter = 4000 + Math.random() * 5000;
let shooterTimer = 0;
function spawnShooter() {
shooters.push({
x: Math.random() * W * 0.6,
y: Math.random() * H * 0.3,
vx: 8 + Math.random() * 6,
vy: 3 + Math.random() * 4,
life: 1.0,
decay: 0.025 + Math.random() * 0.02
});
}
function drawShooters(dt) {
shooterTimer += dt;
if (shooterTimer > nextShooter) {
spawnShooter();
shooterTimer = 0;
nextShooter = 4000 + Math.random() * 6000;
}
shooters = shooters.filter(s => s.life > 0);
for (const s of shooters) {
const tailLen = 80 * s.life;
const grad = ctx.createLinearGradient(s.x - s.vx * 4, s.y - s.vy * 4, s.x, s.y);
grad.addColorStop(0, 'transparent');
grad.addColorStop(1, `rgba(255, 255, 255, ${s.life * 0.9})`);
ctx.beginPath();
ctx.moveTo(s.x - s.vx * (tailLen / s.vx), s.y - s.vy * (tailLen / s.vy));
ctx.lineTo(s.x, s.y);
ctx.strokeStyle = grad;
ctx.lineWidth = 1.5 * s.life;
ctx.stroke();
s.x += s.vx;
s.y += s.vy;
s.life -= s.decay;
}
}
let last = 0;
function animate(ts) {
const dt = ts - last;
last = ts;
ctx.fillStyle = '#000208';
ctx.fillRect(0, 0, W, H);
drawNebulae();
drawStars();
drawAurora();
drawShooters(dt);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);