|
|
import React, { useState, useEffect, useRef } from 'react'; |
|
|
import * as d3 from 'd3'; |
|
|
import '../FontMap.css'; |
|
|
|
|
|
|
|
|
import { useFontData } from './hooks/useFontData'; |
|
|
import { useD3Visualization } from './hooks/useD3Visualization'; |
|
|
import { useArrowNavigation } from './hooks/useArrowNavigation'; |
|
|
|
|
|
|
|
|
import FilterControls from './components/controls/FilterControls'; |
|
|
import SearchBar from './components/controls/SearchBar'; |
|
|
import ZoomControls from './components/controls/ZoomControls'; |
|
|
import ActiveFont from './components/ActiveFont'; |
|
|
import TooltipManager from './components/TooltipManager'; |
|
|
import IntroModal from './components/IntroModal'; |
|
|
import AboutModal from './components/AboutModal'; |
|
|
import FPSMonitor from './components/FPSMonitor'; |
|
|
|
|
|
|
|
|
import { useTweakpane } from './hooks/useTweakpane'; |
|
|
import './styles/intro-modal.css'; |
|
|
import './styles/about-modal.css'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const FontMap = ({ darkMode = false }) => { |
|
|
|
|
|
const [filter, setFilter] = useState('all'); |
|
|
const [searchTerm, setSearchTerm] = useState(''); |
|
|
const [dilationFactor, setDilationFactor] = useState(0.055); |
|
|
const [characterSize, setCharacterSize] = useState(1); |
|
|
const [variantSizeImpact, setVariantSizeImpact] = useState(false); |
|
|
const [selectedFont, setSelectedFont] = useState(null); |
|
|
const [hoveredFont, setHoveredFont] = useState(null); |
|
|
const [appState, setAppState] = useState('loading'); |
|
|
const [showAboutModal, setShowAboutModal] = useState(false); |
|
|
|
|
|
|
|
|
const dataset = 'new'; |
|
|
console.log('🔧 FontMap using dataset:', dataset); |
|
|
|
|
|
const handleFilterChange = (newFilter) => { |
|
|
setFilter(newFilter); |
|
|
}; |
|
|
|
|
|
const handleSearchChange = (newSearchTerm) => { |
|
|
setSearchTerm(newSearchTerm); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const handleFontSelect = (font) => { |
|
|
console.log('FontMap: handleFontSelect called with', font); |
|
|
setSelectedFont(font); |
|
|
}; |
|
|
|
|
|
const handleCloseDetails = () => { |
|
|
setSelectedFont(null); |
|
|
}; |
|
|
|
|
|
const handleShowAbout = () => { |
|
|
setShowAboutModal(true); |
|
|
}; |
|
|
|
|
|
const handleCloseAbout = () => { |
|
|
setShowAboutModal(false); |
|
|
}; |
|
|
|
|
|
const handleFontHover = (font) => { |
|
|
setHoveredFont(font); |
|
|
}; |
|
|
|
|
|
const handleFontUnhover = () => { |
|
|
setHoveredFont(null); |
|
|
}; |
|
|
|
|
|
const handleStartExploring = () => { |
|
|
setAppState('ready'); |
|
|
}; |
|
|
|
|
|
|
|
|
const { fonts, loading, error } = useFontData(dataset); |
|
|
|
|
|
|
|
|
const { canNavigate, filteredFontsCount } = useArrowNavigation( |
|
|
selectedFont, |
|
|
fonts, |
|
|
filter, |
|
|
searchTerm, |
|
|
handleFontSelect |
|
|
); |
|
|
|
|
|
const svgRef = useD3Visualization(fonts, filter, searchTerm, darkMode, loading, dilationFactor, characterSize, handleFontSelect, selectedFont, hoveredFont, 0.8, variantSizeImpact, canNavigate); |
|
|
|
|
|
|
|
|
const { isDebugMode } = useTweakpane(dilationFactor, setDilationFactor, characterSize, setCharacterSize, darkMode, variantSizeImpact, setVariantSizeImpact); |
|
|
|
|
|
|
|
|
const totalFonts = fonts.length; |
|
|
|
|
|
|
|
|
const filterOnlyCount = filter === 'all' ? totalFonts : fonts.filter(font => font.family === filter).length; |
|
|
|
|
|
|
|
|
const filteredFonts = fonts.filter(font => { |
|
|
const familyMatch = filter === 'all' || font.family === filter; |
|
|
const searchMatch = !searchTerm || font.name.toLowerCase().includes(searchTerm.toLowerCase()) || |
|
|
font.family.toLowerCase().includes(searchTerm.toLowerCase()); |
|
|
return familyMatch && searchMatch; |
|
|
}); |
|
|
const filteredCount = filteredFonts.length; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
window.onFontHover = handleFontHover; |
|
|
window.onFontUnhover = handleFontUnhover; |
|
|
|
|
|
return () => { |
|
|
delete window.onFontHover; |
|
|
delete window.onFontUnhover; |
|
|
}; |
|
|
}, [handleFontHover, handleFontUnhover]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
console.log('FontMap: selectedFont state changed to', selectedFont); |
|
|
}, [selectedFont]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (loading) { |
|
|
setAppState('loading'); |
|
|
} else if (fonts.length > 0 && appState === 'loading') { |
|
|
setAppState('intro'); |
|
|
} |
|
|
}, [loading, fonts.length, appState]); |
|
|
|
|
|
|
|
|
|
|
|
if (error) { |
|
|
return ( |
|
|
<div className="fontmap-container"> |
|
|
<div className="error"> |
|
|
<h3>Erreur de chargement</h3> |
|
|
<p>{error}</p> |
|
|
<button onClick={() => window.location.reload()}> |
|
|
Recharger la page |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
return ( |
|
|
<div className={`fontmap-container ${darkMode ? 'dark-mode' : ''}`}> |
|
|
{/* Sidebar */} |
|
|
<div className="sidebar"> |
|
|
<div className="sidebar-header"> |
|
|
<div className="search-section"> |
|
|
<SearchBar |
|
|
searchTerm={searchTerm} |
|
|
onSearchChange={handleSearchChange} |
|
|
darkMode={darkMode} |
|
|
big={true} |
|
|
filteredCount={filteredCount} |
|
|
totalCount={filterOnlyCount} |
|
|
filter={filter} |
|
|
/> |
|
|
|
|
|
<FilterControls |
|
|
fonts={fonts} |
|
|
filter={filter} |
|
|
onFilterChange={handleFilterChange} |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div className="sidebar-content"> |
|
|
<ActiveFont |
|
|
selectedFont={selectedFont} |
|
|
fonts={fonts} |
|
|
darkMode={darkMode} |
|
|
onClose={handleCloseDetails} |
|
|
onFontSelect={handleFontSelect} |
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
{/* Footer avec liens How it works et Source */} |
|
|
<div className="sidebar-footer"> |
|
|
<button |
|
|
className="about-link" |
|
|
onClick={handleShowAbout} |
|
|
title="How FontMap Works" |
|
|
> |
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> |
|
|
<circle cx="12" cy="12" r="10"/> |
|
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/> |
|
|
<line x1="12" y1="17" x2="12.01" y2="17"/> |
|
|
</svg> |
|
|
How it works |
|
|
</button> |
|
|
|
|
|
<a |
|
|
className="source-link" |
|
|
href="https://huggingface.co/spaces/huggingface/fontmap" |
|
|
target="_blank" |
|
|
rel="noopener noreferrer" |
|
|
title="View Source on Hugging Face Spaces" |
|
|
> |
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> |
|
|
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/> |
|
|
</svg> |
|
|
Source |
|
|
</a> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Zone principale */} |
|
|
<div className="main-area"> |
|
|
{/* Titre FontMap en position absolue */} |
|
|
<h1 className="map-title" data-text="FontMap">FontMap</h1> |
|
|
|
|
|
{/* Contrôles du bas */} |
|
|
<div className="bottom-controls"> |
|
|
<ZoomControls /> |
|
|
</div> |
|
|
|
|
|
{/* Rendu de la carte */} |
|
|
<div className="map-container"> |
|
|
<svg ref={svgRef} className="fontmap-svg"></svg> |
|
|
{appState === 'loading' && ( |
|
|
<div className="map-loading-overlay"> |
|
|
<div className="map-loading-spinner"> |
|
|
<div className="spinner-large"></div> |
|
|
<div className="loading-text">Loading map...</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* Gestionnaire de tooltips */} |
|
|
{!loading && fonts.length > 0 && ( |
|
|
<TooltipManager |
|
|
selectedFont={selectedFont} |
|
|
hoveredFont={hoveredFont} |
|
|
darkMode={darkMode} |
|
|
onFontHover={handleFontHover} |
|
|
onFontUnhover={handleFontUnhover} |
|
|
/> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* Overlay unifié pour loading */} |
|
|
{appState === 'loading' && ( |
|
|
<div className="unified-overlay"> |
|
|
<div className="loading">Loading fonts...</div> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Modale d'introduction */} |
|
|
{appState === 'intro' && ( |
|
|
<IntroModal |
|
|
onStartExploring={handleStartExploring} |
|
|
darkMode={darkMode} |
|
|
/> |
|
|
)} |
|
|
|
|
|
{/* Modale About */} |
|
|
{showAboutModal && ( |
|
|
<AboutModal |
|
|
onClose={handleCloseAbout} |
|
|
darkMode={darkMode} |
|
|
/> |
|
|
)} |
|
|
|
|
|
{/* Moniteur FPS (dev seulement) */} |
|
|
<FPSMonitor isDebugMode={isDebugMode} /> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default FontMap; |
|
|
export { FontMap }; |
|
|
|