Blueprint Draw
Technical drawings sketch themselves across a blueprint grid behind your content. Construction lines go down first, then each part is drafted: its outline, its dimension lines and arrows, and finally the measurements. Loop it and the sheet clears and a fresh set of parts is drawn.
Built to spec
How to use it
- Paste the snippet just before
</head>. - Add
data-blueprint-drawto a section with a blueprint-blue background.
Options
Add any of these attributes to the same element.
data-loop | Clear the sheet and draft new parts, again and again. |
data-color | Line colour. Default white. |
data-grid | Grid square in px. Default 24; 0 for no grid. |
data-opacity | How strongly the drawing shows, from 0 to 1. Default 0.75. |
data-speed | Speed multiplier. Default 1. |
The drawing is an SVG laid out to fit the section, with a fresh random set of plates, discs, nuts, brackets and angles each time. It starts when scrolled into view and pauses when off screen. With reduced motion the finished drawing shows straight away.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Blueprint Draw: technical drawings sketch themselves across a blueprint
// grid behind your content. Construction lines go down first, then each
// part's outline is drawn, its dimension lines and arrows are added and
// the measurements appear. Loop it and the sheet clears and a new set of
// parts is drafted. Each drawing is a fresh random set of plates, discs,
// nuts, brackets and angles laid out to fit the section.
// Usage: <section data-blueprint-draw style="background:#0b3b8c">…</section>
// Optional: data-loop (clear the sheet and draw new parts, again and
// again), data-color (line colour, default #fff), data-grid (grid square
// in px, default 24; 0 for no grid), data-opacity (how strongly the
// drawing shows, from 0 to 1, default 0.75), data-speed (default 1).
var DEFAULTS = { color: '#fff', grid: 24, opacity: 0.75, speed: 1 };
var css =
'[data-blueprint-draw]{position:relative;isolation:isolate;overflow:hidden}' +
'.bpw-layer{position:absolute;inset:0;z-index:-1;pointer-events:none;overflow:hidden}' +
'.bpw-svg{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}' +
'.bpw-o{stroke-width:1.5}.bpw-m{stroke-width:.8}.bpw-c{stroke-width:.7;opacity:.4}' +
'path.bpw-t{fill:currentColor;stroke:none}' +
'text.bpw-t{fill:currentColor;stroke:none;font-family:ui-monospace,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.04em}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var R = Math.random;
function f(n) { return Math.round(n * 10) / 10; }
function pick(a) { return a[Math.floor(R() * a.length)]; }
// ---- Drawing pieces, as SVG strings ----
function line(cls, x1, y1, x2, y2) { return '<path class="' + cls + '" pathLength="1" d="M' + f(x1) + ' ' + f(y1) + 'L' + f(x2) + ' ' + f(y2) + '"/>'; }
function circle(cls, x, y, r) { return '<circle class="' + cls + '" pathLength="1" cx="' + f(x) + '" cy="' + f(y) + '" r="' + f(r) + '"/>'; }
function poly(cls, pts, close) {
return '<path class="' + cls + '" pathLength="1" d="M' + pts.map(function (p) { return f(p[0]) + ' ' + f(p[1]); }).join('L') + (close ? 'Z' : '') + '"/>';
}
function arrow(x, y, a, u) {
var c = Math.cos(a), s = Math.sin(a), b = 6 * u, hw = 2.2 * u;
return '<path class="bpw-t" d="M' + f(x) + ' ' + f(y) + 'L' + f(x - c * b - s * hw) + ' ' + f(y - s * b + c * hw) + 'L' + f(x - c * b + s * hw) + ' ' + f(y - s * b - c * hw) + 'Z"/>';
}
function label(x, y, text, a, u) {
var deg = (a * 180) / Math.PI;
if (deg > 90 || deg < -90) deg += 180;
return '<text class="bpw-t" x="' + f(x) + '" y="' + f(y) + '" font-size="' + f(10 * u) + '" text-anchor="middle" dominant-baseline="middle"' +
(Math.abs(deg) > 1 ? ' transform="rotate(' + f(deg) + ' ' + f(x) + ' ' + f(y) + ')"' : '') + '>' + text + '</text>';
}
// A dimension between two points, set off to one side, with extension
// lines, arrows at both ends and the measurement in the middle.
function dim(x1, y1, x2, y2, off, text, u) {
var dx = x2 - x1, dy = y2 - y1, L = Math.sqrt(dx * dx + dy * dy), nx = -dy / L, ny = dx / L, sg = off > 0 ? 1 : -1;
var a = Math.atan2(dy, dx);
var ax = x1 + nx * off, ay = y1 + ny * off, bx = x2 + nx * off, by = y2 + ny * off;
return {
lines: line('bpw-m', x1 + nx * 3 * sg, y1 + ny * 3 * sg, ax + nx * 4 * sg, ay + ny * 4 * sg) +
line('bpw-m', x2 + nx * 3 * sg, y2 + ny * 3 * sg, bx + nx * 4 * sg, by + ny * 4 * sg) +
line('bpw-m', ax, ay, bx, by),
marks: arrow(ax, ay, a + Math.PI, u) + arrow(bx, by, a, u) +
label((ax + bx) / 2 + nx * 8 * u * sg, (ay + by) / 2 + ny * 8 * u * sg, text, a, u)
};
}
// ---- The parts ----
var PARTS = {
plate: function (x, y, s, u) {
var W = s * 1.25, H = s * 0.72, x0 = x - W / 2, y0 = y - H / 2, hr = s * 0.08;
var d1 = dim(x0, y0 + H, x0 + W, y0 + H, 13 * u, Math.round(W * 0.8), u);
var d2 = dim(x0 + W, y0 + H, x0 + W, y0, 13 * u, Math.round(H * 0.8), u);
return {
c: line('bpw-c', x0 + W * 0.2, y - H * 0.75, x0 + W * 0.2, y + H * 0.75) + line('bpw-c', x0 + W * 0.8, y - H * 0.75, x0 + W * 0.8, y + H * 0.75),
o: poly('bpw-o', [[x0, y0], [x0 + W, y0], [x0 + W, y0 + H], [x0, y0 + H]], true) + circle('bpw-o', x0 + W * 0.2, y, hr) + circle('bpw-o', x0 + W * 0.8, y, hr),
m: d1.lines + d2.lines,
t: d1.marks + d2.marks
};
},
disc: function (x, y, s, u) {
var r = s * 0.46, a = -0.75, c = Math.cos(a), sn = Math.sin(a);
var ex = x + c * (r + s * 0.32), ey = y + sn * (r + s * 0.32), tail = 26 * u;
return {
c: line('bpw-c', x - r * 1.3, y, x + r * 1.3, y) + line('bpw-c', x, y - r * 1.3, x, y + r * 1.3),
o: circle('bpw-o', x, y, r) + circle('bpw-o', x, y, r * 0.42),
m: line('bpw-m', x + c * r * 0.42, y + sn * r * 0.42, ex, ey) + line('bpw-m', ex, ey, ex + tail, ey),
t: arrow(x + c * r, y + sn * r, a + Math.PI, u) + label(ex + tail / 2, ey - 7 * u, 'R' + Math.round(r * 0.8), 0, u)
};
},
nut: function (x, y, s, u) {
var r = s * 0.44, pts = [];
for (var i = 0; i < 6; i++) pts.push([x + Math.cos(Math.PI / 6 + (i * Math.PI) / 3) * r, y + Math.sin(Math.PI / 6 + (i * Math.PI) / 3) * r]);
var d = dim(x - r * 0.866, y + r, x + r * 0.866, y + r, 10 * u, Math.round(r * 1.4), u);
return {
c: line('bpw-c', x - r * 1.3, y, x + r * 1.3, y) + line('bpw-c', x, y - r * 1.3, x, y + r * 1.3),
o: poly('bpw-o', pts, true) + circle('bpw-o', x, y, r * 0.5) + circle('bpw-o', x, y, r * 0.78),
m: d.lines,
t: d.marks + label(x + r * 1.1, y - r * 0.95, 'M' + Math.round(r * 0.5), 0, u)
};
},
angle: function (x, y, s, u) {
var ax = x - s * 0.62, ay = y + s * 0.36, bx = x + s * 0.62, top = y - s * 0.38;
var th = Math.atan2(ay - top, bx - ax), ar = s * 0.4, k = 7 * u;
var d = dim(ax, ay, bx, ay, 12 * u, Math.round((bx - ax) * 0.8), u);
return {
c: line('bpw-c', ax - s * 0.15, ay, bx + s * 0.2, ay),
o: poly('bpw-o', [[ax, ay], [bx, ay], [bx, top]], true),
m: d.lines + poly('bpw-m', [[bx - k, ay], [bx - k, ay - k], [bx, ay - k]]) +
'<path class="bpw-m" pathLength="1" d="M' + f(ax + ar) + ' ' + f(ay) + 'A' + f(ar) + ' ' + f(ar) + ' 0 0 0 ' + f(ax + Math.cos(th) * ar) + ' ' + f(ay - Math.sin(th) * ar) + '"/>',
t: d.marks + label(ax + Math.cos(th / 2) * (ar + 14 * u), ay - Math.sin(th / 2) * (ar + 14 * u), Math.round((th * 180) / Math.PI) + '°', 0, u)
};
},
bracket: function (x, y, s, u) {
var W = s * 1.1, H = s * 0.85, t = s * 0.22, x0 = x - W / 2, y0 = y - H / 2;
var d1 = dim(x0, y0 + H, x0, y0, -12 * u, Math.round(H * 0.8), u);
var d2 = dim(x0, y0, x0 + W, y0, -12 * u, Math.round(W * 0.8), u);
return {
c: line('bpw-c', x0 + t / 2, y0 - s * 0.12, x0 + t / 2, y0 + H + s * 0.12),
o: poly('bpw-o', [[x0, y0], [x0 + t, y0], [x0 + t, y0 + H - t], [x0 + W, y0 + H - t], [x0 + W, y0 + H], [x0, y0 + H]], true) +
circle('bpw-o', x0 + t + (W - t) * 0.6, y0 + H - t / 2, t * 0.22),
m: d1.lines + d2.lines,
t: d1.marks + d2.marks
};
}
};
function build(el) {
var d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
var color = d.color || DEFAULTS.color;
var grid = d.grid !== undefined ? +d.grid : DEFAULTS.grid;
var opacity = d.opacity !== undefined ? +d.opacity : DEFAULTS.opacity;
var speed = +d.speed || DEFAULTS.speed;
var still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var layer = document.createElement('div');
layer.className = 'bpw-layer';
layer.setAttribute('aria-hidden', 'true');
layer.style.color = color;
if (grid > 0) {
// Fine squares, with every fifth line a little stronger.
var fine = 'color-mix(in srgb,' + color + ' 9%,transparent)', bold = 'color-mix(in srgb,' + color + ' 20%,transparent)';
layer.style.backgroundImage =
'linear-gradient(' + bold + ' 1px,transparent 1px),linear-gradient(90deg,' + bold + ' 1px,transparent 1px),' +
'linear-gradient(' + fine + ' 1px,transparent 1px),linear-gradient(90deg,' + fine + ' 1px,transparent 1px)';
layer.style.backgroundSize = grid * 5 + 'px ' + grid * 5 + 'px,' + grid * 5 + 'px ' + grid * 5 + 'px,' + grid + 'px ' + grid + 'px,' + grid + 'px ' + grid + 'px';
layer.style.backgroundPosition = 'center';
}
el.insertBefore(layer, el.firstChild);
var svg = null, tl = null, visible = false, w = 0, h = 0;
function sheet() {
w = el.clientWidth;
h = el.clientHeight;
var u = Math.max(0.75, Math.min(1.1, Math.min(w, h) / 360));
var cols = Math.max(2, Math.round(w / 250)), rows = Math.max(2, Math.round(h / 180));
var cw = w / cols, ch = h / rows, kinds = Object.keys(PARTS), html = '';
// A few long construction lines across the whole sheet first.
var cons = '';
for (var i = 1; i < rows; i++) cons += line('bpw-c', 0, ch * i + (R() - 0.5) * 20, w, ch * i + (R() - 0.5) * 20);
for (i = 1; i < cols; i++) cons += line('bpw-c', cw * i + (R() - 0.5) * 30, 0, cw * i + (R() - 0.5) * 30, h);
html += '<g class="bpw-cons">' + cons + '</g>';
var last = '';
for (var r = 0; r < rows; r++) {
for (var c = 0; c < cols; c++) {
var kind = pick(kinds);
if (kind === last) kind = pick(kinds);
last = kind;
var s = Math.min(cw * 0.62, ch * 0.6);
var x = cw * (c + 0.5) + (R() - 0.5) * cw * 0.12, y = ch * (r + 0.5) + (R() - 0.5) * ch * 0.1;
var p = PARTS[kind](x, y, s, u);
html += '<g class="bpw-fig">' + p.c + p.o + p.m + p.t + '</g>';
}
}
if (svg) svg.remove();
layer.insertAdjacentHTML('beforeend', '<svg class="bpw-svg" viewBox="0 0 ' + w + ' ' + h + '" preserveAspectRatio="none" style="opacity:' + opacity + '">' + html + '</svg>');
svg = layer.lastChild;
}
function shuffle(a) {
for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(R() * (i + 1)), t = a[i]; a[i] = a[j]; a[j] = t; }
return a;
}
function draw() {
sheet();
if (still) return;
var drawn = svg.querySelectorAll('[pathLength]'), marks = svg.querySelectorAll('.bpw-t');
anime.utils.set(drawn, { strokeDasharray: '1 1', strokeDashoffset: 1 });
anime.utils.set(marks, { opacity: 0 });
var k = 1 / speed;
tl = anime.createTimeline({ autoplay: false, onComplete: function () { if (loop) draw(); } });
tl.add(svg.querySelectorAll('.bpw-cons path'), { strokeDashoffset: [1, 0], duration: 900 * k, delay: anime.stagger(140 * k), ease: 'inOut(2)' }, 0);
// Then each part in turn: centre lines, outline, dimensions, numbers.
var at = 500 * k;
shuffle(Array.prototype.slice.call(svg.querySelectorAll('.bpw-fig'))).forEach(function (g) {
var c = g.querySelectorAll('.bpw-c'), o = g.querySelectorAll('.bpw-o'), m = g.querySelectorAll('.bpw-m'), t = g.querySelectorAll('.bpw-t');
if (c.length) tl.add(c, { strokeDashoffset: [1, 0], duration: 500 * k, delay: anime.stagger(80 * k), ease: 'out(2)' }, at);
tl.add(o, { strokeDashoffset: [1, 0], duration: 900 * k, delay: anime.stagger(180 * k), ease: 'inOut(2)' }, at + 250 * k);
tl.add(m, { strokeDashoffset: [1, 0], duration: 380 * k, delay: anime.stagger(70 * k), ease: 'out(2)' }, at + 900 * k);
tl.add(t, { opacity: [0, 1], duration: 300 * k, delay: anime.stagger(50 * k), ease: 'out(2)' }, at + 1250 * k);
at += 1100 * k;
});
// Hold the finished sheet, then wipe it for the next one.
if (loop) tl.add(svg, { opacity: [opacity, 0], duration: 900, ease: 'in(2)' }, at + 900 * k + 3000);
if (visible) tl.play();
}
draw();
var lastW = w, lastH = h;
if (window.ResizeObserver) {
new ResizeObserver(function () {
if (Math.abs(el.clientWidth - lastW) < 8 && Math.abs(el.clientHeight - lastH) < 8) return;
lastW = el.clientWidth; lastH = el.clientHeight;
if (tl) tl.pause();
draw();
}).observe(el);
}
if (still) return;
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (tl) { if (visible) tl.play(); else tl.pause(); }
}, { threshold: 0.2 }).observe(el);
}
function init() {
document.querySelectorAll('[data-blueprint-draw]').forEach(function (el) {
if (el._bpw) return;
el._bpw = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-blueprint-draw data-loop style="background: #0b3b8c; color: #fff">
<h1>Built to spec</h1>
</section>