Pumpkin Flicker
A carved jack-o’-lantern glows from a candle inside. The light wavers, dips and flares like a real flame, and a warm glow pulses round the pumpkin and onto the ground.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-pumpkin-flicker>where the pumpkin should sit. It looks best on a dark background.
Options
Add any of these attributes to the same element.
data-face | grin or spooky. Default grin. |
data-speed | Flicker speed multiplier. Default 1. |
data-glow | Colour of the candlelight. Default #fde047. |
It is 8:5 with a transparent background by default; set your own height or background on the element. It pauses when off screen. With reduced motion the face glows steadily.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Pumpkin Flicker: a carved jack-o'-lantern whose face glows from a candle
// inside. The light wavers, dips and flares like a real flame, and a warm
// glow pulses round the pumpkin and onto the ground. Drawn as SVG that
// fills the element, and paused when off screen.
// Usage: <div data-pumpkin-flicker></div>
// It is 8:5 by default with a transparent background; it looks best on
// something dark.
// Optional: data-face ("grin", default, or "spooky"), data-speed (flicker
// speed multiplier, default 1), data-glow (light colour, default #fde047).
var DEFAULTS = { face: 'grin', speed: 1, glow: '#fde047' };
var uid = 0;
var css =
'[data-pumpkin-flicker]{display:block;position:relative;width:100%;aspect-ratio:8/5}' +
'.pkf-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// Face cut-outs, in viewBox units, centred on the pumpkin at (200, 148).
var FACES = {
grin:
'M160 126 L178 104 L190 130 Z' +
'M240 126 L222 104 L210 130 Z' +
'M200 134 L192 150 L208 150 Z' +
'M150 156 Q200 196 250 156 L240 160 L234 154 L226 166 L216 160 L208 172 L200 162 L192 172 L184 160 L174 166 L166 154 L160 160 Z',
spooky:
'M156 130 Q170 104 190 124 Q176 120 170 134 Z' +
'M244 130 Q230 104 210 124 Q224 120 230 134 Z' +
'M194 138 L200 150 L206 138 Z' +
'M156 170 L168 158 L176 172 L186 160 L194 174 L200 162 L206 174 L214 160 L224 172 L232 158 L244 170 Q200 190 156 170 Z'
};
function build(el) {
var d = el.dataset;
var n = ++uid;
var face = FACES[d.face] || FACES[DEFAULTS.face];
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var light = d.glow || DEFAULTS.glow;
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A carved pumpkin glowing with candlelight');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
var g = 'pkf' + n;
var rib = function (cx, rx, ry) {
return '<ellipse cx="' + cx + '" cy="150" rx="' + rx + '" ry="' + ry + '" fill="url(#' + g + '-skin)" stroke="#9a3412" stroke-opacity=".45" stroke-width="1.5"/>';
};
el.innerHTML =
'<svg class="pkf-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
'<defs>' +
'<radialGradient id="' + g + '-halo"><stop offset="0" stop-color="#fb923c" stop-opacity=".5"/><stop offset=".5" stop-color="#ea580c" stop-opacity=".14"/><stop offset="1" stop-color="#ea580c" stop-opacity="0"/></radialGradient>' +
'<radialGradient id="' + g + '-pool"><stop offset="0" stop-color="#fdba74" stop-opacity=".6"/><stop offset="1" stop-color="#f97316" stop-opacity="0"/></radialGradient>' +
'<radialGradient id="' + g + '-skin" cx=".4" cy=".35" r=".75"><stop offset="0" stop-color="#fb923c"/><stop offset=".6" stop-color="#ea580c"/><stop offset="1" stop-color="#9a3412"/></radialGradient>' +
'<radialGradient id="' + g + '-fire" cx=".5" cy=".62" r=".6"><stop offset="0" stop-color="#fffbe6"/><stop offset=".55" stop-color="' + light + '"/><stop offset="1" stop-color="#facc15"/></radialGradient>' +
'<radialGradient id="' + g + '-face" cx=".5" cy=".6" r=".55"><stop offset="0" stop-color="#fde68a" stop-opacity=".55"/><stop offset="1" stop-color="#fde68a" stop-opacity="0"/></radialGradient>' +
'<filter id="' + g + '-blur" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="3.5"/></filter>' +
'<clipPath id="' + g + '-cut"><path d="' + face + '"/></clipPath>' +
'</defs>' +
'<circle class="pkf-halo" cx="200" cy="150" r="170" fill="url(#' + g + '-halo)"/>' +
'<ellipse class="pkf-pool" cx="200" cy="210" rx="130" ry="16" fill="url(#' + g + '-pool)"/>' +
'<ellipse cx="200" cy="210" rx="86" ry="8" fill="#000" opacity=".35"/>' +
// Stem.
'<path d="M194 94 Q192 76 186 68 Q196 62 206 70 Q204 80 208 94 Z" fill="#4d7c0f"/>' +
'<path d="M199 92 Q198 78 194 70" stroke="#365314" stroke-width="2" fill="none"/>' +
// Ribs, back to front, then a soft sheen.
rib(146, 44, 58) + rib(254, 44, 58) +
rib(166, 46, 62) + rib(234, 46, 62) +
rib(200, 50, 64) +
'<ellipse class="pkf-skinglow" cx="200" cy="160" rx="80" ry="48" fill="url(#' + g + '-face)"/>' +
// The face: a dark rim (the cut flesh), then the candlelit inside.
'<path d="' + face + '" fill="#7c2d12" transform="translate(-1.5 -2)"/>' +
'<g clip-path="url(#' + g + '-cut)">' +
'<rect x="140" y="96" width="120" height="96" fill="#ea580c"/>' +
'<rect class="pkf-fire" x="140" y="96" width="120" height="96" fill="url(#' + g + '-fire)"/>' +
'<path class="pkf-shadow" d="' + face + '" fill="none" stroke="#9a3412" stroke-width="3" opacity=".55" transform="translate(1 1.5)"/>' +
'</g>' +
// Light spilling out of the holes.
'<path class="pkf-spill" d="' + face + '" fill="' + light + '" filter="url(#' + g + '-blur)" opacity="0"/>' +
'</svg>';
var svg = el.firstChild;
var fire = svg.querySelector('.pkf-fire');
var halo = svg.querySelector('.pkf-halo');
var pool = svg.querySelector('.pkf-pool');
var skin = svg.querySelector('.pkf-skinglow');
var spill = svg.querySelector('.pkf-spill');
// Candle flicker: layered sines for the steady waver, plus random dips
// and flares that ease in and out.
var t = 0, gust = 0, gustTarget = 0, nextGust = 600;
function level() {
var s = t / 1000;
var base = 0.86 + 0.08 * Math.sin(s * 7.3) + 0.06 * Math.sin(s * 13.1 + 1.7) + 0.04 * Math.sin(s * 23.7 + 0.4);
return Math.max(0.4, Math.min(1, base + gust));
}
function frame(dt) {
t += dt * speed;
nextGust -= dt * speed;
if (nextGust <= 0) {
gustTarget = Math.random() < 0.65 ? -(0.2 + Math.random() * 0.35) : 0.12 + Math.random() * 0.1;
nextGust = 250 + Math.random() * 1400;
}
gust += (gustTarget - gust) * Math.min(1, dt / 90);
gustTarget *= Math.pow(0.5, dt / 220);
var v = level();
fire.setAttribute('opacity', Math.min(1, 0.3 + v * 0.8).toFixed(3));
halo.setAttribute('opacity', (0.35 + v * 0.65).toFixed(3));
pool.setAttribute('opacity', (v * v).toFixed(3));
skin.setAttribute('opacity', (v * 0.9).toFixed(3));
spill.setAttribute('opacity', (Math.pow(v, 2) * 0.55).toFixed(3));
// The light leans a touch as the flame sways.
fire.setAttribute('transform', 'translate(' + (Math.sin(t / 340) * 4).toFixed(2) + ' ' + ((1 - v) * 6).toFixed(2) + ')');
}
if (reduced()) {
frame(0);
return;
}
frame(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;
frame(dt);
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-pumpkin-flicker]').forEach(function (el) {
if (el._pkf) return; // idempotent
el._pkf = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-pumpkin-flicker style="background: #120a1f"></div>
<!-- spooky face, eerie green light: -->
<div data-pumpkin-flicker data-face="spooky" data-glow="#bef264"></div>