live - 60fps 1920 x 1080
<!DOCTYPE html>
<html>
<head>
<title>Synthwave Grid</title>
<style>
:root {
--bg-top: #0a0420;
--bg-horizon: #ff2e97;
--grid-color: #22d3ee;
--sun-color: #ffcc33;
}
* { margin: 0; padding: 0; }
body { height: 100vh; overflow: hidden; background: var(--bg-top); }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const cv = document.getElementById('c');
const ctx = cv.getContext('2d');
const css = getComputedStyle(document.documentElement);
const bgTop = (css.getPropertyValue('--bg-top') || '#0a0420').trim();
const horizonCol = (css.getPropertyValue('--bg-horizon') || '#ff2e97').trim();
const gridCol = (css.getPropertyValue('--grid-color') || '#22d3ee').trim();
const sunCol = (css.getPropertyValue('--sun-color') || '#ffcc33').trim();
let w, h, horizon;
const COLS = 26, ROWS = 40, SPACING = 1;
const FOCAL = 260, CAM_H = 2.4;
function build() {
w = cv.width = innerWidth;
h = cv.height = innerHeight;
horizon = h * 0.42;
}
addEventListener('resize', build);
build();
// deterministic ridge height for a world row/col — flat "road" in the middle
function height(col, row) {
const edge = Math.abs(col - COLS / 2) / (COLS / 2); // 0 centre -> 1 edge
const ridge = Math.sin(col * 1.3 + row * 0.7) * 0.5 + Math.sin(row * 0.4 + col * 0.9) * 0.5;
return ridge * edge * edge * 3.2;
}
function project(wx, wy, wz) {
const s = FOCAL / wz;
return { x: w / 2 + wx * s, y: horizon - (wy - CAM_H) * s, s };
}
function drawSky() {
const g = ctx.createLinearGradient(0, 0, 0, horizon);
g.addColorStop(0, bgTop);
g.addColorStop(1, horizonCol);
ctx.fillStyle = g; ctx.fillRect(0, 0, w, horizon);
// sun with scanline gaps
const sr = Math.min(w, h) * 0.16, sx = w / 2, sy = horizon - sr * 0.35;
const sg = ctx.createLinearGradient(0, sy - sr, 0, sy + sr);
sg.addColorStop(0, sunCol); sg.addColorStop(1, horizonCol);
ctx.save();
ctx.beginPath(); ctx.arc(sx, sy, sr, 0, Math.PI * 2); ctx.clip();
ctx.fillStyle = sg; ctx.fillRect(sx - sr, sy - sr, sr * 2, sr * 2);
ctx.fillStyle = bgTop;
for (let i = 0; i < 9; i++) { const yy = sy + sr * 0.15 + i * sr * 0.14; ctx.fillRect(sx - sr, yy, sr * 2, i * 0.7 + 1); }
ctx.restore();
// ground
const gg = ctx.createLinearGradient(0, horizon, 0, h);
gg.addColorStop(0, '#1a0a2e'); gg.addColorStop(1, bgTop);
ctx.fillStyle = gg; ctx.fillRect(0, horizon, w, h - horizon);
}
function step(t) {
ctx.fillStyle = bgTop; ctx.fillRect(0, 0, w, h);
drawSky();
const scroll = (t * 2.4) % SPACING;
const base = Math.floor(t * 2.4);
ctx.lineWidth = 1;
// depth lines (running away) + horizontal lines
for (let r = 0; r < ROWS - 1; r++) {
const wz1 = (r + 1) * SPACING - scroll;
const wz2 = (r + 2) * SPACING - scroll;
if (wz1 <= 0.2) continue;
const alpha = Math.max(0, 1 - r / (ROWS - 2));
ctx.strokeStyle = gridCol;
ctx.globalAlpha = alpha * 0.8;
ctx.beginPath();
for (let c = 0; c <= COLS; c++) {
const wx = (c - COLS / 2) * SPACING;
const p1 = project(wx, height(c, base + r), wz1);
const p2 = project(wx, height(c, base + r + 1), wz2);
ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y);
}
// horizontal segment at this row
for (let c = 0; c < COLS; c++) {
const wx1 = (c - COLS / 2) * SPACING, wx2 = (c + 1 - COLS / 2) * SPACING;
const a = project(wx1, height(c, base + r), wz1);
const b = project(wx2, height(c + 1, base + r), wz1);
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
}
ctx.stroke();
}
ctx.globalAlpha = 1;
}
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
step(0.5);
} else {
let t = 0;
(function loop() { t += 0.016; step(t); requestAnimationFrame(loop); })();
}
</script>
</body>
</html> About this animation
An endless neon wireframe terrain races toward a retro striped sun on the horizon — the quintessential 80s synthwave flythrough.