@kolkrabbi/kol-foundry 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/FontPreviewSection.jsx +1 -1
- package/src/GlyphItem.jsx +51 -0
- package/src/GlyphMetricsSection.jsx +1 -1
- package/src/TypefaceStyleSection.jsx +1 -1
- package/src/VariableFontSection.jsx +1 -1
- package/src/engine/FontInfo.js +333 -0
- package/src/engine/FontLoader.js +76 -0
- package/src/engine/FontViewerComponent.jsx +449 -0
- package/src/engine/FontViewerSection.jsx +10 -0
- package/src/engine/GlyphAnimator.js +119 -0
- package/src/engine/MetricsOverlay.js +169 -0
- package/src/engine/Types.js +22 -0
- package/src/engine/UIControls.js +92 -0
- package/src/engine/VariationAxes.js +71 -0
- package/src/index.js +18 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { FontLoader } from './FontLoader.js'
|
|
3
|
+
import { GlyphAnimator } from './GlyphAnimator.js'
|
|
4
|
+
import { MetricsOverlay } from './MetricsOverlay.js'
|
|
5
|
+
import { UIControls } from './UIControls.js'
|
|
6
|
+
import { FontInfoRenderer } from './FontInfo.js'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_VERTICAL = 50
|
|
9
|
+
|
|
10
|
+
const FontViewer = ({
|
|
11
|
+
fontUrl,
|
|
12
|
+
showControls = true,
|
|
13
|
+
showMetrics = false,
|
|
14
|
+
initialFontSize = 600,
|
|
15
|
+
autoStart = true,
|
|
16
|
+
animationDelay = 1000,
|
|
17
|
+
config = {}
|
|
18
|
+
}) => {
|
|
19
|
+
const mergedConfig = {
|
|
20
|
+
showControls,
|
|
21
|
+
showMetrics,
|
|
22
|
+
initialFontSize,
|
|
23
|
+
autoStart,
|
|
24
|
+
animationDelay,
|
|
25
|
+
...config
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const wrapperRef = useRef(null)
|
|
29
|
+
const displayRef = useRef(null)
|
|
30
|
+
const metricsOverlayElementRef = useRef(null)
|
|
31
|
+
const fontInfoContentRef = useRef(null)
|
|
32
|
+
const glyphInfoContentRef = useRef(null)
|
|
33
|
+
|
|
34
|
+
const fontLoaderRef = useRef(null)
|
|
35
|
+
const glyphAnimatorRef = useRef(null)
|
|
36
|
+
const metricsOverlayRef = useRef(null)
|
|
37
|
+
const uiControlsRef = useRef(null)
|
|
38
|
+
const currentFontRef = useRef(null)
|
|
39
|
+
|
|
40
|
+
const fontSizeRef = useRef(mergedConfig.initialFontSize)
|
|
41
|
+
const verticalRef = useRef(DEFAULT_VERTICAL)
|
|
42
|
+
const delayRef = useRef(mergedConfig.animationDelay)
|
|
43
|
+
const metricsVisibleRef = useRef(mergedConfig.showMetrics)
|
|
44
|
+
|
|
45
|
+
const [fontInfoVisible, setFontInfoVisible] = useState(false)
|
|
46
|
+
const [glyphInfoVisible, setGlyphInfoVisible] = useState(false)
|
|
47
|
+
const [metricsVisible, setMetricsVisible] = useState(mergedConfig.showMetrics)
|
|
48
|
+
const [randomOrder, setRandomOrder] = useState(false)
|
|
49
|
+
const [fontLoaded, setFontLoaded] = useState(false)
|
|
50
|
+
const [fontSizeValue, setFontSizeValue] = useState(mergedConfig.initialFontSize)
|
|
51
|
+
const [verticalValue, setVerticalValue] = useState(DEFAULT_VERTICAL)
|
|
52
|
+
const [delayValue, setDelayValue] = useState(mergedConfig.animationDelay)
|
|
53
|
+
const [variationAxes, setVariationAxes] = useState([])
|
|
54
|
+
const [variationValues, setVariationValues] = useState({})
|
|
55
|
+
const [controlsVisible, setControlsVisible] = useState(false)
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const displayEl = displayRef.current
|
|
59
|
+
const metricsEl = metricsOverlayElementRef.current
|
|
60
|
+
if (!displayEl || !metricsEl) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
uiControlsRef.current = new UIControls()
|
|
65
|
+
metricsOverlayRef.current = new MetricsOverlay(metricsEl)
|
|
66
|
+
|
|
67
|
+
fontLoaderRef.current = new FontLoader({
|
|
68
|
+
onFontLoaded: ({ font, fontInfo, fontFamily }) => {
|
|
69
|
+
currentFontRef.current = font
|
|
70
|
+
displayEl.style.fontFamily = `"${fontFamily}"`
|
|
71
|
+
displayEl.style.fontSize = `${fontSizeRef.current}px`
|
|
72
|
+
displayEl.style.top = `${verticalRef.current - 50}%`
|
|
73
|
+
|
|
74
|
+
FontInfoRenderer.renderFontInfo(fontInfoContentRef.current, fontInfo)
|
|
75
|
+
|
|
76
|
+
if (fontInfo.axes?.length > 0) {
|
|
77
|
+
setVariationAxes(fontInfo.axes.map((axis) => ({ ...axis })))
|
|
78
|
+
const defaults = fontInfo.axes.reduce((acc, axis) => {
|
|
79
|
+
const numericDefault = Number(axis.default)
|
|
80
|
+
acc[axis.tag] = Number.isNaN(numericDefault) ? Number(axis.min) || 0 : numericDefault
|
|
81
|
+
return acc
|
|
82
|
+
}, {})
|
|
83
|
+
setVariationValues(defaults)
|
|
84
|
+
} else {
|
|
85
|
+
setVariationAxes([])
|
|
86
|
+
setVariationValues({})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
glyphAnimatorRef.current = new GlyphAnimator({
|
|
90
|
+
displayElement: displayEl,
|
|
91
|
+
onGlyphChange: (glyph) => {
|
|
92
|
+
FontInfoRenderer.renderGlyphInfo(
|
|
93
|
+
glyphInfoContentRef.current,
|
|
94
|
+
font,
|
|
95
|
+
glyph
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if (metricsOverlayRef.current.isVisible) {
|
|
99
|
+
metricsOverlayRef.current.render(font, displayEl)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
glyphAnimatorRef.current.setGlyphsFromFont(font).then(() => {
|
|
105
|
+
if (mergedConfig.autoStart) {
|
|
106
|
+
if (delayRef.current >= 1000) {
|
|
107
|
+
glyphAnimatorRef.current.stop()
|
|
108
|
+
} else {
|
|
109
|
+
glyphAnimatorRef.current.start(delayRef.current)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (metricsVisibleRef.current) {
|
|
115
|
+
metricsOverlayRef.current.isVisible = true
|
|
116
|
+
metricsEl.style.display = 'block'
|
|
117
|
+
metricsOverlayRef.current.render(font, displayEl)
|
|
118
|
+
} else {
|
|
119
|
+
metricsOverlayRef.current.isVisible = false
|
|
120
|
+
metricsEl.style.display = 'none'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setFontLoaded(true)
|
|
124
|
+
},
|
|
125
|
+
onError: (error) => {
|
|
126
|
+
console.error('FontViewer error:', error)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
fontLoaderRef.current?.cleanup()
|
|
132
|
+
glyphAnimatorRef.current?.stop()
|
|
133
|
+
metricsOverlayElementRef.current?.replaceChildren()
|
|
134
|
+
}
|
|
135
|
+
}, [mergedConfig.autoStart, mergedConfig.initialFontSize, mergedConfig.showMetrics])
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!fontUrl || !fontLoaderRef.current) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let cancelled = false
|
|
143
|
+
|
|
144
|
+
const loadFont = async () => {
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(fontUrl)
|
|
147
|
+
const buffer = await response.arrayBuffer()
|
|
148
|
+
const filename = fontUrl.split('/').pop() || 'font.ttf'
|
|
149
|
+
if (!cancelled) {
|
|
150
|
+
await fontLoaderRef.current.loadFont(buffer, filename)
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Error loading font:', error)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
loadFont()
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
cancelled = true
|
|
161
|
+
}
|
|
162
|
+
}, [fontUrl])
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
fontSizeRef.current = fontSizeValue
|
|
166
|
+
}, [fontSizeValue])
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
verticalRef.current = verticalValue
|
|
170
|
+
}, [verticalValue])
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
delayRef.current = delayValue
|
|
174
|
+
}, [delayValue])
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
metricsVisibleRef.current = metricsVisible
|
|
178
|
+
}, [metricsVisible])
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const displayEl = displayRef.current
|
|
182
|
+
if (!displayEl) {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const settings = Object.entries(variationValues)
|
|
187
|
+
.filter(([, val]) => typeof val === 'number' && !Number.isNaN(val))
|
|
188
|
+
.map(([tag, val]) => `"${tag}" ${val.toFixed(1)}`)
|
|
189
|
+
.join(', ')
|
|
190
|
+
|
|
191
|
+
displayEl.style.fontVariationSettings = settings || 'normal'
|
|
192
|
+
}, [variationValues])
|
|
193
|
+
|
|
194
|
+
const handleToggleFontInfo = () => {
|
|
195
|
+
setFontInfoVisible((prev) => !prev)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const handleToggleGlyphInfo = () => {
|
|
199
|
+
setGlyphInfoVisible((prev) => !prev)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const handleToggleMetrics = () => {
|
|
203
|
+
if (!metricsOverlayRef.current || !currentFontRef.current || !displayRef.current) {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
metricsOverlayRef.current.toggle()
|
|
208
|
+
const isVisible = metricsOverlayRef.current.isVisible
|
|
209
|
+
setMetricsVisible(isVisible)
|
|
210
|
+
|
|
211
|
+
if (isVisible) {
|
|
212
|
+
metricsOverlayRef.current.render(currentFontRef.current, displayRef.current)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const handleSwapColors = () => {
|
|
217
|
+
uiControlsRef.current?.toggleColorScheme()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const handleRandomize = () => {
|
|
221
|
+
if (!glyphAnimatorRef.current) return
|
|
222
|
+
glyphAnimatorRef.current.toggleOrder()
|
|
223
|
+
setRandomOrder(glyphAnimatorRef.current.isRandomOrder)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const handleFontSizeChange = (value) => {
|
|
227
|
+
const numeric = Number(value)
|
|
228
|
+
setFontSizeValue(numeric)
|
|
229
|
+
|
|
230
|
+
if (displayRef.current) {
|
|
231
|
+
displayRef.current.style.fontSize = `${numeric}px`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (metricsOverlayRef.current?.isVisible && currentFontRef.current && displayRef.current) {
|
|
235
|
+
metricsOverlayRef.current.render(currentFontRef.current, displayRef.current)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const handleVerticalChange = (value) => {
|
|
240
|
+
const numeric = Number(value)
|
|
241
|
+
setVerticalValue(numeric)
|
|
242
|
+
|
|
243
|
+
if (displayRef.current) {
|
|
244
|
+
const offset = numeric - 50
|
|
245
|
+
displayRef.current.style.top = `${offset}%`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (metricsOverlayRef.current?.isVisible && currentFontRef.current && displayRef.current) {
|
|
249
|
+
metricsOverlayRef.current.render(currentFontRef.current, displayRef.current)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const handleDelayChange = (value) => {
|
|
254
|
+
const numeric = Number(value)
|
|
255
|
+
setDelayValue(numeric)
|
|
256
|
+
|
|
257
|
+
if (!glyphAnimatorRef.current) {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (numeric >= 1000) {
|
|
262
|
+
glyphAnimatorRef.current.stop()
|
|
263
|
+
} else {
|
|
264
|
+
glyphAnimatorRef.current.start(numeric)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const handleVariationChange = (tag, value) => {
|
|
269
|
+
const numeric = Number(value)
|
|
270
|
+
setVariationValues((prev) => {
|
|
271
|
+
if (Number.isNaN(numeric)) {
|
|
272
|
+
return prev
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
...prev,
|
|
277
|
+
[tag]: numeric
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div ref={wrapperRef} className="font-viewer-wrapper">
|
|
284
|
+
<div className="font-viewer-display-container">
|
|
285
|
+
<div
|
|
286
|
+
ref={displayRef}
|
|
287
|
+
className="font-viewer-glyph-buffer"
|
|
288
|
+
style={{ top: `${verticalValue - 50}%`, fontSize: `${fontSizeValue}px` }}
|
|
289
|
+
/>
|
|
290
|
+
<div
|
|
291
|
+
ref={metricsOverlayElementRef}
|
|
292
|
+
className="font-viewer-metrics-overlay"
|
|
293
|
+
style={{ display: metricsVisible ? 'block' : 'none' }}
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div
|
|
298
|
+
className="font-viewer-info-panel font-viewer-font-info"
|
|
299
|
+
aria-hidden={!fontInfoVisible}
|
|
300
|
+
style={{ display: fontInfoVisible ? 'block' : 'none' }}
|
|
301
|
+
>
|
|
302
|
+
<div ref={fontInfoContentRef} className="font-viewer-info-content" />
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div
|
|
306
|
+
className="font-viewer-info-panel font-viewer-glyph-info"
|
|
307
|
+
aria-hidden={!glyphInfoVisible}
|
|
308
|
+
style={{ display: glyphInfoVisible ? 'block' : 'none' }}
|
|
309
|
+
>
|
|
310
|
+
<div ref={glyphInfoContentRef} className="font-viewer-info-content" />
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{mergedConfig.showControls ? (
|
|
314
|
+
<div
|
|
315
|
+
className={`font-viewer-controls${controlsVisible ? ' is-visible' : ''}`}
|
|
316
|
+
role="toolbar"
|
|
317
|
+
aria-label="Font viewer controls"
|
|
318
|
+
onMouseEnter={() => setControlsVisible(true)}
|
|
319
|
+
onMouseLeave={() => setControlsVisible(false)}
|
|
320
|
+
>
|
|
321
|
+
<div className="font-viewer-buttons">
|
|
322
|
+
<ControlButton active={fontInfoVisible} onClick={handleToggleFontInfo}>
|
|
323
|
+
{fontInfoVisible ? 'Hide font info' : 'Show font info'}
|
|
324
|
+
</ControlButton>
|
|
325
|
+
<ControlButton active={glyphInfoVisible} onClick={handleToggleGlyphInfo}>
|
|
326
|
+
{glyphInfoVisible ? 'Hide glyph info' : 'Show glyph info'}
|
|
327
|
+
</ControlButton>
|
|
328
|
+
<ControlButton active={metricsVisible} onClick={handleToggleMetrics}>
|
|
329
|
+
{metricsVisible ? 'Hide metrics' : 'Show metrics'}
|
|
330
|
+
</ControlButton>
|
|
331
|
+
<ControlButton onClick={handleSwapColors}>Swap colours</ControlButton>
|
|
332
|
+
<ControlButton active={randomOrder} onClick={handleRandomize} disabled={!fontLoaded}>
|
|
333
|
+
{randomOrder ? 'Sequential glyph order' : 'Randomize glyph order'}
|
|
334
|
+
</ControlButton>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div className="font-viewer-sliders">
|
|
338
|
+
<SliderControl
|
|
339
|
+
id="font-viewer-font-size"
|
|
340
|
+
label="Font size"
|
|
341
|
+
min={100}
|
|
342
|
+
max={1200}
|
|
343
|
+
value={fontSizeValue}
|
|
344
|
+
onChange={handleFontSizeChange}
|
|
345
|
+
formattedValue={`${fontSizeValue}px`}
|
|
346
|
+
/>
|
|
347
|
+
|
|
348
|
+
<SliderControl
|
|
349
|
+
id="font-viewer-vertical"
|
|
350
|
+
label="Vertical position"
|
|
351
|
+
min={0}
|
|
352
|
+
max={100}
|
|
353
|
+
value={verticalValue}
|
|
354
|
+
onChange={handleVerticalChange}
|
|
355
|
+
formattedValue={`${verticalValue}%`}
|
|
356
|
+
/>
|
|
357
|
+
|
|
358
|
+
<SliderControl
|
|
359
|
+
id="font-viewer-delay"
|
|
360
|
+
label="Animation delay"
|
|
361
|
+
min={10}
|
|
362
|
+
max={1000}
|
|
363
|
+
value={delayValue}
|
|
364
|
+
onChange={handleDelayChange}
|
|
365
|
+
formattedValue={delayValue >= 1000 ? 'Off' : `${delayValue}ms`}
|
|
366
|
+
/>
|
|
367
|
+
|
|
368
|
+
{variationAxes.map((axis) => (
|
|
369
|
+
<VariationControl
|
|
370
|
+
key={axis.tag}
|
|
371
|
+
axis={axis}
|
|
372
|
+
value={variationValues[axis.tag]}
|
|
373
|
+
onChange={(newValue) => handleVariationChange(axis.tag, newValue)}
|
|
374
|
+
/>
|
|
375
|
+
))}
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
) : null}
|
|
379
|
+
</div>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const ControlButton = ({ active, disabled, onClick, children }) => (
|
|
384
|
+
<button
|
|
385
|
+
type="button"
|
|
386
|
+
onClick={onClick}
|
|
387
|
+
disabled={disabled}
|
|
388
|
+
className={`font-viewer-btn${active ? ' active' : ''}${disabled ? ' disabled' : ''}`}
|
|
389
|
+
>
|
|
390
|
+
{children}
|
|
391
|
+
</button>
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
const SliderControl = ({ id, label, min, max, step, value, onChange, formattedValue }) => {
|
|
395
|
+
const handleChange = (event) => {
|
|
396
|
+
onChange(event.target.value)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<div className="font-viewer-slider-group">
|
|
401
|
+
<label htmlFor={id}>{label}</label>
|
|
402
|
+
<input
|
|
403
|
+
id={id}
|
|
404
|
+
type="range"
|
|
405
|
+
min={min}
|
|
406
|
+
max={max}
|
|
407
|
+
step={step}
|
|
408
|
+
value={value}
|
|
409
|
+
onChange={handleChange}
|
|
410
|
+
/>
|
|
411
|
+
<span>{formattedValue}</span>
|
|
412
|
+
</div>
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const VariationControl = ({ axis, value, onChange }) => {
|
|
417
|
+
const sliderId = `font-axis-${axis.tag}`
|
|
418
|
+
const min = Number(axis.min)
|
|
419
|
+
const max = Number(axis.max)
|
|
420
|
+
const defaultValue = Number(axis.default ?? axis.min ?? 0)
|
|
421
|
+
let numericValue = typeof value === 'number' && !Number.isNaN(value) ? value : defaultValue
|
|
422
|
+
|
|
423
|
+
if (!Number.isNaN(min)) {
|
|
424
|
+
numericValue = Math.max(numericValue, min)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!Number.isNaN(max)) {
|
|
428
|
+
numericValue = Math.min(numericValue, max)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const formattedValue = Number.isFinite(numericValue) ? numericValue.toFixed(1) : '0.0'
|
|
432
|
+
const rawStep = axis.step ? Number(axis.step) : 0.1
|
|
433
|
+
const step = Number.isNaN(rawStep) || rawStep === 0 ? 0.1 : rawStep
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<SliderControl
|
|
437
|
+
id={sliderId}
|
|
438
|
+
label={`${axis.name} (${axis.tag})`}
|
|
439
|
+
min={Number.isNaN(min) ? undefined : min}
|
|
440
|
+
max={Number.isNaN(max) ? undefined : max}
|
|
441
|
+
step={step}
|
|
442
|
+
value={numericValue}
|
|
443
|
+
onChange={onChange}
|
|
444
|
+
formattedValue={formattedValue}
|
|
445
|
+
/>
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export default FontViewer
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import FontViewer from './FontViewerComponent.jsx'
|
|
3
|
+
|
|
4
|
+
const FontViewerSection = ({ fontUrl, ...viewerProps }) => (
|
|
5
|
+
<section className="fontviewer-section">
|
|
6
|
+
<FontViewer fontUrl={fontUrl} {...viewerProps} />
|
|
7
|
+
</section>
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export default FontViewerSection
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// hyperflip/GlyphAnimator.js
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
export class GlyphAnimator {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.displayElement = options.displayElement;
|
|
8
|
+
this.onGlyphChange = options.onGlyphChange;
|
|
9
|
+
this.glyphs = [];
|
|
10
|
+
this.sequentialGlyphs = [];
|
|
11
|
+
this.currentIndex = 0;
|
|
12
|
+
this.isAnimating = false;
|
|
13
|
+
this.animationFrameId = null;
|
|
14
|
+
this.isRandomOrder = false;
|
|
15
|
+
|
|
16
|
+
// Animation timing variables
|
|
17
|
+
this.lastFrameTime = 0;
|
|
18
|
+
this.interval = 100; // Default interval
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async setGlyphsFromFont(font) {
|
|
22
|
+
if (!font) {
|
|
23
|
+
throw new Error('No font provided');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const chars = [];
|
|
27
|
+
for (let i = 0; i < font.glyphs.length; i++) {
|
|
28
|
+
const glyph = font.glyphs.get(i);
|
|
29
|
+
if (glyph.name === '.notdef' || glyph.unicode === undefined) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const char = String.fromCodePoint(glyph.unicode);
|
|
33
|
+
chars.push(char);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.glyphs = chars;
|
|
37
|
+
this.sequentialGlyphs = [...chars];
|
|
38
|
+
this.currentIndex = 0;
|
|
39
|
+
|
|
40
|
+
if (this.glyphs.length > 0) {
|
|
41
|
+
const firstGlyph = this.glyphs[0];
|
|
42
|
+
this.displayElement.textContent = firstGlyph;
|
|
43
|
+
this.onGlyphChange?.(firstGlyph);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
start(interval) {
|
|
48
|
+
if (this.glyphs.length === 0) {
|
|
49
|
+
console.error('No glyphs available for animation');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.interval = interval;
|
|
54
|
+
this.isAnimating = true;
|
|
55
|
+
this.lastFrameTime = performance.now();
|
|
56
|
+
this.animate();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stop() {
|
|
60
|
+
this.isAnimating = false;
|
|
61
|
+
if (this.animationFrameId) {
|
|
62
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
63
|
+
this.animationFrameId = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
animate(currentTime = performance.now()) {
|
|
68
|
+
if (!this.isAnimating) return;
|
|
69
|
+
|
|
70
|
+
const elapsed = currentTime - this.lastFrameTime;
|
|
71
|
+
|
|
72
|
+
if (elapsed >= this.interval) {
|
|
73
|
+
// Update the glyph
|
|
74
|
+
const currentChar = this.glyphs[this.currentIndex];
|
|
75
|
+
this.displayElement.textContent = currentChar;
|
|
76
|
+
this.onGlyphChange?.(currentChar);
|
|
77
|
+
|
|
78
|
+
// Move to next glyph
|
|
79
|
+
this.currentIndex = (this.currentIndex + 1) % this.glyphs.length;
|
|
80
|
+
|
|
81
|
+
// Reset timer
|
|
82
|
+
this.lastFrameTime = currentTime - (elapsed % this.interval);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
moveForward(steps = 1) {
|
|
89
|
+
this.currentIndex = (this.currentIndex + steps) % this.glyphs.length;
|
|
90
|
+
const currentChar = this.glyphs[this.currentIndex];
|
|
91
|
+
this.displayElement.textContent = currentChar;
|
|
92
|
+
this.onGlyphChange?.(currentChar);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
moveBack(steps = 1) {
|
|
96
|
+
this.currentIndex = (this.currentIndex - steps + this.glyphs.length) % this.glyphs.length;
|
|
97
|
+
const currentChar = this.glyphs[this.currentIndex];
|
|
98
|
+
this.displayElement.textContent = currentChar;
|
|
99
|
+
this.onGlyphChange?.(currentChar);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
toggleOrder() {
|
|
103
|
+
if (this.isRandomOrder) {
|
|
104
|
+
this.glyphs = [...this.sequentialGlyphs];
|
|
105
|
+
} else {
|
|
106
|
+
this.glyphs = this.shuffleArray([...this.glyphs]);
|
|
107
|
+
}
|
|
108
|
+
this.isRandomOrder = !this.isRandomOrder;
|
|
109
|
+
this.currentIndex = 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
shuffleArray(array) {
|
|
113
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
114
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
115
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
116
|
+
}
|
|
117
|
+
return array;
|
|
118
|
+
}
|
|
119
|
+
}
|