Swinging Lamp Shadow
A little lamp hangs above the text and sways on its cord, and the text’s soft shadow swings the other way across the wall behind it, stretching out and blurring as the light moves off to the side.
Late night
How to use it
- Paste the snippet just before
</head>. - Add
data-swinging-lamp-shadowto a heading, with about 2.5em of room above it for the lamp. A pale, wall-like background shows the shadow best.
Options
Add any of these attributes to the same element.
data-swing | How far the lamp sways each way, in degrees. Default 30. |
data-period | Time for one swing there and back. Default 3000 ms. |
data-cord | Length of the cord, in em of the text. Default 1.4. |
data-depth | How far behind the text the wall is: bigger throws the shadow further. Default 0.5. |
data-color | Shadow colour. Default rgba(0,0,0,.6). |
data-light | Colour of the bulb and its glow. Default #ffd27a. |
data-lamp | false hides the drawn lamp and keeps just the swinging shadow. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Swinging Lamp Shadow: a lamp hangs above the text and sways on its cord,
// and the text's soft shadow swings the other way across the wall behind,
// growing longer and softer as the light moves off to the side.
// Usage: <h1 data-swinging-lamp-shadow>Lights on</h1>
// Leave room above the text for the lamp (about 2.5em), or add
// data-lamp="false" to keep just the swinging shadow.
// Optional: data-swing (how far it sways each way, in degrees; default 30),
// data-period (ms for one swing there and back; default 3000),
// data-cord (cord length, in em of the text; default 1.4),
// data-color (shadow colour; default rgba(0,0,0,.6)), data-depth (how far
// behind the text the wall is: bigger throws the shadow further; default 0.5),
// data-lamp ("false" hides the drawn lamp), data-light (bulb colour;
// default #ffd27a).
var DEFAULTS = { swing: 30, period: 3000, cord: 1.4, color: 'rgba(0,0,0,.6)', depth: 0.5, light: '#ffd27a' };
// Lamp parts are drawn in "lamp em", half the text's em.
var css =
'.lmp-text{position:relative}' +
'.lmp-pivot{position:absolute;left:50%;width:0;height:0;font-size:.5em;pointer-events:none}' +
'.lmp-mount{position:absolute;left:-.45em;top:-.2em;width:.9em;height:.3em;border-radius:.1em .1em .4em .4em;background:#2b2433}' +
'.lmp-arm{position:absolute;left:0;top:0;width:0;transform-origin:0 0;will-change:transform}' +
'.lmp-cord{position:absolute;left:-.04em;top:0;width:.08em;background:#5b5266}' +
'.lmp-shade{position:absolute;left:-.8em;width:1.6em;height:.8em;border-radius:.9em .9em .08em .08em;background:linear-gradient(#3a3145,#1f1a26);box-shadow:inset 0 .08em 0 rgba(255,255,255,.18)}' +
'.lmp-bulb{position:absolute;left:-.32em;width:.64em;height:.36em;border-radius:0 0 .36em .36em;background:var(--lmp-light);box-shadow:0 0 .5em .2em var(--lmp-light),0 0 3em 1.4em var(--lmp-glow)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function rgba(color, a) {
var probe = document.createElement('span');
probe.style.color = color;
document.body.appendChild(probe);
var m = getComputedStyle(probe).color.match(/[\d.]+/g) || [0, 0, 0, 1];
probe.remove();
var base = m.length > 3 ? +m[3] : 1;
return 'rgba(' + m[0] + ',' + m[1] + ',' + m[2] + ',' + (base * a).toFixed(3) + ')';
}
function setup(el) {
var d = el.dataset;
var swing = d.swing !== undefined ? +d.swing : DEFAULTS.swing;
var period = +d.period || DEFAULTS.period;
var cord = +d.cord || DEFAULTS.cord;
var depth = d.depth !== undefined ? +d.depth : DEFAULTS.depth;
var color = d.color || DEFAULTS.color;
var light = d.light || DEFAULTS.light;
var showLamp = d.lamp !== 'false';
el.classList.add('lmp-text');
// Geometry in text em. The bulb hangs GAP above the top of the text.
var GAP = 0.35, C = cord * 2; // cord in lamp em
var L = (C + 1.0) / 2; // pivot to bulb, text em
var pivotTop = -(GAP + L); // pivot, from the text's top
var arm = null;
if (showLamp) {
var pivot = document.createElement('span');
pivot.className = 'lmp-pivot';
pivot.setAttribute('aria-hidden', 'true');
pivot.style.top = (pivotTop * 2) + 'em'; // lamp em are half the text's
pivot.style.setProperty('--lmp-light', light);
pivot.style.setProperty('--lmp-glow', rgba(light, 0.28));
pivot.innerHTML = '<span class="lmp-mount"></span><span class="lmp-arm">' +
'<span class="lmp-cord" style="height:' + (C + 0.1) + 'em"></span>' +
'<span class="lmp-bulb" style="top:' + (C + 0.64) + 'em"></span>' +
'<span class="lmp-shade" style="top:' + C + 'em"></span></span>';
el.appendChild(pivot);
arm = pivot.querySelector('.lmp-arm');
}
var fs = 16, halfH = 0.6;
function measure() {
fs = parseFloat(getComputedStyle(el).fontSize);
halfH = el.offsetHeight / fs / 2;
}
measure();
new ResizeObserver(measure).observe(el);
var c1 = rgba(color, 0.9), c2 = rgba(color, 0.45), c3 = rgba(color, 0.18);
function px(v) { return (v * fs).toFixed(1) + 'px'; }
function draw(deg) {
var a = deg * Math.PI / 180;
if (arm) arm.style.transform = 'rotate(' + (-deg).toFixed(2) + 'deg)';
// Bulb position relative to the text's centre, in em.
var lx = Math.sin(a) * L;
var ly = (pivotTop + Math.cos(a) * L) - halfH;
// The shadow lands on the far side of the text from the light, longer
// and softer the lower and further off the light is.
var ox = -lx * depth, oy = -ly * depth;
var len = Math.sqrt(ox * ox + oy * oy);
el.style.textShadow =
px(ox * 0.3) + ' ' + px(oy * 0.3) + ' ' + px(0.02 + len * 0.04) + ' ' + c1 + ',' +
px(ox * 0.75) + ' ' + px(oy * 0.75) + ' ' + px(0.04 + len * 0.12) + ' ' + c2 + ',' +
px(ox * 1.3) + ' ' + px(oy * 1.3) + ' ' + px(0.08 + len * 0.3) + ' ' + c3;
}
if (reduced()) { draw(swing * 0.6); return; }
draw(swing);
var running = false, raf = 0, clock = 0, last = 0;
function frame(now) {
if (!running) return;
clock += Math.min(50, now - last); last = now;
var p = clock / period * Math.PI * 2;
// A pendulum, with a faint second wobble so it never looks mechanical.
draw(swing * Math.cos(p) + swing * 0.06 * Math.sin(p * 2.7 + 1));
raf = requestAnimationFrame(frame);
}
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) {
if (!running) { running = true; last = performance.now(); raf = requestAnimationFrame(frame); }
} else {
running = false; cancelAnimationFrame(raf);
}
}).observe(el);
}
function init() {
document.querySelectorAll('[data-swinging-lamp-shadow]').forEach(function (el) {
if (el._lmp) return;
el._lmp = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-swinging-lamp-shadow>Late night</h1>
<!-- just the shadow, a warmer one, swinging slowly: -->
<h1 data-swinging-lamp-shadow data-lamp="false" data-color="#7c2d12" data-period="4500">Late night</h1>