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

+44 -4
View File
@@ -1,4 +1,5 @@
import type { Project, Scene } from '@/types/scene'
import type { Asset, Project, Scene } from '@/types/scene'
import { AVAILABLE_FONTS, fontFaceCss, fontPrimary, SYSTEM_SANS } from './fonts'
/** Input accepted by every export helper below. A Project exports all
* its scenes (jumpToScene works in the bundle); a single Scene is
@@ -10,10 +11,18 @@ function isProject(p: ExportPayload): p is Project {
return Array.isArray((p as Partial<Project>).scenes)
}
const RUNTIME_VERSION = '27'
const RUNTIME_VERSION = '28'
const FONT_FAMILY_MAP_JSON = JSON.stringify(
Object.fromEntries(AVAILABLE_FONTS.map(f => [f.id, f.family])),
)
const ENGINE_SOURCE = `(function(){
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
var FONT_FAMILIES = ${FONT_FAMILY_MAP_JSON};
function fontFam(asset){
return (asset && asset.fontFamily && FONT_FAMILIES[asset.fontFamily]) || FONT_FAMILIES['system-sans'] || 'sans-serif';
}
function easing(name, t){
if(name==='easeIn')return t*t;
@@ -702,7 +711,7 @@ const ENGINE_SOURCE = `(function(){
} else if(asset.kind==='text'){
var mtOff=layer.offsetX!=null?layer.offsetX:0;
var mtOffY=layer.offsetY!=null?layer.offsetY:0;
ctxm.font=asset.fontSize+'px sans-serif';
ctxm.font=asset.fontSize+'px '+fontFam(asset);
ctxm.textBaseline='top';
ctxm.fillText(asset.text,-mtOff,-mtOffY);
} else {
@@ -827,7 +836,7 @@ const ENGINE_SOURCE = `(function(){
}else if(asset.kind==='text'){
var tOffX=layer.offsetX!=null?layer.offsetX:0;
var tOffY=layer.offsetY!=null?layer.offsetY:0;
ctx.font=asset.fontSize+'px sans-serif';
ctx.font=asset.fontSize+'px '+fontFam(asset);
ctx.textBaseline='top';
var tFill=p.fill||asset.fill;
if(tFill && tFill!=='transparent'){
@@ -1330,6 +1339,22 @@ function jsonForExport(payload: ExportPayload): string {
return JSON.stringify(out)
}
function collectFontIds(payload: ExportPayload): string[] {
const ids = new Set<string>()
const assets: readonly Asset[] = payload.assets ?? []
for (const a of assets) {
if (a.kind === 'text' && a.fontFamily) ids.add(a.fontFamily)
}
return Array.from(ids)
}
function escapeHtml(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}
function sceneToLegacyProject(scene: Scene): Project {
return {
version: 1,
@@ -1415,6 +1440,19 @@ export function generateExampleHtml(payload: ExportPayload, options: ExportOptio
const sceneBoot = generateSceneScriptBody(payload, options)
const canvas = isProject(payload) ? payload.canvas : payload.canvas
const aspect = `${canvas.width} / ${canvas.height}`
const usedFontIds = collectFontIds(payload)
const usedFonts = usedFontIds
.map(id => AVAILABLE_FONTS.find(f => f.id === id))
.filter((f): f is typeof AVAILABLE_FONTS[number] => !!f && !!f.file)
const fontHint = usedFonts.length === 0 ? '' : `
<div class="usage">
<h2>Fonts used in this animation</h2>
<p style="margin-bottom: 8px;">This preview uses ${SYSTEM_SANS.label} for missing fonts. To render with the correct typefaces, host the .woff2 files alongside this HTML and add the @font-face rules below to your CSS:</p>
<ul style="margin: 0 0 12px 18px; padding: 0; font-size: 12px; color: #6e6e73;">
${usedFonts.map(f => ` <li>${fontPrimary(f.id)} — <code>${f.file.replace(/^\//, '')}</code></li>`).join('\n')}
</ul>
<pre>${escapeHtml(fontFaceCss(usedFontIds))}</pre>
</div>`
return `<!doctype html>
<html lang="en">
<head>
@@ -1489,6 +1527,8 @@ ${sceneBoot}
</script>
</div>
${fontHint}
<div class="usage">
<h2>Embed on your own page</h2>
<pre>&lt;!-- once per page --&gt;