fontmap / src /components /FontMap /utils /voronoiDilation.js
tfrere's picture
tfrere HF Staff
first commit
eebc40f
raw
history blame
7.75 kB
import * as d3 from 'd3';
import { forceSimulation, forceManyBody, forceCollide } from 'd3-force';
// Cache pour éviter de recréer les simulations
let simulationCache = new Map();
let lastPositionsHash = null;
/**
* Calcule la bounding box d'un groupe de positions
* @param {Array} positions - Positions des polices
* @returns {Object} Bounding box {minX, maxX, minY, maxY, width, height, centerX, centerY}
*/
const calculateBoundingBox = (positions) => {
if (!positions || positions.length === 0) {
return { minX: 0, maxX: 0, minY: 0, maxY: 0, width: 0, height: 0, centerX: 0, centerY: 0 };
}
const xs = positions.map(p => p.x);
const ys = positions.map(p => p.y);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const minY = Math.min(...ys);
const maxY = Math.max(...ys);
return {
minX,
maxX,
minY,
maxY,
width: maxX - minX,
height: maxY - minY,
centerX: (minX + maxX) / 2,
centerY: (minY + maxY) / 2
};
};
/**
* Génère un hash simple pour les positions (pour le cache)
* @param {Array} positions - Positions des polices
* @returns {string} Hash des positions
*/
const generatePositionsHash = (positions) => {
return positions.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join('|');
};
/**
* Applique une dilatation optimisée avec cache et interpolation
* @param {Array} positions - Positions initiales des polices
* @param {number} sliderValue - Valeur du slider (0-1)
* @returns {Array} Positions après simulation
*/
export const applySimpleDilation = (positions, sliderValue) => {
if (!positions || positions.length === 0 || sliderValue === 0) {
return positions;
}
// Configuration basée sur la valeur du slider (0-1 → équivalent 0-0.35)
const mappedValue = sliderValue * 0.35;
// Points de cache pour interpolation
const cachePoints = [0, 0.1, 0.2, 0.3, 0.35];
const cacheKey = Math.min(...cachePoints.filter(p => p >= mappedValue));
// Vérifier si on a déjà calculé cette valeur
const positionsHash = generatePositionsHash(positions);
const cacheEntryKey = `${positionsHash}-${cacheKey}`;
if (simulationCache.has(cacheEntryKey) && lastPositionsHash === positionsHash) {
const cachedResult = simulationCache.get(cacheEntryKey);
// Si c'est exactement la valeur cachée, la retourner directement
if (Math.abs(mappedValue - cacheKey) < 0.001) {
return cachedResult;
}
// Sinon, interpoler avec la valeur précédente
const prevCacheKey = cachePoints[cachePoints.indexOf(cacheKey) - 1];
if (prevCacheKey !== undefined) {
const prevCacheEntryKey = `${positionsHash}-${prevCacheKey}`;
if (simulationCache.has(prevCacheEntryKey)) {
const prevResult = simulationCache.get(prevCacheEntryKey);
const interpolationFactor = (mappedValue - prevCacheKey) / (cacheKey - prevCacheKey);
return interpolatePositions(prevResult, cachedResult, interpolationFactor);
}
}
}
// Calculer la nouvelle simulation si pas en cache
console.log('🎯 Computing new simulation for slider:', sliderValue, 'mapped:', mappedValue);
const result = computeSimulation(positions, mappedValue);
// Mettre en cache
simulationCache.set(cacheEntryKey, result);
lastPositionsHash = positionsHash;
// Limiter la taille du cache
if (simulationCache.size > 10) {
const firstKey = simulationCache.keys().next().value;
simulationCache.delete(firstKey);
}
return result;
};
/**
* Calcule une simulation D3 pour une valeur donnée
* @param {Array} positions - Positions initiales
* @param {number} mappedValue - Valeur mappée (0-0.35)
* @returns {Array} Positions après simulation
*/
const computeSimulation = (positions, mappedValue) => {
// Calculer la bounding box initiale
const initialBBox = calculateBoundingBox(positions);
// Créer une copie des positions pour la simulation
const simulationData = positions.map(pos => ({
x: pos.x,
y: pos.y,
name: pos.name,
originalX: pos.x,
originalY: pos.y
}));
const maxSteps = 100;
const simulationSteps = Math.round(mappedValue * maxSteps);
const forceStrength = -mappedValue * 500;
const collideRadius = 15 + (mappedValue * 25);
// Créer la simulation D3
const simulation = forceSimulation(simulationData)
.force("charge", forceManyBody().strength(forceStrength))
.force("collide", forceCollide(collideRadius))
.stop();
// Exécuter la simulation
for (let i = 0; i < simulationSteps; i++) {
simulation.tick();
}
// Calculer la bounding box après simulation
const finalBBox = calculateBoundingBox(simulationData);
// Calculer le facteur de réduction
const widthRatio = initialBBox.width / finalBBox.width;
const heightRatio = initialBBox.height / finalBBox.height;
const reductionFactor = Math.min(widthRatio, heightRatio);
// Appliquer la réduction
const reducedPositions = simulationData.map(pos => ({
x: initialBBox.centerX + (pos.x - finalBBox.centerX) * reductionFactor,
y: initialBBox.centerY + (pos.y - finalBBox.centerY) * reductionFactor
}));
// Retourner les positions mises à jour
return reducedPositions.map((simPos, index) => ({
...positions[index],
x: simPos.x,
y: simPos.y
}));
};
/**
* Interpole entre deux ensembles de positions
* @param {Array} positionsA - Positions A
* @param {Array} positionsB - Positions B
* @param {number} factor - Facteur d'interpolation (0-1)
* @returns {Array} Positions interpolées
*/
const interpolatePositions = (positionsA, positionsB, factor) => {
return positionsA.map((posA, index) => {
const posB = positionsB[index];
if (!posB || posA.name !== posB.name) return posA;
return {
...posA,
x: posA.x + (posB.x - posA.x) * factor,
y: posA.y + (posB.y - posA.y) * factor
};
});
};
/**
* Vide le cache des simulations (utile quand les données changent)
*/
export const clearSimulationCache = () => {
simulationCache.clear();
lastPositionsHash = null;
console.log('🧹 Simulation cache cleared');
};
/**
* Obtient les statistiques du cache
* @returns {Object} Statistiques du cache
*/
export const getCacheStats = () => {
return {
cacheSize: simulationCache.size,
lastPositionsHash: lastPositionsHash ? lastPositionsHash.substring(0, 20) + '...' : null,
cacheKeys: Array.from(simulationCache.keys())
};
};
/**
* Calcule les positions avec simulation simple
* @param {Array} fonts - Données des polices avec coordonnées UMAP
* @param {number} width - Largeur du conteneur
* @param {number} height - Hauteur du conteneur
* @param {number} sliderValue - Valeur du slider (0-1)
* @returns {Array} Positions calculées pour l'affichage
*/
export const calculatePositions = (fonts, width, height, sliderValue) => {
if (!fonts || fonts.length === 0) {
return [];
}
// Créer les échelles pour convertir les coordonnées UMAP en coordonnées d'écran
const xExtent = d3.extent(fonts, d => d.x);
const yExtent = d3.extent(fonts, d => d.y);
const xScale = d3.scaleLinear()
.domain(xExtent)
.range([50, width - 50]);
const yScale = d3.scaleLinear()
.domain(yExtent)
.range([height - 50, 50]);
// Convertir les positions UMAP en coordonnées d'écran (positions de base)
const basePositions = fonts.map(font => ({
...font,
originalX: font.x,
originalY: font.y,
x: xScale(font.x),
y: yScale(font.y)
}));
// Si slider à 0, retourner les positions de base
if (sliderValue === 0) {
return basePositions;
}
// Appliquer la dilatation optimisée
return applySimpleDilation(basePositions, sliderValue);
};