Spaces:
Running
Running
Download static/index.html from surogate/doodle-decoder: direct link, hf CLI and curl.
- Browser
- Download file 9 kB
-
https://huggingface.co/spaces/surogate/doodle-decoder/resolve/main/static/index.html
- Command line
-
hf download hf://spaces/surogate/doodle-decoder/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/surogate/doodle-decoder/resolve/main/static/index.html
9 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Doodle Decoder</title> | |
| <meta name="description" content="Draw the word you're given. Surogate Rune looks at your pixels and guesses, live."> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600&family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --ink: #2a102d; --ink-2: #52504c; --ink-3: #8c847a; --paper: #ffffff; --card: #ffffff; | |
| --amber: #ffaf10; --amber-soft: rgba(255, 175, 16, 0.14); --line: #ece6da; --good: #22c55e; | |
| } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; font: 16px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; | |
| background: var(--paper); color: var(--ink); -webkit-font-smoothing: antialiased; } | |
| main { max-width: 1040px; margin: 0 auto; padding: 24px 20px 40px; display: grid; gap: 24px; | |
| grid-template-columns: minmax(0, 1fr) 320px; } | |
| @media (max-width: 800px) { main { grid-template-columns: minmax(0, 1fr); } } | |
| header { grid-column: 1 / -1; } | |
| h1 { margin: 0; font-family: Fraunces, Georgia, serif; font-weight: 600; font-size: clamp(30px, 5vw, 44px); letter-spacing: -0.02em; line-height: 1.05; } | |
| .rune-embedded header, .rune-embedded footer { display: none; } | |
| .rune-embedded main { padding-top: 8px; } | |
| .sub { margin: 6px 0 0; color: var(--ink-2); } | |
| .prompt { display: flex; align-items: baseline; gap: 10px; margin: 0 0 12px; font-size: 20px; color: var(--ink-2); } | |
| .prompt b { font-size: 30px; color: var(--ink); background: var(--amber-soft); padding: 0 10px; border-radius: 8px; } | |
| canvas { display: block; width: 100%; aspect-ratio: 1; background: #fff; border: 1px solid var(--line); | |
| border-radius: 16px; touch-action: none; cursor: crosshair; box-shadow: 0 24px 50px -30px rgba(42, 16, 45, 0.35); } | |
| .tools { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; } | |
| button { font: inherit; font-weight: 600; padding: 9px 16px; border-radius: 10px; border: 2px solid var(--ink); | |
| background: var(--card); color: var(--ink); cursor: pointer; } | |
| button.primary { background: var(--amber); border-color: var(--amber); } | |
| button:focus-visible { outline: 3px solid var(--amber); outline-offset: 3px; } | |
| aside { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 18px 18px 14px; | |
| align-self: start; position: sticky; top: 16px; } | |
| .head { margin: 0 0 12px; font-family: Fraunces, Georgia, serif; font-weight: 600; font-size: 26px; min-height: 1.4em; } | |
| .bar { display: grid; grid-template-columns: 96px 1fr 42px; gap: 8px; align-items: center; margin: 7px 0; font-size: 15px; } | |
| .bar .track { height: 12px; background: var(--line); border-radius: 6px; overflow: hidden; } | |
| .bar .fill { height: 100%; background: var(--amber); border-radius: 6px; transition: width 0.3s ease; } | |
| .bar.top .fill { background: var(--ink); } | |
| .bar .pct { text-align: right; font-variant-numeric: tabular-nums; color: var(--ink-2); } | |
| .status { color: var(--ink-3); font-size: 14px; min-height: 1.5em; margin: 10px 0 0; } | |
| .win { display: none; margin-top: 14px; padding: 14px; border-radius: 12px; background: var(--ink); color: #fff; } | |
| .win.show { display: block; animation: pop 0.35s cubic-bezier(.2, 1.4, .4, 1); } | |
| .win p { margin: 0 0 10px; } | |
| .win button { border-color: var(--amber); background: var(--amber); color: var(--ink); } | |
| @keyframes pop { from { transform: scale(0.92); opacity: 0; } to { transform: none; opacity: 1; } } | |
| footer { grid-column: 1 / -1; color: var(--ink-3); font-size: 14px; } | |
| footer a { color: var(--ink); } | |
| @media (prefers-reduced-motion: reduce) { .bar .fill { transition: none; } .win.show { animation: none; } } | |
| </style> | |
| <script src="embed.js"></script> | |
| <script src="client.js"></script> | |
| </head> | |
| <body> | |
| <main> | |
| <header> | |
| <h1>Doodle Decoder</h1> | |
| <p class="sub">Draw the word. Every time you lift the pen, Rune looks at your pixels and guesses.</p> | |
| </header> | |
| <section> | |
| <p class="prompt">Draw <b id="word"></b></p> | |
| <canvas id="canvas" width="512" height="512" aria-label="Drawing canvas"></canvas> | |
| <div class="tools"> | |
| <button id="clear" type="button">Clear</button> | |
| <button id="next" type="button" class="primary">New word</button> | |
| </div> | |
| </section> | |
| <aside aria-live="polite"> | |
| <h2 class="head" id="head">Rune is watching…</h2> | |
| <div id="bars"></div> | |
| <p class="status" id="status">Start drawing.</p> | |
| <div class="win" id="win"> | |
| <p id="win-text"></p> | |
| <button id="share" type="button">Share</button> | |
| </div> | |
| </aside> | |
| <footer> | |
| Powered by <a href="https://huggingface.co/surogate/rune-26b-a4b-GGUF" target="_blank" rel="noopener">Surogate Rune</a>, | |
| an open decision model. <a href="https://surogate.ai/labs/rune-examples/" target="_blank" rel="noopener">More demos</a>. | |
| Nothing you draw is stored. | |
| </footer> | |
| </main> | |
| <script> | |
| const WORDS = ["cat", "dog", "fish", "bird", "house", "tree", "flower", "sun", "moon", "star", "car", "bicycle", | |
| "boat", "airplane", "train", "rocket", "cup", "chair", "table", "lamp", "clock", "key", "umbrella", | |
| "hat", "shoe", "glasses", "guitar", "pizza", "apple", "banana", "ice cream", "cake", "heart", | |
| "smiley face", "snake", "spider", "butterfly", "mountain", "cloud", "lightning", "snowman", | |
| "ghost", "crown", "sword", "light bulb", "phone", "envelope", "scissors", "pencil", "eye"]; | |
| const canvas = document.getElementById("canvas"); | |
| const ctx = canvas.getContext("2d"); | |
| ctx.lineWidth = 8; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.strokeStyle = "#111"; | |
| const $ = (id) => document.getElementById(id); | |
| let strokes = 0, drawing = false, busy = false, pending = false, target = "", won = false; | |
| let round = 0; | |
| let last = null; // the previous pointer position; each move draws only the new segment // bumped by Clear and New word; an answer from an earlier round is dropped | |
| function clearCanvas() { | |
| ctx.fillStyle = "#fff"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| strokes = 0; won = false; pending = false; round++; | |
| $("bars").replaceChildren(); | |
| $("win").classList.remove("show"); | |
| $("head").textContent = "Rune is watching…"; | |
| $("status").textContent = "Start drawing."; | |
| } | |
| function newWord(asked) { | |
| let next = WORDS.includes(asked) ? asked : null; | |
| while (!next || next === target) next = WORDS[Math.floor(Math.random() * WORDS.length)]; | |
| target = next; | |
| $("word").textContent = target; | |
| clearCanvas(); | |
| } | |
| function point(e) { | |
| const r = canvas.getBoundingClientRect(); | |
| return [(e.clientX - r.left) * canvas.width / r.width, (e.clientY - r.top) * canvas.height / r.height]; | |
| } | |
| canvas.addEventListener("pointerdown", (e) => { | |
| drawing = true; | |
| canvas.setPointerCapture(e.pointerId); | |
| last = point(e); | |
| }); | |
| canvas.addEventListener("pointermove", (e) => { | |
| if (!drawing) return; | |
| const p = point(e); | |
| ctx.beginPath(); | |
| ctx.moveTo(...last); | |
| ctx.lineTo(...p); | |
| ctx.stroke(); | |
| last = p; | |
| }); | |
| function endStroke() { | |
| if (!drawing) return; | |
| drawing = false; | |
| strokes++; | |
| guess(); | |
| } | |
| canvas.addEventListener("pointerup", endStroke); | |
| canvas.addEventListener("pointercancel", endStroke); | |
| function setBusy(on) { | |
| busy = on; | |
| if (on) document.body.dataset.busy = "1"; else delete document.body.dataset.busy; | |
| } | |
| async function guess() { | |
| if (won) return; | |
| if (busy) { pending = true; return; } // one request at a time; the latest drawing is sent next | |
| setBusy(true); | |
| const mine = round; | |
| try { | |
| const t = performance.now(); | |
| const out = await post("decide", { image: canvas.toDataURL("image/png"), context: {} }); | |
| if (mine !== round) return; | |
| const probs = Object.entries(out.labels.guess).sort((a, b) => b[1] - a[1]); | |
| const [top, p] = probs[0]; | |
| $("head").textContent = p < 0.5 ? `Hmm… a ${top}?` : `It's a ${top}!`; | |
| $("bars").replaceChildren(...probs.slice(0, 5).map(([w, q], i) => bar(w, q, i === 0))); | |
| $("status").textContent = `${Math.round(performance.now() - t)} ms · stroke ${strokes}`; | |
| if (top === target && p >= 0.5) { | |
| won = true; | |
| $("win-text").textContent = `Rune got it in ${strokes} stroke${strokes === 1 ? "" : "s"}, ${Math.round(p * 100)}% sure it's a ${target}.`; | |
| $("win").classList.add("show"); | |
| } | |
| } catch (err) { | |
| if (mine === round) $("status").textContent = err.message; | |
| } finally { | |
| setBusy(false); | |
| if (pending && !won) { pending = false; guess(); } | |
| } | |
| } | |
| // The link challenges a friend to draw the same word. | |
| $("share").addEventListener("click", () => share($("share"), `${$("win-text").textContent} Can you beat it?`, { word: target })); | |
| $("clear").addEventListener("click", clearCanvas); | |
| $("next").addEventListener("click", () => newWord()); | |
| const asked = new URLSearchParams(location.search).get("word"); | |
| newWord(asked); | |
| </script> | |
| </body> | |
| </html> | |