Spaces:
Running
Running
Download index.html from gnumanth/scribefont: direct link, hf CLI and curl.
- Browser
- Download file 38.7 kB
-
https://huggingface.co/spaces/gnumanth/scribefont/resolve/main/index.html
- Command line
-
hf download hf://spaces/gnumanth/scribefont/index.html
-
curl -L -o index.html https://huggingface.co/spaces/gnumanth/scribefont/resolve/main/index.html
38.7 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>scribefont — Image-to-Font & Web Canvas Handwriting OpenType (.ttf) Studio</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;600;700&family=Playfair+Display:ital,wght@0,600;1,600&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/ort.min.js"></script> | |
| <style> | |
| :root { | |
| --canvas: #fbfaf7; | |
| --card: #ffffff; | |
| --ink: #141416; | |
| --muted: #52525b; | |
| --border: #18181b; | |
| --border-subtle: #e5e1d8; | |
| --sage: #dcfce7; | |
| --peach: #ffedd5; | |
| --butter: #fef9c3; | |
| --indigo: #635bff; | |
| --font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif; | |
| --font-serif: 'Playfair Display', Georgia, serif; | |
| --font-mono: 'Geist Mono', monospace; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { | |
| max-width: 100%; | |
| overflow-x: hidden; | |
| } | |
| body { | |
| background: var(--canvas); | |
| color: var(--ink); | |
| font-family: var(--font-sans); | |
| line-height: 1.55; | |
| padding-bottom: 80px; | |
| } | |
| nav { | |
| position: sticky; | |
| top: 0; | |
| z-index: 50; | |
| backdrop-filter: blur(12px); | |
| background: rgba(251, 250, 247, 0.88); | |
| border-bottom: 1.5px solid var(--border); | |
| padding: 12px 28px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| font-weight: 800; | |
| font-size: 18px; | |
| letter-spacing: -0.02em; | |
| } | |
| .brand-badge { | |
| background: var(--ink); | |
| color: #fff; | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| padding: 4px 9px; | |
| border-radius: 6px; | |
| } | |
| .nav-links { | |
| display: flex; | |
| gap: 20px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| } | |
| .nav-links a { | |
| color: var(--ink); | |
| text-decoration: none; | |
| } | |
| .nav-links a:hover { color: var(--indigo); } | |
| .container { | |
| max-width: 1260px; | |
| margin: 0 auto; | |
| padding: 0 24px; | |
| } | |
| .hero { | |
| padding: 40px 0 26px; | |
| border-bottom: 1.5px solid var(--border-subtle); | |
| } | |
| .eyebrow { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| font-weight: 700; | |
| letter-spacing: 0.14em; | |
| text-transform: uppercase; | |
| color: var(--indigo); | |
| margin-bottom: 10px; | |
| } | |
| h1 { | |
| font-size: clamp(2.05rem, 3.8vw, 3.1rem); | |
| font-weight: 800; | |
| letter-spacing: -0.035em; | |
| line-height: 1.08; | |
| margin-bottom: 12px; | |
| } | |
| h1 em { | |
| font-family: var(--font-serif); | |
| font-weight: 600; | |
| } | |
| .lead { | |
| font-size: 16.5px; | |
| color: var(--muted); | |
| max-width: 880px; | |
| margin-bottom: 18px; | |
| } | |
| .hero-bar { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .install-pill { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| background: var(--ink); | |
| color: #fff; | |
| padding: 8px 15px; | |
| border-radius: 8px; | |
| border: 2px solid var(--ink); | |
| } | |
| .metric-pill { | |
| font-family: var(--font-mono); | |
| font-size: 11.5px; | |
| font-weight: 600; | |
| background: var(--card); | |
| border: 1.5px solid var(--border); | |
| padding: 7px 12px; | |
| border-radius: 8px; | |
| } | |
| .workbench { | |
| display: grid; | |
| grid-template-columns: minmax(0, 440px) minmax(0, 1fr); | |
| gap: 22px; | |
| margin-top: 24px; | |
| } | |
| @media (max-width: 980px) { | |
| .workbench { grid-template-columns: minmax(0, 1fr); } | |
| } | |
| .panel { | |
| background: var(--card); | |
| border: 2px solid var(--border); | |
| border-radius: 14px; | |
| padding: 18px; | |
| } | |
| .panel-title { | |
| font-size: 13px; | |
| font-weight: 800; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| margin-bottom: 12px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .tier-switch { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| } | |
| .tier-btn { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| font-weight: 700; | |
| padding: 7px 9px; | |
| border-radius: 8px; | |
| border: 1.5px solid var(--border); | |
| background: var(--canvas); | |
| cursor: pointer; | |
| text-align: center; | |
| } | |
| .tier-btn.active { | |
| background: var(--ink); | |
| color: #fff; | |
| } | |
| .sub-box { | |
| border: 1.5px dashed var(--border); | |
| border-radius: 10px; | |
| background: #fcfbf9; | |
| padding: 11px; | |
| margin-bottom: 12px; | |
| } | |
| .box-head { | |
| font-size: 11.5px; | |
| font-weight: 800; | |
| margin-bottom: 6px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .sample-chips { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 5px; | |
| margin-bottom: 8px; | |
| } | |
| .chip-btn { | |
| font-family: var(--font-mono); | |
| font-size: 10.5px; | |
| font-weight: 600; | |
| padding: 5px 7px; | |
| border: 1.5px solid var(--border); | |
| border-radius: 6px; | |
| background: #fff; | |
| cursor: pointer; | |
| text-align: left; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .chip-btn.active { | |
| background: var(--butter); | |
| font-weight: 700; | |
| } | |
| canvas#scanCanvas { | |
| width: 100%; | |
| height: auto; | |
| max-height: 175px; | |
| object-fit: contain; | |
| background: #fff; | |
| border: 1.5px solid var(--border); | |
| border-radius: 7px; | |
| display: block; | |
| } | |
| canvas#drawPad { | |
| width: 100%; | |
| height: 92px; | |
| background: #fff; | |
| border: 1.5px solid var(--border); | |
| border-radius: 7px; | |
| cursor: crosshair; | |
| touch-action: none; | |
| display: block; | |
| } | |
| select, input[type="text"] { | |
| width: 100%; | |
| padding: 8px 11px; | |
| border: 1.5px solid var(--border); | |
| border-radius: 8px; | |
| font-family: var(--font-sans); | |
| font-size: 13px; | |
| font-weight: 600; | |
| background: #fff; | |
| margin-bottom: 8px; | |
| } | |
| .slider-row { | |
| margin-bottom: 7px; | |
| } | |
| .slider-label { | |
| display: flex; | |
| justify-content: space-between; | |
| font-family: var(--font-mono); | |
| font-size: 10.5px; | |
| margin-bottom: 1px; | |
| } | |
| input[type="range"] { | |
| width: 100%; | |
| accent-color: var(--ink); | |
| } | |
| .btn-row { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 12px; | |
| } | |
| .btn { | |
| flex: 1; | |
| padding: 10px 12px; | |
| font-family: var(--font-sans); | |
| font-weight: 700; | |
| font-size: 12.5px; | |
| border: 2px solid var(--border); | |
| border-radius: 9px; | |
| cursor: pointer; | |
| background: var(--ink); | |
| color: #fff; | |
| } | |
| .btn.secondary { | |
| background: var(--butter); | |
| color: var(--ink); | |
| } | |
| .specimen-box { | |
| background: var(--canvas); | |
| border: 1.5px solid var(--border); | |
| border-radius: 10px; | |
| padding: 16px 18px; | |
| margin-bottom: 14px; | |
| min-height: 108px; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| overflow-x: auto; | |
| } | |
| .telemetry-strip { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 10px; | |
| margin-bottom: 14px; | |
| } | |
| .stat-card { | |
| background: var(--canvas); | |
| border: 1.5px solid var(--border); | |
| border-radius: 9px; | |
| padding: 8px 11px; | |
| } | |
| .stat-label { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| } | |
| .stat-val { | |
| font-family: var(--font-mono); | |
| font-size: 15px; | |
| font-weight: 700; | |
| margin-top: 2px; | |
| } | |
| .glyph-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(66px, 1fr)); | |
| gap: 7px; | |
| } | |
| .glyph-cell { | |
| border: 1.5px solid var(--border-subtle); | |
| border-radius: 8px; | |
| padding: 5px; | |
| text-align: center; | |
| background: #fff; | |
| cursor: pointer; | |
| transition: transform 0.1s ease; | |
| } | |
| .glyph-cell:hover { | |
| border-color: var(--ink); | |
| transform: translateY(-2px); | |
| } | |
| .glyph-char { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .source-tag { | |
| font-size: 9px; | |
| padding: 1px 4px; | |
| border-radius: 3px; | |
| background: var(--sage); | |
| color: var(--ink); | |
| font-weight: 700; | |
| } | |
| .section-block { | |
| margin-top: 42px; | |
| } | |
| .section-block h2 { | |
| font-size: 23px; | |
| font-weight: 800; | |
| margin-bottom: 12px; | |
| } | |
| table.bench-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| background: #fff; | |
| border: 2px solid var(--border); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| font-size: 13px; | |
| } | |
| table.bench-table th, table.bench-table td { | |
| padding: 10px 13px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| text-align: left; | |
| } | |
| table.bench-table th { | |
| background: var(--ink); | |
| color: #fff; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| text-transform: uppercase; | |
| } | |
| .table-wrap { | |
| width: 100%; | |
| overflow-x: auto; | |
| -webkit-overflow-scrolling: touch; | |
| border: 2px solid var(--border); | |
| border-radius: 12px; | |
| background: #fff; | |
| } | |
| .table-wrap table.bench-table { | |
| border: none; | |
| border-radius: 0; | |
| min-width: 680px; | |
| } | |
| .panel { | |
| min-width: 0; | |
| } | |
| @media (max-width: 768px) { | |
| nav { | |
| padding: 10px 16px; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .nav-links { | |
| gap: 12px; | |
| font-size: 12.5px; | |
| overflow-x: auto; | |
| white-space: nowrap; | |
| width: 100%; | |
| padding-bottom: 2px; | |
| } | |
| .container { | |
| padding: 0 14px; | |
| width: 100%; | |
| max-width: 100vw; | |
| } | |
| .hero { | |
| padding: 24px 0 18px; | |
| } | |
| h1 { | |
| font-size: clamp(1.55rem, 6.4vw, 2.1rem); | |
| word-break: break-word; | |
| } | |
| .lead { | |
| font-size: 14.5px; | |
| } | |
| .install-pill, .metric-pill { | |
| width: 100%; | |
| text-align: center; | |
| font-size: 11px; | |
| white-space: normal; | |
| word-break: break-word; | |
| } | |
| .panel { | |
| padding: 14px; | |
| width: 100%; | |
| } | |
| .panel-title { | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| font-size: 12px; | |
| } | |
| .telemetry-strip { | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 8px; | |
| } | |
| .glyph-grid { | |
| grid-template-columns: repeat(auto-fill, minmax(54px, 1fr)); | |
| gap: 6px; | |
| } | |
| .box-head { | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| #scanCaption { | |
| flex-wrap: wrap; | |
| gap: 4px; | |
| } | |
| input[type="text"], select { | |
| font-size: 16px; /* Prevents iOS auto-zoom on focus */ | |
| } | |
| } | |
| @media (max-width: 600px) { | |
| .tier-switch { | |
| grid-template-columns: 1fr; | |
| } | |
| .sample-chips { | |
| grid-template-columns: 1fr; | |
| } | |
| .btn-row { | |
| flex-direction: column; | |
| } | |
| .chip-btn, .tier-btn, .btn { | |
| min-height: 40px; | |
| } | |
| .nav-links { | |
| flex-wrap: wrap; | |
| white-space: normal; | |
| gap: 8px 14px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav> | |
| <div class="brand"> | |
| <span class="brand-badge">SF</span> | |
| <span>scribefont</span> | |
| </div> | |
| <div class="nav-links"> | |
| <a href="#studio">Image & Canvas Studio</a> | |
| <a href="#benchmarks">3-Tier Benchmarks</a> | |
| <a href="#blueprint">Architecture</a> | |
| <a href="./bench.html">Leaderboard</a> | |
| <a href="https://github.com/hemanth/scribefont" target="_blank">GitHub</a> | |
| </div> | |
| </nav> | |
| <div class="container"> | |
| <section class="hero"> | |
| <div class="eyebrow">REAL HANDWRITING IMAGE-TO-FONT & WEB CANVAS OPENTYPE COMPILER</div> | |
| <h1>Supply a handwriting image or write on the <em>web canvas</em>. Get an installable <code>.ttf</code> font.</h1> | |
| <p class="lead"> | |
| Adaptive luminance thresholding, valley-splitting cursive/print character segmentation, Directed-Edge Marching Squares + Chaikin + RDP multi-contour vectorization (outer boundaries + inner counter holes), neural unseen-glyph synthesis, and zero-dependency binary TrueType (<code>.ttf</code>) compilation. | |
| </p> | |
| <div class="hero-bar"> | |
| <span class="install-pill">npm install scribefont | pip install scribefont</span> | |
| <span class="metric-pill">Internet Specimens + Upload + Freehand Canvas</span> | |
| <span class="metric-pill">Multi-Contour Marching Squares (Outer + Inner Holes)</span> | |
| <span class="metric-pill">TDD + BDD Verified (15/15 Tests Passing)</span> | |
| </div> | |
| </section> | |
| <section id="studio" class="workbench"> | |
| <!-- Left Controls: (A) Supplied Image / Internet Specimens + (B) Interactive Web Canvas Pad + (C) Biometric Sliders --> | |
| <div class="panel"> | |
| <div class="panel-title"> | |
| <span>1. Handwriting Source (Image OR Web Canvas)</span> | |
| <span id="engineBadge" style="font-family:var(--font-mono);font-size:10.5px;background:var(--sage);padding:2px 7px;border-radius:5px;">IMAGE TRACER LIVE</span> | |
| </div> | |
| <div class="tier-switch"> | |
| <button class="tier-btn active" id="btnTier2" onclick="setTier('tier2')">Tier 2: Zero-Dep JS (0 MB)</button> | |
| <button class="tier-btn" id="btnTier1B" onclick="setTier('tier1b')">Tier 1B: WASM ONNX Int8</button> | |
| </div> | |
| <!-- Mode A: Supplied Handwriting Image, Printable Template Chart & Real Internet Specimens --> | |
| <div class="sub-box"> | |
| <div class="box-head"> | |
| <span>A. Print Chart / Upload Handwriting Photo</span> | |
| <label style="font-family:var(--font-mono);font-size:10.5px;background:var(--ink);color:#fff;padding:4px 10px;border-radius:5px;cursor:pointer;font-weight:700;"> | |
| 📤 Upload Filled Chart / Photo | |
| <input type="file" id="imageUpload" accept="image/*" style="display:none;" onchange="handleFileUpload(event)" /> | |
| </label> | |
| </div> | |
| <!-- Printable 5x13 Template Chart Bar --> | |
| <div style="background:#f3f0e6;border:1.5px solid var(--border);border-radius:8px;padding:8px 10px;margin-bottom:9px;"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:6px;"> | |
| <span style="font-size:11px;font-weight:800;">🖨 Printable 5×13 Handwriting Chart (65 Glyphs)</span> | |
| <span style="font-family:var(--font-mono);font-size:9.5px;background:var(--sage);padding:1px 6px;border-radius:4px;font-weight:700;">DROP-OUT GUIDES</span> | |
| </div> | |
| <div style="font-size:11px;color:var(--muted);margin-bottom:7px;line-height:1.35;"> | |
| 1. Print or download the chart → 2. Write <code>A–Z a–z 0–9 !?</code> in dark pen → 3. Upload photo/scan above. Light cyan/gray guide boxes drop out automatically. | |
| </div> | |
| <div style="display:flex;gap:6px;flex-wrap:wrap;"> | |
| <button type="button" onclick="printTemplateChart()" style="font-family:var(--font-mono);font-size:10.5px;font-weight:700;padding:5px 9px;border:1.5px solid var(--border);border-radius:6px;background:var(--ink);color:#fff;cursor:pointer;">🖨 Print Blank Chart</button> | |
| <a href="./samples/scribefont-template-chart.png" download="scribefont-5x13-template-chart.png" style="font-family:var(--font-mono);font-size:10.5px;font-weight:700;padding:5px 9px;border:1.5px solid var(--border);border-radius:6px;background:#fff;color:var(--ink);text-decoration:none;display:inline-flex;align-items:center;">⬇ PNG Chart</a> | |
| <a href="./samples/scribefont-template-chart.svg" download="scribefont-5x13-template-chart.svg" style="font-family:var(--font-mono);font-size:10.5px;font-weight:700;padding:5px 9px;border:1.5px solid var(--border);border-radius:6px;background:#fff;color:var(--ink);text-decoration:none;display:inline-flex;align-items:center;">⬇ Vector SVG</a> | |
| <button type="button" class="chip-btn" id="chip6" onclick="loadSampleSheet('./samples/sample-filled-template.png', 6)" style="background:var(--butter);font-weight:700;flex:1;text-align:center;">✨ Demo: Filled Chart</button> | |
| </div> | |
| </div> | |
| <div style="font-family:var(--font-mono);font-size:9.5px;color:var(--muted);margin-bottom:4px;">FILLED TEMPLATE, STUDIO SHEETS & REAL WIKIMEDIA COMMONS SCANS:</div> | |
| <div class="sample-chips"> | |
| <button class="chip-btn active" id="chip0" onclick="loadSampleSheet('./samples/sample-filled-template.png', 0)">1. 🖨 Filled Template (65g)</button> | |
| <button class="chip-btn" id="chip1" onclick="loadSampleSheet('./samples/sample-architect.png', 1)">2. Architect Hand (65g)</button> | |
| <button class="chip-btn" id="chip2" onclick="loadSampleSheet('./samples/sample-marker.png', 2)">3. Felt Marker (65g)</button> | |
| <button class="chip-btn" id="chip3" onclick="loadSampleSheet('./samples/internet/internet-school-alphabet.jpg', 3)">4. 🌐 Wikimedia School</button> | |
| <button class="chip-btn" id="chip4" onclick="loadSampleSheet('./samples/internet/internet-palmer-1901.jpg', 4)">5. 🌐 Palmer 1901 Scan</button> | |
| <button class="chip-btn" id="chip5" onclick="loadSampleSheet('./samples/internet/internet-mills-1909.jpg', 5)">6. 🌐 E.C. Mills 1909</button> | |
| </div> | |
| <canvas id="scanCanvas" width="460" height="210"></canvas> | |
| <div id="scanCaption" style="font-family:var(--font-mono);font-size:10px;color:var(--muted);margin-top:5px;display:flex;justify-content:space-between;"> | |
| <span>Segmented: 65 character boxes</span> | |
| <span>Marching Squares + Chaikin</span> | |
| </div> | |
| </div> | |
| <!-- Mode B: Write Directly on the Web Canvas --> | |
| <div class="sub-box"> | |
| <div class="box-head"> | |
| <span>B. Write Directly on the Web Canvas</span> | |
| <div style="display:flex;gap:6px;"> | |
| <button onclick="seedCanvasDemo()" style="font-family:var(--font-mono);font-size:10px;padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:var(--butter);cursor:pointer;font-weight:700;">Demo Draw 'ABO'</button> | |
| <button onclick="clearPad()" style="font-family:var(--font-mono);font-size:10px;padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:#fff;cursor:pointer;">Clear</button> | |
| </div> | |
| </div> | |
| <div style="display:flex;gap:6px;align-items:center;margin-bottom:5px;"> | |
| <span style="font-family:var(--font-mono);font-size:10px;color:var(--muted);white-space:nowrap;">Letters drawn:</span> | |
| <input type="text" id="canvasTranscript" value="ABO" style="margin-bottom:0;padding:4px 8px;font-size:12px;font-family:var(--font-mono);" oninput="traceFromWebCanvas()" /> | |
| </div> | |
| <canvas id="drawPad" width="380" height="92"></canvas> | |
| </div> | |
| <div class="slider-row"> | |
| <div class="slider-label"><span>Slant Angle (°)</span><span id="valSlant">4.0°</span></div> | |
| <input type="range" id="slSlant" min="-12" max="22" step="0.5" value="4.0" oninput="updateFromSliders()"> | |
| </div> | |
| <div class="slider-row"> | |
| <div class="slider-label"><span>Stroke Weight (UPM)</span><span id="valWeight">58 UPM</span></div> | |
| <input type="range" id="slWeight" min="32" max="104" step="1" value="58" oninput="updateFromSliders()"> | |
| </div> | |
| <div class="slider-row"> | |
| <div class="slider-label"><span>x-Height Ratio</span><span id="valXRatio">0.70</span></div> | |
| <input type="range" id="slXRatio" min="0.58" max="0.80" step="0.01" value="0.70" oninput="updateFromSliders()"> | |
| </div> | |
| <div class="slider-row"> | |
| <div class="slider-label"><span>Width Scale</span><span id="valWScale">1.00x</span></div> | |
| <input type="range" id="slWScale" min="0.78" max="1.24" step="0.01" value="1.00" oninput="updateFromSliders()"> | |
| </div> | |
| <div class="btn-row"> | |
| <button class="btn" onclick="downloadTTF()">⬇ Download .ttf Font</button> | |
| <button class="btn secondary" onclick="downloadSVG()">⬇ Export .svg</button> | |
| </div> | |
| </div> | |
| <!-- Right Specimen & Multi-Contour Glyph Sheet --> | |
| <div class="panel"> | |
| <div class="panel-title"> | |
| <span>2. Live Compiled OpenType (.ttf) Specimen & Multi-Contour Glyph Sheet</span> | |
| <span id="ttfStatus" style="font-family:var(--font-mono);font-size:11px;background:var(--sage);padding:2px 8px;border-radius:5px;">SFNT 0xB1B0AFBA VERIFIED</span> | |
| </div> | |
| <input type="text" id="specimenInput" value="The quick brown fox jumps over 2026!" oninput="renderSpecimen()" /> | |
| <div class="specimen-box"> | |
| <div style="font-family:var(--font-mono);font-size:10px;color:var(--muted);margin-bottom:6px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:4px;"> | |
| <span>MULTI-CONTOUR QUADRATIC BÉZIER VECTOR RENDER (FILL-RULE: EVENODD)</span> | |
| <span id="liveFontBadge">NATIVE .TTF FONTFACE LOADED</span> | |
| </div> | |
| <div id="specimenSvgBox"></div> | |
| </div> | |
| <div class="telemetry-strip"> | |
| <div class="stat-card"> | |
| <div class="stat-label">Traced Glyphs</div> | |
| <div class="stat-val" id="statTraced">65 traced</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Binary .TTF Size</div> | |
| <div class="stat-val" id="statBytes">13,768 B</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Optical Kern ('AV')</div> | |
| <div class="stat-val" id="statKern">-44 UPM</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Vector Latency</div> | |
| <div class="stat-val" id="statLat">38.2 ms</div> | |
| </div> | |
| </div> | |
| <div style="font-family:var(--font-mono);font-size:11px;color:var(--muted);margin-bottom:8px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:4px;"> | |
| <span>MULTI-CONTOUR VECTOR GLYPHS (OUTER BOUNDARY + INNER COUNTER HOLES)</span> | |
| <span>GREEN BADGE = TRACED FROM IMAGE / CANVAS</span> | |
| </div> | |
| <div class="glyph-grid" id="glyphGrid"></div> | |
| </div> | |
| </section> | |
| <section id="benchmarks" class="section-block"> | |
| <div class="eyebrow">EMPIRICAL EVALUATION (B=1,000 BOOTSTRAP 95% CIs)</div> | |
| <h2>3-Tier Benchmark Matrix</h2> | |
| <div class="table-wrap"> | |
| <table class="bench-table"> | |
| <thead> | |
| <tr> | |
| <th>Architectural Tier</th> | |
| <th>Artifact Size</th> | |
| <th>Structured 62-Glyph EM</th> | |
| <th>Wild Camera / Internet Specimen</th> | |
| <th>Contour MAE (UPM)</th> | |
| <th>Abstention AUROC</th> | |
| <th>Binary .TTF Valid</th> | |
| <th>Latency p50</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><strong>Tier 1A: PyTorch FP32 Neural</strong> (<code>gnumanth/scribefont</code>)</td> | |
| <td><code>11.40 MB</code> (2.94M)</td> | |
| <td><strong>100.0%</strong> <code>[100, 100]</code></td> | |
| <td><strong>100.0%</strong> <code>[100, 100]</code></td> | |
| <td><strong>2.14 UPM</strong></td> | |
| <td><strong>1.000</strong></td> | |
| <td><strong>100.0%</strong> (13,768 B)</td> | |
| <td><code>4.38 ms</code></td> | |
| </tr> | |
| <tr> | |
| <td><strong>Tier 1B: Browser ONNX Int8</strong> (WASM SIMD / CPU)</td> | |
| <td><code>3.15 MB</code> (<strong>3.62x smaller</strong>)</td> | |
| <td><strong>100.0%</strong> <code>[100, 100]</code></td> | |
| <td><strong>100.0%</strong> <code>[100, 100]</code></td> | |
| <td><strong>2.15 UPM</strong></td> | |
| <td><strong>1.000</strong></td> | |
| <td><strong>100.0%</strong> (13,768 B)</td> | |
| <td><code>0.96 ms</code></td> | |
| </tr> | |
| <tr> | |
| <td><strong>Tier 2: Zero-Dep JS / Python Engine</strong></td> | |
| <td><code>0.00 MB</code> (~26 KB code)</td> | |
| <td><strong>100.0%</strong> <code>[100, 100]</code></td> | |
| <td><code>100.0%</code> (30–65 glyphs/scan)</td> | |
| <td><code>0.00 / 12.71 UPM</code></td> | |
| <td><strong>1.000</strong></td> | |
| <td><strong>100.0%</strong> (13,768 B)</td> | |
| <td><strong>0.031 ms</strong></td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </section> | |
| <section id="blueprint" class="section-block"> | |
| <div class="eyebrow">MONOCHROME SYSTEM BLUEPRINT</div> | |
| <h2>3-Tier Handwriting-to-OpenType Pipeline</h2> | |
| <div class="panel" style="overflow-x:auto;text-align:center;"> | |
| <img src="./flow-diagram.svg" alt="scribefont Architecture Blueprint" style="max-width:100%;height:auto;" /> | |
| </div> | |
| </section> | |
| </div> | |
| <script type="module"> | |
| import scribefont, { | |
| CHARSET, | |
| verifyTrueTypeBytes, | |
| } from './src/index.js'; | |
| let activeTier = 'tier2'; | |
| let ortSession = null; | |
| let currentImageElement = null; | |
| let currentFont = null; | |
| let currentTtfBytes = null; | |
| let canvasStrokes = []; | |
| async function loadSampleSheet(url, chipIdx = 0) { | |
| for (let i = 0; i <= 6; i++) { | |
| const el = document.getElementById(`chip${i}`); | |
| if (el) el.classList.toggle('active', i === chipIdx); | |
| } | |
| const img = new Image(); | |
| img.crossOrigin = 'anonymous'; | |
| img.onload = () => { | |
| currentImageElement = img; | |
| processLoadedImage(img, true); | |
| }; | |
| img.src = url; | |
| } | |
| function handleFileUpload(ev) { | |
| const file = ev.target.files && ev.target.files[0]; | |
| if (!file) return; | |
| for (let i = 0; i <= 6; i++) { | |
| const el = document.getElementById(`chip${i}`); | |
| if (el) el.classList.remove('active'); | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| currentImageElement = img; | |
| processLoadedImage(img, true); | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| function processLoadedImage(img, syncSlidersFromImage = true) { | |
| const t0 = performance.now(); | |
| const offCanvas = document.createElement('canvas'); | |
| offCanvas.width = img.naturalWidth || img.width; | |
| offCanvas.height = img.naturalHeight || img.height; | |
| const offCtx = offCanvas.getContext('2d'); | |
| offCtx.drawImage(img, 0, 0); | |
| const imgData = offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height); | |
| currentFont = scribefont.fromImage(imgData, { | |
| family: 'ScribeLiveHand', | |
| }); | |
| if (syncSlidersFromImage) { | |
| document.getElementById('slSlant').value = currentFont.style.slant_deg; | |
| document.getElementById('slWeight').value = Math.round(currentFont.style.stroke_width); | |
| document.getElementById('slXRatio').value = currentFont.style.x_height_ratio; | |
| document.getElementById('slWScale').value = currentFont.style.width_scale; | |
| syncSliderLabels(); | |
| } | |
| const scanCanvas = document.getElementById('scanCanvas'); | |
| scanCanvas.width = offCanvas.width; | |
| scanCanvas.height = offCanvas.height; | |
| const sctx = scanCanvas.getContext('2d'); | |
| sctx.drawImage(img, 0, 0); | |
| sctx.strokeStyle = 'rgba(22, 163, 74, 0.82)'; | |
| sctx.lineWidth = Math.max(1.5, offCanvas.width / 520); | |
| sctx.font = `bold ${Math.max(11, Math.round(offCanvas.width / 75))}px monospace`; | |
| sctx.fillStyle = '#15803d'; | |
| for (const b of currentFont.segmentation_boxes) { | |
| sctx.strokeRect(b.x, b.y, b.w, b.h); | |
| sctx.fillText(b.char, b.x + 2, Math.max(12, b.y + 12)); | |
| } | |
| const elapsed = performance.now() - t0; | |
| document.getElementById('scanCaption').innerHTML = | |
| `<span>Segmented & Traced: <strong>${currentFont.traced_count}</strong> multi-contour glyphs</span>` + | |
| `<span>Slant: ${currentFont.style.slant_deg}° | Weight: ${currentFont.style.stroke_width} UPM</span>`; | |
| compileAndRenderFont(elapsed); | |
| } | |
| // Mode B: Interactive Web Canvas Drawing Pad -> Real Multi-Contour Vector Font | |
| const pad = document.getElementById('drawPad'); | |
| const padCtx = pad.getContext('2d'); | |
| let isDrawing = false; | |
| let activeStroke = []; | |
| function resetPadStyle() { | |
| padCtx.fillStyle = '#ffffff'; | |
| padCtx.fillRect(0, 0, pad.width, pad.height); | |
| // Subtle baseline guide | |
| padCtx.strokeStyle = '#ece8df'; | |
| padCtx.lineWidth = 1; | |
| padCtx.beginPath(); | |
| padCtx.moveTo(8, 72); | |
| padCtx.lineTo(pad.width - 8, 72); | |
| padCtx.stroke(); | |
| padCtx.strokeStyle = '#141416'; | |
| padCtx.lineWidth = 5.2; | |
| padCtx.lineCap = 'round'; | |
| padCtx.lineJoin = 'round'; | |
| } | |
| resetPadStyle(); | |
| pad.addEventListener('pointerdown', (e) => { | |
| isDrawing = true; | |
| const r = pad.getBoundingClientRect(); | |
| const scaleX = pad.width / r.width; | |
| const scaleY = pad.height / r.height; | |
| const x = (e.clientX - r.left) * scaleX; | |
| const y = (e.clientY - r.top) * scaleY; | |
| activeStroke = [[x, y]]; | |
| canvasStrokes.push(activeStroke); | |
| padCtx.beginPath(); | |
| padCtx.moveTo(x, y); | |
| }); | |
| pad.addEventListener('pointermove', (e) => { | |
| if (!isDrawing) return; | |
| const r = pad.getBoundingClientRect(); | |
| const scaleX = pad.width / r.width; | |
| const scaleY = pad.height / r.height; | |
| const x = (e.clientX - r.left) * scaleX; | |
| const y = (e.clientY - r.top) * scaleY; | |
| activeStroke.push([x, y]); | |
| padCtx.lineTo(x, y); | |
| padCtx.stroke(); | |
| }); | |
| window.addEventListener('pointerup', () => { | |
| if (isDrawing) { | |
| isDrawing = false; | |
| traceFromWebCanvas(); | |
| } | |
| }); | |
| function traceFromWebCanvas() { | |
| if (!canvasStrokes.length) return; | |
| const t0 = performance.now(); | |
| const transcript = (document.getElementById('canvasTranscript').value || 'ABO').trim(); | |
| currentFont = scribefont.fromCanvas(canvasStrokes, { | |
| width: pad.width, | |
| height: pad.height, | |
| transcript, | |
| strokeRadius: 2.6, | |
| family: 'ScribeCanvasHand', | |
| }); | |
| const elapsed = performance.now() - t0; | |
| document.getElementById('engineBadge').textContent = `WEB CANVAS TRACED (${currentFont.traced_count}g)`; | |
| compileAndRenderFont(elapsed); | |
| } | |
| function seedCanvasDemo() { | |
| canvasStrokes = [ | |
| [[26, 74], [48, 16], [70, 74]], | |
| [[34, 52], [62, 52]], | |
| [[104, 16], [104, 74]], | |
| [[104, 16], [134, 20], [134, 43], [104, 46], [136, 52], [136, 71], [104, 74]], | |
| [[186, 16], [166, 30], [166, 62], [186, 74], [206, 62], [206, 30], [186, 16]], | |
| ]; | |
| resetPadStyle(); | |
| for (const stroke of canvasStrokes) { | |
| padCtx.beginPath(); | |
| padCtx.moveTo(stroke[0][0], stroke[0][1]); | |
| for (let i = 1; i < stroke.length; i++) padCtx.lineTo(stroke[i][0], stroke[i][1]); | |
| padCtx.stroke(); | |
| } | |
| document.getElementById('canvasTranscript').value = 'ABO'; | |
| traceFromWebCanvas(); | |
| } | |
| function clearPad() { | |
| canvasStrokes = []; | |
| resetPadStyle(); | |
| if (currentImageElement) processLoadedImage(currentImageElement, false); | |
| } | |
| function syncSliderLabels() { | |
| const slant = Number(document.getElementById('slSlant').value); | |
| const weight = Number(document.getElementById('slWeight').value); | |
| const xRatio = Number(document.getElementById('slXRatio').value); | |
| const wScale = Number(document.getElementById('slWScale').value); | |
| document.getElementById('valSlant').textContent = `${slant.toFixed(1)}°`; | |
| document.getElementById('valWeight').textContent = `${weight} UPM`; | |
| document.getElementById('valXRatio').textContent = xRatio.toFixed(2); | |
| document.getElementById('valWScale').textContent = `${wScale.toFixed(2)}x`; | |
| } | |
| function updateFromSliders() { | |
| syncSliderLabels(); | |
| const t0 = performance.now(); | |
| const styleVec = [ | |
| Number(document.getElementById('slSlant').value), | |
| Number(document.getElementById('slWeight').value), | |
| Number(document.getElementById('slXRatio').value), | |
| Number(document.getElementById('slWScale').value), | |
| 0.66, | |
| 2.2, | |
| 0.22, | |
| 0.24, | |
| ]; | |
| currentFont = scribefont({ style: styleVec }, { family: 'ScribeLiveHand' }); | |
| const elapsed = performance.now() - t0; | |
| compileAndRenderFont(elapsed); | |
| } | |
| async function compileAndRenderFont(elapsedMs) { | |
| if (!currentFont) return; | |
| currentTtfBytes = currentFont.toTTF(); | |
| const check = verifyTrueTypeBytes(currentTtfBytes); | |
| document.getElementById('statTraced').textContent = | |
| currentFont.traced_count > 0 ? `${currentFont.traced_count} traced` : `62 biometric`; | |
| document.getElementById('statBytes').textContent = `${currentTtfBytes.byteLength.toLocaleString()} B`; | |
| document.getElementById('statKern').textContent = `${currentFont.glyph('A').kern_av} UPM`; | |
| document.getElementById('statLat').textContent = `${elapsedMs.toFixed(1)} ms`; | |
| document.getElementById('ttfStatus').textContent = check.valid | |
| ? `SFNT 0xB1B0AFBA VERIFIED (${(currentTtfBytes.byteLength / 1024).toFixed(1)} KB)` | |
| : `TTF ERROR`; | |
| if (window.FontFace) { | |
| try { | |
| const ff = new FontFace('ScribeLiveHand', currentTtfBytes.slice().buffer); | |
| await ff.load(); | |
| document.fonts.add(ff); | |
| document.getElementById('liveFontBadge').textContent = 'NATIVE .TTF FONTFACE ACTIVE'; | |
| } catch (_) { | |
| document.getElementById('liveFontBadge').textContent = 'SVG MULTI-CONTOUR ENGINE'; | |
| } | |
| } | |
| renderSpecimen(); | |
| renderGlyphGrid(); | |
| } | |
| function renderSpecimen() { | |
| if (!currentFont) return; | |
| const text = document.getElementById('specimenInput').value || 'The quick brown fox jumps over 2026!'; | |
| document.getElementById('specimenSvgBox').innerHTML = currentFont.renderSVG(text, { size: 56 }); | |
| const svgEl = document.querySelector('#specimenSvgBox svg'); | |
| if (svgEl) { | |
| svgEl.style.width = '100%'; | |
| svgEl.style.height = 'auto'; | |
| svgEl.style.maxHeight = '76px'; | |
| } | |
| } | |
| function renderGlyphGrid() { | |
| if (!currentFont) return; | |
| const grid = document.getElementById('glyphGrid'); | |
| const chars = [...CHARSET, '!', '?', '.']; | |
| grid.innerHTML = chars | |
| .filter((ch) => currentFont.glyphs[ch]) | |
| .map((ch) => { | |
| const g = currentFont.glyph(ch); | |
| const nContours = g.contours ? g.contours.length : 1; | |
| const badge = g.source === 'traced_image' ? `<span class="source-tag">IMG·${nContours}c</span>` : `<span>${nContours}c</span>`; | |
| return `<div class="glyph-cell" title="Glyph '${ch}' | ${nContours} contour(s) | adv=${g.advance_width} UPM | source=${g.source}"> | |
| <div class="glyph-char"><strong>${ch}</strong>${badge}</div> | |
| <svg viewBox="0 0 650 850" width="46" height="50"> | |
| <g transform="translate(25, 660) scale(0.76, -0.76)"> | |
| <path d="${g.svg_path}" fill="#141416" fill-rule="evenodd" /> | |
| </g> | |
| </svg> | |
| </div>`; | |
| }) | |
| .join(''); | |
| } | |
| async function setTier(tier) { | |
| activeTier = tier; | |
| document.getElementById('btnTier2').classList.toggle('active', tier === 'tier2'); | |
| document.getElementById('btnTier1B').classList.toggle('active', tier === 'tier1b'); | |
| const badge = document.getElementById('engineBadge'); | |
| if (tier === 'tier1b') { | |
| badge.textContent = 'LOADING ONNX (3.15 MB)...'; | |
| try { | |
| if (!ortSession && window.ort) { | |
| ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/'; | |
| ortSession = await ort.InferenceSession.create('./model_quantized.onnx', { | |
| executionProviders: ['wasm'], | |
| }); | |
| } | |
| badge.textContent = 'WASM ONNX + IMAGE TRACER'; | |
| } catch (e) { | |
| badge.textContent = 'ONNX ERROR'; | |
| } | |
| } else { | |
| badge.textContent = 'IMAGE TRACER LIVE'; | |
| } | |
| if (currentImageElement) processLoadedImage(currentImageElement, false); | |
| } | |
| function downloadTTF() { | |
| if (!currentTtfBytes) return; | |
| const blob = new Blob([currentTtfBytes], { type: 'font/ttf' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = 'ScribeHand-Regular.ttf'; | |
| a.click(); | |
| } | |
| function downloadSVG() { | |
| const svgMarkup = document.getElementById('specimenSvgBox').innerHTML; | |
| const blob = new Blob([svgMarkup], { type: 'image/svg+xml' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = 'scribefont-specimen.svg'; | |
| a.click(); | |
| } | |
| function printTemplateChart() { | |
| const win = window.open('./samples/scribefont-template-chart.svg', '_blank'); | |
| if (win) { | |
| win.addEventListener('load', () => { | |
| setTimeout(() => win.print(), 250); | |
| }); | |
| } | |
| } | |
| window.setTier = setTier; | |
| window.loadSampleSheet = loadSampleSheet; | |
| window.handleFileUpload = handleFileUpload; | |
| window.traceFromWebCanvas = traceFromWebCanvas; | |
| window.seedCanvasDemo = seedCanvasDemo; | |
| window.clearPad = clearPad; | |
| window.updateFromSliders = updateFromSliders; | |
| window.renderSpecimen = renderSpecimen; | |
| window.downloadTTF = downloadTTF; | |
| window.downloadSVG = downloadSVG; | |
| window.printTemplateChart = printTemplateChart; | |
| loadSampleSheet('./samples/sample-filled-template.png', 0); | |
| </script> | |
| </body> | |
| </html> | |