Egg Crack
A painted Easter egg wobbles, and a crack zigzags round it in fits and starts. Then the top pops off and tumbles aside, and a chick pops up, flaps, blinks and hops.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-egg-crack>where the egg should sit. It hatches when it scrolls into view.
Options
Add any of these attributes to the same element.
data-loop | Put the egg back together and hatch again. |
data-color | Shell colour. Default lavender #c4b5fd. |
data-plain | No painted pattern on the shell. |
data-surprise="none" | No chick inside, just a burst of sparkle. |
data-delay | How long the egg sits still before it starts to wobble. Default 600 ms. |
It is 8:5 with a transparent background by default; set your own height or background on the element. animacious.eggCrack(el) hatches it again from script. With reduced motion it shows the egg already open, chick and all.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Egg Crack: a painted Easter egg wobbles, a crack zigzags round it in
// fits and starts, then the top of the shell pops off and tumbles aside and
// a chick pops up, blinks and flaps. Drawn as SVG that fills the element.
// Usage: <div data-egg-crack></div>
// It is 8:5 by default with a transparent background.
// Optional: data-loop (put the egg back together and hatch again),
// data-color (shell colour, default #c4b5fd), data-plain (no painted
// pattern), data-surprise="none" (no chick, just a burst of sparkle),
// data-delay (ms it sits still before wobbling, default 600).
// Script use: animacious.eggCrack(el) hatches it again.
var DEFAULTS = { color: '#c4b5fd', delay: 600 };
var uid = 0;
var EGG = 'M200 40 C244 40 266 112 266 150 C266 188 238 210 200 210 C162 210 134 188 134 150 C134 112 156 40 200 40 Z';
var ZIG = [];
for (var zx = 124, zi = 0; zx <= 276; zx += 16, zi++) ZIG.push(zx + ' ' + (zi % 2 ? 136 : 121));
// The bottom half reaches a little higher, under the top half, so no seam shows.
var ZIGLOW = ZIG.map(function (pt) { var xy = pt.split(' '); return xy[0] + ' ' + (xy[1] - 5); }).join(' L');
ZIG = ZIG.join(' L');
var css =
'[data-egg-crack]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
'.egc-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 shell(g, plain) {
var deco = plain ? '' :
'<path d="M130 78 L' + [146, 162, 178, 194, 210, 226, 242, 258, 274].map(function (x, i) { return x + ' ' + (i % 2 ? 78 : 66); }).join(' L') + '" fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"/>' +
'<path d="M128 104 Q144 94 160 104 T192 104 T224 104 T256 104 T288 104" fill="none" stroke="#f9a8d4" stroke-width="5"/>' +
[148, 172, 196, 220, 244].map(function (x, i) { return '<circle cx="' + (x + 4) + '" cy="160" r="5" fill="' + (i % 2 ? '#fde68a' : '#fff') + '"/>'; }).join('') +
'<rect x="120" y="180" width="160" height="10" fill="#fde68a"/>' +
'<rect x="120" y="194" width="160" height="4" fill="#f9a8d4"/>';
return '<g clip-path="url(#' + g + '-egg)">' +
'<path d="' + EGG + '" fill="url(#' + g + '-base)"/>' + deco +
'<path d="' + EGG + '" fill="url(#' + g + '-shade)"/>' +
'</g>';
}
function build(el) {
var d = el.dataset;
var n = ++uid;
var g = 'egc' + n;
var loop = d.loop !== undefined && d.loop !== 'false';
var color = d.color || DEFAULTS.color;
var plain = d.plain !== undefined && d.plain !== 'false';
var chick = d.surprise !== 'none';
var wait = d.delay !== undefined ? +d.delay : DEFAULTS.delay;
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', chick ? 'An Easter egg wobbles, cracks open and a chick pops out' : 'An Easter egg wobbles and cracks open');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
var sparks = '';
for (var k = 0; k < 10; k++) sparks += '<path class="egc-spark" data-a="' + (k * 36 + 18) + '" stroke="' + (k % 2 ? '#fde68a' : '#f9a8d4') + '" stroke-width="3" stroke-linecap="round"/>';
var shards = '';
for (k = 0; k < 6; k++) shards += '<path class="egc-shard" d="M0 -5 L5 4 L-4 3 Z" fill="' + (k % 2 ? '#fff' : color) + '" opacity="0"/>';
el.innerHTML =
'<svg class="egc-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
'<defs>' +
'<clipPath id="' + g + '-egg"><path d="' + EGG + '"/></clipPath>' +
'<clipPath id="' + g + '-top"><path d="M100 0 L100 121 L' + ZIG + ' L300 136 L300 0 Z"/></clipPath>' +
'<clipPath id="' + g + '-bot"><path d="M100 260 L100 116 L' + ZIGLOW + ' L300 131 L300 260 Z"/></clipPath>' +
'<linearGradient id="' + g + '-base" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff"/><stop offset=".25" stop-color="' + color + '"/><stop offset="1" stop-color="' + color + '"/></linearGradient>' +
'<radialGradient id="' + g + '-shade" cx=".36" cy=".3" r=".85"><stop offset="0" stop-color="#fff" stop-opacity=".55"/><stop offset=".35" stop-color="#fff" stop-opacity="0"/><stop offset=".8" stop-color="#1e1b4b" stop-opacity=".06"/><stop offset="1" stop-color="#1e1b4b" stop-opacity=".3"/></radialGradient>' +
'<radialGradient id="' + g + '-chick" cx=".38" cy=".32" r=".8"><stop offset="0" stop-color="#fef9c3"/><stop offset=".55" stop-color="#fde047"/><stop offset="1" stop-color="#eab308"/></radialGradient>' +
'</defs>' +
'<ellipse class="egc-ground" cx="200" cy="212" rx="78" ry="8" fill="#1e1b4b" opacity=".16"/>' +
'<g class="egc-sparks" opacity="0">' + sparks + '</g>' +
'<g class="egc-egg">' +
(chick ?
'<g class="egc-chick">' +
'<g class="egc-wl"><ellipse cx="-30" cy="6" rx="9" ry="15" fill="#facc15" transform="rotate(30 -30 6)"/></g>' +
'<g class="egc-wr"><ellipse cx="30" cy="6" rx="9" ry="15" fill="#facc15" transform="rotate(-30 30 6)"/></g>' +
'<circle r="34" fill="url(#' + g + '-chick)"/>' +
'<path d="M-4 -33 Q-8 -46 0 -44 Q-2 -38 2 -34 Q4 -44 10 -40 Q4 -36 4 -32 Z" fill="#facc15"/>' +
'<g class="egc-eyes"><ellipse cx="-11" cy="-6" rx="3.6" ry="4.2" fill="#1c1917"/><ellipse cx="11" cy="-6" rx="3.6" ry="4.2" fill="#1c1917"/>' +
'<circle cx="-10" cy="-7.5" r="1.2" fill="#fff"/><circle cx="12" cy="-7.5" r="1.2" fill="#fff"/></g>' +
'<ellipse cx="-19" cy="4" rx="5" ry="3" fill="#fb7185" opacity=".45"/><ellipse cx="19" cy="4" rx="5" ry="3" fill="#fb7185" opacity=".45"/>' +
'<path class="egc-beak" d="M-6 1 L0 -3 L6 1 L0 8 Z" fill="#f97316"/>' +
'</g>' : '') +
'<g clip-path="url(#' + g + '-bot)">' + shell(g, plain) +
'<path d="M124 121 L' + ZIG + '" fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round" clip-path="url(#' + g + '-egg)" class="egc-rim" opacity="0"/>' +
'</g>' +
'<g class="egc-top"><g clip-path="url(#' + g + '-top)">' + shell(g, plain) + '</g></g>' +
'<path class="egc-crack" d="M124 121 L' + ZIG + '" fill="none" stroke="#3b0764" stroke-opacity=".75" stroke-width="2.2" stroke-linejoin="round" clip-path="url(#' + g + '-egg)"/>' +
'</g>' +
shards +
'</svg>';
var svg = el.firstChild;
var eggG = svg.querySelector('.egc-egg');
var topG = svg.querySelector('.egc-top');
var crack = svg.querySelector('.egc-crack');
var rim = svg.querySelector('.egc-rim');
var chickG = svg.querySelector('.egc-chick');
var eyes = svg.querySelector('.egc-eyes');
var wl = svg.querySelector('.egc-wl'), wr = svg.querySelector('.egc-wr');
var sparkG = svg.querySelector('.egc-sparks');
var sparkEls = svg.querySelectorAll('.egc-spark');
var shardEls = svg.querySelectorAll('.egc-shard');
var len = crack.getTotalLength ? crack.getTotalLength() : 220;
crack.setAttribute('stroke-dasharray', len + ' ' + len);
var S;
function reset() {
S = { rot: 0, crack: 0, tx: 0, ty: 0, tr: 0, cy: 62, flap: 0, blink: 1, hop: 0, spark: 0, shard: 0, fade: 1 };
}
var shardDir = [];
for (k = 0; k < shardEls.length; k++) shardDir.push({ a: -Math.PI * (0.15 + 0.7 * k / (shardEls.length - 1)), r: 70 + (k % 3) * 22, x: 150 + k * 20 });
function render() {
eggG.setAttribute('transform', 'rotate(' + S.rot.toFixed(2) + ' 200 210)');
eggG.setAttribute('opacity', S.fade);
crack.setAttribute('stroke-dashoffset', (len * (1 - S.crack)).toFixed(1));
crack.setAttribute('opacity', S.ty < -2 ? 0 : 1);
rim.setAttribute('opacity', S.ty < -2 ? 0.9 : 0);
topG.setAttribute('transform', 'translate(' + S.tx.toFixed(1) + ' ' + S.ty.toFixed(1) + ') rotate(' + S.tr.toFixed(1) + ' 200 86)');
if (chickG) {
chickG.setAttribute('transform', 'translate(200 ' + (112 + S.cy - S.hop).toFixed(1) + ')');
eyes.setAttribute('transform', 'translate(0 -6) scale(1 ' + S.blink.toFixed(2) + ') translate(0 6)');
wl.setAttribute('transform', 'rotate(' + (S.flap * 40).toFixed(1) + ' -24 -4)');
wr.setAttribute('transform', 'rotate(' + (-S.flap * 40).toFixed(1) + ' 24 -4)');
}
sparkG.setAttribute('opacity', S.spark > 0 && S.spark < 1 ? (1 - S.spark).toFixed(2) : 0);
sparkEls.forEach(function (p) {
var a = (+p.getAttribute('data-a') - 90) * Math.PI / 180;
var r1 = 50 + S.spark * 40, r2 = r1 + 16 * (1 - S.spark) + 2;
p.setAttribute('d', 'M' + (200 + Math.cos(a) * r1 * 1.2).toFixed(1) + ' ' + (110 + Math.sin(a) * r1).toFixed(1) +
' L' + (200 + Math.cos(a) * r2 * 1.2).toFixed(1) + ' ' + (110 + Math.sin(a) * r2).toFixed(1));
});
shardEls.forEach(function (p, i) {
var sd = shardDir[i], u = S.shard;
p.setAttribute('opacity', u > 0 && u < 1 ? (1 - u * u).toFixed(2) : 0);
p.setAttribute('transform', 'translate(' + (sd.x + Math.cos(sd.a) * sd.r * u).toFixed(1) + ' ' + (126 + Math.sin(sd.a) * sd.r * u + 140 * u * u).toFixed(1) + ') rotate(' + (u * 400 + i * 60).toFixed(0) + ')');
});
}
reset();
if (reduced()) {
// The finished scene: shell open, chick out.
S.crack = 1; S.tx = -140; S.ty = 86; S.tr = -164; S.cy = 0;
render();
return;
}
render();
var tl = null, visible = false, pending = false;
function hatch() {
if (tl) tl.pause();
reset();
render();
var wobble = function (a) { return { rot: [0, -a, a * 0.85, -a * 0.6, a * 0.3, 0], duration: 620, ease: 'inOutSine' }; };
tl = anime.createTimeline({
onUpdate: render,
onComplete: function () {
tl = null;
if (!loop) return;
if (visible) hatch(); else pending = true;
}
});
tl.add(S, { fade: [0, 1], duration: 400 })
.add(S, wobble(5), '+=' + wait)
.add(S, { crack: 0.3, duration: 220, ease: 'out(3)' })
.add(S, wobble(8), '+=380')
.add(S, { crack: 0.65, duration: 220, ease: 'out(3)' })
.add(S, wobble(11), '+=320')
.add(S, { crack: 1, duration: 260, ease: 'out(3)' })
.add(S, { rot: [0, 2, -2, 0], duration: 260, ease: 'linear' }, '+=200')
// Pop! The top flies up and tumbles over to the side.
.add(S, { ty: -74, tx: -34, tr: -40, duration: 340, ease: 'out(3)' })
.add(S, { spark: [0, 1], duration: 620, ease: 'out(2)' }, '<<')
.add(S, { shard: [0, 1], duration: 900, ease: 'linear' }, '<<')
.add(S, { tx: -140, duration: 820, ease: 'linear' })
.add(S, { ty: 86, tr: -164, duration: 820, ease: 'outBounce' }, '<<');
if (chickG) {
tl.add(S, { cy: 0, duration: 620, ease: 'outBack(2)' }, '-=1150')
.add(S, { flap: [0, 1, 0, 1, 0, 1, 0], duration: 700, ease: 'inOutSine' }, '-=100')
.add(S, { blink: [1, 0.1, 1], duration: 220, ease: 'linear' }, '+=200')
.add(S, { hop: [0, 14, 0, 9, 0], duration: 700, ease: 'inOutQuad' }, '+=160')
.add(S, { blink: [1, 0.1, 1], duration: 220, ease: 'linear' }, '+=500');
}
if (loop) tl.add(S, { fade: 0, duration: 450, ease: 'in(2)' }, '+=1600');
}
el._egcHatch = hatch;
var started = false;
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (tl) { if (visible) tl.play(); else tl.pause(); }
if (visible && !started) { started = true; hatch(); }
else if (visible && pending) { pending = false; hatch(); }
}, { threshold: 0.25 }).observe(el);
}
window.animacious = window.animacious || {};
window.animacious.eggCrack = function (el) { if (el && el._egcHatch) el._egcHatch(); };
function init() {
document.querySelectorAll('[data-egg-crack]').forEach(function (el) {
if (el._egc) return; // idempotent
el._egc = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-egg-crack></div>
<!-- a plain blue egg, hatching again and again: -->
<div data-egg-crack data-loop data-plain data-color="#93c5fd"></div>