1/** 2 * Animation.ts 3 * Main controller for Animation fth Fascination 4 */ 5import { FontLoader } from '../core/FontLoader.js'; 6import { UIControls } from '../shared/UIControls.js'; 7import { GlyphGrid } from './GlyphGrid.js'; 8import { GridAnimator } from './GridAnimator.js'; 9import { initAppNav } from '../shared/AppNav.js'; 10export class Animation { 11 fontLoader; 12 uiControls; 13 glyphGrid; 14 gridAnimator; 15 zoomContainer; 16 gridContainer; 17 currentFont = null; 18 currentFontFamily = ''; 19 resizeTimeouts = null; 20 _keyHandler; 21 _resizeHandler; 22 constructor() { 23 const zoomContainer = document.getElementById('zoom-container'); 24 const gridContainer = document.getElementById('grid-container'); 25 this.zoomContainer = zoomContainer; 26 this.gridContainer = gridContainer; 27 this.glyphGrid = new GlyphGrid(); 28 this.gridAnimator = new GridAnimator(gridContainer, this.glyphGrid); 29 this.fontLoader = new FontLoader({ 30 onFontLoaded: (result) => { this.handleFontLoaded(result); }, 31 onError: (err) => { this.handleError(err); }, 32 }); 33 this.uiControls = new UIControls(); 34 this._keyHandler = (e) => { this.handleKeyPress(e); }; 35 this._resizeHandler = () => { this.handleResize(); }; 36 this.setupEventListeners(); 37 console.log('Animation initialized'); 38 } 39 // --------------------------------------------------------------------------- 40 // Event wiring 41 // --------------------------------------------------------------------------- 42 setupEventListeners() { 43 this.uiControls.setupSharedButtons(); 44 document.addEventListener('keydown', this._keyHandler); 45 window.addEventListener('resize', this._resizeHandler); 46 document.addEventListener('fullscreenchange', this._resizeHandler); 47 document.addEventListener('webkitfullscreenchange', this._resizeHandler); 48 document.addEventListener('mozfullscreenchange', this._resizeHandler); 49 document.addEventListener('MSFullscreenChange', this._resizeHandler); 50 } 51 handleKeyPress(event) { 52 switch (event.key.toLowerCase()) { 53 case 'f': 54 this.uiControls.toggleFullscreen(); 55 break; 56 case ' ': 57 event.preventDefault(); 58 if (this.gridAnimator.isPaused) { 59 this.gridAnimator.resume(); 60 } 61 else { 62 this.gridAnimator.pause(); 63 } 64 break; 65 } 66 } 67 handleResize() { 68 if (!this.fontLoader.currentFont) 69 return; 70 this.scheduleViewportRebuilds(); 71 } 72 scheduleViewportRebuilds() { 73 if (this.resizeTimeouts) { 74 this.resizeTimeouts.forEach(id => clearTimeout(id)); 75 } 76 const delays = [120, 450, 900]; 77 this.resizeTimeouts = delays.map(delay => setTimeout(() => { this.rebuildForViewportChange(); }, delay)); 78 } 79 rebuildForViewportChange() { 80 if (!this.fontLoader.currentFont) 81 return; 82 const initialWidth = window.innerWidth; 83 const initialHeight = window.innerHeight; 84 requestAnimationFrame(() => { 85 requestAnimationFrame(() => { 86 if (window.innerWidth !== initialWidth || window.innerHeight !== initialHeight) { 87 this.rebuildForViewportChange(); 88 return; 89 } 90 this.syncZoomOutScaleToViewport(); 91 this.gridAnimator.pause(); 92 this.gridAnimator.reset(); 93 this.populateGrid(); 94 requestAnimationFrame(() => { 95 requestAnimationFrame(() => { this.gridAnimator.start(); }); 96 }); 97 }); 98 }); 99 } 100 // --------------------------------------------------------------------------- 101 // Font loaded 102 // --------------------------------------------------------------------------- 103 handleFontLoaded({ font, fontInfo, fontFamily }) { 104 console.log(`Font loaded: ${font.names.fullName?.['en'] ?? 'Unknown'}`); 105 console.log(`Unique family name: ${fontFamily}`); 106 this.currentFont = font; 107 this.currentFontFamily = fontFamily; 108 const glyphList = this.extractGlyphs(font); 109 const axes = this.extractAxes(font); 110 const features = this.extractFeatures(font); 111 console.log(`Extracted ${glyphList.length} glyphs`); 112 if (axes.length > 0) 113 console.log(`Found ${axes.length} variable font axes:`, axes.map(a => a.tag)); 114 if (features.length > 0) 115 console.log(`Found ${features.length} OpenType features:`, features);
116 this.syncZoomOutScaleToViewport(); 117 this.gridAnimator.pause(); 118 this.gridAnimator.reset(); 119 this.glyphGrid.populate(this.gridContainer, glyphList, axes, features, fontFamily, font, this.gridAnimator.zoomScale.min); 120 requestAnimationFrame(() => { 121 requestAnimationFrame(() => { this.gridAnimator.start(); }); 122 }); 123 } 124 handleError(error) { 125 console.error('Font loading error:', error); 126 alert(`Error loading font: ${error.message}\nPlease check the console for details.`); 127 } 128 // --------------------------------------------------------------------------- 129 // Font data extraction 130 // --------------------------------------------------------------------------- 131 extractGlyphs(font) { 132 const glyphs = []; 133 const excluded = /[\p{White_Space}\p{M}\p{Diacritic}\p{C}]/u; 134 for (let i = 0; i < font.glyphs.length; i++) { 135 const glyph = font.glyphs.get(i); 136 if (glyph.name === '.notdef' || glyph.unicode === undefined) 137 continue; 138 const char = String.fromCodePoint(glyph.unicode); 139 if (excluded.test(char)) 140 continue; 141 glyphs.push(char); 142 } 143 return glyphs.length > 0 ? glyphs : ['A']; 144 } 145 extractAxes(font) { 146 const axes = []; 147 if (font.tables.fvar?.axes) { 148 for (const axis of font.tables.fvar.axes) { 149 axes.push({ 150 tag: axis.tag, 151 name: axis.name?.['en'] ?? axis.tag, 152 min: axis.minValue, 153 max: axis.maxValue, 154 default: axis.defaultValue, 155 }); 156 } 157 } 158 return axes; 159 } 160 extractFeatures(font) { 161 const features = new Set(); 162 if (font.tables.gsub?.features) { 163 for (const feature of font.tables.gsub.features) { 164 features.add(feature.tag); 165 } 166 } 167 return Array.from(features); 168 } 169 // --------------------------------------------------------------------------- 170 // Grid repopulation 171 // --------------------------------------------------------------------------- 172 populateGrid() { 173 if (!this.currentFont) 174 return; 175 this.syncZoomOutScaleToViewport(); 176 this.glyphGrid.populate(this.gridContainer, this.extractGlyphs(this.currentFont), this.extractAxes(this.currentFont), this.extractFeatures(this.currentFont), this.currentFontFamily, this.currentFont, this.gridAnimator.zoomScale.min); 177 } 178 syncZoomOutScaleToViewport() { 179 this.gridAnimator.zoomScale.min = 0.19; 180 } 181 // --------------------------------------------------------------------------- 182 // Cleanup 183 // --------------------------------------------------------------------------- 184 destroy() { 185 if (this.resizeTimeouts) { 186 this.resizeTimeouts.forEach(id => clearTimeout(id)); 187 this.resizeTimeouts = null; 188 } 189 this.gridAnimator.pause(); 190 this.gridAnimator.reset(); 191 document.removeEventListener('keydown', this._keyHandler); 192 window.removeEventListener('resize', this._resizeHandler); 193 document.removeEventListener('fullscreenchange', this._resizeHandler); 194 document.removeEventListener('webkitfullscreenchange', this._resizeHandler); 195 document.removeEventListener('mozfullscreenchange', this._resizeHandler); 196 document.removeEventListener('MSFullscreenChange', this._resizeHandler); 197 this.uiControls.destroy(); 198 } 199}
200// --------------------------------------------------------------------------- 201// Standalone bootstrap 202// --------------------------------------------------------------------------- 203document.addEventListener('DOMContentLoaded', () => { 204 const app = new Animation(); 205 initAppNav(); 206 void app.fontLoader.restoreFromSession(); 207}); 208//# sourceMappingURL=Animation.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.