357 lines
No EOL
11 KiB
JavaScript
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(); |