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
+44
-4
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
}
|
||||
|
||||
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><!-- once per page -->
|
||||
|
||||
Reference in new issue
Block a user