PageSourceSearch

https://forthehearts.net/fth/animation/Animation.js

js forthehearts.net collected 2026-10-02 07:54:06 UTC 8,697 bytes, 208 lines download raw bytes

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.