Skip to content

Commit d99503a

Browse files
authored
Merge pull request #68 from shwetharbaliga/main
Add Step-by-Step Execution Mode, Speed Control Slider, and Clear Canvas Feature to KidCode Web
2 parents 253def8 + f624d93 commit d99503a

File tree

3 files changed

+229
-38
lines changed

3 files changed

+229
-38
lines changed

kidcode-web/src/main/resources/static/app.js

Lines changed: 180 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
// --- 1. GET REFERENCES TO OUR HTML ELEMENTS ---
44
const runButton = document.getElementById("run-button");
5+
const clearButton = document.getElementById("clear-btn");
56
const editorContainer = document.getElementById("editor-container");
67
const drawingCanvas = document.getElementById("drawing-canvas");
78
const outputArea = document.getElementById("output-area");
@@ -17,6 +18,68 @@ const visualPanel = document.querySelector(".visual-panel");
1718
// --- Key for browser's local storage ---
1819
const KIDCODE_STORAGE_KEY = "kidcode.savedCode";
1920

21+
const speedRange = document.getElementById("speedRange");
22+
const speedLabel = document.getElementById("speedLabel");
23+
24+
const speedText = {
25+
"0": "Step-by-Step",
26+
"1": "Normal",
27+
"2": "Fast",
28+
};
29+
30+
function updateSpeedUI() {
31+
if (!speedRange || !speedLabel) return;
32+
speedLabel.textContent = speedText[speedRange.value] || "Normal";
33+
}
34+
35+
if (speedRange) {
36+
speedRange.addEventListener("input", updateSpeedUI);
37+
updateSpeedUI();
38+
}
39+
40+
// Step-by-step control using keyboard
41+
let nextResolve = null;
42+
let stepModalShown = false;
43+
44+
45+
function waitForNextKey() {
46+
return new Promise((resolve) => {
47+
nextResolve = resolve;
48+
});
49+
}
50+
51+
52+
// Step modal elements
53+
const stepModal = document.getElementById("step-modal");
54+
const closeStepModalBtn = document.getElementById("close-step-modal");
55+
56+
if (closeStepModalBtn) {
57+
const closeModal = () => {
58+
stepModal.classList.add("hidden");
59+
stepModal.dispatchEvent(new Event("closed"));
60+
};
61+
62+
closeStepModalBtn.addEventListener("click", closeModal);
63+
64+
window.addEventListener("keydown", (e) => {
65+
if ((e.key === "Enter" || e.key === "Escape") && !stepModal.classList.contains("hidden")) {
66+
closeModal();
67+
}
68+
});
69+
}
70+
71+
72+
window.addEventListener("keydown", (e) => {
73+
const isInMonaco = document.activeElement?.closest('.monaco-editor'); // detect if typing in editor
74+
75+
if (e.key === "Enter" && nextResolve && !isInMonaco) {
76+
e.preventDefault();
77+
nextResolve();
78+
nextResolve = null;
79+
}
80+
});
81+
82+
2083
// --- MONACO: Global variable to hold the editor instance ---
2184
let editor;
2285
let validationTimeout;
@@ -174,13 +237,26 @@ function initializeExamples() {
174237
});
175238
}
176239

