Take input by keypresses and animate
dkl9

dkl9 commited on 2025-209 17:22:12
Showing 1 changed files, with 165 additions and 93 deletions.

... ...
@@ -6,13 +6,20 @@
6 6
             body { font-family: sans-serif; }
7 7
             #plot { border: 1px solid grey; flex-shrink: 0; height: 400px; }
8 8
             #grid { border-collapse: separate; border-spacing: 3px 12px; margin-top: 10px; }
9
-            #grid td { min-width: 40px; height: 40px; border: 1px solid #ccc; text-align: center; vertical-align: middle; font-weight: bold; }
9
+            #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; }
10 10
             .grey { background: #3a3a3c; color: white; }
11 11
             .yellow { background: #b59f3b; color: white; }
12 12
             .green { background: #538e4e; color: white; }
13 13
             #message { color: red; margin-top: 5px; }
14 14
             .side { margin-left: 20px; }
15
-            #guessInput { width: 6em; }
15
+            .shake { animation: shake 0.3s; }
16
+            @keyframes shake {
17
+                0% { transform: translateX(0); }
18
+                25% { transform: translateX(-5px); }
19
+                50% { transform: translateX(5px); }
20
+                75% { transform: translateX(-5px); }
21
+                100% { transform: translateX(0); }
22
+            }
16 23
             @media (min-aspect-ratio: 4/3) { .container { display: flex; } }
17 24
             @media (prefers-color-scheme: dark) { body { color: white; background-color: black; } }
18 25
         </style>
... ...
@@ -22,8 +29,6 @@
22 29
             <canvas id="plot" width="400" height="400"></canvas>
23 30
             <div class="side">
24 31
                 <div>
25
-                    <input id="guessInput" placeholder="token1 token2 ... token5">
26
-                    <button id="submitBtn">Guess</button>
27 32
                     <div id="message"></div>
28 33
                 </div>
29 34
                 <table id="grid"></table>
... ...
@@ -34,7 +39,7 @@
34 39
                 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>
35 40
                 <p>Enter your answer in postfix (reverse Polish) notation.
36 41
                 For example, express 1 - <var>x</var> / (sin <var>x</var>) as "1 x x sin / -".</p>
37
-                <p>The correct answer will be six tokens.
42
+                <p>The correct answer will be five tokens.
38 43
                 Numerically equivalent rearranged answers will be accepted.</p>
39 44
                 <p>Enhanced from a prototype by ChatGPT o4-mini.</p>
40 45
             </div>
... ...
@@ -47,6 +52,153 @@
47 52
             };
48 53
             const allTokens = [...allowed.terminals, ...allowed.unary, ...allowed.binary];
49 54
             const tl = 5;
55
+            const keyToToken = { x:"x", 1:"1", 2:"2", q:"^2", c:"^3", s:"sin", e:"exp", l:"ln", "+":"+", "-":"-", "*":"*", "/":"/" };
56
+            const target = generateExpr();
57
+            const maxGuesses = 6;
58
+            const width = 400, height = 400;
59
+            const ctx = document.getElementById("plot").getContext("2d");
60
+            const bound = 10;
61
+            const xs = [];
62
+            const targetYs = [];
63
+            for (let i = 0; i <= width; i++) {
64
+                const x = bound * (2 * i / width - 1);
65
+                xs.push(x);
66
+                const yv = evalPostfix(target, x);
67
+                targetYs.push(yv);
68
+            }
69
+            const clampLow = 1.5;
70
+            const clampHigh = 20;
71
+            let yMin = Math.min(...targetYs);
72
+            let yMax = Math.max(...targetYs);
73
+            if (isNaN(yMin) || yMin < -clampHigh) yMin = -clampHigh;
74
+            else if (yMin > -clampLow) yMin = -clampLow;
75
+            if (isNaN(yMax) || yMax > clampHigh) yMax = clampHigh;
76
+            else if (yMax < clampLow) yMax = clampLow;
77
+            const guessYsList = [];
78
+            const darkTheme = matchMedia('(prefers-color-scheme: dark)').matches;
79
+
80
+            function shakeRow(row) {
81
+                row.classList.remove("shake");
82
+                void row.offsetWidth;
83
+                row.classList.add("shake");
84
+            }
85
+
86
+            const grid = document.getElementById("grid");
87
+            for (let i = 0; i < 6; i++) {
88
+                const tr = document.createElement("tr");
89
+                for (let j = 0; j < tl; j++) {
90
+                    const td = document.createElement("td");
91
+                    tr.appendChild(td);
92
+                }
93
+                grid.appendChild(tr);
94
+            }
95
+
96
+            let currentRow = 0;
97
+            let currentInput = [];
98
+            redrawAll();
99
+
100
+            document.addEventListener("keydown", e => {
101
+                const row = grid.rows[currentRow];
102
+                if (e.key === "Backspace") {
103
+                    if (currentInput.length > 0) {
104
+                        currentInput.pop();
105
+                        row.cells[currentInput.length].textContent = "";
106
+                    } else {
107
+                        shakeRow(row);
108
+                    }
109
+                } else if (e.key === "Enter") {
110
+                    submitGuess();
111
+                } else if (keyToToken.hasOwnProperty(e.key)) {
112
+                    if (currentInput.length < tl) {
113
+                        const token = keyToToken[e.key];
114
+                        currentInput.push(token);
115
+                        const c = row.cells[currentInput.length - 1];
116
+                        c.style.fontSize = `${3.5 - 0.5 * token.length}em`;
117
+                        c.textContent = token;
118
+                    } else {
119
+                        shakeRow(row);
120
+                    }
121
+                }
122
+            });
123
+
124
+            function submitGuess() {
125
+                const row = grid.rows[currentRow];
126
+                document.getElementById("message").textContent = "";
127
+                if (currentInput.length !== tl) {
128
+                    document.getElementById("message").textContent = `Use exactly ${tl} tokens.`;
129
+                    shakeRow(row);
130
+                    return;
131
+                }
132
+                for (const t of currentInput) {
133
+                    if (!allTokens.includes(t)) {
134
+                        document.getElementById("message").textContent = "Invalid token: " + t;
135
+                        shakeRow(row);
136
+                        return;
137
+                    }
138
+                }
139
+                let ys;
140
+                try {
141
+                    ys = xs.map(x => evalPostfix(currentInput, x));
142
+                } catch (e) {
143
+                    document.getElementById("message").textContent = "Syntax error.";
144
+                    shakeRow(row);
145
+                    return;
146
+                }
147
+                guessYsList.push(ys);
148
+                redrawAll();
149
+                const close = closeEnough(ys, targetYs);
150
+                animateFlip(currentInput, close);
151
+                if (close) {
152
+                    setTimeout(() => fillRow(currentInput, "green"), 300 * tl);
153
+                    return;
154
+                }
155
+                currentRow++;
156
+                currentInput = [];
157
+                if (currentRow >= 6) return;
158
+            }
159
+
160
+            function animateFlip(tokens, close) {
161
+                const row = grid.rows[currentRow];
162
+                const colors = Array(tl);
163
+                if (close) {
164
+                    colors.fill("green");
165
+                } else {
166
+                    const freq = {};
167
+                    for (const t of target) freq[t] = (freq[t] || 0) + 1;
168
+                    colors.fill("grey");
169
+                    for (let i = 0; i < tl; i++) {
170
+                        if (tokens[i] === target[i]) {
171
+                            colors[i] = "green";
172
+                            freq[tokens[i]]--;
173
+                        }
174
+                    }
175
+                    for (let i = 0; i < tl; i++) {
176
+                        if (colors[i] === "grey" && freq[tokens[i]] > 0) {
177
+                            colors[i] = "yellow";
178
+                            freq[tokens[i]]--;
179
+                        }
180
+                    }
181
+                }
182
+                tokens.forEach((token, i) => {
183
+                    const cell = row.cells[i];
184
+                    setTimeout(() => {
185
+                        cell.style.transform = "rotateX(90deg)";
186
+                        setTimeout(() => {
187
+                            cell.classList.add(colors[i]);
188
+                            cell.style.transform = "rotateX(0deg)";
189
+                        }, 150);
190
+                    }, 150 * i);
191
+                });
192
+            }
193
+
194
+            function fillRow(tokens, cls) {
195
+                const row = grid.rows[currentRow];
196
+                for (let i = 0; i < tl; i++) {
197
+                    const cell = row.cells[i];
198
+                    cell.textContent = tokens[i];
199
+                    cell.classList.add(cls);
200
+                }
201
+            }
50 202
 
51 203
             function evalPostfix(tokens, x) {
52 204
                 const s = [];
... ...
@@ -98,31 +250,6 @@
98 250
                 }
99 251
             }
100 252
 
101
-            const target = generateExpr();
102
-            const maxGuesses = 6;
103
-            let currentRow = 0;
104
-            const width = 400, height = 400;
105
-            const ctx = document.getElementById("plot").getContext("2d");
106
-            const bound = 10;
107
-            const xs = [];
108
-            const targetYs = [];
109
-            for (let i = 0; i <= width; i++) {
110
-                const x = bound * (2 * i / width - 1);
111
-                xs.push(x);
112
-                const yv = evalPostfix(target, x);
113
-                targetYs.push(yv);
114
-            }
115
-            const clampLow = 1.5;
116
-            const clampHigh = 20;
117
-            let yMin = Math.min(...targetYs);
118
-            let yMax = Math.max(...targetYs);
119
-            if (isNaN(yMin) || yMin < -clampHigh) yMin = -clampHigh;
120
-            else if (yMin > -clampLow) yMin = -clampLow;
121
-            if (isNaN(yMax) || yMax > clampHigh) yMax = clampHigh;
122
-            else if (yMax < clampLow) yMax = clampLow;
123
-            const guessYsList = [];
124
-            const darkTheme = matchMedia('(prefers-color-scheme: dark)').matches;
125
-
126 253
             function drawAxes() {
127 254
                 ctx.clearRect(0, 0, width, height);
128 255
                 ctx.beginPath();
... ...
@@ -165,69 +292,6 @@
165 292
                 guessYsList.forEach((ys, idx) => drawCurve(ys, "red", (idx < guessYsList.length - 1) ? 1 : 4));
166 293
             }
167 294
 
168
-            redrawAll();
169
-            
170
-            const grid = document.getElementById("grid");
171
-            for (let i = 0; i < maxGuesses; i++) {
172
-                const tr = document.createElement("tr");
173
-                for (let j = 0; j < tl; j++) {
174
-                    const td = document.createElement("td");
175
-                    tr.appendChild(td);
176
-                }
177
-                grid.appendChild(tr);
178
-            }
179
-            
180
-            document.getElementById("submitBtn").onclick = () => {
181
-                const inp = document.getElementById("guessInput").value.trim().split(/\s+/);
182
-                document.getElementById("message").textContent = "";
183
-                if (inp.length !== tl) {
184
-                    document.getElementById("message").textContent = `Use exactly ${tl} tokens.`;
185
-                    return;
186
-                }
187
-                for (const t of inp) {
188
-                    if (!allTokens.includes(t)) {
189
-                        document.getElementById("message").textContent = "Invalid token: " + t;
190
-                        return;
191
-                    }
192
-                }
193
-                let ys;
194
-                try {
195
-                    ys = xs.map(x => evalPostfix(inp, x));
196
-                } catch (e) {
197
-                    document.getElementById("message").textContent = "Syntax error.";
198
-                    return;
199
-                }
200
-                if (closeEnough(ys, targetYs)) {
201
-                    fillRow(inp, "green");
202
-                    document.getElementById("submitBtn").disabled = true;
203
-                    return;
204
-                }
205
-                markRow(inp);
206
-                guessYsList.push(ys);
207
-                redrawAll();
208
-                currentRow++;
209
-                if (currentRow >= maxGuesses) {
210
-                    document.getElementById("submitBtn").disabled = true;
211
-                }
212
-            };
213
-            
214
-            function closeEnough(a, b) {
215
-                for (let i = 0; i < a.length; i++) {
216
-                    if (isNaN(a[i]) && !isNaN(b[i])) return false;
217
-                    if (Math.abs(a[i] - b[i]) > 0.1 * (Math.abs(b[i]) + 1)) return false;
218
-                }
219
-                return true;
220
-            }
221
-            
222
-            function fillRow(tokens, cls) {
223
-                const row = grid.rows[currentRow];
224
-                for (let i = 0; i < tl; i++) {
225
-                    const cell = row.cells[i];
226
-                    cell.textContent = tokens[i];
227
-                    cell.classList.add(cls);
228
-                }
229
-            }
230
-            
231 295
             function markRow(tokens) {
232 296
                 const freq = {};
233 297
                 for (const t of target) freq[t] = (freq[t] || 0) + 1;
... ...
@@ -254,6 +318,14 @@
254 318
 
255 319
             function mapX(x) { return (x + bound) / (2 * bound) * width; }
256 320
             function mapY(y) { return height - (y - yMin) / (yMax - yMin) * height; }
321
+
322
+            function closeEnough(a, b) {
323
+                for (let i = 0; i < a.length; i++) {
324
+                    if (isNaN(a[i]) && !isNaN(b[i])) return false;
325
+                    if (Math.abs(a[i] - b[i]) > 0.1 * (Math.abs(b[i]) + 1)) return false;
326
+                }
327
+                return true;
328
+            }
257 329
         </script>
258 330
     </body>
259 331
 </html>
260 332