Files
font-design/app.js
admin 81dc2c211d
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
deploy 1779568775
2026-05-23 20:39:35 +00:00

455 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const button = document.getElementById('cycleButton');
const fontName = document.getElementById('fontName');
const fontSize = document.getElementById('fontSize');
const fontWeight = document.getElementById('fontWeight');
const letterSpacing = document.getElementById('letterSpacing');
const fontSizeValue = document.getElementById('fontSizeValue');
const fontWeightValue = document.getElementById('fontWeightValue');
const letterSpacingValue = document.getElementById('letterSpacingValue');
const sizeGuide = document.getElementById('sizeGuide');
const weightGuide = document.getElementById('weightGuide');
const spacingGuide = document.getElementById('spacingGuide');
const debugFontSize = document.getElementById('debugFontSize');
const debugWeight = document.getElementById('debugWeight');
const debugLetterSpacing = document.getElementById('debugLetterSpacing');
const debugGuide = document.getElementById('debugGuide');
const debugFamily = document.getElementById('debugFamily');
const debugCategory = document.getElementById('debugCategory');
const editableFields = document.querySelectorAll('.editable');
const transformInputs = document.querySelectorAll('input[name="transform"]');
const loadedFonts = new Map();
let current = 0;
let activeEditable = document.querySelector('.editable');
const fonts = [
{ name: 'Satoshi', category: 'Sans', slug: 'satoshi' },
{ name: 'General Sans', category: 'Sans', slug: 'general-sans' },
{ name: 'Switzer', category: 'Sans', slug: 'switzer' },
{ name: 'Cabinet Grotesk', category: 'Sans', slug: 'cabinet-grotesk' },
{ name: 'Ranade', category: 'Sans', slug: 'ranade' },
{ name: 'Clash Display', category: 'Display', slug: 'clash-display' },
{ name: 'Stardom', category: 'Display', slug: 'stardom' },
{ name: 'Melodrama', category: 'Display', slug: 'melodrama' },
{ name: 'Panchang', category: 'Display', slug: 'panchang' },
{ name: 'Telma', category: 'Display', slug: 'telma' }
];
async function fetchAllFontshareFonts() {
try {
const response = await fetch('https://api.fontshare.com/v2/fonts');
if (!response.ok) {
return;
}
const data = await response.json();
if (!Array.isArray(data)) {
return;
}
data.forEach((font) => {
if (!font?.slug || !font?.family) {
return;
}
if (
fonts.some(
(entry) =>
entry.slug === font.slug
)
) {
return;
}
fonts.push({
name: font.family,
category:
font.styles?.[0]?.name || 'Sans',
slug: font.slug
});
});
} catch {
return;
}
}
function buildFontUrl(font) {
return `https://api.fontshare.com/v2/css?f[]=${font.slug}@300,400,500,600,700,800&display=swap`;
}
function injectFont(font) {
if (loadedFonts.has(font.slug)) {
return loadedFonts.get(font.slug);
}
const promise = new Promise((resolve, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = buildFontUrl(font);
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
loadedFonts.set(font.slug, promise);
return promise;
}
function updateLiveValues() {
fontSizeValue.textContent = `${fontSize.value}px`;
fontWeightValue.textContent = fontWeight.value;
letterSpacingValue.textContent = `${letterSpacing.value}px`;
debugFontSize.textContent = `${fontSize.value}px`;
debugWeight.textContent = fontWeight.value;
debugLetterSpacing.textContent = `${letterSpacing.value}px`;
}
function setGuide(node, min, max, rangeMin, rangeMax) {
const left =
((min - rangeMin) / (rangeMax - rangeMin)) * 100;
const width =
((max - min) / (rangeMax - rangeMin)) * 100;
node.style.left = `${left}%`;
node.style.width = `${width}%`;
}
function updateSmartGuides() {
const text =
activeEditable?.textContent?.trim() || '';
const size = Number(fontSize.value);
const weight = Number(fontWeight.value);
const spacing = Number(letterSpacing.value);
const activeFont = fonts[current];
const [sizeMin, sizeMax] =
SmartGuides.getOptimalSize(text.length);
const [weightMin, weightMax] =
SmartGuides.getOptimalWeight(
activeFont.category,
size
);
const [spacingMin, spacingMax] =
SmartGuides.getOptimalSpacing(
size,
weight
);
setGuide(sizeGuide, sizeMin, sizeMax, 8, 320);
setGuide(weightGuide, weightMin, weightMax, 100, 900);
setGuide(spacingGuide, spacingMin, spacingMax, -12, 40);
debugGuide.textContent =
SmartGuides.getGuideLabel(
size,
weight,
spacing
);
}
function getTransformValue() {
return (
document.querySelector(
'input[name="transform"]:checked'
)?.value || 'none'
);
}
function applyStyles() {
const size = `${fontSize.value}px`;
const weight = fontWeight.value;
const spacing = `${letterSpacing.value}px`;
const transform = getTransformValue();
editableFields.forEach((field) => {
field.style.fontSize = size;
field.style.fontWeight = weight;
field.style.letterSpacing = spacing;
field.style.textTransform = transform;
});
updateLiveValues();
updateSmartGuides();
}
async function applyFont(index) {
const font = fonts[index];
if (!font) {
return;
}
try {
await injectFont(font);
document.documentElement.style.setProperty(
'--active-font',
`"${font.name}",system-ui,sans-serif`
);
fontName.textContent =
`${font.name} · ${font.category}`;
debugFamily.textContent = font.name;
debugCategory.textContent =
font.category;
} catch {
document.documentElement.style.setProperty(
'--active-font',
'system-ui,sans-serif'
);
fontName.textContent =
`Fallback · System`;
}
updateSmartGuides();
}
async function cycleFont() {
current =
(current + 1) % fonts.length;
await applyFont(current);
}
function bindEditable(field) {
field.addEventListener('focus', () => {
activeEditable = field;
updateSmartGuides();
});
field.addEventListener('input', () => {
updateSmartGuides();
});
}
function bindEvents() {
button.addEventListener('click', cycleFont);
fontSize.addEventListener('input', applyStyles);
fontWeight.addEventListener('input', applyStyles);
letterSpacing.addEventListener('input', applyStyles);
transformInputs.forEach((input) => {
input.addEventListener('change', applyStyles);
});
editableFields.forEach(bindEditable);
}
async function init() {
bindEvents();
updateLiveValues();
applyStyles();
await fetchAllFontshareFonts();
await applyFont(current);
}
// v26
const fontUpload =
document.getElementById(
'fontUpload'
);
const customFonts =
new Map();
function sanitizeFontName(
value
) {
return value
.replace(/\.[^/.]+$/, '')
.replace(/[^a-z0-9]/gi, '-');
}
async function loadLocalFont(
file
) {
if (!file) {
return;
}
const extension =
file.name
.split('.')
.pop()
?.toLowerCase();
const supported =
['ttf','otf','woff','woff2'];
if (
!supported.includes(
extension
)
) {
return;
}
const family =
sanitizeFontName(
file.name
);
const objectUrl =
URL.createObjectURL(
file
);
try {
const fontFace =
new FontFace(
family,
`url(${objectUrl})`
);
await fontFace.load();
document.fonts.add(
fontFace
);
customFonts.set(
family,
objectUrl
);
document.documentElement.style.setProperty(
'--active-font',
`"${family}",system-ui,sans-serif`
);
fontName.textContent =
`${family} · Local`;
debugFamily.textContent =
family;
debugCategory.textContent =
'Local';
requestAnimationFrame(
() => {
applyStyles();
updateSmartGuides();
}
);
} catch {
document.documentElement.style.setProperty(
'--active-font',
'system-ui,sans-serif'
);
fontName.textContent =
'Fallback · System';
}
}
fontUpload.addEventListener(
'change',
async (event) => {
const file =
event.target.files?.[0];
await loadLocalFont(
file
);
event.target.value =
'';
}
);
const controlsPane =
document.querySelector(
'.controls-pane'
);
const controlsToggle =
document.getElementById(
'controlsToggle'
);
let minimized = false;
function syncControlsState() {
controlsPane.classList.toggle(
'minimized',
minimized
);
controlsToggle.textContent =
minimized
? ''
: '✕';
}
controlsToggle.addEventListener(
'click',
() => {
minimized = !minimized;
syncControlsState();
}
);
syncControlsState();
init();