Synthwave Sunset
A backdrop straight off an eighties album cover. A big striped sun sinks slowly behind neon-edged mountains, a glowing grid floor rolls towards you and the stars come out in the purple sky. Then the sun rises again and it all carries on. Your content sits on top.
Night
Drive
How to use it
- Paste the snippet just before
</head>. - Add
data-synthwave-sunsetto a section and give it a height. Its content sits on top of the scene.
Options
Add any of these attributes to the same element.
data-speed | How fast the grid rolls. Default 1. |
data-duration | Time for the sun to set. Default 14000 ms. |
data-color | Colour of the grid and the mountain edge. Default #ff3df0. |
data-sun | The sun’s two colours, top then bottom. Default #ffe66d,#ff2d95. |
data-stars | How many stars, from 0 to 2. Default 1. |
With reduced motion it shows a still frame, the sun part-way down.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Synthwave Sunset: an eighties retro-futurist backdrop. A big striped sun
// sinks slowly behind neon-edged mountains, a glowing grid floor rolls
// towards you, and stars come out in the purple sky. Then the sun rises
// again and the loop carries on. Your content sits on top.
// Usage: <section data-synthwave-sunset><h1>Night Drive</h1></section>
// Optional: data-speed (how fast the grid rolls, default 1),
// data-duration (ms for the sun to set, default 14000), data-color (grid
// and mountain-edge colour, default #ff3df0), data-sun (two colours for the
// sun, top then bottom, default "#ffe66d,#ff2d95"), data-stars (how many,
// 0 to 2, default 1).
var DEFAULTS = { speed: 1, duration: 14000, color: '#ff3df0', sun: '#ffe66d,#ff2d95', stars: 1 };
var css =
'.sws{position:relative;overflow:hidden;background:#0b0322;isolation:isolate}' +
'.sws-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// Repeatable pseudo-random numbers, so the mountains and stars keep their shape on resize.
function rng(seed) {
return function () {
seed = (seed * 16807) % 2147483647;
return (seed - 1) / 2147483646;
};
}
function setup(el) {
var d = el.dataset;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var duration = +d.duration || DEFAULTS.duration;
var neon = d.color || DEFAULTS.color;
var sunCols = (d.sun || DEFAULTS.sun).split(',');
var starAmt = d.stars !== undefined ? +d.stars : DEFAULTS.stars;
var canvas = document.createElement('canvas');
canvas.className = 'sws-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var w = 0, h = 0, dpr = 1, horizon = 0, stars = [], far = [], near = [];
function ridge(rand, n, base, amp) {
var pts = [];
for (var i = 0; i <= n; i++) {
var peak = i % 2 ? amp * (0.55 + rand() * 0.45) : amp * rand() * 0.25;
pts.push([i / n, base - peak]);
}
return pts;
}
function build() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
if (!w || !h) return;
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
horizon = Math.round(h * 0.62);
var rand = rng(7);
var count = Math.round(w * horizon / 2600 * starAmt);
stars = [];
for (var i = 0; i < count; i++) {
stars.push({ x: rand() * w, y: Math.pow(rand(), 1.4) * horizon * 0.85, r: 0.4 + rand() * 1.1, p: rand() * 6.28, s: 0.6 + rand() * 1.6 });
}
var peaks = Math.max(6, Math.round(w / 90));
far = ridge(rand, peaks + 3, 0, h * 0.16);
near = ridge(rand, peaks, 0, h * 0.11);
}
function drawRidge(pts, fill, edge, offset) {
ctx.beginPath();
ctx.moveTo(-2, horizon + 1);
pts.forEach(function (p) { ctx.lineTo(p[0] * (w + 4) - 2, horizon + p[1] + offset); });
ctx.lineTo(w + 2, horizon + 1);
ctx.closePath();
ctx.fillStyle = fill;
ctx.fill();
if (edge) {
ctx.strokeStyle = edge;
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
function draw(t, set) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// set: 0 = sun high, 1 = sun gone below the horizon.
var glow = 1 - set * 0.7;
// Sky: deep purple at the top, hot pink at the horizon while the sun is up.
var sky = ctx.createLinearGradient(0, 0, 0, horizon);
sky.addColorStop(0, '#07021a');
sky.addColorStop(0.5, 'rgba(' + Math.round(40 + 30 * glow) + ',8,' + Math.round(80 + 20 * glow) + ',1)');
sky.addColorStop(1, 'rgba(' + Math.round(120 + 135 * glow) + ',' + Math.round(30 + 40 * glow) + ',' + Math.round(120 + 30 * glow) + ',1)');
ctx.fillStyle = sky;
ctx.fillRect(0, 0, w, horizon + 1);
// Stars, brighter once the sun is down.
for (var i = 0; i < stars.length; i++) {
var s = stars[i];
var a = (0.35 + 0.65 * set) * (0.55 + 0.45 * Math.sin(t / 1000 * s.s + s.p)) * (1 - s.y / horizon * 0.8);
if (a <= 0.02) continue;
ctx.globalAlpha = a;
ctx.fillStyle = '#fff';
ctx.fillRect(s.x, s.y, s.r * 1.4, s.r * 1.4);
}
ctx.globalAlpha = 1;
// The sun: a gradient disc with horizontal slots that thicken towards the bottom and slide down.
var R = Math.min(w * 0.24, h * 0.3);
var cy = horizon - R * 0.95 + set * R * 2.15;
var cx = w / 2;
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, w, horizon);
ctx.clip();
var halo = ctx.createRadialGradient(cx, cy, R * 0.6, cx, cy, R * 2.2);
halo.addColorStop(0, 'rgba(255,60,160,.45)');
halo.addColorStop(1, 'rgba(255,60,160,0)');
ctx.fillStyle = halo;
ctx.fillRect(0, 0, w, horizon);
// Clip to the disc, then fill the solid strips between the slots.
ctx.beginPath();
ctx.arc(cx, cy, R, 0, Math.PI * 2);
ctx.clip();
var sg = ctx.createLinearGradient(0, cy - R, 0, cy + R);
sg.addColorStop(0, sunCols[0]);
sg.addColorStop(1, sunCols[1] || sunCols[0]);
ctx.fillStyle = sg;
ctx.beginPath();
var bands = 7, roll = (t / 2600 * speed) % 1, top = cy - R;
for (var b = 0; b < bands; b++) {
var f = (b + roll) / bands;
var y = cy - R * 0.1 + f * R * 1.1, gap = 1 + f * R * 0.12;
if (y > top) ctx.rect(cx - R - 1, top, 2 * R + 2, y - top);
top = Math.max(top, y + gap);
}
if (top < cy + R) ctx.rect(cx - R - 1, top, 2 * R + 2, cy + R - top);
ctx.fill();
ctx.restore();
// Mountains, a far pale range and a nearer dark one with a neon edge.
drawRidge(far, '#2a0d52', 'rgba(150,80,255,.35)', 0);
drawRidge(near, '#140530', neon, 0);
// The floor and its grid rolling towards you.
var floor = ctx.createLinearGradient(0, horizon, 0, h);
floor.addColorStop(0, '#1a0640');
floor.addColorStop(1, '#07021a');
ctx.fillStyle = floor;
ctx.fillRect(0, horizon, w, h - horizon);
var depth = h - horizon;
ctx.strokeStyle = neon;
ctx.shadowColor = neon;
ctx.shadowBlur = 8;
ctx.lineWidth = 1.3;
ctx.beginPath();
var move = (t / 1000 * speed * 0.9) % 1;
for (var k = 0; k < 18; k++) {
// Rows sit at 1 / distance below the horizon, so they bunch up far away.
var z = k + 1 - move;
var py = horizon + (depth * 1.1) / z;
if (py > h + 2) continue;
ctx.moveTo(0, py);
ctx.lineTo(w, py);
}
var lines = Math.max(10, Math.round(w / 40));
for (var v = -lines; v <= lines; v++) {
var xb = cx + v * (w / lines) * 1.6;
ctx.moveTo(cx + v * (w / lines) * 0.08, horizon);
ctx.lineTo(xb, h);
}
ctx.stroke();
ctx.shadowBlur = 0;
// Mist on the horizon blends the grid into the mountains.
var mist = ctx.createLinearGradient(0, horizon - 4, 0, horizon + depth * 0.25);
mist.addColorStop(0, 'rgba(255,61,240,' + (0.22 * glow + 0.06) + ')');
mist.addColorStop(1, 'rgba(26,6,64,0)');
ctx.fillStyle = mist;
ctx.fillRect(0, horizon - 4, w, depth * 0.25 + 4);
}
build();
if (reduced()) {
draw(0, 0.3);
if (window.ResizeObserver) new ResizeObserver(function () { build(); draw(0, 0.3); }).observe(el);
return;
}
var t = 0, cycle = 0;
if (window.ResizeObserver) new ResizeObserver(function () { build(); draw(t, setAt(cycle)); }).observe(el);
// The sun sets over `duration`, waits, then rises again a little faster.
function setAt(c) {
var rise = duration * 0.5, hold = 1600, total = duration + hold + rise + hold;
var p = c % total;
var e;
if (p < duration) { e = p / duration; return e * e * (3 - 2 * e); }
if (p < duration + hold) return 1;
if (p < duration + hold + rise) { e = 1 - (p - duration - hold) / rise; return e * e * (3 - 2 * e); }
return 0;
}
var last = 0;
var timer = anime.createTimer({
loop: true, duration: 1000, autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) : 16;
last = now;
t += dt;
cycle += dt;
draw(t, setAt(cycle));
}
});
draw(0, 0);
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); }
else timer.pause();
}, { threshold: 0.05 }).observe(el);
}
function init() {
document.querySelectorAll('[data-synthwave-sunset]').forEach(function (el) {
if (el._sws) return;
el._sws = true;
el.classList.add('sws');
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-synthwave-sunset style="height:480px">
<h1>Night Drive</h1>
</section>