DKL9 GitList
Repositories
DKL9 home
functle
Code
Commits
Branches
Tags
Search
Tree:
d8328ee
Branches
Tags
master
functle
functle.html
Take input by keypresses and animate
dkl9
commited
d8328ee
at 2025-209 17:22:12
functle.html
Blame
History
Raw
<!DOCTYPE html> <html> <head> <title>Functle</title> <style> body { font-family: sans-serif; } #plot { border: 1px solid grey; flex-shrink: 0; height: 400px; } #grid { border-collapse: separate; border-spacing: 3px 12px; margin-top: 10px; } #grid td { min-width: 60px; height: 60px; border: 1px solid #ccc; text-align: center; vertical-align: middle; font-weight: bold; transition: transform 0.3s ease; } .grey { background: #3a3a3c; color: white; } .yellow { background: #b59f3b; color: white; } .green { background: #538e4e; color: white; } #message { color: red; margin-top: 5px; } .side { margin-left: 20px; } .shake { animation: shake 0.3s; } @keyframes shake { 0% { transform: translateX(0); } 25% { transform: translateX(-5px); } 50% { transform: translateX(5px); } 75% { transform: translateX(-5px); } 100% { transform: translateX(0); } } @media (min-aspect-ratio: 4/3) { .container { display: flex; } } @media (prefers-color-scheme: dark) { body { color: white; background-color: black; } } </style> </head> <body> <div class="container"> <canvas id="plot" width="400" height="400"></canvas> <div class="side"> <div> <div id="message"></div> </div> <table id="grid"></table> </div> <div class="side"> <p>Guess the function based on its plot on the left. Tick marks on axes indicate <var>x</var> = ±1 and <var>y</var> = ±1. Functions here are made of values <var>x</var>, 1, and 2, modified by functions ^2 (square), ^3 (cube), sin, exp, and ln, combined by operators +, -, *, /.</p> <p>Enter your answer in postfix (reverse Polish) notation. For example, express 1 - <var>x</var> / (sin <var>x</var>) as "1 x x sin / -".</p> <p>The correct answer will be five tokens. Numerically equivalent rearranged answers will be accepted.</p> <p>Enhanced from a prototype by ChatGPT o4-mini.</p> </div> </div> <script> const allowed = { terminals: ["x", "2", "1"], unary: ["^2", "^3", "sin", "exp", "ln"], binary: ["+", "-", "*", "/"] }; const allTokens = [...allowed.terminals, ...allowed.unary, ...allowed.binary]; const tl = 5; const keyToToken = { x:"x", 1:"1", 2:"2", q:"^2", c:"^3", s:"sin", e:"exp", l:"ln", "+":"+", "-":"-", "*":"*", "/":"/" }; const target = generateExpr(); const maxGuesses = 6; const width = 400, height = 400; const ctx = document.getElementById("plot").getContext("2d"); const bound = 10; const xs = []; const targetYs = []; for (let i = 0; i <= width; i++) { const x = bound * (2 * i / width - 1); xs.push(x); const yv = evalPostfix(target, x); targetYs.push(yv); } const clampLow = 1.5; const clampHigh = 20; let yMin = Math.min(...targetYs); let yMax = Math.max(...targetYs); if (isNaN(yMin) || yMin < -clampHigh) yMin = -clampHigh; else if (yMin > -clampLow) yMin = -clampLow; if (isNaN(yMax) || yMax > clampHigh) yMax = clampHigh; else if (yMax < clampLow) yMax = clampLow; const guessYsList = []; const darkTheme = matchMedia('(prefers-color-scheme: dark)').matches; function shakeRow(row) { row.classList.remove("shake"); void row.offsetWidth; row.classList.add("shake"); } const grid = document.getElementById("grid"); for (let i = 0; i < 6; i++) { const tr = document.createElement("tr"); for (let j = 0; j < tl; j++) { const td = document.createElement("td"); tr.appendChild(td); } grid.appendChild(tr); } let currentRow = 0; let currentInput = []; redrawAll(); document.addEventListener("keydown", e => { const row = grid.rows[currentRow]; if (e.key === "Backspace") { if (currentInput.length > 0) { currentInput.pop(); row.cells[currentInput.length].textContent = ""; } else { shakeRow(row); } } else if (e.key === "Enter") { submitGuess(); } else if (keyToToken.hasOwnProperty(e.key)) { if (currentInput.length < tl) { const token = keyToToken[e.key]; currentInput.push(token); const c = row.cells[currentInput.length - 1]; c.style.fontSize = `${3.5 - 0.5 * token.length}em`; c.textContent = token; } else { shakeRow(row); } } }); function submitGuess() { const row = grid.rows[currentRow]; document.getElementById("message").textContent = ""; if (currentInput.length !== tl) { document.getElementById("message").textContent = `Use exactly ${tl} tokens.`; shakeRow(row); return; } for (const t of currentInput) { if (!allTokens.includes(t)) { document.getElementById("message").textContent = "Invalid token: " + t; shakeRow(row); return; } } let ys; try { ys = xs.map(x => evalPostfix(currentInput, x)); } catch (e) { document.getElementById("message").textContent = "Syntax error."; shakeRow(row); return; } guessYsList.push(ys); redrawAll(); const close = closeEnough(ys, targetYs); animateFlip(currentInput, close); if (close) { setTimeout(() => fillRow(currentInput, "green"), 300 * tl); return; } currentRow++; currentInput = []; if (currentRow >= 6) return; } function animateFlip(tokens, close) { const row = grid.rows[currentRow]; const colors = Array(tl); if (close) { colors.fill("green"); } else { const freq = {}; for (const t of target) freq[t] = (freq[t] || 0) + 1; colors.fill("grey"); for (let i = 0; i < tl; i++) { if (tokens[i] === target[i]) { colors[i] = "green"; freq[tokens[i]]--; } } for (let i = 0; i < tl; i++) { if (colors[i] === "grey" && freq[tokens[i]] > 0) { colors[i] = "yellow"; freq[tokens[i]]--; } } } tokens.forEach((token, i) => { const cell = row.cells[i]; setTimeout(() => { cell.style.transform = "rotateX(90deg)"; setTimeout(() => { cell.classList.add(colors[i]); cell.style.transform = "rotateX(0deg)"; }, 150); }, 150 * i); }); } function fillRow(tokens, cls) { const row = grid.rows[currentRow]; for (let i = 0; i < tl; i++) { const cell = row.cells[i]; cell.textContent = tokens[i]; cell.classList.add(cls); } } function evalPostfix(tokens, x) { const s = []; for (const t of tokens) { if (t === "x") s.push(x); else if (t === "1") s.push(1); else if (t === "2") s.push(2); else if (allowed.unary.includes(t)) { if (s.length < 1) throw 0; const v = s.pop(); if (t === "^2") s.push(v ** 2); if (t === "^3") s.push(v ** 3); if (t === "sin") s.push(Math.sin(v)); if (t === "exp") s.push(Math.exp(v)); if (t === "ln") s.push(Math.log(v)); } else if (allowed.binary.includes(t)) { if (s.length < 2) throw 0; const b = s.pop(), a = s.pop(); if (t === "+") s.push(a + b); if (t === "-") s.push(a - b); if (t === "*") s.push(a * b); if (t === "/") s.push(a / b); } else throw 0; } if (s.length !== 1) throw 0; return s[0]; } function rand(l) { return l[Math.floor(l.length * Math.random())]; } function generateExpr(len = tl, force = true) { if (len == 1) { return force ? ["x"] : [rand(allowed.terminals)]; } else if (len == 2 || Math.random >= 0.2) { const e = generateExpr(len - 1, force); e.push(rand(evalPostfix(e, 0.6) == 1 ? ["sin", "exp"] : allowed.unary)); return e; } else { const k = Math.floor(1 + (len - 2) * Math.random()); const f = force && Math.random() > 0.5; const a = generateExpr(k, f); const b = generateExpr(len - 1 - k, force && !f); const one = evalPostfix(a, 0.6) == 1 || evalPostfix(b, 0.6) == 1; const e = a.concat(b) e.push(rand(one ? ["+", "-"] : allowed.binary)); return e; } } function drawAxes() { ctx.clearRect(0, 0, width, height); ctx.beginPath(); const y0 = mapY(0); ctx.moveTo(0, y0); ctx.lineTo(width, y0); const x0 = mapX(0); ctx.moveTo(x0, 0); ctx.lineTo(x0, height); [1, -1].forEach(val => { const px = mapX(val); ctx.moveTo(px, y0 - 5); ctx.lineTo(px, y0 + 5); }); [1, -1].forEach(val => { const py = mapY(val); ctx.moveTo(x0 - 5, py); ctx.lineTo(x0 + 5, py); }); ctx.lineWidth = 1; ctx.strokeStyle = darkTheme ? "white" : "black"; ctx.stroke(); } function drawCurve(ys, color, width = 4) { ctx.beginPath(); for (let i = 0; i < xs.length; i++) { const px = mapX(xs[i]), py = mapY(ys[i]); if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.lineWidth = width; ctx.strokeStyle = color; ctx.stroke(); } function redrawAll() { drawAxes(); drawCurve(targetYs, darkTheme ? "lime" : "green"); guessYsList.forEach((ys, idx) => drawCurve(ys, "red", (idx < guessYsList.length - 1) ? 1 : 4)); } function markRow(tokens) { const freq = {}; for (const t of target) freq[t] = (freq[t] || 0) + 1; const colors = Array(tl).fill("grey"); for (let i = 0; i < tokens.length; i++) { if (tokens[i] === target[i]) { colors[i] = "green"; freq[tokens[i]]--; } } for (let i = 0; i < tokens.length; i++) { if (colors[i] === "grey" && freq[tokens[i]] > 0) { colors[i] = "yellow"; freq[tokens[i]]--; } } const row = grid.rows[currentRow]; for (let i = 0; i < tl; i++) { const cell = row.cells[i]; cell.textContent = tokens[i]; cell.classList.add(colors[i]); } } function mapX(x) { return (x + bound) / (2 * bound) * width; } function mapY(y) { return height - (y - yMin) / (yMax - yMin) * height; } function closeEnough(a, b) { for (let i = 0; i < a.length; i++) { if (isNaN(a[i]) && !isNaN(b[i])) return false; if (Math.abs(a[i] - b[i]) > 0.1 * (Math.abs(b[i]) + 1)) return false; } return true; } </script> </body> </html>