font-map / src /components /FontMap /utils /voronoiDilation.js
tfrere's picture
tfrere HF Staff
feat: mobile UX overhaul + focus mode polish + visual hover hitbox
4319071
import * as d3 from 'd3';
/**
* Overlap removal via D3 force simulation — minimum displacement strategy.
*
* forceCollide only pushes pairs that actually overlap; points already well
* separated don't move at all. A weak forceX/forceY pulls every point back
* toward its original UMAP position, so the layout drifts as little as possible.
*
* @param {Array<{id: string, x: number, y: number}>} points - initial screen-space positions
* @param {Object} opts
* @param {number} opts.collideRadius - collision radius per glyph in pixels
* @param {number} opts.ticks - number of simulation steps (more = fuller resolution)
* @param {number} opts.originStrength - pull-back toward UMAP origin [0, 1] (keep small, e.g. 0.1)
* @returns {Array<{id: string, x: number, y: number}>}
*/
export function applyOverlapRemoval(points, { collideRadius = 10, ticks = 140, originStrength = 0.03 }) {
if (ticks === 0 || collideRadius === 0 || points.length < 2) return points;
// d3 forceSimulation mutates node objects in-place
const nodes = points.map(p => ({ id: p.id, x: p.x, y: p.y, ox: p.x, oy: p.y }));
const sim = d3.forceSimulation(nodes)
.force('collide', d3.forceCollide(collideRadius).strength(1).iterations(3))
.force('x', d3.forceX(n => n.ox).strength(originStrength))
.force('y', d3.forceY(n => n.oy).strength(originStrength))
.stop();
// Run synchronously (no animation loop needed)
for (let i = 0; i < ticks; i++) sim.tick();
return nodes.map(n => ({ id: n.id, x: n.x, y: n.y }));
}