first commit

This commit is contained in:
Christian Lawson-Perfect 2025-02-11 14:20:15 +00:00
commit 51d57259b0
4 changed files with 360 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.make.*

62
index.html Normal file
View file

@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Fontsource font preview</title>
<script type="module" src="script.js"></script>
<style id="font-style"></style>
<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">
<label for="font-family">Font:</label>
<select id="font-family">
</select>
<button type="button" id="random-font">I'm feeling lucky</button>
</section>
<section id="info">
<h2>About this font</h2>
<p><a id="font-link" target="fontsource"></a></p>
<section id="variable">
<h3>Variable axes</h3>
<ul id="variable-axes">
</ul>
</section>
<dl>
<dt>Category</dt>
<dd id="category"></dd>
<dt>Styles</dt>
<dd><ul id="styles"></ul></dd>
<dt>Weights</dt>
<dd><ul id="weights"></ul></dd>
<dt>Unicode ranges</dt>
<dd><ul id="unicode-ranges"></ul></dd>
</dl>
</section>
<section id="code">
<h2>CSS</h2>
<pre id="css-display"></pre>
</section>
</main>
</body>
</html>

265
script.js Normal file
View file

@ -0,0 +1,265 @@
const select = document.getElementById('font-family');
const link = document.getElementById('font-link');
const font_info_url = id => `https://api.fontsource.org/v1/fonts/${id}`;
const variable_info_url = id => `https://api.fontsource.org/v1/variable/${id}`;
const font_page_url = id => `https://fontsource.org/fonts/${id}`;
function chars_in_range(v) {
if(!v) {
return '';
}
const ranges = v.split(',');
return ranges.map(r => {
let [_,start,end] = r.match(/U\+([A-Z0-9]+)(?:-([A-Z0-9]+))?/i);
start = parseInt(start,16);
end = parseInt(end,16) || start;
let c = '';
for(let i = start; i<=end; i++) {
c += String.fromCharCode(i);
}
return c;
}).join(' ');
}
function element(name, attr, content) {
const el = document.createElement(name);
if(attr) {
Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
}
if(content !== undefined) {
el.innerHTML = content;
}
return el;
}
const css_template = (info,range,weight,style,variable_info) => {
const axes = Object.keys(variable_info.axes || {}).filter(a=>a!='ital');
function get_axis() {
if (axes.length === 1 && axes[0]=='wght') {
return 'wght';
}
if (axes.length === 2 && axes.includes('wght')) {
const selected =
axes.find((axis) => axis !== 'wght')?.toLowerCase() ?? 'wght';
return selected;
}
const isStandard = axes.every((axis) =>
['wght', 'wdth', 'slnt', 'opsz'].includes(axis),
);
return isStandard ? 'standard' : 'full';
}
const axis = get_axis();
let urls;
if(info.variable) {
urls = [`url(https://cdn.jsdelivr.net/fontsource/fonts/${info.id}:vf@latest/${range}-${axis}-${style}.woff2) format('woff2-variations')`];
} else {
const variant = info.variants[weight][style][range];
urls = Object.entries(variant.url).map(([format,url]) => {
return `url(${url}) format('${format}')`;
});
}
const extras = [];
if(info.variable) {
if(axes.includes('wght')) {
extras.push(` font-weight: ${variable_info.axes.wght.min} ${variable_info.axes.wght.max};`);
}
if(axes.includes('wdth')) {
extras.push(` font-stretch: ${variable_info.axes.wdth.min} ${variable_info.axes.wdth.max};`);
}
if(axes.includes('slnt')) {
extras.push(` font-style: oblique ${variable_info.axes.slnt.min}deg ${variable_info.axes.slnt.max}deg;`);
}
}
if(info.variable) {
return `
/* ${info.family} variable ${style} ${range} */
@font-face ${'{'}
font-family: '${info.family}';
font-style: ${style};
font-display: swap;
${extras.join('\n')}
src: ${urls.join(', ')};
unicode-range: ${info.unicodeRange[range]};
${'}'}
`.trim();
} else {
return `
/* ${info.family} ${weight} ${style} ${range} */
@font-face ${'{'}
font-family: '${info.family}';
font-style: ${style};
font-weight: ${weight};
font-display: swap;
src: ${urls.join(', ')};
unicode-range: ${info.unicodeRange[range]};
${'}'}
`.trim();
}
}
async function fetch_json(url) {
return await (await fetch(url)).json();
}
async function go() {
let [fonts, axes_info] = await Promise.all([
fetch_json('https://api.fontsource.org/v1/fonts'),
fetch_json('https://api.fontsource.org/v1/axis-registry')
]);
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);
document.body.style.setProperty('--family',info.family);
function show_list(id, list, style_fn, text_fn) {
text_fn = text_fn || (v => v);
const ul = document.getElementById(id);
ul.innerHTML = '';
for(let v of list) {
const li = document.createElement('li');
ul.append(li);
li.innerHTML = text_fn(v);
style_fn(li.style, v);
}
}
show_list('styles',info.styles, (style,v) => { style['font-style'] = v });
show_list('weights',info.weights, (style,v) => { style['font-weight'] = v; style['font-variation-settings'] = `"wght" ${v}`; });
show_list('unicode-ranges',info.subsets, () => {}, v => `${v}: <p class="char-range">${chars_in_range(info.unicodeRange[v])}</p>`);
document.getElementById('category').textContent = info.category;
document.getElementById('variable').classList.toggle('hidden',!info.variable);
if(info.variable) {
const variable_axes = document.getElementById('variable-axes');
variable_axes.innerHTML = '';
console.log(variable_info);
const ranges = {};
Object.entries(variable_info.axes).filter(([axis,d]) => !['opsz','ital'].includes(axis.toLowerCase())).forEach(([axis,d]) => {
const axis_info = axes_info[axis.toLowerCase()];
const li = document.createElement('li');
const id = `axis-${axis}`;
variable_axes.append(li);
const label = element('label',{for: id}, axis_info.name);
li.append(label);
const range = element(
'input',
{
type: 'range',
id: id,
min: d.min,
max: d.max,
step: d.step,
value: d.default,
list: `axis-list-${axis}`
}
);
li.append(range);
ranges[axis] = range;
const datalist = element('datalist',{id: `axis-list-${axis}`})
li.append(datalist);
const default_option = element('option',{value:d.default});
datalist.append(default_option);
const output = element(
'output',
{
for: id
},
d.default
);
li.append(output);
const desc = element('p',{class:'help-text'},axis_info.description);
li.append(desc);
range.addEventListener('input', e => {
output.textContent = range.value;
update_variables();
});
})
function update_variables() {
const settings = Object.entries(ranges).map(([axis,range]) => `"${axis}" ${range.value}`).join(', ');
document.body.style['font-variation-settings'] = settings;
}
update_variables();
}
const css_declarations = [];
if(info.variable) {
for(let style of info.styles) {
for(let range of info.subsets) {
const css = css_template(info, range, '', style, variable_info);
css_declarations.push(css);
}
}
} else {
for(let style of info.styles) {
for(let weight of info.weights) {
for(let range of info.subsets) {
const css = css_template(info, range, weight, style, variable_info);
css_declarations.push(css);
}
}
}
}
const css = css_declarations.join('\n\n');
document.getElementById('font-style').textContent = css;
document.getElementById('css-display').textContent = css;
link.href = font_page_url(info.id);
link.textContent = info.family;
}
fonts = fonts.filter(f=>!f.id.includes('noto'));
select.innerHTML = '';
for(let font of fonts) {
const option = document.createElement('option');
select.append(option);
option.textContent = font.family;
option.value = font.id;
}
select.addEventListener('change', use_font);
function random_font() {
const font = fonts[Math.floor(Math.random()*fonts.length)];
select.value = font.id;
use_font();
}
document.getElementById('random-font').addEventListener('click', random_font);
random_font();
}
go();

32
style.css Normal file
View file

@ -0,0 +1,32 @@
:root {
--spacing: 1em;
--family: sans-serif;
color-scheme: light dark;
}
body {
font-family: var(--family);
}
.fixed-font {
font-family: sans-serif;
}
dt {
font-weight: bold;
text-decoration: underline;
}
.hidden {
display: none;
}
.help-text {
font-size: small;
}
.char-range {
max-width: 100%;
max-height: 3em;
overflow: auto;
}