Add app.js

This commit is contained in:
admin
2026-05-23 18:46:49 +00:00
parent ff131c1999
commit 5c3f8dc7ca

454
app.js Normal file
View File

@@ -0,0 +1,454 @@
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();