Boiling Line Text
The edges of the letters wobble non-stop, redrawn a few times a second like the “line boil” in a hand-drawn cartoon. A handful of noise filters take turns, so it’s nearly free to run.
Hand drawn
How to use it
- Paste the snippet just before
</head>. - Add
data-boiling-line-textto any heading. Outlines and cartoon shadows make the wobble easy to see.
Options
Add any of these attributes to the same element.
data-strength | How far the edges wander, in em. Default 0.05. |
data-detail | How many wobbles fit in each em: higher is crinklier. Default 2.5. |
data-fps | How many times a second it redraws. Default 9; cartoons use 8 to 12. |
data-frames | How many different drawings it cycles through. Default 4. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Boiling Line Text: the edges of the letters wobble non-stop, redrawn a
// few times a second like the "line boil" of a hand-drawn cartoon. A small
// set of noise filters is swapped in turn, so it costs almost nothing.
// Usage: <h1 data-boiling-line-text>Doodle</h1>
// Optional: data-strength (how far the edges wander, in em; default
// 0.05), data-detail (how many wobbles per em; default 2.5), data-fps
// (redraws per second; default 9, cartoons use 8 to 12), data-frames
// (how many different drawings it cycles through; default 4).
var DEFAULTS = { strength: 0.05, detail: 2.5, fps: 9, frames: 4 };
var count = 0;
var holder;
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function svgEl(tag, attrs) {
var n = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function setup(el) {
var d = el.dataset;
var strength = d.strength !== undefined ? +d.strength : DEFAULTS.strength;
var detail = +d.detail || DEFAULTS.detail;
var fps = +d.fps || DEFAULTS.fps;
var frames = Math.max(1, Math.min(10, +d.frames || DEFAULTS.frames));
if (!holder) {
holder = svgEl('svg', { width: 0, height: 0, 'aria-hidden': 'true', focusable: 'false' });
holder.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden';
document.body.appendChild(holder);
}
var id = 'blt-' + (++count);
var noises = [], maps = [];
for (var i = 0; i < frames; i++) {
var f = svgEl('filter', { id: id + '-' + i, x: '-10%', y: '-20%', width: '120%', height: '140%' });
var noise = svgEl('feTurbulence', { type: 'fractalNoise', numOctaves: 2, seed: count * 13 + i * 7, result: 'n' });
var map = svgEl('feDisplacementMap', { in: 'SourceGraphic', in2: 'n', xChannelSelector: 'R', yChannelSelector: 'G' });
f.appendChild(noise);
f.appendChild(map);
holder.appendChild(f);
noises.push(noise);
maps.push(map);
}
// Wobble sizes follow the font size, so big and small text boil alike.
function size() {
var fs = parseFloat(getComputedStyle(el).fontSize);
noises.forEach(function (n) { n.setAttribute('baseFrequency', (detail / fs).toFixed(4)); });
maps.forEach(function (m) { m.setAttribute('scale', (strength * fs * 2).toFixed(2)); });
}
size();
new ResizeObserver(size).observe(el);
var frame = 0;
function show(i) { el.style.filter = 'url(#' + id + '-' + i + ')'; }
show(0);
if (reduced()) return;
var acc = 0, prev = 0;
var ticker = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
acc += prev ? Math.min(200, now - prev) : 0;
prev = now;
if (acc < 1000 / fps) return;
acc = 0;
frame = (frame + 1) % frames;
show(frame);
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { prev = 0; ticker.play(); }
else ticker.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-boiling-line-text]').forEach(function (el) {
if (el._blt) return;
el._blt = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-boiling-line-text>Hand drawn</h1>
<!-- shakier and faster: -->
<h1 data-boiling-line-text data-strength="0.08" data-fps="12">Jittery</h1>