Day-night Sky Cycle
The sky behind a section lives through a whole day: a pink dawn, a bright blue midday, an orange dusk and a starry night. The sun climbs and sets, the moon rises after it, stars come out and the hills on the horizon take on the light.
Around the clock
How to use it
- Paste the snippet just before
</head>. - Add
data-day-night-sky-cycleto a section, hero or card.
Options
Add any of these attributes to the same element.
data-duration | Seconds for a whole day. Default 24. |
data-start | Where the day begins: dawn, day, dusk or night. Default dawn. |
data-hills="false" | No hills along the bottom. |
The sky is painted on a canvas behind your content, so the section needs no background of its own, and it pauses when off screen. Keep text white with a soft shadow so it reads at noon and at midnight. With reduced motion it shows a still dusk, or the data-start you choose.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Day-night Sky Cycle: the sky behind a section runs through a whole day,
// from a pink dawn to a bright blue midday, an orange dusk and a starry
// night. The sun climbs and sets, the moon follows it, stars come out and
// rolling hills on the horizon take on the light. Drawn on a canvas behind
// your content, and paused when off screen.
// Usage: <section data-day-night-sky-cycle>…</section>
// Optional: data-duration (seconds for a whole day, default 24),
// data-start (dawn, day, dusk or night, default dawn; with reduced motion
// this is the still shown, default dusk), data-hills="false" (no hills).
var DEFAULTS = { duration: 24, start: 'dawn' };
var STARTS = { dawn: 0, day: 0.3, dusk: 0.56, night: 0.8 };
var css =
'[data-day-night-sky-cycle]{position:relative;isolation:isolate;overflow:hidden}' +
'.dns-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);
var TAU = Math.PI * 2;
// The sky through the day: [phase, top, middle, horizon].
var SKY = [
[0.00, '#2c3a7a', '#b0729a', '#fbb47a'],
[0.10, '#3f7ad6', '#8fbdf0', '#f7d6b0'],
[0.30, '#2a6fd8', '#68a9ee', '#c4e2fb'],
[0.46, '#3570cc', '#86b2e6', '#f3dcae'],
[0.56, '#29306f', '#a4508a', '#fb8a4c'],
[0.66, '#11153f', '#3b2a6a', '#7a3f6e'],
[0.78, '#050a1d', '#0c1638', '#1a2650'],
[0.94, '#0a1030', '#1d2456', '#4a3d72'],
[1.00, '#2c3a7a', '#b0729a', '#fbb47a']
];
function hex(c) { return [parseInt(c.slice(1, 3), 16), parseInt(c.slice(3, 5), 16), parseInt(c.slice(5, 7), 16)]; }
SKY.forEach(function (s) { for (var i = 1; i < 4; i++) s[i] = hex(s[i]); });
function mix(a, b, k) { return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k]; }
function css3(c, a) { return 'rgba(' + Math.round(c[0]) + ',' + Math.round(c[1]) + ',' + Math.round(c[2]) + ',' + (a === undefined ? 1 : a) + ')'; }
function smooth(k) { return k * k * (3 - 2 * k); }
// 0 to 1 as p moves from a to b.
function ramp(p, a, b) { return Math.max(0, Math.min(1, (p - a) / (b - a))); }
function skyAt(p) {
for (var i = 0; i < SKY.length - 1; i++) {
var a = SKY[i], b = SKY[i + 1];
if (p >= a[0] && p <= b[0]) {
var k = smooth((p - a[0]) / (b[0] - a[0]));
return [mix(a[1], b[1], k), mix(a[2], b[2], k), mix(a[3], b[3], k)];
}
}
return [SKY[0][1], SKY[0][2], SKY[0][3]];
}
function build(el) {
var d = el.dataset;
var duration = (+d.duration || DEFAULTS.duration) * 1000;
var hills = d.hills !== 'false';
var still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var phase = STARTS[d.start] !== undefined ? STARTS[d.start] : still ? STARTS.dusk : STARTS[DEFAULTS.start];
var canvas = document.createElement('canvas');
canvas.className = 'dns-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var R = anime.utils.random;
var w = 0, h = 0, dpr = 1, horizon = 0, stars = [], ridges = [], clock = 0;
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
horizon = h * (hills ? 0.8 : 0.9);
stars = [];
var n = Math.round(Math.min(260, (w * h) / 2200));
for (var i = 0; i < n; i++) {
stars.push({ x: R(0, w, 1), y: Math.pow(Math.random(), 1.4) * horizon, r: R(0.4, 1.4, 2), tw: R(0, TAU, 2), sp: R(1, 3, 2) });
}
// Two ridges of hills, the far one paler.
ridges = [0, 1].map(function (layer) {
var pts = [], a = R(0, TAU, 2), b = R(0, TAU, 2);
var base = horizon + (layer ? h * 0.06 : -h * 0.02);
var amp = h * (layer ? 0.05 : 0.07);
for (var x = 0; x <= w + 12; x += 12) {
pts.push([x, base - amp * (0.6 * Math.sin(x / (w * 0.22) + a) + 0.4 * Math.sin(x / (w * 0.09) + b))]);
}
return pts;
});
draw();
}
// The sky's colour at height y, to cut the crescent with.
function skyBehind(sky, y) {
var f = Math.max(0, y / horizon);
return f < 0.6 ? mix(sky[0], sky[1], f / 0.6) : mix(sky[1], sky[2], Math.min(1, (f - 0.6) / 0.4));
}
function body(x, y, r, core, halo, haloSize) {
var g = ctx.createRadialGradient(x, y, r * 0.6, x, y, r * haloSize);
g.addColorStop(0, css3(halo, 0.55));
g.addColorStop(1, css3(halo, 0));
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(x, y, r * haloSize, 0, TAU);
ctx.fill();
ctx.fillStyle = css3(core);
ctx.beginPath();
ctx.arc(x, y, r, 0, TAU);
ctx.fill();
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
var p = phase, sky = skyAt(p);
var g = ctx.createLinearGradient(0, 0, 0, horizon);
g.addColorStop(0, css3(sky[0]));
g.addColorStop(0.6, css3(sky[1]));
g.addColorStop(1, css3(sky[2]));
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);
// Stars fade in after dusk and out before dawn.
var night = smooth(ramp(p, 0.6, 0.7)) * (1 - smooth(ramp(p, 0.92, 0.99)));
if (night > 0.01) {
ctx.fillStyle = '#fff';
for (var i = 0; i < stars.length; i++) {
var s = stars[i];
ctx.globalAlpha = night * (0.6 + 0.4 * Math.sin(clock * s.sp + s.tw)) * (1 - (s.y / horizon) * 0.5);
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// The sun is up from dawn to dusk; the moon from dusk to dawn.
var rad = Math.max(12, Math.min(w, h) * 0.07);
var arcH = horizon * 0.72;
var sp = (p - (p > 0.9 ? 1 : 0) + 0.04) / 0.66;
if (sp > 0 && sp < 1) {
var low = 1 - Math.sin(Math.PI * sp);
var sun = mix([255, 246, 214], [255, 140, 60], low * low);
body(w * (0.08 + 0.84 * sp), horizon + rad * 0.6 - Math.sin(Math.PI * sp) * arcH, rad, sun, mix([255, 230, 160], [255, 120, 60], low), 3.6);
}
var mp = (p - 0.6 + (p < 0.3 ? 1 : 0)) / 0.42;
if (mp > 0 && mp < 1) {
var mx = w * (0.1 + 0.8 * mp), my = horizon + rad * 0.6 - Math.sin(Math.PI * mp) * arcH * 0.85, mr = rad * 0.75;
body(mx, my, mr, [240, 238, 228], [180, 190, 255], 3);
// A crescent: the sky's colour bites into the moon.
ctx.fillStyle = css3(skyBehind(sky, my));
ctx.save();
ctx.beginPath();
ctx.arc(mx, my, mr + 0.5, 0, TAU);
ctx.clip();
ctx.globalAlpha = 0.92;
ctx.beginPath();
ctx.arc(mx + mr * 0.45, my - mr * 0.3, mr * 0.85, 0, TAU);
ctx.fill();
ctx.restore();
}
if (hills) {
ridges.forEach(function (pts, layer) {
var tone = mix(sky[layer ? 0 : 1], [6, 8, 20], layer ? 0.78 : 0.6);
ctx.fillStyle = css3(tone);
ctx.beginPath();
ctx.moveTo(0, h);
for (var k = 0; k < pts.length; k++) ctx.lineTo(pts[k][0], pts[k][1]);
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
});
}
}
resize();
if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
if (still) return;
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;
clock += dt / 1000;
phase = (phase + dt / duration) % 1;
draw();
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-day-night-sky-cycle]').forEach(function (el) {
if (el._dns) return;
el._dns = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-day-night-sky-cycle style="color: #fff; text-shadow: 0 2px 12px rgba(0, 0, 0, .45)">
<h1>Around the clock</h1>
</section>