Spaces:
Running
Running
| <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> | |
| Β· 571 PyFiglet fonts Β· 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,'&').replace(/</g,'<').replace(/>/g,'>'); | |
| 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> |