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:
thandClaude Opus 4.7 committed 2026-04-26 00:32:42 +02:00
1 parent b32b8a6a3d
commit 4c2b7c7c35
20 files changed
+339 -12

No files matched your search

+137
View File
@@ -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"')
})
})
}