beta3's picture
Update index.html
88319df verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ASCII Font Explorer Β· beta3</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✦</text></svg>" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet" />
<style>
/* ═══ RESET & TOKENS ══════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #07070f;
--s0: #0c0c18;
--s1: #111120;
--s2: #181828;
--s3: #202035;
--bd: rgba(255,255,255,.065);
--bd-hi: rgba(255,255,255,.14);
--txt: #eaeaf5;
--m1: #55556e;
--m2: #7a7a98;
--v: #7c3aed;
--c: #06b6d4;
--a: #f59e0b;
--pk: #ec4899;
--gv: linear-gradient(135deg,#7c3aed,#6366f1);
--gc: linear-gradient(135deg,#0891b2,#06b6d4);
--ga: linear-gradient(135deg,#d97706,#f59e0b);
--gm: linear-gradient(135deg,#7c3aed,#06b6d4);
--galt:linear-gradient(135deg,#ec4899,#f59e0b);
--glow-v: rgba(124,58,237,.22);
--glow-c: rgba(6,182,212,.22);
--r: 14px;
--rsm: 8px;
--rxs: 5px;
--mono: 'JetBrains Mono','Fira Code','Cascadia Code',monospace;
--sans: 'Inter',system-ui,sans-serif;
}
html { scroll-behavior: smooth; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--txt);
min-height: 100dvh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
/* ═══ NOISE GRAIN ═════════════════════════════════════════════════ */
body::before {
content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.025'/%3E%3C/svg%3E");
opacity: .6;
}
/* ═══ BACKGROUND ORBS ════════════════════════════════════════════ */
.bg-canvas { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.orb { position: absolute; border-radius: 50%; filter: blur(110px); animation: orb-drift 22s ease-in-out infinite; }
.oa { width:700px;height:700px;background:#7c3aed;opacity:.065;top:-280px;left:-220px;animation-delay:0s; }
.ob { width:600px;height:600px;background:#06b6d4;opacity:.055;bottom:-240px;right:-180px;animation-delay:-8s; }
.oc { width:420px;height:420px;background:#f59e0b;opacity:.04;top:42%;left:38%;animation-delay:-15s; }
.od { width:320px;height:320px;background:#ec4899;opacity:.04;top:12%;right:16%;animation-delay:-5s; }
@keyframes orb-drift {
0%,100%{transform:translate(0,0)scale(1)}
25%{transform:translate(28px,-32px)scale(1.06)}
50%{transform:translate(-18px,24px)scale(0.94)}
75%{transform:translate(38px,10px)scale(1.03)}
}
/* ═══ LAYOUT ══════════════════════════════════════════════════════ */
.wrap {
position: relative; z-index: 1;
max-width: 1480px; margin: 0 auto; padding: 0 28px 80px;
}
/* ═══ KEYFRAMES ═══════════════════════════════════════════════════ */
@keyframes sd { from{opacity:0;transform:translateY(-18px)} to{opacity:1;transform:none} }
@keyframes su { from{opacity:0;transform:translateY(18px)} to{opacity:1;transform:none} }
@keyframes fi { from{opacity:0} to{opacity:1} }
@keyframes cpop{ from{opacity:0;transform:scale(.6)} to{opacity:1;transform:scale(1)} }
@keyframes shim{ from{background-position:200% 0} to{background-position:-200% 0} }
@keyframes spin{ to{transform:rotate(360deg)} }
@keyframes pulse-d{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.65)}}
/* ═══ LOADER ══════════════════════════════════════════════════════ */
#loader {
position: fixed; inset: 0; z-index: 300; background: var(--bg);
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px;
transition: opacity .4s;
}
#loader.out { opacity: 0; pointer-events: none; }
.l-ring {
width: 44px; height: 44px; border-radius: 50%;
border: 3px solid var(--bd); border-top-color: var(--v);
animation: spin .65s linear infinite;
}
.l-msg { font-size: 14px; color: var(--m2); }
.l-sub { font-size: 12px; color: var(--m1); font-family: var(--mono); }
/* ═══ HEADER ══════════════════════════════════════════════════════ */
header { text-align: center; padding: 68px 0 52px; }
.pill {
display: inline-flex; align-items: center; gap: 8px;
background: rgba(124,58,237,.11); border: 1px solid rgba(124,58,237,.28);
border-radius: 100px; padding: 5px 14px 5px 10px;
font-size: 12px; font-weight: 500; color: #a78bfa; letter-spacing: .04em;
margin-bottom: 26px;
animation: sd .6s cubic-bezier(.16,1,.3,1) both;
}
.pill-dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--v); box-shadow: 0 0 8px var(--v);
animation: pulse-d 2.2s ease infinite;
}
h1 {
font-size: clamp(2.6rem,6.5vw,4.8rem); font-weight: 800;
letter-spacing: -.035em; line-height: 1.06;
animation: sd .6s .07s cubic-bezier(.16,1,.3,1) both;
}
.gt {
background: var(--gm); -webkit-background-clip: text;
-webkit-text-fill-color: transparent; background-clip: text;
}
.subtitle {
margin-top: 16px; color: var(--m2); font-size: 1.05rem;
line-height: 1.65; animation: sd .6s .14s cubic-bezier(.16,1,.3,1) both;
}
/* Stat strip */
.stat-strip {
display: inline-flex; margin-top: 36px;
border: 1px solid var(--bd); border-radius: var(--r); overflow: hidden;
animation: sd .6s .2s cubic-bezier(.16,1,.3,1) both;
}
.si {
padding: 13px 28px; text-align: center; background: var(--s0);
border-right: 1px solid var(--bd);
}
.si:last-child { border-right: none; }
.si-n {
font-size: 1.5rem; font-weight: 800;
background: var(--gm); -webkit-background-clip: text;
-webkit-text-fill-color: transparent; background-clip: text;
}
.si-l { font-size: 11px; color: var(--m1); text-transform: uppercase; letter-spacing: .09em; margin-top: 2px; }
/* ═══ CARD ════════════════════════════════════════════════════════ */
.card { background: var(--s0); border: 1px solid var(--bd); border-radius: var(--r); }
.card-body { padding: 22px 26px; }
/* ═══ SECTION HEAD ════════════════════════════════════════════════ */
.sh { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.sh-label { font-size: 11px; font-weight: 600; color: var(--m1); text-transform: uppercase; letter-spacing: .1em; white-space: nowrap; }
.sh-line { flex: 1; height: 1px; background: var(--bd); }
.badge {
font-size: 11px; font-weight: 700; padding: 3px 9px;
border-radius: 100px; white-space: nowrap;
}
.bv { background: rgba(124,58,237,.17); color: #c4b5fd; border: 1px solid rgba(124,58,237,.3); }
.bc { background: rgba(6,182,212,.13); color: #67e8f9; border: 1px solid rgba(6,182,212,.24); }
.ba { background: rgba(245,158,11,.13); color: #fcd34d; border: 1px solid rgba(245,158,11,.25); }
/* ═══ FONT SECTION ════════════════════════════════════════════════ */
.font-section { margin-bottom: 22px; animation: su .6s .28s cubic-bezier(.16,1,.3,1) both; }
/* Controls */
.ctrl-row { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.srch-wrap { position: relative; flex: 1; min-width: 180px; }
.srch-ico { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--m1); pointer-events: none; }
.srch-in {
width: 100%; padding: 9px 12px 9px 33px;
background: var(--s1); border: 1px solid var(--bd); border-radius: var(--rsm);
font-family: var(--sans); font-size: 13px; color: var(--txt); outline: none;
transition: border-color .18s, box-shadow .18s;
}
.srch-in:focus { border-color: var(--c); box-shadow: 0 0 0 3px var(--glow-c); }
.srch-in::placeholder { color: var(--m1); }
.btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 9px 14px; border-radius: var(--rsm);
font-family: var(--sans); font-size: 13px; font-weight: 500;
cursor: pointer; border: 1px solid var(--bd); transition: all .14s;
white-space: nowrap; color: var(--txt); background: var(--s1);
}
.btn:hover { border-color: var(--bd-hi); background: var(--s2); }
.btn-grad { background: var(--gm); border-color: transparent; color: #fff; font-weight: 600; }
.btn-grad:hover { opacity: .84; transform: translateY(-1px); }
.btn-dim { background: transparent; color: var(--m1); }
.btn-dim:hover { border-color: rgba(239,68,68,.3); color: #fca5a5; }
/* Chips */
.chips-row { display: flex; flex-wrap: wrap; gap: 7px; min-height: 30px; margin-bottom: 14px; align-items: center; }
.chip-hint { font-size: 12px; color: var(--m1); font-style: italic; }
.chip {
display: flex; align-items: center; gap: 5px;
border-radius: 100px; padding: 4px 10px 4px 12px;
font-size: 12px; font-family: var(--mono);
animation: cpop .22s cubic-bezier(.34,1.56,.64,1) both;
}
.chip-font { background: rgba(124,58,237,.12); border: 1px solid rgba(124,58,237,.3); color: #c4b5fd; }
.chip-x {
background: none; border: none; cursor: pointer; opacity: .5; font-size: 15px;
line-height: 1; padding: 0; transition: opacity .12s; color: inherit;
}
.chip-x:hover { opacity: 1; }
/* Font grid */
#font-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(144px, 1fr));
gap: 7px; max-height: 290px; overflow-y: auto; padding-right: 4px;
scrollbar-width: thin; scrollbar-color: var(--bd) transparent;
}
#font-grid::-webkit-scrollbar { width: 4px; }
#font-grid::-webkit-scrollbar-thumb { background: var(--bd); border-radius: 10px; }
.fcard {
position: relative; padding: 10px 13px; border-radius: var(--rsm);
border: 1px solid var(--bd); background: var(--s1);
cursor: pointer; user-select: none; overflow: hidden;
transition: border-color .13s, box-shadow .13s, transform .13s;
}
.fcard::after {
content: ''; position: absolute; inset: 0;
background: var(--gm); opacity: 0; transition: opacity .13s;
}
.fcard:hover:not(.fd) { border-color: var(--bd-hi); transform: translateY(-1px); }
.fcard:hover:not(.fd)::after { opacity: .055; }
.fcard.fs {
border-color: var(--v);
box-shadow: 0 0 0 1px var(--v), 0 4px 16px var(--glow-v);
}
.fcard.fs::after { opacity: .09; }
.fcard.fd { opacity: .28; cursor: not-allowed; pointer-events: none; }
.fcard-n {
position: relative; z-index: 1;
font-family: var(--mono); font-size: 11.5px; color: var(--txt); word-break: break-all; line-height: 1.4;
}
.fcard.fs .fcard-n { color: #c4b5fd; }
.fcard-i { position: relative; z-index: 1; font-size: 10px; color: var(--m1); margin-top: 3px; opacity: .5; }
.skel {
background: linear-gradient(90deg,var(--s1) 25%,var(--s2) 50%,var(--s1) 75%);
background-size: 200% 100%; animation: shim 1.4s ease infinite;
border-color: transparent !important; cursor: default; pointer-events: none;
}
/* ═══ LETTER SECTION ══════════════════════════════════════════════ */
.letter-section { margin-bottom: 22px; animation: su .6s .36s cubic-bezier(.16,1,.3,1) both; }
.alpha-grid {
display: flex; flex-wrap: wrap; gap: 6px;
}
.lkey {
width: 38px; height: 38px; border-radius: var(--rsm);
display: flex; align-items: center; justify-content: center;
font-family: var(--mono); font-size: 14px; font-weight: 600;
border: 1px solid var(--bd); background: var(--s1);
cursor: pointer; user-select: none;
transition: border-color .13s, background .13s, box-shadow .13s, transform .13s;
color: var(--m2);
}
.lkey:hover:not(.ld) { border-color: var(--bd-hi); background: var(--s2); color: var(--txt); transform: translateY(-1px); }
.lkey.ls {
border-color: var(--c);
background: rgba(6,182,212,.12);
color: #67e8f9;
box-shadow: 0 0 0 1px var(--c), 0 4px 14px var(--glow-c);
}
.lkey.ld { opacity: .28; cursor: not-allowed; }
/* ═══ ERROR BOX ═══════════════════════════════════════════════════ */
.err-box {
background: rgba(239,68,68,.09); border: 1px solid rgba(239,68,68,.3);
border-radius: var(--rsm); padding: 11px 15px; font-size: 13px;
color: #fca5a5; margin-bottom: 14px; display: none;
}
/* ═══ MATRIX PREVIEW ══════════════════════════════════════════════ */
#matrix-wrap { margin-top: 28px; animation: su .4s ease both; }
/*
CSS Grid matrix: rows = letters (max 2), cols = fonts (max 3).
*/
#matrix-grid {
display: grid;
gap: 16px;
/* columns injected by JS */
}
.row-label {
display: flex; flex-direction: column; justify-content: center; align-items: center;
gap: 4px; padding: 0 4px;
}
.row-letter {
width: 36px; height: 36px; border-radius: var(--rsm);
display: flex; align-items: center; justify-content: center;
font-family: var(--mono); font-size: 15px; font-weight: 700;
border: 1px solid var(--c); color: #67e8f9;
background: rgba(6,182,212,.1);
}
/* Each cell in the matrix */
.mcell {
background: var(--s0); border: 1px solid var(--bd); border-radius: var(--r);
overflow: hidden; transition: border-color .18s, box-shadow .18s;
min-width: 0;
}
.mcell:hover { border-color: var(--bd-hi); box-shadow: 0 6px 24px rgba(0,0,0,.3); }
.mcell.col-0 .cell-pre { background: linear-gradient(100deg,#c4b5fd,#93c5fd); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.mcell.col-1 .cell-pre { background: linear-gradient(100deg,#67e8f9,#6ee7b7); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.mcell.col-2 .cell-pre { background: linear-gradient(100deg,#fcd34d,#fb923c); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.cell-head {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 14px; background: var(--s1); border-bottom: 1px solid var(--bd);
}
.cell-font-name {
font-family: var(--mono); font-size: 12px; font-weight: 500; color: var(--m2);
}
.mcell.col-0 .cell-font-name { background: var(--gv); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.mcell.col-1 .cell-font-name { background: var(--gc); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.mcell.col-2 .cell-font-name { background: var(--ga); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.cell-lbl {
font-size: 11px; font-weight: 600; font-family: var(--mono);
padding: 2px 7px; border-radius: 4px;
}
.mcell.col-0 .cell-lbl { background: rgba(124,58,237,.18); color: #c4b5fd; }
.mcell.col-1 .cell-lbl { background: rgba(6,182,212,.15); color: #67e8f9; }
.mcell.col-2 .cell-lbl { background: rgba(245,158,11,.15); color: #fcd34d; }
.cell-acts { display: flex; gap: 5px; align-items: center; }
.ib {
width: 26px; height: 26px; border-radius: var(--rxs);
background: rgba(255,255,255,.04); border: 1px solid var(--bd);
color: var(--m1); cursor: pointer; display: flex; align-items: center;
justify-content: center; transition: all .12s; position: relative; flex-shrink: 0;
}
.ib:hover { background: rgba(255,255,255,.09); color: var(--txt); }
.ib.ok { border-color: #10b981; color: #10b981; }
.tip {
position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
background: var(--s3); border: 1px solid var(--bd);
border-radius: 5px; padding: 3px 7px; font-size: 10px; color: var(--txt);
white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity .12s; z-index: 20;
}
.ib:hover .tip { opacity: 1; }
.cell-body {
padding: 18px 16px; overflow-x: auto; min-height: 88px;
display: flex; align-items: flex-start;
scrollbar-width: thin; scrollbar-color: var(--bd) transparent;
}
.cell-body::-webkit-scrollbar { height: 4px; }
.cell-body::-webkit-scrollbar-thumb { background: var(--bd); border-radius: 10px; }
.cell-pre {
font-family: var(--mono); font-size: 12px; line-height: 1.25;
white-space: pre; margin: 0; animation: fi .3s ease;
}
.cell-spin {
display: flex; align-items: center; gap: 8px;
color: var(--m1); font-size: 12px; width: 100%;
}
.spin-sm {
width: 13px; height: 13px; border-radius: 50%;
border: 2px solid var(--bd); border-top-color: var(--v);
animation: spin .55s linear infinite; flex-shrink: 0;
}
/* Column header (font name) above each column */
.col-header {
text-align: center; padding-bottom: 2px;
}
.col-header-name {
font-family: var(--mono); font-size: 12px; color: var(--m2); font-weight: 500;
}
/* Platform links in stat strip */
.si-links { padding: 12px 22px; }
.platform-link {
display: inline-flex; align-items: center; gap: 5px;
font-size: 12px; font-weight: 600; text-decoration: none;
padding: 5px 10px; border-radius: var(--rxs);
border: 1px solid; transition: all .15s;
line-height: 1;
}
.hf-link {
color: #f0c060;
border-color: rgba(240,192,96,.3);
background: rgba(240,192,96,.08);
}
.hf-link:hover {
background: rgba(240,192,96,.16);
border-color: rgba(240,192,96,.55);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(240,192,96,.15);
}
.kg-link {
color: #5ac8fa;
border-color: rgba(90,200,250,.3);
background: rgba(90,200,250,.08);
}
.kg-link:hover {
background: rgba(90,200,250,.16);
border-color: rgba(90,200,250,.55);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(90,200,250,.15);
}
/* ═══ FOOTER ══════════════════════════════════════════════════════ */
footer {
text-align: center; padding: 30px 0 0; margin-top: 58px;
border-top: 1px solid var(--bd); color: var(--m1); font-size: 12px; line-height: 1.9;
}
footer a { color: #a78bfa; text-decoration: none; }
footer a:hover { text-decoration: underline; }
/* ═══ RESPONSIVE ══════════════════════════════════════════════════ */
@media(max-width:640px) {
.wrap { padding: 0 14px 48px; }
header { padding: 44px 0 34px; }
.stat-strip { flex-direction: column; width: 100%; }
.si { border-right: none; border-bottom: 1px solid var(--bd); }
.si:last-child { border-bottom: none; }
#font-grid { grid-template-columns: repeat(auto-fill,minmax(120px,1fr)); }
.alpha-grid .lkey { width: 33px; height: 33px; font-size: 12px; }
}
</style>
</head>
<body>
<!-- Loader -->
<div id="loader">
<div class="l-ring"></div>
<div class="l-msg">Loading font index…</div>
<div class="l-sub" id="l-sub">connecting to Hugging Face</div>
</div>
<!-- BG orbs -->
<div class="bg-canvas">
<div class="orb oa"></div><div class="orb ob"></div>
<div class="orb oc"></div><div class="orb od"></div>
</div>
<div class="wrap">
<!-- ═══ HEADER ═══════════════════════════════════════════════════ -->
<header>
<div class="pill"><div class="pill-dot"></div>beta3 Β· ASCII Alphabet Dataset</div>
<h1>Explore <span class="gt">571 Fonts</span><br>side by side</h1>
<p class="subtitle">
Pick up to 3 fonts Β· Choose up to 3 letters<br>
Compare how each font renders them in a live matrix
</p>
<div class="stat-strip">
<div class="si"><div class="si-n" id="sn-f">571</div><div class="si-l">Fonts</div></div>
<div class="si"><div class="si-n">26</div><div class="si-l">Letters A–Z</div></div>
<div class="si si-links">
<div class="si-l" style="margin-bottom:8px;font-size:10px;">Available on</div>
<div style="display:flex;gap:8px;justify-content:center;align-items:center;">
<a href="https://huggingface.co/datasets/beta3/ASCII_Alphabet_Dataset_571_Fonts" target="_blank" rel="noopener" class="platform-link hf-link">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.514 2 12 6.486 2 12 2zm-1 5v2H9v2h2v6h2v-6h2V9h-2V7h-2z"/></svg>
Hugging Face
</a>
<span style="color:var(--bd-hi);font-size:11px;">Β·</span>
<a href="https://www.kaggle.com/datasets/beta3logic/ascii-alphabet-dataset-571-fonts" target="_blank" rel="noopener" class="platform-link kg-link">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M18.825 23.859c-.022.092-.117.141-.281.141h-3.139c-.187 0-.351-.082-.492-.248l-5.178-6.589-1.448 1.374v5.111c0 .235-.117.352-.351.352H5.505c-.236 0-.354-.117-.354-.352V.353c0-.233.118-.353.354-.353h2.431c.234 0 .351.12.351.353v14.343l6.203-6.272c.165-.165.33-.246.495-.246h3.239c.144 0 .236.06.285.18.046.149.034.254-.036.315l-6.555 6.344 6.836 8.507c.095.104.117.208.07.311z"/></svg>
Kaggle
</a>
</div>
</div>
</div>
</header>
<!-- ═══ FONT SELECTOR ════════════════════════════════════════════ -->
<div class="font-section card">
<div class="card-body">
<div class="sh">
<span class="sh-label">1 Β· Select fonts</span>
<div class="sh-line"></div>
<span class="badge bv" id="sel-badge">0 / 3 selected</span>
<span class="badge bc" id="tot-badge">571 available</span>
</div>
<div class="err-box" id="err-box"></div>
<div class="ctrl-row">
<div class="srch-wrap">
<svg class="srch-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
<input id="srch" class="srch-in" type="text" placeholder="Search 571 fonts…" autocomplete="off" />
</div>
<button class="btn btn-grad" id="btn-random">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/>
<polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/>
</svg>
Random
</button>
<button class="btn btn-dim" id="btn-clear-fonts">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/>
</svg>
Clear
</button>
</div>
<!-- Font chips -->
<div class="chips-row" id="font-chips">
<span class="chip-hint" id="font-chips-hint">← select fonts from the grid below</span>
</div>
<!-- Font grid -->
<div id="font-grid"></div>
</div>
</div>
<!-- ═══ LETTER SELECTOR ══════════════════════════════════════════ -->
<div class="letter-section card" style="margin-top:18px;">
<div class="card-body">
<div class="sh">
<span class="sh-label">2 Β· Select letters</span>
<div class="sh-line"></div>
<span class="badge ba" id="let-badge">0 / 3 selected</span>
</div>
<div class="alpha-grid" id="alpha-grid"></div>
<!-- Letter chips -->
<div class="chips-row" id="let-chips" style="margin-top:14px;margin-bottom:0;">
<span class="chip-hint" id="let-chips-hint">← pick up to 3 letters</span>
</div>
</div>
</div>
<!-- ═══ MATRIX PREVIEW ══════════════════════════════════════════ -->
<div id="matrix-wrap" style="display:none;">
<div class="sh" style="margin-bottom:14px;">
<span class="sh-label">Live matrix</span>
<div class="sh-line"></div>
<span class="badge bc" id="matrix-dims">β€”</span>
</div>
<div id="matrix-grid"></div>
</div>
<!-- ═══ FOOTER ══════════════════════════════════════════════════ -->
<footer>
<p>
Dataset β†’
<a href="https://huggingface.co/datasets/beta3/ASCII_Alphabet_Dataset_571_Fonts" target="_blank" rel="noopener">
beta3/ASCII_Alphabet_Dataset_571_Fonts
</a>
&nbsp;Β·&nbsp; 571 PyFiglet fonts &nbsp;Β·&nbsp; Letters A–Z uppercase
</p>
<p style="opacity:.5;margin-top:3px;">Static Hugging Face Space Β· fully client-side Β· no backend</p>
</footer>
</div><!-- /wrap -->
<script>
/* ================================================================
CONFIG
================================================================ */
const OWNER = 'beta3';
const DATASET = 'ASCII_Alphabet_Dataset_571_Fonts';
const BRANCH = 'main';
const RAW = `https://huggingface.co/datasets/${OWNER}/${DATASET}/resolve/${BRANCH}`;
const API_DIR = `https://huggingface.co/api/datasets/${OWNER}/${DATASET}/tree/${BRANCH}/uppercase`;
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
/* ================================================================
UTILS
================================================================ */
const toFile = ch =>
`letter_${String(ch.charCodeAt(0) - 64).padStart(2,'0')}.txt`;
const esc = s =>
s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const IC_COPY = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
const IC_CHECK = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>`;
const IC_X = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
function pickRandom(arr, n) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a.slice(0, n);
}
/* ================================================================
STATE
================================================================ */
let allFonts = [];
let filteredFonts = [];
let selFonts = [];
let selLetters = [];
let renderTimer = null;
const cache = {};
/* ================================================================
DOM REFS
================================================================ */
const $loader = document.getElementById('loader');
const $lSub = document.getElementById('l-sub');
const $srch = document.getElementById('srch');
const $fontGrid = document.getElementById('font-grid');
const $fontChips = document.getElementById('font-chips');
const $fontHint = document.getElementById('font-chips-hint');
const $letChips = document.getElementById('let-chips');
const $letHint = document.getElementById('let-chips-hint');
const $alphaGrid = document.getElementById('alpha-grid');
const $selBadge = document.getElementById('sel-badge');
const $totBadge = document.getElementById('tot-badge');
const $letBadge = document.getElementById('let-badge');
const $errBox = document.getElementById('err-box');
const $matWrap = document.getElementById('matrix-wrap');
const $matGrid = document.getElementById('matrix-grid');
const $matDims = document.getElementById('matrix-dims');
const $snF = document.getElementById('sn-f');
/* ================================================================
FONT LOADING (paginated HF Tree API)
================================================================ */
async function loadFonts() {
const fonts = [];
let url = API_DIR + '?type=directory';
while (url) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HF API ${res.status} ${res.statusText}`);
const items = await res.json();
items.forEach(it => {
if (it.type === 'directory') {
fonts.push(it.path.split('/').pop());
}
});
const link = res.headers.get('Link') || '';
const next = link.match(/<([^>]+)>;\s*rel="next"/);
url = next ? next[1] : null;
$lSub.textContent = `${fonts.length} fonts discovered…`;
}
return fonts.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
}
/* ================================================================
INIT
================================================================ */
async function init() {
renderSkeleton(24);
buildAlphaGrid();
try {
allFonts = await loadFonts();
} catch (err) {
showErr(`Could not load font list: ${err.message}`);
hideLoader();
return;
}
if (!allFonts.length) {
showErr('No font directories found. Dataset structure may have changed.');
hideLoader();
return;
}
filteredFonts = [...allFonts];
$snF.textContent = allFonts.length;
$totBadge.textContent = `${allFonts.length} available`;
renderFontGrid();
hideLoader();
// Default: 1 random font, letter A
const starter = pickRandom(allFonts, 1)[0];
toggleFont(starter);
toggleLetter('A');
}
function hideLoader() {
$loader.classList.add('out');
setTimeout(() => ($loader.style.display = 'none'), 420);
}
/* ================================================================
FONT GRID
================================================================ */
function renderSkeleton(n) {
$fontGrid.innerHTML = '';
for (let i = 0; i < n; i++) {
const d = document.createElement('div');
d.className = 'fcard skel'; d.style.height = '52px';
$fontGrid.appendChild(d);
}
}
function renderFontGrid() {
$fontGrid.innerHTML = '';
if (!filteredFonts.length) {
$fontGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:var(--m1);padding:24px 0;font-size:13px;">No fonts match your search.</div>';
return;
}
const frag = document.createDocumentFragment();
filteredFonts.forEach(font => {
const isSel = selFonts.includes(font);
const isDis = !isSel && selFonts.length >= 3;
const el = document.createElement('div');
el.className = `fcard${isSel ? ' fs' : ''}${isDis ? ' fd' : ''}`;
el.dataset.font = font;
el.innerHTML = `<div class="fcard-n">${font}</div><div class="fcard-i">#${allFonts.indexOf(font)+1}</div>`;
el.addEventListener('click', () => toggleFont(font));
frag.appendChild(el);
});
$fontGrid.appendChild(frag);
}
function patchFontGrid() {
$fontGrid.querySelectorAll('.fcard').forEach(el => {
const font = el.dataset.font;
if (!font) return;
const isSel = selFonts.includes(font);
const isDis = !isSel && selFonts.length >= 3;
el.className = `fcard${isSel ? ' fs' : ''}${isDis ? ' fd' : ''}`;
});
}
/* ================================================================
FONT TOGGLE
================================================================ */
function toggleFont(font) {
if (selFonts.includes(font)) {
selFonts = selFonts.filter(f => f !== font);
} else {
if (selFonts.length >= 3) return;
selFonts.push(font);
}
patchFontGrid();
renderFontChips();
$selBadge.textContent = `${selFonts.length} / 3 selected`;
scheduleRender();
}
function renderFontChips() {
[...$fontChips.children].forEach(el => { if (el !== $fontHint) el.remove(); });
$fontHint.style.display = selFonts.length ? 'none' : '';
selFonts.forEach(font => {
const chip = document.createElement('div');
chip.className = 'chip chip-font';
chip.innerHTML = `<span>${font}</span><button class="chip-x">Γ—</button>`;
chip.querySelector('.chip-x').addEventListener('click', () => toggleFont(font));
$fontChips.appendChild(chip);
});
}
/* ================================================================
ALPHABET GRID
================================================================ */
function buildAlphaGrid() {
$alphaGrid.innerHTML = '';
ALPHABET.forEach(letter => {
const btn = document.createElement('button');
btn.className = 'lkey';
btn.dataset.letter = letter;
btn.textContent = letter;
btn.addEventListener('click', () => toggleLetter(letter));
$alphaGrid.appendChild(btn);
});
}
function patchAlphaGrid() {
$alphaGrid.querySelectorAll('.lkey').forEach(btn => {
const l = btn.dataset.letter;
const isSel = selLetters.includes(l);
const isDis = !isSel && selLetters.length >= 3;
btn.className = `lkey${isSel ? ' ls' : ''}${isDis ? ' ld' : ''}`;
});
}
function toggleLetter(letter) {
if (selLetters.includes(letter)) {
selLetters = selLetters.filter(l => l !== letter);
} else {
if (selLetters.length >= 3) return;
selLetters.push(letter);
}
patchAlphaGrid();
renderLetterChips();
$letBadge.textContent = `${selLetters.length} / 3 selected`;
scheduleRender();
}
function renderLetterChips() {
[...$letChips.children].forEach(el => { if (el !== $letHint) el.remove(); });
$letHint.style.display = selLetters.length ? 'none' : '';
selLetters.forEach(l => {
const chip = document.createElement('div');
chip.className = 'chip';
chip.style.cssText = 'background:rgba(6,182,212,.1);border:1px solid rgba(6,182,212,.3);color:#67e8f9;';
chip.innerHTML = `<span>${l}</span><button class="chip-x">Γ—</button>`;
chip.querySelector('.chip-x').addEventListener('click', () => toggleLetter(l));
$letChips.appendChild(chip);
});
}
/* ================================================================
LETTER FETCHER
================================================================ */
async function fetchLetter(font, ch) {
const key = `${font}/${toFile(ch)}`;
if (key in cache) return cache[key];
try {
const res = await fetch(`${RAW}/uppercase/${font}/${toFile(ch)}`);
cache[key] = res.ok ? await res.text() : null;
} catch {
cache[key] = null;
}
return cache[key];
}
/* ================================================================
MATRIX RENDER
================================================================ */
function scheduleRender() {
clearTimeout(renderTimer);
renderTimer = setTimeout(renderMatrix, 180);
}
async function renderMatrix() {
const fonts = selFonts;
const letters = selLetters;
if (!fonts.length || !letters.length) {
$matWrap.style.display = 'none';
return;
}
$matWrap.style.display = 'block';
$matDims.textContent = `${letters.length} row${letters.length>1?'s':''} Γ— ${fonts.length} col${fonts.length>1?'s':''}`;
/*
Grid layout (CSS grid):
- 1 column per font
- 1 row per letter
Each cell = (letter, font) pair showing the raw ASCII art
*/
$matGrid.style.gridTemplateColumns = `repeat(${fonts.length}, 1fr)`;
$matGrid.innerHTML = '';
const promises = [];
letters.forEach((letter, ri) => {
fonts.forEach((font, ci) => {
const cell = document.createElement('div');
cell.className = `mcell col-${ci}`;
const rowTag = ci === 0
? `<div class="cell-lbl" style="margin-right:4px;">${letter}</div>`
: '';
cell.innerHTML = `
<div class="cell-head">
<div style="display:flex;align-items:center;gap:6px;">
${rowTag}
<div class="cell-font-name">${font}</div>
</div>
<div class="cell-acts">
<button class="ib copy-btn">${IC_COPY}<span class="tip">Copy</span></button>
</div>
</div>
<div class="cell-body" id="cb-${ri}-${ci}">
<div class="cell-spin"><div class="spin-sm"></div>Loading…</div>
</div>
`;
$matGrid.appendChild(cell);
const body = cell.querySelector('.cell-body');
const copyBtn = cell.querySelector('.copy-btn');
const p = fetchLetter(font, letter).then(txt => {
const ascii = txt
? txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n')
: `(${letter} not found in ${font})`;
body.innerHTML = `<pre class="cell-pre">${esc(ascii)}</pre>`;
copyBtn.addEventListener('click', () => {
navigator.clipboard?.writeText(ascii).then(() => {
copyBtn.classList.add('ok');
copyBtn.innerHTML = IC_CHECK + '<span class="tip">Copied!</span>';
setTimeout(() => {
copyBtn.classList.remove('ok');
copyBtn.innerHTML = IC_COPY + '<span class="tip">Copy</span>';
}, 1800);
});
});
});
promises.push(p);
});
});
await Promise.all(promises);
}
/* ================================================================
EVENTS
================================================================ */
$srch.addEventListener('input', () => {
const q = $srch.value.toLowerCase().trim();
filteredFonts = q
? allFonts.filter(f => f.toLowerCase().includes(q))
: [...allFonts];
$totBadge.textContent = `${filteredFonts.length} available`;
renderFontGrid();
});
document.getElementById('btn-random').addEventListener('click', () => {
const avail = allFonts.filter(f => !selFonts.includes(f));
const slots = 3 - selFonts.length;
if (!slots || !avail.length) return;
pickRandom(avail, Math.min(slots, avail.length)).forEach(f => {
if (selFonts.length < 3) selFonts.push(f);
});
patchFontGrid();
renderFontChips();
$selBadge.textContent = `${selFonts.length} / 3 selected`;
scheduleRender();
});
document.getElementById('btn-clear-fonts').addEventListener('click', () => {
selFonts = [];
patchFontGrid();
renderFontChips();
$selBadge.textContent = '0 / 3 selected';
scheduleRender();
});
/* ================================================================
HELPERS
================================================================ */
function showErr(msg) {
$errBox.textContent = msg;
$errBox.style.display = 'block';
}
/* ================================================================
BOOT
================================================================ */
init();
</script>
</body>
</html>