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 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}:
${chars_in_range(info.unicodeRange[v])}
`); 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 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.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