Crescent Moon Night
A night sky over a quiet town with a domed mosque. The crescent moon glows and breathes, stars twinkle, a shooting star streaks past now and then, and lanterns hang from the top, swaying and glowing.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-crescent-moon-night>where the scene should go, or put it behind a heading as a banner.
Options
Add any of these attributes to the same element.
data-lanterns | How many lanterns, 0 to 5. Default 3. |
data-stars | How many stars, as a multiplier. Default 1. |
data-skyline="false" | No town along the bottom. |
data-shooting="false" | No shooting stars. |
data-speed | Speed multiplier. Default 1. |
It is 8:5 with a night-sky background by default; set your own height or background on the element. The scene spreads across the full width, is laid out again when it resizes, and pauses when off screen. With reduced motion it is a still night scene.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Crescent Moon Night: a night sky over a quiet town with a domed mosque.
// A crescent moon glows and breathes, stars twinkle, a shooting star now
// and then, and glowing lanterns hang on strings from the top, swaying
// gently. Drawn as SVG that fills the element and spreads across its full
// width; paused when off screen.
// Usage: <div data-crescent-moon-night></div>
// It is 8:5 by default with a night-sky background; give it your own
// height or background to change that.
// Optional: data-stars (how many stars, as a multiplier, default 1),
// data-lanterns (how many lanterns, 0 to 5, default 3), data-skyline="false"
// (no town), data-shooting="false" (no shooting stars), data-speed
// (speed multiplier, default 1).
var DEFAULTS = { stars: 1, lanterns: 3, speed: 1 };
var H = 250;
var uid = 0;
var INK = '#0b0a24';
var css =
'[data-crescent-moon-night]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5;' +
'background:linear-gradient(#050a22 0%,#141a4a 55%,#3a2a6e 100%)}' +
'.cmn-svg{position:absolute;inset:0;width:100%;height:100%;display:block}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// A small seeded random, so the town and stars stay put on resize.
function rng(seed) {
return function () { seed = (seed * 16807) % 2147483647; return (seed - 1) / 2147483646; };
}
// A hanging lantern (fanous) from (0, 0) on a string of length L.
function fanous(g, L, tint) {
var y = function (v) { return (L + v).toFixed(1); };
return '<path d="M0 -400 L0 ' + L + '" stroke="#c9a227" stroke-opacity=".7" stroke-width="1"/>' +
'<circle class="cmn-lhalo" cx="0" cy="' + y(38) + '" r="46" fill="url(#' + g + '-lhalo)"/>' +
'<circle cx="0" cy="' + y(2) + '" r="3" fill="none" stroke="#e0b53a" stroke-width="1.4"/>' +
'<path d="M-11 ' + y(17) + ' C-11 ' + y(9) + ' -4 ' + y(6) + ' 0 ' + y(4) + ' C4 ' + y(6) + ' 11 ' + y(9) + ' 11 ' + y(17) + ' Z" fill="#c9a227"/>' +
'<rect x="-13" y="' + y(16) + '" width="26" height="4" rx="1" fill="#e0b53a"/>' +
'<path class="cmn-glass" d="M-12 ' + y(20) + ' L12 ' + y(20) + ' L16 ' + y(38) + ' L10 ' + y(56) + ' L-10 ' + y(56) + ' L-16 ' + y(38) + ' Z" fill="url(#' + g + '-glass' + tint + ')"/>' +
'<path d="M-12 ' + y(20) + ' L12 ' + y(20) + ' L16 ' + y(38) + ' L10 ' + y(56) + ' L-10 ' + y(56) + ' L-16 ' + y(38) + ' Z M-16 ' + y(38) + ' L16 ' + y(38) + ' M-5 ' + y(20) + ' L-6 ' + y(56) + ' M5 ' + y(20) + ' L6 ' + y(56) + '" fill="none" stroke="#c9a227" stroke-width="1.6" stroke-linejoin="round"/>' +
'<rect x="-11" y="' + y(55) + '" width="22" height="4" rx="1" fill="#e0b53a"/>' +
'<path d="M-8 ' + y(59) + ' L8 ' + y(59) + ' L0 ' + y(72) + ' Z" fill="#c9a227"/>' +
'<circle cx="0" cy="' + y(74) + '" r="2" fill="#e0b53a"/>';
}
function build(el) {
var d = el.dataset;
var n = ++uid;
var g = 'cmn' + n;
var starMul = Math.max(0, d.stars !== undefined ? +d.stars : DEFAULTS.stars);
var nLanterns = Math.max(0, Math.min(5, d.lanterns !== undefined ? Math.round(+d.lanterns) : DEFAULTS.lanterns));
var skyline = d.skyline !== 'false';
var shooting = d.shooting !== 'false';
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A crescent moon glows over a town while stars twinkle and lanterns hang');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
var W = 0, stars = [], lamps = [], windows = [], moonGlow, shoot, svg;
function layout() {
var w = el.clientWidth, h = el.clientHeight;
if (!w || !h) return;
var newW = Math.max(220, Math.round(H * w / h));
if (svg && Math.abs(newW - W) < 4) return;
W = newW;
var r = rng(7 + n);
var moonX = W * 0.74, moonY = 64;
// Stars, kept out of the moon and above the rooftops.
var starHtml = '';
var nStars = Math.round(W * 0.16 * starMul);
for (var i = 0; i < nStars; i++) {
var sx = r() * W, sy = r() * (skyline ? 180 : 235);
if (Math.hypot(sx - moonX, sy - moonY) < 52) continue;
var big = r() < 0.14;
starHtml += big ?
'<path class="cmn-star" d="M' + sx.toFixed(1) + ' ' + (sy - 5).toFixed(1) + ' Q' + sx.toFixed(1) + ' ' + sy.toFixed(1) + ' ' + (sx + 5).toFixed(1) + ' ' + sy.toFixed(1) + ' Q' + sx.toFixed(1) + ' ' + sy.toFixed(1) + ' ' + sx.toFixed(1) + ' ' + (sy + 5).toFixed(1) + ' Q' + sx.toFixed(1) + ' ' + sy.toFixed(1) + ' ' + (sx - 5).toFixed(1) + ' ' + sy.toFixed(1) + ' Q' + sx.toFixed(1) + ' ' + sy.toFixed(1) + ' ' + sx.toFixed(1) + ' ' + (sy - 5).toFixed(1) + ' Z" fill="#fef9c3"/>' :
'<circle class="cmn-star" cx="' + sx.toFixed(1) + '" cy="' + sy.toFixed(1) + '" r="' + (0.6 + r() * 1.1).toFixed(2) + '" fill="#fff"/>';
}
// The town: houses with little domes, and a mosque with two minarets.
var town = '', lit = '';
if (skyline) {
var p = '';
var x = 0;
var mx = W * 0.34;
while (x < W) {
var bw = 26 + r() * 30, bh = 12 + r() * 26;
if (x + bw > mx - 80 && x < mx + 80) { x = mx + 80; continue; }
p += 'M' + x.toFixed(1) + ' ' + H + ' L' + x.toFixed(1) + ' ' + (232 - bh).toFixed(1) + ' ';
if (r() < 0.35) {
var cx = x + bw / 2, dr = Math.min(bw / 2 - 3, 10);
p += 'L' + (cx - dr).toFixed(1) + ' ' + (232 - bh).toFixed(1) + ' A' + dr.toFixed(1) + ' ' + dr.toFixed(1) + ' 0 0 1 ' + (cx + dr).toFixed(1) + ' ' + (232 - bh).toFixed(1) + ' ';
}
p += 'L' + (x + bw).toFixed(1) + ' ' + (232 - bh).toFixed(1) + ' L' + (x + bw).toFixed(1) + ' ' + H + ' Z ';
if (r() < 0.6) lit += '<rect class="cmn-win" x="' + (x + bw * 0.3).toFixed(1) + '" y="' + (240 - bh).toFixed(1) + '" width="4" height="5" rx="1" fill="#fcd34d"/>';
if (r() < 0.4) lit += '<rect class="cmn-win" x="' + (x + bw * 0.62).toFixed(1) + '" y="' + (240 - bh).toFixed(1) + '" width="4" height="5" rx="1" fill="#fcd34d"/>';
x += bw + 2 + r() * 6;
}
p += 'M0 ' + H + ' L0 236 L' + W + ' 236 L' + W + ' ' + H + ' Z ';
// Mosque.
p += 'M' + (mx - 50) + ' ' + H + ' L' + (mx - 50) + ' 192 L' + (mx + 50) + ' 192 L' + (mx + 50) + ' ' + H + ' Z ';
p += 'M' + (mx - 34) + ' 193 C' + (mx - 36) + ' 164 ' + (mx - 10) + ' 156 ' + mx + ' 142 C' + (mx + 10) + ' 156 ' + (mx + 36) + ' 164 ' + (mx + 34) + ' 193 Z ';
p += 'M' + (mx - 1) + ' 143 L' + (mx - 1) + ' 132 L' + (mx + 1) + ' 132 L' + (mx + 1) + ' 143 Z ';
[-68, 68].forEach(function (o) {
var m = mx + o;
p += 'M' + (m - 5) + ' ' + H + ' L' + (m - 5) + ' 150 L' + (m - 8) + ' 150 L' + (m - 8) + ' 145 L' + (m + 8) + ' 145 L' + (m + 8) + ' 150 L' + (m + 5) + ' 150 L' + (m + 5) + ' ' + H + ' Z ';
p += 'M' + (m - 5) + ' 145 L' + (m - 5) + ' 128 L' + m + ' 114 L' + (m + 5) + ' 128 L' + (m + 5) + ' 145 Z ';
});
[-30, -10, 10, 30].forEach(function (o) {
lit += '<path class="cmn-win" d="M' + (mx + o - 4) + ' 222 L' + (mx + o - 4) + ' 210 A4 4 0 0 1 ' + (mx + o + 4) + ' 210 L' + (mx + o + 4) + ' 222 Z" fill="#fcd34d"/>';
});
town = '<path d="' + p + '" fill="' + INK + '"/>' + lit;
}
// Lanterns along the top, kept clear of the moon.
var spots = [0.09, 0.25, 0.92, 0.5, 0.38].slice(0, nLanterns);
var lens = [34, 66, 48, 24, 54];
var lampHtml = '';
spots.forEach(function (f, i) {
lampHtml += '<g class="cmn-lamp" data-x="' + (W * f).toFixed(1) + '">' + fanous(g, lens[i], i % 3) + '</g>';
});
var tints = ['#fde047', '#fb923c', '#f472b6'];
var glassDefs = tints.map(function (c, i) {
return '<radialGradient id="' + g + '-glass' + i + '" cx=".5" cy=".5" r=".7"><stop offset="0" stop-color="#fffbeb"/><stop offset=".45" stop-color="' + c + '"/><stop offset="1" stop-color="#9a3412"/></radialGradient>';
}).join('');
el.innerHTML =
'<svg class="cmn-svg" viewBox="0 0 ' + W + ' ' + H + '" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">' +
'<defs>' + glassDefs +
'<radialGradient id="' + g + '-glow"><stop offset="0" stop-color="#fef3c7" stop-opacity=".55"/><stop offset=".35" stop-color="#fde68a" stop-opacity=".18"/><stop offset="1" stop-color="#fde68a" stop-opacity="0"/></radialGradient>' +
'<radialGradient id="' + g + '-lhalo"><stop offset="0" stop-color="#fde68a" stop-opacity=".4"/><stop offset="1" stop-color="#f97316" stop-opacity="0"/></radialGradient>' +
'<linearGradient id="' + g + '-moon" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fffbeb"/><stop offset="1" stop-color="#fcd34d"/></linearGradient>' +
'<linearGradient id="' + g + '-trail" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient>' +
'<mask id="' + g + '-cut"><rect x="0" y="0" width="' + W + '" height="' + H + '" fill="#fff"/><circle cx="' + (moonX + 15) + '" cy="' + (moonY - 9) + '" r="30" fill="#000"/></mask>' +
'</defs>' +
starHtml +
'<circle class="cmn-glow" cx="' + moonX + '" cy="' + moonY + '" r="100" fill="url(#' + g + '-glow)"/>' +
'<circle cx="' + moonX + '" cy="' + moonY + '" r="34" fill="url(#' + g + '-moon)" mask="url(#' + g + '-cut)"/>' +
'<g class="cmn-shoot" opacity="0"><rect x="-70" y="-1" width="70" height="2" rx="1" fill="url(#' + g + '-trail)"/><circle r="2" fill="#fff"/></g>' +
town + lampHtml +
'</svg>';
svg = el.firstChild;
moonGlow = svg.querySelector('.cmn-glow');
shoot = svg.querySelector('.cmn-shoot');
stars = [];
svg.querySelectorAll('.cmn-star').forEach(function (s) { stars.push({ el: s, f: 0.6 + r() * 2.2, ph: r() * 6.28, lo: 0.15 + r() * 0.4 }); });
windows = [];
svg.querySelectorAll('.cmn-win').forEach(function (s) { windows.push({ el: s, f: 0.3 + r() * 0.8, ph: r() * 6.28 }); });
lamps = [];
svg.querySelectorAll('.cmn-lamp').forEach(function (l, i) {
lamps.push({ el: l, x: +l.getAttribute('data-x'), period: 2800 + i * 450, ph: i * 1.9, amp: 3.5 + i % 2, halo: l.querySelector('.cmn-lhalo'), glass: l.querySelector('.cmn-glass') });
});
frame(0);
}
var t = 0, nextShot = 2500, shot = null;
function frame(dt) {
t += dt * speed;
var s = t / 1000;
stars.forEach(function (st) {
var v = st.lo + (1 - st.lo) * (0.5 + 0.5 * Math.sin(s * st.f + st.ph));
st.el.setAttribute('opacity', v.toFixed(2));
});
windows.forEach(function (w) { w.el.setAttribute('opacity', (0.65 + 0.3 * Math.sin(s * w.f + w.ph)).toFixed(2)); });
if (moonGlow) {
var breathe = 0.5 + 0.5 * Math.sin(s * 0.9);
moonGlow.setAttribute('opacity', (0.7 + 0.3 * breathe).toFixed(3));
moonGlow.setAttribute('r', (92 + 14 * breathe).toFixed(1));
}
lamps.forEach(function (l) {
var a = l.amp * Math.sin(2 * Math.PI * t / l.period + l.ph);
l.el.setAttribute('transform', 'translate(' + l.x + ' 0) rotate(' + a.toFixed(2) + ')');
var v = 0.8 + 0.12 * Math.sin(s * 5 + l.ph) + 0.08 * Math.sin(s * 13 + l.ph * 2);
l.halo.setAttribute('opacity', v.toFixed(3));
l.glass.setAttribute('opacity', (0.75 + v * 0.25).toFixed(3));
});
if (shooting && shoot && dt) {
nextShot -= dt * speed;
if (!shot && nextShot <= 0) {
shot = { x: W * (0.15 + Math.random() * 0.4), y: 14 + Math.random() * 40, age: 0, ang: 14 + Math.random() * 16 };
nextShot = 5000 + Math.random() * 5000;
}
if (shot) {
shot.age += dt * speed;
var u = shot.age / 900;
if (u >= 1) { shot = null; shoot.setAttribute('opacity', 0); }
else {
var dist = 160 * u;
var rad = shot.ang * Math.PI / 180;
shoot.setAttribute('transform', 'translate(' + (shot.x + Math.cos(rad) * dist).toFixed(1) + ' ' + (shot.y + Math.sin(rad) * dist).toFixed(1) + ') rotate(' + shot.ang + ')');
shoot.setAttribute('opacity', Math.sin(Math.PI * u).toFixed(2));
}
}
}
}
layout();
if (window.ResizeObserver) new ResizeObserver(layout).observe(el);
if (reduced()) 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;
frame(dt);
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-crescent-moon-night]').forEach(function (el) {
if (el._cmn) return; // idempotent
el._cmn = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-crescent-moon-night></div>
<!-- a wide banner with more stars and no town: -->
<div data-crescent-moon-night data-stars="2" data-skyline="false" style="height: 220px"></div>