Add app.js
This commit is contained in:
454
app.js
Normal file
454
app.js
Normal 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();
|
||||
Reference in New Issue
Block a user