font-picker/script.js
Christian Lawson-Perfect 2737afdc15 Put the file links code back in.
I reloaded the editor and lost it!
2025-03-20 13:27:16 +00:00

357 lines
No EOL
11 KiB
JavaScript

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;
}
function variable_font_url(info,range,axis,style) {
return `https://cdn.jsdelivr.net/fontsource/fonts/${info.id}:vf@latest/${range}-${axis}-${style}.woff2`
}
function get_variable_axis(variable_info) {
const axes = Object.keys(variable_info.axes || {}).filter(a=>a!='ital');
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 css_template = (info,range,weight,style,variable_info) => {
const axes = Object.keys(variable_info.axes || {}).filter(a=>a!='ital');
const axis = get_variable_axis(variable_info);
let urls;
if(info.variable) {
urls = [`url(${variable_font_url(info,range,axis,style)}) 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 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([
fetch_json('https://api.fontsource.org/v1/fonts'),
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;
const info = await fetch_json(font_info_url(name));
const variable_info = info.variable ? await fetch_json(variable_info_url(info.id)) : {};
const preview = document.querySelector(`[data-font="${name}"]`);
if(preview) {
preview.scrollIntoView({block: 'center'});
document.body.scrollTop = 0;
}
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 = '';
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 file_list = document.getElementById('file-links');
file_list.innerHTML = '';
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);
const li = element('li',{}, `<a href="${variable_font_url(info,range,get_variable_axis(variable_info),style)}">${range} ${style}</a>`)
file_list.append(li);
}
}
} 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 variant = info.variants[weight][style][range];
const ul = element('ul');
file_list.append(ul);
Object.entries(variant.url).forEach(([format,url]) => {
const li = element('li',{}, `<a href="${url}">${weight} ${style} ${range} (${format})</a>`);
ul.append(li);
});
}
}
}
}
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.match(/noto|playwrite/i));
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();
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();