Snowman Build
Snowballs roll in across the snow, growing as they go, and hop up onto each other to stack into a snowman. Then his coal eyes, carrot nose, stick arms, scarf and hat pop on, and he gives you a wave.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-snowman-build>where the snowman should stand. He is built when it scrolls into view.
Options
Add any of these attributes to the same element.
data-loop | Melt into a puddle after a while and build him again. |
data-color | Scarf colour. Default red #dc2626. |
data-hat | Hat colour. Default #1f2937. |
data-snow="false" | No falling snow. |
data-speed | Speed multiplier. Default 1. |
It is 8:5 with a transparent background by default; set your own height or background on the element. The snowy ground and the snowfall stretch to its full width. It pauses when off screen. With reduced motion the snowman is shown finished.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Snowman Build: three snowballs roll in across the snow, growing as they
// go, and hop up onto each other to stack into a snowman. Then his coal
// eyes, carrot nose, buttons, stick arms, scarf and hat pop on, and he
// waves. Loop it and he melts into a puddle and is built again. Drawn as
// SVG that fills the element, with snow falling behind.
// Usage: <div data-snowman-build></div>
// It is 8:5 by default with a transparent background; give it a sky.
// Optional: data-loop (melt and build again), data-color (scarf colour,
// default #dc2626), data-hat (hat colour, default #1f2937),
// data-snow="false" (no falling snow), data-speed (speed multiplier,
// default 1).
var DEFAULTS = { color: '#dc2626', hat: '#1f2937', speed: 1 };
var uid = 0;
var GROUND = 214;
var RB = 40, RM = 29, RH = 20;
var css =
'[data-snowman-build]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
'.smb-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; }
function f(n) { return (+n).toFixed(2); }
// A snowball: a rolling part (with a few specks so you can see it turn)
// under a shading layer that stays put.
function ball(g, r, cls) {
var specks = '';
for (var i = 0; i < 6; i++) {
var a = i * 2.1 + r, d = r * (0.25 + (i * 0.37) % 0.55);
specks += '<circle cx="' + f(Math.cos(a) * d) + '" cy="' + f(Math.sin(a) * d) + '" r="' + f(1.2 + (i % 3) * 0.7) + '" fill="#cbd5e1" opacity=".75"/>';
}
return '<g class="' + cls + '"><g class="smb-roll"><circle r="' + r + '" fill="#f8fafc"/>' + specks + '</g>' +
'<circle r="' + r + '" fill="url(#' + g + '-shade)"/></g>';
}
function build(el) {
var d = el.dataset;
var g = 'smb' + (++uid);
var loop = d.loop !== undefined && d.loop !== 'false';
var scarf = d.color || DEFAULTS.color;
var hat = d.hat || DEFAULTS.hat;
var speed = Math.max(0.2, +d.speed || DEFAULTS.speed);
var snowOn = d.snow !== 'false';
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'Snowballs roll in and stack up into a snowman');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
var stick = 'fill="none" stroke="#78350f" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"';
el.innerHTML =
'<svg class="smb-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
'<defs>' +
'<radialGradient id="' + g + '-shade" cx=".36" cy=".3" r=".78"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".65" stop-color="#bfdbfe" stop-opacity=".2"/><stop offset="1" stop-color="#7aa7e0" stop-opacity=".6"/></radialGradient>' +
'<linearGradient id="' + g + '-ground" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f8fafc"/><stop offset="1" stop-color="#cfe0f5"/></linearGradient>' +
'</defs>' +
'<path d="M-3000 ' + (GROUND - 2) + ' Q-60 ' + (GROUND - 18) + ' 120 ' + (GROUND - 4) + ' T420 ' + (GROUND - 6) + ' T3400 ' + (GROUND - 2) + ' L3400 400 L-3000 400 Z" fill="url(#' + g + '-ground)"/>' +
'<ellipse class="smb-puddle" cx="200" cy="' + (GROUND + 6) + '" rx="40" ry="7" fill="#bfdbfe" opacity="0"/>' +
'<g class="smb-man">' +
'<ellipse cx="200" cy="' + (GROUND + 4) + '" rx="46" ry="6" fill="#94a3b8" opacity=".25" class="smb-shadow"/>' +
'<g class="smb-acc" data-p="174 112"><path d="M174 112 L140 88 M152 96 L147 82 M146 92 L132 90" ' + stick + '/></g>' +
'<g class="smb-acc" data-p="226 112"><path d="M226 112 L262 88 M250 96 L256 82 M256 92 L270 91" ' + stick + '/></g>' +
ball(g, RB, 'smb-b') + ball(g, RM, 'smb-m') + ball(g, RH, 'smb-h') +
'<g class="smb-acc" data-p="200 71"><circle cx="193" cy="71" r="2.7" fill="#111827"/><circle cx="207" cy="71" r="2.7" fill="#111827"/><circle cx="194" cy="70" r=".9" fill="#fff"/><circle cx="208" cy="70" r=".9" fill="#fff"/></g>' +
'<g class="smb-acc" data-p="200 78"><path d="M199 74.5 L223 79 L199 81.5 Z" fill="#f97316"/><path d="M205 76 L206 80 M212 77.5 L213 80" stroke="#c2410c" stroke-width=".8"/></g>' +
'<g class="smb-acc" data-p="200 86"><circle cx="191" cy="84" r="1.4" fill="#111827"/><circle cx="195" cy="86.6" r="1.4" fill="#111827"/><circle cx="200" cy="87.6" r="1.4" fill="#111827"/><circle cx="205" cy="86.6" r="1.4" fill="#111827"/><circle cx="209" cy="84" r="1.4" fill="#111827"/></g>' +
'<g class="smb-acc" data-p="200 118"><circle cx="200" cy="106" r="3" fill="#111827"/><circle cx="200" cy="119" r="3" fill="#111827"/><circle cx="200" cy="132" r="3" fill="#111827"/></g>' +
'<g class="smb-acc" data-p="200 95"><path d="M206 96 L214 126 L204 127 L199 97 Z" fill="' + scarf + '"/><path d="M207 122 L215 123 M206 126 L214 126" stroke="#fff" stroke-opacity=".5" stroke-width="1.4"/><rect x="179" y="89" width="42" height="10" rx="5" fill="' + scarf + '"/><path d="M186 89 L186 99 M194 89 L194 99 M202 89 L202 99 M210 89 L210 99" stroke="#000" stroke-opacity=".15" stroke-width="2"/></g>' +
'<g class="smb-acc" data-p="200 57"><rect x="179" y="53" width="42" height="6" rx="3" fill="' + hat + '"/><rect x="187" y="28" width="26" height="27" rx="2" fill="' + hat + '"/><rect x="187" y="46" width="26" height="6" fill="' + scarf + '"/><rect x="190" y="30" width="4" height="22" rx="2" fill="#fff" opacity=".12"/></g>' +
'</g>' +
'<g class="smb-snow" fill="#fff"></g>' +
'</svg>';
var svg = el.firstChild;
var man = svg.querySelector('.smb-man');
var puddle = svg.querySelector('.smb-puddle');
var shadowEl = svg.querySelector('.smb-shadow');
var snowG = svg.querySelector('.smb-snow');
function ballObj(cls, r) {
var b = svg.querySelector('.' + cls);
return { el: b, roll: b.firstChild, r: r, x: 200, y: 0, rot: 0, q: 0, s: 1 };
}
var B = ballObj('smb-b', RB), M = ballObj('smb-m', RM), H = ballObj('smb-h', RH);
var ACC = [];
svg.querySelectorAll('.smb-acc').forEach(function (a) {
var p = a.getAttribute('data-p').split(' ');
ACC.push({ el: a, px: +p[0], py: +p[1], s: 0, rot: 0, dy: 0 });
});
// Markup order: left arm, right arm, eyes, nose, mouth, buttons, scarf, hat.
var armL = ACC[0], armR = ACC[1], eyes = ACC[2], nose = ACC[3], mouth = ACC[4], buttons = ACC[5], scarfA = ACC[6], hatA = ACC[7];
var MAN = { m: 0, o: 1 };
var VW = 400, flakes = [];
function layout() {
var w = el.clientWidth, h = el.clientHeight;
if (!w || !h) return;
VW = Math.max(400, 250 * w / h);
svg.setAttribute('viewBox', f(200 - VW / 2) + ' 0 ' + f(VW) + ' 250');
if (!snowOn) return;
var n = Math.round(VW / 7), html = '';
flakes = [];
for (var i = 0; i < n; i++) {
var fl = { x: 200 - VW / 2 + Math.random() * VW, y: Math.random() * 250, r: 0.7 + Math.random() * 1.8, ph: Math.random() * 6.3 };
fl.v = 10 + fl.r * 9;
flakes.push(fl);
html += '<circle r="' + f(fl.r) + '" opacity="' + f(0.45 + fl.r * 0.2) + '"/>';
}
snowG.innerHTML = html;
var cs = snowG.childNodes;
flakes.forEach(function (fl, i) { fl.el = cs[i]; });
}
function placeBall(b) {
var sx = b.s * (1 + b.q * 0.6), sy = b.s * (1 - b.q);
b.el.setAttribute('transform', 'translate(' + f(b.x) + ' ' + f(b.y + b.r * b.s) + ') scale(' + f(sx) + ' ' + f(sy) + ') translate(0 ' + (-b.r) + ')');
b.roll.setAttribute('transform', 'rotate(' + f(b.rot) + ')');
}
var t = 0;
function render(dt) {
t += dt || 0;
B.y = GROUND + 4 - RB * B.s;
placeBall(B); placeBall(M); placeBall(H);
ACC.forEach(function (a) {
a.el.setAttribute('transform', 'translate(' + a.px + ' ' + f(a.py + a.dy) + ') rotate(' + f(a.rot) + ') scale(' + f(Math.max(0, a.s)) + ') translate(' + (-a.px) + ' ' + (-a.py) + ')');
});
var m = MAN.m;
man.setAttribute('transform', 'translate(200 ' + (GROUND + 4) + ') scale(' + f(1 + m * 0.3) + ' ' + f(Math.max(0.02, 1 - m * 0.94)) + ') translate(-200 ' + (-(GROUND + 4)) + ')');
man.setAttribute('opacity', f(MAN.o * (1 - m * 0.35)));
puddle.setAttribute('rx', f(30 + m * 70));
puddle.setAttribute('opacity', f(Math.min(1, m * 1.5) * 0.8 * MAN.o));
shadowEl.setAttribute('opacity', f(0.25 * Math.max(0, (B.x - 120) / 80)));
if (dt) flakes.forEach(function (fl) {
fl.y += fl.v * dt / 1000;
if (fl.y > 255) { fl.y = -4; fl.x = 200 - VW / 2 + Math.random() * VW; }
fl.el.setAttribute('cx', f(fl.x + Math.sin(t / 900 + fl.ph) * 4));
fl.el.setAttribute('cy', f(fl.y));
});
else flakes.forEach(function (fl) { fl.el.setAttribute('cx', f(fl.x)); fl.el.setAttribute('cy', f(fl.y)); });
}
function finished() {
B.x = M.x = H.x = 200; B.s = 1; M.y = 118; H.y = 76; B.rot = M.rot = H.rot = 0; B.q = M.q = H.q = 0;
ACC.forEach(function (a) { a.s = 1; a.rot = 0; a.dy = 0; });
MAN.m = 0; MAN.o = 1;
}
layout();
if (window.ResizeObserver) new ResizeObserver(function () { layout(); render(0); }).observe(el);
if (reduced()) { finished(); render(0); return; }
var tl = null, visible = false, pending = false;
function reset() {
var L = 200 - VW / 2 - 60, R = 200 + VW / 2 + 40;
B.x = L; B.s = 0.4; B.rot = 0; B.q = 0;
M.x = R; M.y = GROUND + 4 - RM; M.rot = 0; M.q = 0;
H.x = L; H.y = GROUND + 4 - RH; H.rot = 0; H.q = 0;
ACC.forEach(function (a) { a.s = 0; a.rot = 0; a.dy = 0; });
MAN.m = 0; MAN.o = 1;
return { L: L, R: R };
}
function squash(list, at) {
tl.add(list, { q: [{ to: 0.12, duration: 90 / speed, ease: 'out(2)' }, { to: 0, duration: 650 / speed, ease: 'outElastic(1, .45)' }] }, at);
}
function play() {
if (tl) tl.pause();
var e = reset(), k = 1 / speed;
var DEG = 180 / Math.PI;
var mRoll = -(e.R - 266) / RM * DEG, hRoll = (136 - e.L) / RH * DEG;
tl = anime.createTimeline({
onComplete: function () {
tl = null;
if (!loop) return;
if (visible) play(); else pending = true;
}
});
tl.add(B, { x: [e.L, 200], s: [0.4, 1], rot: [0, (200 - e.L) / (RB * 0.72) * DEG], duration: 1800 * k, ease: 'out(2)' }, 200 * k)
.add(M, { x: [e.R, 266], rot: [0, mRoll], duration: 1200 * k, ease: 'out(2)' }, 1700 * k)
.add(M, {
x: { to: 200, ease: 'inOut(1.5)' },
y: [{ to: 58, duration: 300 * k, ease: 'out(2)' }, { to: 118, duration: 270 * k, ease: 'in(2)' }],
rot: { to: mRoll - 70 }, duration: 570 * k
}, 2950 * k);
squash([M, B], 3520 * k);
tl.add(H, { x: [e.L, 136], rot: [0, hRoll], duration: 1150 * k, ease: 'out(2)' }, 3800 * k)
.add(H, {
x: { to: 200, ease: 'inOut(1.5)' },
y: [{ to: 18, duration: 300 * k, ease: 'out(2)' }, { to: 76, duration: 260 * k, ease: 'in(2)' }],
rot: { to: hRoll + 90 }, duration: 560 * k
}, 5000 * k);
squash([H, M, B], 5560 * k);
tl.add(eyes, { s: [0, 1], duration: 420 * k, ease: 'outBack(3)' }, 6000 * k)
.add(nose, { s: [0, 1], rot: [-30, 0], duration: 480 * k, ease: 'outBack(2.5)' }, 6250 * k)
.add(mouth, { s: [0, 1], duration: 380 * k, ease: 'outBack(2)' }, 6480 * k)
.add(buttons, { s: [0, 1], duration: 420 * k, ease: 'outBack(3)' }, 6650 * k)
.add(armL, { s: [0, 1], rot: [50, 0], duration: 700 * k, ease: 'outElastic(1, .5)' }, 6850 * k)
.add(armR, { s: [0, 1], rot: [-50, 0], duration: 700 * k, ease: 'outElastic(1, .5)' }, 6950 * k)
.add(scarfA, { s: [0, 1], duration: 450 * k, ease: 'outBack(2.5)' }, 7200 * k)
.add(hatA, { s: [1, 1], dy: [-150, 0], duration: 800 * k, ease: 'outBounce' }, 7450 * k)
.add(armR, { rot: [{ to: -28, duration: 220 * k }, { to: 6, duration: 220 * k }, { to: -28, duration: 220 * k }, { to: 6, duration: 220 * k }, { to: 0, duration: 260 * k }], ease: 'inOut(2)' }, 8500 * k);
if (loop) {
tl.add(MAN, { m: [0, 1], duration: 2200 * k, ease: 'in(1.6)' }, 11500 * k)
.add(MAN, { o: [1, 0], duration: 600 * k, ease: 'linear' }, 13700 * k)
.add({ duration: 500 * k });
}
}
reset();
render(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;
render(dt);
}
});
var started = false;
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (visible) { last = 0; timer.play(); if (tl) tl.play(); }
else { timer.pause(); if (tl) tl.pause(); }
if (visible && !started) { started = true; play(); }
else if (visible && pending) { pending = false; play(); }
}, { threshold: 0.25 }).observe(el);
}
function init() {
document.querySelectorAll('[data-snowman-build]').forEach(function (el) {
if (el._smb) return; // idempotent
el._smb = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-snowman-build style="background: #bfdbfe"></div>
<!-- a green scarf, building again and again: -->
<div data-snowman-build data-loop data-color="#16a34a"></div>