<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Starfield Warp</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000;
height: 100vh;
}
canvas {
display: block;
}
/* Click warp flash overlay */
#warp-flash {
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0;
background: radial-gradient(ellipse at 50% 50%,
rgba(100, 180, 255, 0) 30%,
rgba(60, 120, 255, 0.4) 70%,
rgba(0, 60, 180, 0.7) 100%);
transition: opacity 0.08s ease-in;
}
#warp-flash.on {
opacity: 1;
transition: opacity 0.08s ease-in;
}
#warp-flash.off {
opacity: 0;
transition: opacity 0.6s ease-out;
}
#hint {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
color: rgba(120, 180, 255, 0.45);
font-family: 'Courier New', monospace;
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
pointer-events: none;
animation: fadehint 1s ease-out 3s forwards;
}
@keyframes fadehint {
to {
opacity: 0;
}
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="warp-flash"></div>
<div id="hint">Scroll to accelerate · Click to warp</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const flash = document.getElementById('warp-flash');
let W = canvas.width = window.innerWidth;
let H = canvas.height = window.innerHeight;
const CX = () => W / 2;
const CY = () => H / 2;
window.addEventListener('resize', () => {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
});
// ----- Speed state -----
let targetSpeed = 5;
let speed = 5;
let warpBurst = 0; // 0–1, decays after click
const MIN_SPD = 2;
const MAX_SPD = 45;
window.addEventListener('wheel', e => {
targetSpeed = Math.max(MIN_SPD, Math.min(MAX_SPD, targetSpeed + e.deltaY * 0.04));
}, { passive: true });
canvas.addEventListener('click', () => {
warpBurst = 1.0;
flash.className = 'on';
setTimeout(() => { flash.className = 'off'; }, 100);
});
// ----- Milky Way band params -----
const BAND_ANGLE = 0.35; // radians
const BAND_WIDTH = H * 0.18;
function inMilkyWay(x, y) {
// Rotate point and check if near diagonal band
const rx = (x - W / 2) * Math.cos(-BAND_ANGLE) - (y - H / 2) * Math.sin(-BAND_ANGLE);
return Math.abs(rx) < BAND_WIDTH / 2;
}
// ----- Stars -----
const STAR_COUNT = 900;
class Star {
constructor(reset = false) { this.init(reset); }
init(reset = false) {
// Milky way density: 30% of stars cluster in the band
if (Math.random() < 0.3) {
// Place star near band
const along = (Math.random() - 0.5) * Math.max(W, H) * 2;
const perp = (Math.random() - 0.5) * BAND_WIDTH;
this.sx = along * Math.cos(BAND_ANGLE) - perp * Math.sin(BAND_ANGLE) + W / 2;
this.sy = along * Math.sin(BAND_ANGLE) + perp * Math.cos(BAND_ANGLE) + H / 2;
} else {
this.sx = (Math.random() - 0.5) * W * 2;
this.sy = (Math.random() - 0.5) * H * 2;
}
this.z = reset ? Math.random() * W : W;
this.pz = this.z;
this.milky = inMilkyWay(this.sx + W / 2, this.sy + H / 2);
}
update(spd) {
this.pz = this.z;
this.z -= spd;
if (this.z <= 1) { this.init(false); }
}
draw(spd) {
const scale = W / 2;
const sx = (this.sx / this.z) * scale + CX();
const sy = (this.sy / this.z) * scale + CY();
if (sx < -10 || sx > W + 10 || sy < -10 || sy > H + 10) return;
const t = 1 - this.z / W; // 0 at far, 1 at near
const r = t * (this.milky ? 1.5 : 2.2);
// Color: blue-white at center → warm gold near edge → pure white
let hue, sat, lit;
if (warpBurst > 0.3 || spd > 22) {
hue = 205; sat = 100; lit = 60 + t * 40;
} else if (this.milky) {
// milky way: cool blue-white
hue = 210; sat = 30; lit = 70 + t * 30;
} else {
hue = 45 + t * 15; sat = 10 + t * 30; lit = 60 + t * 40;
}
const alpha = Math.min(1, t * 1.4 + 0.1);
// Draw streaks at speed
if (spd > 10 || warpBurst > 0.1) {
const px = (this.sx / this.pz) * scale + CX();
const py = (this.sy / this.pz) * scale + CY();
const streakAlpha = alpha * Math.min(1, spd / 20) * (warpBurst > 0 ? 1.4 : 1);
const grad = ctx.createLinearGradient(px, py, sx, sy);
grad.addColorStop(0, `hsla(${hue},${sat}%,${lit}%,0)`);
grad.addColorStop(1, `hsla(${hue},${sat}%,${lit}%,${Math.min(1, streakAlpha)})`);
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(sx, sy);
ctx.strokeStyle = grad;
ctx.lineWidth = r * 0.9;
ctx.stroke();
}
// Draw star point
if (r > 0.3) {
ctx.beginPath();
ctx.arc(sx, sy, r, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${hue},${sat}%,${lit}%,${alpha})`;
ctx.fill();
// Lens cross at high speed
if (spd > 28 || warpBurst > 0.5) {
const arm = r * 5;
ctx.strokeStyle = `hsla(${hue},${sat}%,${lit}%,${alpha * 0.3})`;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(sx - arm, sy); ctx.lineTo(sx + arm, sy);
ctx.moveTo(sx, sy - arm); ctx.lineTo(sx, sy + arm);
ctx.stroke();
}
}
}
}
const stars = Array.from({ length: STAR_COUNT }, () => new Star(true));
function animate() {
// Smooth speed with exponential easing
const effectiveTarget = warpBurst > 0 ? MAX_SPD : targetSpeed;
speed += (effectiveTarget - speed) * 0.05;
warpBurst *= 0.94; // decay warp burst
const trailAlpha = warpBurst > 0.2 ? 0.25 : Math.max(0.08, 1 - speed / 15);
ctx.fillStyle = `rgba(0, 0, 0, ${trailAlpha})`;
ctx.fillRect(0, 0, W, H);
for (const s of stars) {
s.update(speed);
s.draw(speed);
}
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>
<canvas id="canvas"></canvas>
<div id="warp-flash"></div>
<div id="hint">Scroll to accelerate · Click to warp</div>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000;
height: 100vh;
}
canvas {
display: block;
}
/* Click warp flash overlay */
#warp-flash {
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0;
background: radial-gradient(ellipse at 50% 50%,
rgba(100, 180, 255, 0) 30%,
rgba(60, 120, 255, 0.4) 70%,
rgba(0, 60, 180, 0.7) 100%);
transition: opacity 0.08s ease-in;
}
#warp-flash.on {
opacity: 1;
transition: opacity 0.08s ease-in;
}
#warp-flash.off {
opacity: 0;
transition: opacity 0.6s ease-out;
}
#hint {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
color: rgba(120, 180, 255, 0.45);
font-family: 'Courier New', monospace;
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
pointer-events: none;
animation: fadehint 1s ease-out 3s forwards;
}
@keyframes fadehint {
to {
opacity: 0;
}
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const flash = document.getElementById('warp-flash');
let W = canvas.width = window.innerWidth;
let H = canvas.height = window.innerHeight;
const CX = () => W / 2;
const CY = () => H / 2;
window.addEventListener('resize', () => {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
});
// ----- Speed state -----
let targetSpeed = 5;
let speed = 5;
let warpBurst = 0; // 0–1, decays after click
const MIN_SPD = 2;
const MAX_SPD = 45;
window.addEventListener('wheel', e => {
targetSpeed = Math.max(MIN_SPD, Math.min(MAX_SPD, targetSpeed + e.deltaY * 0.04));
}, { passive: true });
canvas.addEventListener('click', () => {
warpBurst = 1.0;
flash.className = 'on';
setTimeout(() => { flash.className = 'off'; }, 100);
});
// ----- Milky Way band params -----
const BAND_ANGLE = 0.35; // radians
const BAND_WIDTH = H * 0.18;
function inMilkyWay(x, y) {
// Rotate point and check if near diagonal band
const rx = (x - W / 2) * Math.cos(-BAND_ANGLE) - (y - H / 2) * Math.sin(-BAND_ANGLE);
return Math.abs(rx) < BAND_WIDTH / 2;
}
// ----- Stars -----
const STAR_COUNT = 900;
class Star {
constructor(reset = false) { this.init(reset); }
init(reset = false) {
// Milky way density: 30% of stars cluster in the band
if (Math.random() < 0.3) {
// Place star near band
const along = (Math.random() - 0.5) * Math.max(W, H) * 2;
const perp = (Math.random() - 0.5) * BAND_WIDTH;
this.sx = along * Math.cos(BAND_ANGLE) - perp * Math.sin(BAND_ANGLE) + W / 2;
this.sy = along * Math.sin(BAND_ANGLE) + perp * Math.cos(BAND_ANGLE) + H / 2;
} else {
this.sx = (Math.random() - 0.5) * W * 2;
this.sy = (Math.random() - 0.5) * H * 2;
}
this.z = reset ? Math.random() * W : W;
this.pz = this.z;
this.milky = inMilkyWay(this.sx + W / 2, this.sy + H / 2);
}
update(spd) {
this.pz = this.z;
this.z -= spd;
if (this.z <= 1) { this.init(false); }
}
draw(spd) {
const scale = W / 2;
const sx = (this.sx / this.z) * scale + CX();
const sy = (this.sy / this.z) * scale + CY();
if (sx < -10 || sx > W + 10 || sy < -10 || sy > H + 10) return;
const t = 1 - this.z / W; // 0 at far, 1 at near
const r = t * (this.milky ? 1.5 : 2.2);
// Color: blue-white at center → warm gold near edge → pure white
let hue, sat, lit;
if (warpBurst > 0.3 || spd > 22) {
hue = 205; sat = 100; lit = 60 + t * 40;
} else if (this.milky) {
// milky way: cool blue-white
hue = 210; sat = 30; lit = 70 + t * 30;
} else {
hue = 45 + t * 15; sat = 10 + t * 30; lit = 60 + t * 40;
}
const alpha = Math.min(1, t * 1.4 + 0.1);
// Draw streaks at speed
if (spd > 10 || warpBurst > 0.1) {
const px = (this.sx / this.pz) * scale + CX();
const py = (this.sy / this.pz) * scale + CY();
const streakAlpha = alpha * Math.min(1, spd / 20) * (warpBurst > 0 ? 1.4 : 1);
const grad = ctx.createLinearGradient(px, py, sx, sy);
grad.addColorStop(0, `hsla(${hue},${sat}%,${lit}%,0)`);
grad.addColorStop(1, `hsla(${hue},${sat}%,${lit}%,${Math.min(1, streakAlpha)})`);
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(sx, sy);
ctx.strokeStyle = grad;
ctx.lineWidth = r * 0.9;
ctx.stroke();
}
// Draw star point
if (r > 0.3) {
ctx.beginPath();
ctx.arc(sx, sy, r, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${hue},${sat}%,${lit}%,${alpha})`;
ctx.fill();
// Lens cross at high speed
if (spd > 28 || warpBurst > 0.5) {
const arm = r * 5;
ctx.strokeStyle = `hsla(${hue},${sat}%,${lit}%,${alpha * 0.3})`;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(sx - arm, sy); ctx.lineTo(sx + arm, sy);
ctx.moveTo(sx, sy - arm); ctx.lineTo(sx, sy + arm);
ctx.stroke();
}
}
}
}
const stars = Array.from({ length: STAR_COUNT }, () => new Star(true));
function animate() {
// Smooth speed with exponential easing
const effectiveTarget = warpBurst > 0 ? MAX_SPD : targetSpeed;
speed += (effectiveTarget - speed) * 0.05;
warpBurst *= 0.94; // decay warp burst
const trailAlpha = warpBurst > 0.2 ? 0.25 : Math.max(0.08, 1 - speed / 15);
ctx.fillStyle = `rgba(0, 0, 0, ${trailAlpha})`;
ctx.fillRect(0, 0, W, H);
for (const s of stars) {
s.update(speed);
s.draw(speed);
}
requestAnimationFrame(animate);
}
animate();