455 lines
8.7 KiB
JavaScript
455 lines
8.7 KiB
JavaScript
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();
|