A small interactive toy in the lusion-inspired direction: a grid of ink dots on the lavender canvas that bend away from your cursor on spring physics, blushing electric blue as they move. Move your pointer (or finger) across the field.
No libraries, no WebGL — one canvas, ~100 lines of vanilla JavaScript, and the colors come straight from the design-token CSS variables, so the field follows the light/dark theme toggle. The site’s persisted motion control follows the operating system until overridden; in reduced or off the grid renders statically.
Teardown
var GAP = 26, R = 2, PUSH = 90, SPRING = 0.06, DAMP = 0.86;
All physics tuning on one line. GAP is the grid spacing, PUSH is the cursor radius, SPRING pulls dots home, DAMP bleeds energy.
step: function (s) {
var px = s.pointer.x, py = s.pointer.y;
for (var k = 0; k < dots.length; k++) {
var d = dots[k];
var dx = d.x - px, dy = d.y - py;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < PUSH && dist > 0.001) {
var f = (1 - dist / PUSH) * 6;
d.vx += (dx / dist) * f;
d.vy += (dy / dist) * f;
}
d.vx = (d.vx + (d.hx - d.x) * SPRING) * DAMP;
d.vy = (d.vy + (d.hy - d.y) * SPRING) * DAMP;
d.x += d.vx;
d.y += d.vy;
}
},
Each tick pushes dots away from the pointer, then springs them back to their home position. The damping factor keeps them from ringing forever.
function dotGeometry(d, tokens) {
var stretch = Math.min(
Math.sqrt(
(d.x - d.hx) * (d.x - d.hx) + (d.y - d.hy) * (d.y - d.hy)
) / 24,
1
);
return {
x: d.x,
y: d.y,
radius: R + stretch * 1.5,
color: stretch > 0.05 ? tokens.primary : tokens.ink,
alpha: 0.35 + stretch * 0.65,
};
}
dotGeometry() computes the circle consumed by both Canvas and the plotter export. Past 5% stretch the dot turns primary blue; opacity rises with distance from home.
function exportSVG(s) {
var circles = [];
for (var i = 0; i < dots.length; i++) {
var shape = dotGeometry(dots[i], s.tokens);
circles.push(
'<circle cx="' + number(shape.x) +
'" cy="' + number(shape.y) +
'" r="' + number(shape.radius) +
'" fill="none" stroke="' + attribute(shape.color) +
'" stroke-width="0.7"/>'
);
}
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' +
number(s.w) + ' ' + number(s.h) +
'" width="' + number(s.w) + '" height="' + number(s.h) +
'" role="img" aria-label="Dot Field plotter paths">' +
'<g vector-effect="non-scaling-stroke">' + circles.join('') +
'</g></svg>';
}
The same geometry becomes path-native SVG circles with finite, rounded coordinates and no raster, font, filter, script, or external reference.
draw: function (s) {
s.ctx.clearRect(0, 0, s.w, s.h);
for (var k = 0; k < dots.length; k++) {
var shape = dotGeometry(dots[k], s.tokens);
s.ctx.fillStyle = shape.color;
s.ctx.globalAlpha = shape.alpha;
s.ctx.beginPath();
s.ctx.arc(shape.x, shape.y, shape.radius, 0, 6.2832);
s.ctx.fill();
}
s.ctx.globalAlpha = 1;
},
exportSVG: exportSVG,
Canvas consumes dotGeometry() directly. The blue circle is the single accent event: colour earned by motion.
Harness lifecycle
The plate code above plugs into The requested state starts immediately, but canonical readiness waits for every declared setup font and crosses a paint boundary before rebuilding. A font-measured plate therefore cannot publish fallback-font geometry. A labCanvas(), which provides the canvas context, design-token colours, pointer tracking, a fixed-timestep loop, and a reduced-motion gate. The three hooks — setup, step, draw — are the only contract a plate needs to implement. The controls below the canvas copy the exact seed/frame/quality tuple, save a high-resolution PNG, or export the same circles as plotter-safe SVG paths. // Init — render one frame synchronously so dots appear on first paint
constructionStep(function () {
build(initialFrame);
if (!reduced) {
scheduleFrame();
}
});
function announceFontFailure() {
var controls = document.getElementById(canvasId + '-lab-controls');
if (!controls || typeof controls.querySelector !== 'function') return;
var status = controls.querySelector('[data-lab-status]');
if (status) {
status.textContent =
'Required fonts failed to load; this frame cannot be shared or exported.';
}
}
function failFonts(error) {
if (!alive) return;
fontFailure = error instanceof Error
? error
: new Error('Required fonts failed to load');
fontsSettled = false;
cancelFrame();
cancelReplay();
syncReadinessDataset();
announceFontFailure();
}
constructionStep(function () {
if (!declaredFonts.length) return;
if (!document.fonts || !document.fonts.ready ||
typeof document.fonts.load !== 'function') {
failFonts(new Error('Required font loading is unavailable'));
} else {
function finishFonts() {
// FontFace promises and the site's `fonts-loaded` class settle in nearby
// microtasks. Cross one paint boundary so setup measures the final stack.
requestAnimationFrame(function () {
if (!alive) return;
fontFailure = null;
fontsSettled = true;
readTokens();
build(currentTargetFrame());
});
}
var fontLoads = Promise.all(declaredFonts.map(function (font) {
// Wait for every attempt so one rejected face cannot make another
// still-loading face appear canonical.
return document.fonts.load(font).then(function (faces) {
return { font: font, faces: faces };
}, function (error) {
return { font: font, error: error };
});
}));
fontLoads.then(function (results) {
var failed = results.filter(function (result) {
return result.error || !result.faces || result.faces.length === 0;
});
if (failed.length) {
throw new Error('Required fonts failed to load: ' +
failed.map(function (result) { return result.font; }).join(', '));
}
return document.fonts.ready;
}).then(finishFonts, failFonts);
}
});
// Theme toggle: re-read tokens and redraw
var fontClassPresent =
document.documentElement.classList.contains('fonts-loaded');
var mo = null;
constructionStep(function () {
mo = new MutationObserver(function () {
if (!alive || tokenFailure) return;
var nextFontClass =
document.documentElement.classList.contains('fonts-loaded');
if (nextFontClass !== fontClassPresent) {
fontClassPresent = nextFontClass;
readTokens();
if (declaredFonts.length) {
build(currentTargetFrame());
} else {
drawCurrent();
}
return;
}
readTokens();
drawCurrent();
});
mo.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
});
MutationObserver distinguishes font activation from theme changes. Fonts rebuild the canonical model; a theme toggle only re-reads tokens and redraws without advancing simulation or PRNG.