Dimension Lines
Measurements draw in like a technical drawing. Extension lines run out from the edges of your picture, each dimension line grows from its middle out to both ends, the arrowheads snap on and the size counts up to its value.
How to use it
- Paste the snippet just before
</head>. - Wrap your picture in an element with
data-dimension-lines. - Add the sizes to show with
data-widthanddata-height, such asdata-width="210 cm".
Options
Add any of these attributes to the same element.
data-width / data-height | The sizes to show, with any units. Leave one out to skip that line. |
data-x-side | Where the width goes: bottom or top. Default bottom. |
data-y-side | Where the height goes: right or left. Default right. |
data-color | Colour of the lines and sizes. Default #7c3aed. |
data-duration | Time to draw each line. Default 1100 ms. |
data-loop | Clear and measure again. |
The lines fit the edges of the picture, so trim any empty space around it. Text size follows the element’s font-size.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Dimension Lines: measurement lines draw in around a picture, like a
// technical drawing. Extension lines run out from its edges, each
// dimension line grows from its middle to both ends, the arrowheads snap
// on and the size counts up to its value.
// Usage:
// <div data-dimension-lines data-width="210 cm" data-height="85 cm">
// <img src="sofa.png" alt="Sofa">
// </div>
// The first child is the thing being measured; the lines fit its edges.
// Optional: data-width and data-height (the sizes shown; leave one out to
// skip that line), data-x-side (bottom or top, default bottom),
// data-y-side (right or left, default right), data-color (default
// #7c3aed), data-duration (ms for each line, default 1100), data-loop
// (clear and measure again).
var DEFAULTS = { color: '#7c3aed', duration: 1100 };
var NS = 'http://www.w3.org/2000/svg';
var css =
'.dml{position:relative;display:inline-block;box-sizing:border-box;max-width:100%;padding:.5em;--dml-c:#7c3aed}' +
'.dml-b{padding-bottom:3em}.dml-t{padding-top:3em}.dml-r,.dml-l{padding-left:3em;padding-right:3em}' +
'.dml>:first-child{display:block;max-width:100%}' +
'.dml-svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}' +
'.dml-text{font-weight:700;font-size:.95em;font-variant-numeric:tabular-nums;fill:var(--dml-c);paint-order:stroke;stroke:rgba(255,255,255,.85);stroke-width:4px;stroke-linejoin:round}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function mk(tag, attrs, parent) {
var n = document.createElementNS(NS, tag);
for (var k in attrs) n.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(n);
return n;
}
// "146.7 mm" -> { n: 146.7, dp: 1, pre: '', post: ' mm' }
function parseSize(str) {
var m = String(str).match(/^(\D*?)(\d+(?:[.,]\d+)?)(.*)$/);
if (!m) return { text: str };
var num = m[2].replace(',', '.');
return { n: parseFloat(num), dp: (num.split('.')[1] || '').length, pre: m[1], post: m[3], comma: m[2].indexOf(',') > -1 };
}
function fmt(p, v) {
if (p.n == null) return p.text;
var t = v.toFixed(p.dp);
return p.pre + (p.comma ? t.replace('.', ',') : t) + p.post;
}
function setup(el) {
var d = el.dataset, color = d.color || DEFAULTS.color;
var s = (el._dml = { dims: [], visible: false, running: false });
s.target = el.firstElementChild;
el.classList.add('dml');
el.style.setProperty('--dml-c', color);
var svg = mk('svg', { class: 'dml-svg', 'aria-hidden': 'true' });
var g = mk('g', { fill: 'none', stroke: color, 'stroke-linecap': 'round' }, svg);
var said = [];
if (d.width) {
el.classList.add(d.xSide === 'top' ? 'dml-t' : 'dml-b');
s.dims.push({ axis: 'x', size: parseSize(d.width) });
said.push(d.width + ' wide');
}
if (d.height) {
el.classList.add(d.ySide === 'left' ? 'dml-l' : 'dml-r');
s.dims.push({ axis: 'y', size: parseSize(d.height) });
said.push(d.height + ' tall');
}
s.dims.forEach(function (dm) {
dm.ext1 = mk('path', { 'stroke-width': 1, opacity: 0.6 }, g);
dm.ext2 = mk('path', { 'stroke-width': 1, opacity: 0.6 }, g);
dm.half1 = mk('path', { 'stroke-width': 1.5 }, g);
dm.half2 = mk('path', { 'stroke-width': 1.5 }, g);
dm.head1 = mk('path', { fill: color, stroke: 'none' }, g);
dm.head2 = mk('path', { fill: color, stroke: 'none' }, g);
dm.text = mk('text', { class: 'dml-text', 'text-anchor': 'middle', 'dominant-baseline': 'central' }, svg);
dm.text.textContent = fmt(dm.size, dm.size.n == null ? 0 : dm.size.n);
});
el.appendChild(svg);
if (said.length && !el.getAttribute('aria-label')) {
var alt = s.target && (s.target.getAttribute('alt') || s.target.getAttribute('aria-label'));
el.setAttribute('role', 'img');
el.setAttribute('aria-label', (alt ? alt + ': ' : 'Measured: ') + said.join(' by '));
}
layout(el);
new ResizeObserver(function () { layout(el); }).observe(el);
if (s.target && s.target.tagName === 'IMG' && !s.target.complete) s.target.addEventListener('load', function () { layout(el); });
}
function layout(el) {
var s = el._dml, d = el.dataset;
if (!s.target) return;
var fs = parseFloat(getComputedStyle(el).fontSize) || 14;
var tr = s.target.getBoundingClientRect(), er = el.getBoundingClientRect();
var x0 = tr.left - er.left, y0 = tr.top - er.top;
var x1 = x0 + tr.width, y1 = y0 + tr.height;
var gap = 4, off = fs * 1.4, ah = 7, aw = 3.2;
s.dims.forEach(function (dm) {
var a, b, ext1, ext2, line, h1, h2, tx, ty, rot = 0;
if (dm.axis === 'x') {
var top = d.xSide === 'top', ly = top ? y0 - off : y1 + off, dir = top ? -1 : 1;
ext1 = 'M' + x0 + ',' + (top ? y0 - gap : y1 + gap) + ' L' + x0 + ',' + (ly + dir * 5);
ext2 = 'M' + x1 + ',' + (top ? y0 - gap : y1 + gap) + ' L' + x1 + ',' + (ly + dir * 5);
var mx = (x0 + x1) / 2;
dm.half1.setAttribute('d', 'M' + mx + ',' + ly + ' L' + x0 + ',' + ly);
dm.half2.setAttribute('d', 'M' + mx + ',' + ly + ' L' + x1 + ',' + ly);
h1 = 'M' + x0 + ',' + ly + ' l' + ah + ',' + -aw + ' v' + aw * 2 + ' Z';
h2 = 'M' + x1 + ',' + ly + ' l' + -ah + ',' + -aw + ' v' + aw * 2 + ' Z';
tx = mx; ty = ly + dir * fs * 0.95;
dm.origin = [mx, ly];
} else {
var left = d.ySide === 'left', lx = left ? x0 - off : x1 + off, dr = left ? -1 : 1;
ext1 = 'M' + (left ? x0 - gap : x1 + gap) + ',' + y0 + ' L' + (lx + dr * 5) + ',' + y0;
ext2 = 'M' + (left ? x0 - gap : x1 + gap) + ',' + y1 + ' L' + (lx + dr * 5) + ',' + y1;
var my = (y0 + y1) / 2;
dm.half1.setAttribute('d', 'M' + lx + ',' + my + ' L' + lx + ',' + y0);
dm.half2.setAttribute('d', 'M' + lx + ',' + my + ' L' + lx + ',' + y1);
h1 = 'M' + lx + ',' + y0 + ' l' + -aw + ',' + ah + ' h' + aw * 2 + ' Z';
h2 = 'M' + lx + ',' + y1 + ' l' + -aw + ',' + -ah + ' h' + aw * 2 + ' Z';
tx = lx + dr * fs * 0.95; ty = my; rot = -90;
dm.origin = [lx, my];
}
dm.ext1.setAttribute('d', ext1);
dm.ext2.setAttribute('d', ext2);
dm.head1.setAttribute('d', h1);
dm.head2.setAttribute('d', h2);
dm.text.setAttribute('transform', 'translate(' + tx + ',' + ty + ')' + (rot ? ' rotate(' + rot + ')' : ''));
});
}
function measure(el, dm, delay) {
var dur = +el.dataset.duration || DEFAULTS.duration;
[dm.ext1, dm.ext2].forEach(function (p) {
anime.animate(anime.svg.createDrawable(p), { draw: ['0 0', '0 1'], duration: dur * 0.3, delay: delay, ease: 'out(2)' });
});
[dm.half1, dm.half2].forEach(function (p) {
anime.animate(anime.svg.createDrawable(p), { draw: ['0 0', '0 1'], duration: dur * 0.6, delay: delay + dur * 0.25, ease: 'inOut(3)' });
});
anime.animate([dm.head1, dm.head2], { opacity: [0, 1], duration: 150, delay: delay + dur * 0.8 });
anime.animate(dm.text, { opacity: [0, 1], duration: 300, delay: delay + dur * 0.3 });
if (dm.size.n != null) {
var o = { v: 0 };
anime.animate(o, {
v: dm.size.n, duration: dur * 0.75, delay: delay + dur * 0.3, ease: 'out(3)',
onUpdate: function () { dm.text.textContent = fmt(dm.size, o.v); },
});
}
}
function clear(el, dm) {
[dm.ext1, dm.ext2, dm.half1, dm.half2, dm.head1, dm.head2, dm.text].forEach(function (p) { anime.animate(p, { opacity: 0, duration: 400 }); });
}
function reset(dm) {
[dm.ext1, dm.ext2, dm.half1, dm.half2].forEach(function (p) {
anime.utils.set(p, { opacity: p === dm.ext1 || p === dm.ext2 ? 0.6 : 1 });
anime.utils.set(anime.svg.createDrawable(p), { draw: '0 0' });
});
anime.utils.set([dm.head1, dm.head2, dm.text], { opacity: 0 });
}
function play(el) {
var s = el._dml, d = el.dataset, dur = +d.duration || DEFAULTS.duration;
s.running = true;
s.dims.forEach(function (dm, i) { measure(el, dm, i * dur * 0.75); });
var end = s.dims.length * dur * 0.75 + dur * 0.4;
setTimeout(function () {
if (d.loop == null) { s.running = false; return; }
setTimeout(function () {
s.dims.forEach(function (dm) { clear(el, dm); });
setTimeout(function () {
s.dims.forEach(reset);
s.running = false;
if (s.visible) play(el);
}, 600);
}, 2200);
}, end);
}
function init() {
var still = reduced();
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._dml;
s.visible = e.isIntersecting;
if (s.visible && !s.running) play(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-dimension-lines]').forEach(function (el) {
if (el._dml) return;
setup(el);
if (still) return;
el._dml.dims.forEach(reset);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-dimension-lines data-width="210 cm" data-height="85 cm">
<img src="sofa.png" alt="Sofa" width="320">
</div>