|
|
import { useCallback, useRef } from 'react'; |
|
|
import * as d3 from 'd3'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const useVisualState = () => { |
|
|
const visualStateRef = useRef({ |
|
|
isTransitioning: false, |
|
|
selectedFont: null, |
|
|
hoveredFont: null |
|
|
}); |
|
|
|
|
|
|
|
|
const updateVisualStates = useCallback((svg, viewportGroup, selectedFont, hoveredFont, darkMode) => { |
|
|
if (!svg || !viewportGroup) return; |
|
|
|
|
|
const glyphGroups = viewportGroup.selectAll('.font-glyph-group'); |
|
|
|
|
|
glyphGroups.each(function(d) { |
|
|
const group = d3.select(this); |
|
|
const isActive = selectedFont && selectedFont.name === d.name; |
|
|
const isHovered = hoveredFont && hoveredFont.name === d.name; |
|
|
|
|
|
|
|
|
group.select('.active-background-circle').remove(); |
|
|
group.select('.hover-background-circle').remove(); |
|
|
|
|
|
if (isActive) { |
|
|
|
|
|
|
|
|
group.insert('circle', ':first-child') |
|
|
.attr('class', 'active-background-circle') |
|
|
.attr('r', 12) |
|
|
.attr('fill', darkMode ? '#000000' : '#ffffff') |
|
|
.attr('stroke', 'none') |
|
|
.attr('stroke-width', 0) |
|
|
.style('pointer-events', 'none') |
|
|
.style('opacity', 1); |
|
|
|
|
|
|
|
|
group.style('opacity', 1); |
|
|
|
|
|
|
|
|
} else if (isHovered) { |
|
|
|
|
|
group.insert('circle', ':first-child') |
|
|
.attr('class', 'hover-background-circle') |
|
|
.attr('r', 12) |
|
|
.attr('fill', darkMode ? '#000000' : '#ffffff') |
|
|
.style('pointer-events', 'none') |
|
|
.style('opacity', 1); |
|
|
|
|
|
|
|
|
group.style('opacity', 1); |
|
|
} |
|
|
}); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const updateGlyphSizes = useCallback((viewportGroup, selectedFont, characterSize) => { |
|
|
if (!viewportGroup) return; |
|
|
|
|
|
console.log('🎯 updateGlyphSizes called with characterSize:', characterSize); |
|
|
|
|
|
const baseSize = 16; |
|
|
const currentSize = baseSize * characterSize; |
|
|
|
|
|
|
|
|
|
|
|
const fontGlyphs = viewportGroup.selectAll('.font-glyph'); |
|
|
|
|
|
fontGlyphs |
|
|
.attr('width', d => { |
|
|
const isActive = selectedFont && selectedFont.name === d.name; |
|
|
const isMerged = d.fusionInfo && d.fusionInfo.merged; |
|
|
|
|
|
|
|
|
if (isMerged) { |
|
|
return isActive ? d.__logSize * 2 : d.__logSize; |
|
|
} |
|
|
|
|
|
|
|
|
return isActive ? currentSize * 2 : currentSize; |
|
|
}) |
|
|
.attr('height', d => { |
|
|
const isActive = selectedFont && selectedFont.name === d.name; |
|
|
const isMerged = d.fusionInfo && d.fusionInfo.merged; |
|
|
|
|
|
|
|
|
if (isMerged) { |
|
|
return isActive ? d.__logSize * 2 : d.__logSize; |
|
|
} |
|
|
|
|
|
|
|
|
return isActive ? currentSize * 2 : currentSize; |
|
|
}) |
|
|
.attr('x', d => { |
|
|
const isActive = selectedFont && selectedFont.name === d.name; |
|
|
const isMerged = d.fusionInfo && d.fusionInfo.merged; |
|
|
|
|
|
|
|
|
if (isMerged) { |
|
|
const logSize = d.__logSize; |
|
|
const activeOffset = isActive ? -(logSize * 2) / 2 : -logSize / 2; |
|
|
return activeOffset; |
|
|
} |
|
|
|
|
|
|
|
|
const activeOffset = isActive ? -(currentSize * 2) / 2 : -currentSize / 2; |
|
|
return activeOffset; |
|
|
}) |
|
|
.attr('y', d => { |
|
|
const isActive = selectedFont && selectedFont.name === d.name; |
|
|
const isMerged = d.fusionInfo && d.fusionInfo.merged; |
|
|
|
|
|
|
|
|
if (isMerged) { |
|
|
const logSize = d.__logSize; |
|
|
const activeOffset = isActive ? -(logSize * 2) / 2 : -logSize / 2; |
|
|
return activeOffset; |
|
|
} |
|
|
|
|
|
|
|
|
const activeOffset = isActive ? -(currentSize * 2) / 2 : -currentSize / 2; |
|
|
return activeOffset; |
|
|
}); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const updateGlyphOpacity = useCallback((viewportGroup, positions, filter, searchTerm, selectedFont) => { |
|
|
if (!viewportGroup) return; |
|
|
|
|
|
const glyphGroups = viewportGroup.selectAll('.font-glyph-group'); |
|
|
|
|
|
glyphGroups |
|
|
.data(positions) |
|
|
.style('opacity', d => { |
|
|
const familyMatch = filter === 'all' || d.family === filter; |
|
|
const searchMatch = !searchTerm || |
|
|
d.name.toLowerCase().includes(searchTerm.toLowerCase()) || |
|
|
d.family.toLowerCase().includes(searchTerm.toLowerCase()); |
|
|
const isActive = selectedFont && selectedFont.name === d.name; |
|
|
|
|
|
|
|
|
return isActive ? 1 : (familyMatch && searchMatch ? 1 : 0.2); |
|
|
}); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const updateGlyphColors = useCallback((viewportGroup, fonts, darkMode) => { |
|
|
if (!viewportGroup) return; |
|
|
|
|
|
const colorScale = d3.scaleOrdinal( |
|
|
darkMode |
|
|
? ['#ffffff', '#cccccc', '#999999', '#666666', '#333333'] |
|
|
: ['#000000', '#333333', '#666666', '#999999', '#cccccc'] |
|
|
); |
|
|
const families = [...new Set(fonts.map(d => d.family))]; |
|
|
families.forEach(family => colorScale(family)); |
|
|
|
|
|
const fontGlyphs = viewportGroup.selectAll('.font-glyph'); |
|
|
if (!fontGlyphs.empty()) { |
|
|
fontGlyphs |
|
|
.attr('fill', darkMode ? '#ffffff' : d => colorScale(d.family)) |
|
|
.style('fill', darkMode ? '#ffffff' : null) |
|
|
.style('color', darkMode ? '#ffffff' : null); |
|
|
} |
|
|
}, []); |
|
|
|
|
|
|
|
|
const startTransition = useCallback(() => { |
|
|
visualStateRef.current.isTransitioning = true; |
|
|
}, []); |
|
|
|
|
|
|
|
|
const endTransition = useCallback(() => { |
|
|
visualStateRef.current.isTransitioning = false; |
|
|
}, []); |
|
|
|
|
|
return { |
|
|
visualStateRef, |
|
|
updateVisualStates, |
|
|
updateGlyphSizes, |
|
|
updateGlyphOpacity, |
|
|
updateGlyphColors, |
|
|
startTransition, |
|
|
endTransition |
|
|
}; |
|
|
}; |
|
|
|