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