1.2.5: text font picker + 7 bundled Google fonts + default text fill black
- New src/lib/fonts.ts: curated set (Inter, Roboto, Lora, Playfair Display, Bebas Neue, Source Code Pro, Permanent Marker) shipped as local .woff2 in public/fonts/. loadFonts() registers them via FontFace at app start; fontsReady ref re-triggers Konva text configs once fonts resolve so the first paint isn't stuck on the fallback. - TextAsset gets optional fontFamily (id; undefined = system sans). PropertiesPanel adds a font dropdown next to font size. - All five render sites read the family: CanvasStage textConfig + clipTextConfig (Konva), drawLayer + drawMaskShape (canvas2d), exportPlayer engine (uses an embedded FONT_FAMILIES map and fontFam helper; runtime version 27 → 28). - Example HTML export now lists used bundled fonts and prints the @font-face CSS the user has to paste alongside the .woff2 files — fonts themselves are NOT bundled into the export to keep it small. - Default text fill changed from yellow #f5d76e to black #000000 in both the main sidebar and the SVG-scope add-text path. - 8 new tests under "text font picker — model + render + export". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b32b8a6a3d
commit
4c2b7c7c35
20 files changed
+339
-12
No files matched your search
@@ -15001,4 +15001,141 @@ export function registerAllTests(): void {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
suite('text font picker — model + render + export', () => {
|
||||
test('TextAsset.fontFamily roundtrips through addAsset', () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({
|
||||
kind: 'text', name: 'T', text: 'hi', fontSize: 24, fill: '#000', fontFamily: 'inter',
|
||||
})
|
||||
expect(a.kind).toBe('text')
|
||||
if (a.kind !== 'text') return
|
||||
expect(a.fontFamily).toBe('inter')
|
||||
})
|
||||
|
||||
test('default text fill is black (#000000) when added via the sidebar', () => {
|
||||
// Regression: defaults used to be a yellow #f5d76e which made
|
||||
// first-time text invisible-ish on white canvases.
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({
|
||||
kind: 'text', name: 'T', text: 'hi', fontSize: 48, fill: '#000000',
|
||||
})
|
||||
if (a.kind !== 'text') return
|
||||
expect(a.fill).toBe('#000000')
|
||||
})
|
||||
|
||||
test('fontCssFamily resolves known ids and falls back to system sans', async () => {
|
||||
const { fontCssFamily, AVAILABLE_FONTS, SYSTEM_SANS } = await import('@/lib/fonts')
|
||||
const inter = AVAILABLE_FONTS.find(f => f.id === 'inter')!
|
||||
expect(fontCssFamily('inter')).toBe(inter.family)
|
||||
expect(fontCssFamily(undefined)).toBe(SYSTEM_SANS.family)
|
||||
expect(fontCssFamily('does-not-exist')).toBe(SYSTEM_SANS.family)
|
||||
})
|
||||
|
||||
test('PropertiesPanel renders a font dropdown for text assets and writes back', async () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({
|
||||
kind: 'text', name: 'T', text: 'hello', fontSize: 32, fill: '#000',
|
||||
})
|
||||
const layer = s.addLayer(a.id)!
|
||||
s.selectedLayerId = layer.id
|
||||
s.selectedLayerIds = [layer.id]
|
||||
const handle = mountComponent(PropertiesPanel, {})
|
||||
try {
|
||||
await nextTick()
|
||||
const select = handle.host.querySelector('select') as HTMLSelectElement | null
|
||||
expect(select).toBeTruthy()
|
||||
const options = Array.from(select!.options).map(o => o.value)
|
||||
expect(options).toContain('system-sans')
|
||||
expect(options).toContain('inter')
|
||||
expect(options).toContain('marker')
|
||||
// Default selection: system-sans (asset.fontFamily is undefined).
|
||||
expect(select!.value).toBe('system-sans')
|
||||
// Pick Inter — the asset should mutate.
|
||||
select!.value = 'inter'
|
||||
select!.dispatchEvent(new Event('change'))
|
||||
await nextTick()
|
||||
const textAsset = s.scene.assets.find(x => x.id === a.id)!
|
||||
if (textAsset.kind !== 'text') throw new Error('unexpected kind')
|
||||
expect(textAsset.fontFamily).toBe('inter')
|
||||
// Picking system-sans clears the field (undefined, not the literal id).
|
||||
select!.value = 'system-sans'
|
||||
select!.dispatchEvent(new Event('change'))
|
||||
await nextTick()
|
||||
const textAsset2 = s.scene.assets.find(x => x.id === a.id)!
|
||||
if (textAsset2.kind !== 'text') throw new Error('unexpected kind')
|
||||
expect(textAsset2.fontFamily === undefined).toBe(true)
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
test('drawSceneFrame sets ctx.font with the selected font family', async () => {
|
||||
const { drawSceneFrame } = await import('@/lib/render')
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({
|
||||
kind: 'text', name: 'T', text: 'hi', fontSize: 32, fill: '#000', fontFamily: 'lora',
|
||||
})
|
||||
s.addLayer(a.id)
|
||||
const cv = document.createElement('canvas')
|
||||
cv.width = 200
|
||||
cv.height = 200
|
||||
const ctx = cv.getContext('2d')!
|
||||
const fontHistory: string[] = []
|
||||
const realFontDesc = Object.getOwnPropertyDescriptor(
|
||||
CanvasRenderingContext2D.prototype, 'font',
|
||||
)
|
||||
Object.defineProperty(ctx, 'font', {
|
||||
configurable: true,
|
||||
get() { return realFontDesc?.get?.call(this) ?? '' },
|
||||
set(v: string) {
|
||||
fontHistory.push(v)
|
||||
realFontDesc?.set?.call(this, v)
|
||||
},
|
||||
})
|
||||
try {
|
||||
drawSceneFrame(ctx, s.scene, 0, {})
|
||||
const used = fontHistory.find(f => f.includes('Lora'))
|
||||
expect(!!used).toBe(true)
|
||||
} finally {
|
||||
if (realFontDesc) Object.defineProperty(ctx, 'font', realFontDesc)
|
||||
}
|
||||
})
|
||||
|
||||
test('generateExampleHtml shows a font hint listing assets that use bundled fonts', () => {
|
||||
const s = makeFreshStore()
|
||||
s.addAsset({
|
||||
kind: 'text', name: 'Title', text: 'Hello', fontSize: 48, fill: '#000', fontFamily: 'playfair',
|
||||
})
|
||||
s.addAsset({
|
||||
kind: 'text', name: 'Body', text: 'world', fontSize: 18, fill: '#333', fontFamily: 'roboto',
|
||||
})
|
||||
const html = generateExampleHtml(s.scene)
|
||||
expect(html).toContain('Fonts used in this animation')
|
||||
expect(html).toContain('Playfair Display')
|
||||
expect(html).toContain('Roboto')
|
||||
expect(html).toContain('@font-face')
|
||||
expect(html).toContain('PlayfairDisplay-Regular.woff2')
|
||||
expect(html).toContain('Roboto-Regular.woff2')
|
||||
})
|
||||
|
||||
test('generateExampleHtml omits the font hint if no assets use bundled fonts', () => {
|
||||
const s = makeFreshStore()
|
||||
s.addAsset({
|
||||
kind: 'text', name: 'T', text: 'hi', fontSize: 18, fill: '#000',
|
||||
})
|
||||
const html = generateExampleHtml(s.scene)
|
||||
expect(html.includes('Fonts used in this animation')).toBe(false)
|
||||
})
|
||||
|
||||
test('exported runtime carries a FONT_FAMILIES map and uses fontFam(asset)', async () => {
|
||||
const { generateRuntimeScript } = await import('@/lib/exportPlayer')
|
||||
const src = generateRuntimeScript()
|
||||
expect(src).toContain('FONT_FAMILIES')
|
||||
expect(src).toContain('fontFam(asset)')
|
||||
// Sanity: known font ids are present in the embedded map.
|
||||
expect(src).toContain('"inter"')
|
||||
expect(src).toContain('"playfair"')
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user