The names of fonts are rendered using the corresponding font.

Each font's name is rendered to an OffscreenCanvas, and that image is cached in OPFS.

There's a list of links to download font files in the preview.
This commit is contained in:
Christian Lawson-Perfect 2025-03-20 13:11:40 +00:00
parent 51d57259b0
commit a4b046a784
3 changed files with 165 additions and 15 deletions

View file

@ -9,21 +9,24 @@
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Fontsource font preview</h1>
</header>
<main>
<section id="intro" class="fixed-font">
<p>This page shows fonts loaded from <a href="https://fontsource.org">Fontsource</a>.</p>
</section>
<section id="controls" class="fixed-font">
<nav>
<section id="controls">
<label for="font-family">Font:</label>
<select id="font-family">
</select>
<button type="button" id="random-font">I'm feeling lucky</button>
</section>
<ul id="font-list"></ul>
</nav>
<main>
<header>
<h1>Fontsource font preview</h1>
</header>
<section id="intro">
<p>This page shows fonts loaded from <a href="https://fontsource.org">Fontsource</a>.</p>
</section>
<section id="info">
<h2>About this font</h2>
@ -52,6 +55,12 @@
</section>
<section id="files">
<h2>Files</h2>
<ul id="file-links">
</ul>
</section>
<section id="code">
<h2>CSS</h2>
<pre id="css-display"></pre>

View file

@ -81,6 +81,7 @@ const css_template = (info,range,weight,style,variable_info) => {
}
}
if(info.variable) {
return `
/* ${info.family} variable ${style} ${range} */
@ -113,6 +114,52 @@ async function fetch_json(url) {
}
async function draw_font_preview(f) {
const d = await navigator.storage.getDirectory();
const filename = `${f.family}.png`;
let fh;
try {
fh = await d.getFileHandle(filename);
} catch(e) {
const style = document.createElement('style');
try {
document.head.append(style);
const info = await fetch_json(font_info_url(f.id));
const variable_info = info.variable ? await fetch_json(variable_info_url(info.id)) : {};
const css = css_template(info,info.defSubset,info.weights[0],'normal',variable_info);
style.textContent += css;
await new Promise((resolve,reject) => {
setTimeout(async () => {
const ffs = Array.from(document.fonts).filter(ff=>ff.family == `"${f.family}"`);
await Promise.all(ffs.map(ff => ff.load())).catch(reject)
resolve();
},1);
});
} catch(e) {
return;
}
const cv = new OffscreenCanvas(1,1);
const ctx = cv.getContext('2d');
const font_style = `100px "${f.family}"`;
ctx.font = font_style;
const {width, fontBoundingBoxAscent, fontBoundingBoxDescent} = ctx.measureText(f.family);
cv.width = width;
cv.height = fontBoundingBoxAscent + fontBoundingBoxDescent;
ctx.font = font_style;
ctx.fillText(f.family,0,fontBoundingBoxAscent);
const blob = await cv.toBlob();
fh = await d.getFileHandle(filename,{create:true});
const w = await fh.createWritable();
await w.write(blob);
await w.close();
window.blob = blob;
style.parentElement.removeChild(style);
}
const file = await fh.getFile();
return URL.createObjectURL(file);
}
async function go() {
let [fonts, axes_info] = await Promise.all([
@ -120,14 +167,20 @@ async function go() {
fetch_json('https://api.fontsource.org/v1/axis-registry')
]);
window.available_fonts = fonts;
axes_info = Object.fromEntries(Object.entries(axes_info).map(([k,v]) => [k.toLowerCase(), v]));
async function use_font() {
const name = select.value;
console.log(name);
const info = await fetch_json(font_info_url(name));
const variable_info = info.variable ? await fetch_json(variable_info_url(info.id)) : {};
console.log(info);
const preview = document.querySelector(`[data-font="${name}"]`);
if(preview) {
preview.scrollIntoView({block: 'center'});
document.body.scrollTop = 0;
}
document.body.style.setProperty('--family',info.family);
@ -153,7 +206,6 @@ async function go() {
if(info.variable) {
const variable_axes = document.getElementById('variable-axes');
variable_axes.innerHTML = '';
console.log(variable_info);
const ranges = {};
@ -208,7 +260,6 @@ async function go() {
document.body.style['font-variation-settings'] = settings;
}
update_variables();
}
const css_declarations = [];
@ -240,7 +291,7 @@ async function go() {
}
fonts = fonts.filter(f=>!f.id.includes('noto'));
fonts = fonts.filter(f=>!f.id.match(/noto|playwrite/i));
select.innerHTML = '';
for(let font of fonts) {
const option = document.createElement('option');
@ -260,6 +311,27 @@ async function go() {
document.getElementById('random-font').addEventListener('click', random_font);
random_font();
const fl = document.getElementById('font-list');
const step = 10;
for(let i=0;i<fonts.length;i+=step) {
await Promise.all(fonts.slice(i+0,i+step).map(async f => {
const li = document.createElement('li');
fl.append(li);
const a = document.createElement('a');
li.append(a);
a.addEventListener('click', () => {
select.value = f.id;
use_font();
})
const img = document.createElement('img');
img.dataset.font = f.id;
a.append(img);
img.src = await draw_font_preview(f);
img.style.height = '1em';
img.alt = f.family;
}));
}
}
go();

View file

@ -2,10 +2,59 @@
--spacing: 1em;
--family: sans-serif;
--background: white;
--color: black;
color-scheme: light dark;
height: 100svh;
overflow: hidden;
}
@media (prefers-color-scheme: dark) {
body {
--background: black;
--color: white;
}
#font-list img {
filter: invert(100%);
}
}
body {
display: grid;
grid-template-columns: 15em 1fr;
gap: var(--spacing);
width: 100svw;
height: 100svh;
margin: 0;
overflow: hidden;
color: var(--color);
background: var(--background);
}
nav {
height: 100svh;
display: flex;
flex-direction: column;
}
#controls {
position: sticky;
top: 0;
background: var(--background);
padding: var(--spacing);
& select {
width: 100%;
}
}
main {
font-family: var(--family);
overflow: auto;
}
.fixed-font {
@ -30,3 +79,23 @@ dt {
max-height: 3em;
overflow: auto;
}
#font-list {
margin: 0;
overflow: auto;
width:100%;
height:100%;
font-size: 2rem;
padding: 0;
list-style: none;
& li {
overflow: hidden;
}
}
#css-display {
margin: 0;
}