Add blocks from the palette to start building your program.
`;
}
bcs_programBlocks.forEach((block, index) => {
const blockDiv = document.createElement('div');
blockDiv.className = `bcs-block p-3 rounded-md flex items-center justify-between space-x-4 ${BLOCK_COLORS[block.type.split('_')[0]]}`;
blockDiv.setAttribute('data-id', block.id);
// Block Info (Name + Value Input)
let valueInput = '';
if (block.hasOwnProperty('value')) {
valueInput = `
${block.type.includes('MOVE') ? 'steps' : (block.type.includes('TURN') ? 'degrees' : 'times')}
`;
}
let indentClass = '';
if (block.type === 'LOOP_END') indentClass = 'ml-8';
if (block.type !== 'LOOP_START' && block.type !== 'LOOP_END') {
// Check if inside a loop
let indentLevel = 0;
for (let i = 0; i < index; i++) {
if (bcs_programBlocks[i].type === 'LOOP_START') indentLevel++;
if (bcs_programBlocks[i].type === 'LOOP_END') indentLevel--;
}
if (indentLevel > 0) indentClass = `ml-${indentLevel * 8}`;
}
blockDiv.innerHTML = `
${block.name}
${valueInput}
`;
bcs_programList.appendChild(blockDiv);
});
}
/**
* Handles adding a new block from the palette
*/
function bcs_addBlock(type) {
const newBlock = { id: bcs_blockIdCounter++, ...BLOCK_DEFAULTS[type] };
bcs_programBlocks.push(newBlock);
bcs_renderProgramList();
}
// --- SIMULATOR CANVAS FUNCTIONS ---
/**
* Resizes the canvas to fit its container
*/
function bcs_resizeCanvas() {
if (!bcs_canvas) return;
bcs_canvas.width = bcs_canvas.clientWidth;
bcs_canvas.height = bcs_canvas.clientHeight;
}
/**
* Resets the simulator to its initial state
*/
function bcs_resetSimulator() {
if (!bcs_canvas || !bcs_ctx) return;
bcs_ctx.clearRect(0, 0, bcs_canvas.width, bcs_canvas.height);
bcs_sprite = {
x: bcs_canvas.width / 2,
y: bcs_canvas.height / 2,
angle: -90, // Start facing "up"
isPenDown: false
};
bcs_path = [{ x: bcs_sprite.x, y: bcs_sprite.y }];
bcs_drawSprite();
}
/**
* Draws the sprite (a triangle) on the canvas
*/
function bcs_drawSprite() {
if (!bcs_ctx) return;
const { x, y, angle } = bcs_sprite;
const angleRad = angle * Math.PI / 180;
bcs_ctx.save();
bcs_ctx.translate(x, y);
bcs_ctx.rotate(angleRad);
bcs_ctx.beginPath();
bcs_ctx.moveTo(0, -10); // Tip
bcs_ctx.lineTo(6, 7); // Bottom right
bcs_ctx.lineTo(-6, 7); // Bottom left
bcs_ctx.closePath();
bcs_ctx.fillStyle = bcs_isRunning ? '#e11d48' : '#0284c7'; // Red when running, blue when idle
bcs_ctx.fill();
bcs_ctx.restore();
}
/**
* Draws the entire path stored in the bcs_path array
*/
function bcs_drawPath() {
if (!bcs_ctx || bcs_path.length < 2) return;
bcs_ctx.beginPath();
bcs_ctx.moveTo(bcs_path[0].x, bcs_path[0].y);
for (let i = 1; i < bcs_path.length; i++) {
bcs_ctx.lineTo(bcs_path[i].x, bcs_path[i].y);
}
bcs_ctx.strokeStyle = '#0f172a'; // Black
bcs_ctx.lineWidth = 2;
bcs_ctx.stroke();
}
/**
* Main program execution logic
*/
async function bcs_runProgram() {
if (bcs_isRunning) return;
bcs_isRunning = true;
bcs_runButton.disabled = true;
bcs_runButton.classList.add('opacity-50', 'cursor-not-allowed');
bcs_resetSimulator();
const loopContext = []; // Stack for loop info
for (let i = 0; i < bcs_programBlocks.length; i++) {
const block = bcs_programBlocks[i];
// --- Update Sprite State (Model) ---
switch (block.type) {
case 'MOVE_FORWARD': {
const angleRad = bcs_sprite.angle * Math.PI / 180;
const newX = bcs_sprite.x + block.value * Math.cos(angleRad);
const newY = bcs_sprite.y + block.value * Math.sin(angleRad);
bcs_sprite.x = newX;
bcs_sprite.y = newY;
if (bcs_sprite.isPenDown) {
bcs_path.push({ x: newX, y: newY });
} else {
if (bcs_path.length > 0) bcs_path = [{ x: newX, y: newY }];
}
break;
}
case 'TURN_RIGHT':
bcs_sprite.angle += block.value;
break;
case 'TURN_LEFT':
bcs_sprite.angle -= block.value;
break;
case 'PEN_DOWN':
bcs_sprite.isPenDown = true;
bcs_path = [{ x: bcs_sprite.x, y: bcs_sprite.y }];
break;
case 'PEN_UP':
bcs_sprite.isPenDown = false;
break;
case 'LOOP_START':
loopContext.push({ count: block.value, index: i, current: 0 });
break;
case 'LOOP_END': {
if (loopContext.length > 0) {
const loop = loopContext[loopContext.length - 1];
loop.current++;
if (loop.current < loop.count) {
i = loop.index; // Jump back to loop start
} else {
loopContext.pop(); // Loop finished
}
}
break;
}
}
// --- Render State (View) ---
bcs_ctx.clearRect(0, 0, bcs_canvas.width, bcs_canvas.height);
bcs_drawPath();
bcs_drawSprite();
await bcs_sleep(100);
}
bcs_isRunning = false;
bcs_runButton.disabled = false;
bcs_runButton.classList.remove('opacity-50', 'cursor-not-allowed');
bcs_drawSprite(); // Draw in idle color
}
/**
* Generates and downloads a PDF of the program and canvas
*/
async function bcs_downloadPDF() {
if (typeof jspdf === 'undefined' || typeof html2canvas === 'undefined') {
console.error("BCS Tool Error: jsPDF or html2canvas library not loaded.");
alert("Error: PDF libraries failed to load. Please check console.");
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
// --- Page 1: Program Code ---
doc.setFontSize(18);
doc.text("Block Coding Simulator Report", 40, 60);
doc.setFontSize(14);
doc.text("Program Code:", 40, 90);
doc.setFontSize(10);
doc.setFont("courier", "normal");
let y = 110;
let indent = 0;
bcs_programBlocks.forEach((block, index) => {
if (y > 780) { // Page break
doc.addPage();
y = 60;
}
if (block.type === 'LOOP_END') indent--;
const indentStr = " ".repeat(Math.max(0, indent));
const valueStr = block.hasOwnProperty('value') ? `(${block.value})` : '';
doc.text(`${index + 1}. ${indentStr}${block.name} ${valueStr}`, 50, y);
y += 15;
if (block.type === 'LOOP_START') indent++;
});
// --- Page 2: Canvas Image ---
doc.addPage();
doc.setFont("helvetica", "normal");
doc.setFontSize(14);
doc.text("Final Simulation Output:", 40, 60);
try {
const canvas = document.getElementById('bcs-canvas');
const canvasImg = await html2canvas(canvas);
const imgData = canvasImg.toDataURL('image/png');
const pdfWidth = doc.internal.pageSize.getWidth();
const pdfHeight = doc.internal.pageSize.getHeight();
const margin = 40;
const imgWidth = canvasImg.width;
const imgHeight = canvasImg.height;
const ratio = Math.min((pdfWidth - margin * 2) / imgWidth, (pdfHeight - 100) / imgHeight);
const w = imgWidth * ratio;
const h = imgHeight * ratio;
const x = (pdfWidth - w) / 2; // Center horizontally
doc.addImage(imgData, 'PNG', x, 80, w, h);
} catch (error) {
console.error("BCS Tool Error: PDF canvas capture failed.", error);
doc.text("Error capturing canvas image.", 40, 80);
}
doc.save('Block_Coding_Report.pdf');
}
// --- EVENT LISTENERS ---
// Tab link clicks
bcs_tabLinks.forEach((link, index) => {
link.addEventListener('click', () => bcs_switchTab(index));
});
// Next/Prev button clicks
if (bcs_prevButton) {
bcs_prevButton.addEventListener('click', () => {
if (bcs_currentTab > 0) bcs_switchTab(bcs_currentTab - 1);
});
}
if (bcs_nextButton) {
bcs_nextButton.addEventListener('click', () => {
if (bcs_currentTab < bcs_tabLinks.length - 1) bcs_switchTab(bcs_currentTab + 1);
});
}
// PDF download
if (bcs_downloadPdfButton) {
bcs_downloadPdfButton.addEventListener('click', bcs_downloadPDF);
}
// Simulator controls
if (bcs_runButton) bcs_runButton.addEventListener('click', bcs_runProgram);
if (bcs_resetButton) bcs_resetButton.addEventListener('click', bcs_resetSimulator);
// Palette buttons
bcs_addBlockButtons.forEach(btn => {
btn.addEventListener('click', () => {
bcs_addBlock(btn.getAttribute('data-type'));
});
});
// Program list actions (Event Delegation)
if (bcs_programList) {
bcs_programList.addEventListener('click', (e) => {
const button = e.target.closest('button');
if (!button) return;
const blockDiv = button.closest('.bcs-block');
const id = parseInt(blockDiv.getAttribute('data-id'));
const index = parseInt(button.getAttribute('data-index'));
if (button.classList.contains('bcs-remove-btn')) {
bcs_programBlocks = bcs_programBlocks.filter(b => b.id !== id);
}
if (button.classList.contains('bcs-move-up-btn') && index > 0) {
[bcs_programBlocks[index], bcs_programBlocks[index - 1]] =
[bcs_programBlocks[index - 1], bcs_programBlocks[index]];
}
if (button.classList.contains('bcs-move-down-btn') && index < bcs_programBlocks.length - 1) {
[bcs_programBlocks[index], bcs_programBlocks[index + 1]] =
[bcs_programBlocks[index + 1], bcs_programBlocks[index]];
}
bcs_renderProgramList();
});
// Handle value changes
bcs_programList.addEventListener('change', (e) => {
if (e.target.classList.contains('bcs-block-value')) {
const id = parseInt(e.target.getAttribute('data-id'));
const newValue = parseInt(e.target.value, 10);
const block = bcs_programBlocks.find(b => b.id === id);
if (block) {
block.value = newValue;
}
}
});
}
// Window resize
window.addEventListener('resize', () => {
bcs_resizeCanvas();
bcs_resetSimulator();
});
// --- INITIALIZATION ---
bcs_resizeCanvas();
bcs_loadSampleProgram();
bcs_renderProgramList();
bcs_resetSimulator();
// Set initial tab state
bcs_tabPanes.forEach((pane, index) => {
pane.classList.toggle('hidden', index !== 0);
pane.classList.toggle('bcs-active', index === 0);
});
});