240+
let isExecuting = false;
241+
177242
// --- 2. ADD EVENT LISTENER TO THE RUN BUTTON ---
178243
// --- 1️⃣ Event listener for Run button ---
179244
runButton.addEventListener("click", async () => {
245+
if (isExecuting) {
246+
logToOutput("Execution already in progress. Please wait.", "error");
247+
return;
248+
}
249+
isExecuting = true;
250+
runButton.disabled = true;
251+
if (clearButton) clearButton.disabled = true;
252+
180253
const code = editor.getValue();
181254

182-
// Always start with a fresh canvas before execution
255+
// Always start with a fresh canvas before execution
183256
clearCanvas();
257+
drawnLines = [];
258+
codyState = { x: 250, y: 250, direction: 0, color: "blue" };
259+
stepModalShown = false;
184260
outputArea.textContent = "";
185261

186262
try {
@@ -195,12 +271,31 @@ runButton.addEventListener("click", async () => {
195271
}
196272

197273
const events = await response.json();
198-
renderEvents(events);
274+
await renderEvents(events);
199275
} catch (error) {
200276
logToOutput(`Network or server error: ${error.message}`, "error");
277+
}finally {
278+
isExecuting = false;
279+
nextResolve = null;
280+
runButton.disabled = false;
281+
if (clearButton) clearButton.disabled = false;
282+
editor.focus();
283+
}
284+
});
285+
286+
clearButton.addEventListener("click", () => {
287+
try {
288+
clearCanvas(); // wipes Cody's canvas
289+
outputArea.textContent = ""; // clears the log area
290+
drawnLines = [];
291+
codyState = { x: 250, y: 250, direction: 0, color: "blue" };
292+
logToOutput("Canvas cleared");
293+
} catch (error) {
294+
logToOutput(`Error while clearing: ${error.message}`, "error");
201295
}
202296
});
203297

298+
204299
// --- NEW: Event listener for Download button ---
205300
if (downloadButton) {
206301
downloadButton.addEventListener("click", () => {
@@ -282,46 +377,94 @@ function logToOutput(message, type = "info") {
282377
let drawnLines = [];
283378
let codyState = { x: 250, y: 250, direction: 0, color: "blue" };
284379

285-
function renderEvents(events) {
286-
if (!events || events.length === 0) return;
287-
288-
for (const event of events) {
289-
switch (event.type) {
290-
case "ClearEvent":
291-
drawnLines = [];
292-
codyState = { x: 250, y: 250, direction: 0, color: "blue" };
293-
break;
294-
case "MoveEvent":
295-
if (
296-
event.isPenDown &&
297-
(event.fromX !== event.toX || event.fromY !== event.toY)
298-
) {
299-
drawnLines.push({
300-
fromX: event.fromX,
301-
fromY: event.fromY,
302-
toX: event.toX,
303-
toY: event.toY,
304-
color: event.color,
305-
});
306-
}
307-
codyState = {
308-
x: event.toX,
309-
y: event.toY,
310-
direction: event.newDirection,
311-
color: event.color,
380+
381+
382+
async function renderEvents(events) {
383+
384+
try {
385+
if (!events || events.length === 0) return;
386+
const initialSpeed = parseInt(speedRange.value, 10);
387+
if (initialSpeed === 0 && stepModal && !stepModalShown) {
388+
stepModalShown = true;
389+
stepModal.classList.remove("hidden");
390+
391+
// Wait for the modal to be closed (event-driven)
392+
await new Promise((resolve) => {
393+
const onClose = () => {
394+
stepModal.removeEventListener("closed", onClose);
395+
resolve();
312396
};
313-
break;
314-
case "SayEvent":
315-
logToOutput(`Cody says: ${event.message}`);
316-
break;
317-
case "ErrorEvent":
318-
logToOutput(`ERROR: ${event.errorMessage}`, "error");
319-
break;
397+
stepModal.addEventListener("closed", onClose, { once: true });
398+
});
320399
}
400+
401+
for (const event of events) {
402+
const speed = parseInt(speedRange.value, 10);
403+
const delay = speed === 0 ? null : (speed === 1 ? 300 : 80);
404+
switch (event.type) {
405+
case "ClearEvent":
406+
drawnLines = [];
407+
codyState = { x: 250, y: 250, direction: 0, color: "blue" };
408+
break;
409+
410+
case "MoveEvent":
411+
if (
412+
event.isPenDown &&
413+
(event.fromX !== event.toX || event.fromY !== event.toY)
414+
) {
415+
drawnLines.push({
416+
fromX: event.fromX,
417+
fromY: event.fromY,
418+
toX: event.toX,
419+
toY: event.toY,
420+
color: event.color,
421+
});
422+
}
423+
codyState = {
424+
x: event.toX,
425+
y: event.toY,
426+
direction: event.newDirection,
427+
color: event.color,
428+
};
429+
break;
430+
431+
case "SayEvent":
432+
logToOutput(`Cody says: ${event.message}`);
433+
break;
434+
435+
case "ErrorEvent":
436+
logToOutput(`ERROR: ${event.errorMessage}`, "error");
437+
break;
438+
}
439+
440+
redrawCanvas();
441+
442+
if (speed === 0) {
443+
// Show step modal if user switched to step mode mid-execution
444+
if (stepModal && !stepModalShown) {
445+
stepModalShown = true;
446+
stepModal.classList.remove("hidden");
447+
await new Promise((resolve) => {
448+
const onClose = () => {
449+
stepModal.removeEventListener("closed", onClose);
450+
resolve();
451+
};
452+
stepModal.addEventListener("closed", onClose, { once: true });
453+
});
454+
}
455+
await waitForNextKey(); // step mode
456+
} else {
457+
await new Promise((resolve) => setTimeout(resolve, delay));
458+
}
459+
}
321460
}
322-
redrawCanvas();
461+
catch (error) {
462+
logToOutput(`Rendering error: ${error.message}`, "error");
463+
throw error;
464+
}
323465
}
324466

467+
325468
function redrawCanvas() {
326469
ctx.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height);
327470
drawnLines.forEach((line) => {

kidcode-web/src/main/resources/static/index.html

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ <h3>Code Editor</h3>
3030
<option value="">Select an Example</option>
3131
</select>
3232
</div>
33+
<label for="speedRange">Speed: <span id="speedLabel">Normal</span></label>
34+
<input type="range" id="speedRange" min="0" max="2" step="1" value="1" aria-label="Execution speed control: Step-by-Step, Normal, or Fast">
3335
</div>
3436

3537
<div class="panel-controls">
@@ -60,7 +62,8 @@ <h3>Code Editor</h3>
6062
<div class="panel-header">
6163
<h3>Cody's Canvas</h3>
6264
<div>
63-
<button id="download-btn">Download Drawing</button>
65+
<button id="clear-btn" aria-label="Clear canvas and output log">Clear</button>
66+
<button id="download-btn">Download Drawing</button>
6467
</div>
6568
</div>
6669
<canvas id="drawing-canvas" width="500" height="500"></canvas>
@@ -249,6 +252,18 @@ <h3>6. Lists</h3>
249252
</div>
250253
</div>
251254
</div>
255+
<div id="step-modal" class="modal hidden">
256+
<div class="modal-content" style="max-width:420px; text-align:center;">
257+
<h2 style="margin-top:0;">Step Mode</h2>
258+
<p style="margin:0 0 1rem;">
259+
In Step mode, Cody advances one event at a time. Press <b>Enter</b> to move to the next step.
260+
</p>
261+
<button id="close-step-modal" class="animated-btn run-btn" style="padding:8px 14px;">
262+
Got it
263+
</button>
264+
</div>
265+
</div>
266+
252267
<script src="examples.js"></script>
253268
<script src="app.js"></script>
254269
</body>

kidcode-web/src/main/resources/static/style.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,3 +447,36 @@ footer {
447447
transform: translateY(0) scale(0.98);
448448
box-shadow: 0 3px 6px rgba(243, 156, 18, 0.2);
449449
}
450+
451+
452+
#clear-btn {
453+
background: linear-gradient(145deg, #74b9ff 0%, #0984e3 100%);
454+
color: white;
455+
border: none;
456+
border-radius: 10px;
457+
padding: 10px 20px;
458+
font-weight: 600;
459+
cursor: pointer;
460+
box-shadow: 0 4px 8px rgba(9, 132, 227, 0.25);
461+
transition: all 0.25s ease;
462+
font-family: 'Poppins', sans-serif;
463+
}
464+
465+
#clear-btn:hover {
466+
background: linear-gradient(145deg, #82ccff 0%, #0a74d3 100%);
467+
transform: translateY(-2px) scale(1.02);
468+
box-shadow: 0 6px 14px rgba(9, 132, 227, 0.35);
469+
}
470+
471+
#clear-btn:active {
472+
background: linear-gradient(145deg, #0a6acb 0%, #0756b0 100%);
473+
transform: translateY(0) scale(0.98);
474+
box-shadow: 0 3px 6px rgba(9, 132, 227, 0.2);
475+
}
476+
477+
#speedLabel {
478+
display: inline-block;
479+
min-width: 125px;
480+
text-align: left;
481+
}
482+

0 commit comments

Comments
 (0)