|
|
import * as d3 from 'd3'; |
|
|
import { forceSimulation, forceManyBody, forceCollide } from 'd3-force'; |
|
|
|
|
|
|
|
|
let simulationCache = new Map(); |
|
|
let lastPositionsHash = null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const generatePositionsHash = (positions) => { |
|
|
return positions.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join('|'); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const applySimpleDilation = (positions, sliderValue) => { |
|
|
if (!positions || positions.length === 0 || sliderValue === 0) { |
|
|
return positions; |
|
|
} |
|
|
|
|
|
|
|
|
const mappedValue = sliderValue * 0.35; |
|
|
|
|
|
|
|
|
const cachePoints = [0, 0.1, 0.2, 0.3, 0.35]; |
|
|
const cacheKey = Math.min(...cachePoints.filter(p => p >= mappedValue)); |
|
|
|
|
|
|
|
|
const positionsHash = generatePositionsHash(positions); |
|
|
const cacheEntryKey = `${positionsHash}-${cacheKey}`; |
|
|
|
|
|
if (simulationCache.has(cacheEntryKey) && lastPositionsHash === positionsHash) { |
|
|
const cachedResult = simulationCache.get(cacheEntryKey); |
|
|
|
|
|
|
|
|
if (Math.abs(mappedValue - cacheKey) < 0.001) { |
|
|
return cachedResult; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
console.log('🎯 Computing new simulation for slider:', sliderValue, 'mapped:', mappedValue); |
|
|
|
|
|
const result = computeSimulation(positions, mappedValue); |
|
|
|
|
|
|
|
|
simulationCache.set(cacheEntryKey, result); |
|
|
lastPositionsHash = positionsHash; |
|
|
|
|
|
|
|
|
if (simulationCache.size > 10) { |
|
|
const firstKey = simulationCache.keys().next().value; |
|
|
simulationCache.delete(firstKey); |
|
|
} |
|
|
|
|
|
return result; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const computeSimulation = (positions, mappedValue) => { |
|
|
|
|
|
const initialBBox = calculateBoundingBox(positions); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const simulation = forceSimulation(simulationData) |
|
|
.force("charge", forceManyBody().strength(forceStrength)) |
|
|
.force("collide", forceCollide(collideRadius)) |
|
|
.stop(); |
|
|
|
|
|
|
|
|
for (let i = 0; i < simulationSteps; i++) { |
|
|
simulation.tick(); |
|
|
} |
|
|
|
|
|
|
|
|
const finalBBox = calculateBoundingBox(simulationData); |
|
|
|
|
|
|
|
|
const widthRatio = initialBBox.width / finalBBox.width; |
|
|
const heightRatio = initialBBox.height / finalBBox.height; |
|
|
const reductionFactor = Math.min(widthRatio, heightRatio); |
|
|
|
|
|
|
|
|
const reducedPositions = simulationData.map(pos => ({ |
|
|
x: initialBBox.centerX + (pos.x - finalBBox.centerX) * reductionFactor, |
|
|
y: initialBBox.centerY + (pos.y - finalBBox.centerY) * reductionFactor |
|
|
})); |
|
|
|
|
|
|
|
|
return reducedPositions.map((simPos, index) => ({ |
|
|
...positions[index], |
|
|
x: simPos.x, |
|
|
y: simPos.y |
|
|
})); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
}; |
|
|
}); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const clearSimulationCache = () => { |
|
|
simulationCache.clear(); |
|
|
lastPositionsHash = null; |
|
|
console.log('🧹 Simulation cache cleared'); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getCacheStats = () => { |
|
|
return { |
|
|
cacheSize: simulationCache.size, |
|
|
lastPositionsHash: lastPositionsHash ? lastPositionsHash.substring(0, 20) + '...' : null, |
|
|
cacheKeys: Array.from(simulationCache.keys()) |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const calculatePositions = (fonts, width, height, sliderValue) => { |
|
|
if (!fonts || fonts.length === 0) { |
|
|
return []; |
|
|
} |
|
|
|
|
|
|
|
|
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]); |
|
|
|
|
|
|
|
|
const basePositions = fonts.map(font => ({ |
|
|
...font, |
|
|
originalX: font.x, |
|
|
originalY: font.y, |
|
|
x: xScale(font.x), |
|
|
y: yScale(font.y) |
|
|
})); |
|
|
|
|
|
|
|
|
if (sliderValue === 0) { |
|
|
return basePositions; |
|
|
} |
|
|
|
|
|
|
|
|
return applySimpleDilation(basePositions, sliderValue); |
|
|
}; |