···
···
border: 2px solid #313244;
···
+
transition: background 0.1s ease;
···
background: rgba(166, 227, 161, 0.25);
from { box-shadow: 0 0 8px #a6e3a1, 0 0 16px #a6e3a1; }
to { box-shadow: 0 0 12px #a6e3a1, 0 0 24px #a6e3a1; }
···
let beamTrail = new Set();
const gridEl = document.getElementById('grid');
const stepEl = document.getElementById('step');
···
totalEl.textContent = getStages().length - 1;
+
gridEl.style.gridTemplateColumns = \`repeat(\${COLS}, 1fr)\`;
+
cellElements = new Array(ROWS * COLS);
+
for (let r = 0; r < ROWS; r++) {
+
for (let c = 0; c < COLS; c++) {
+
const cell = document.createElement('div');
+
const char = grid[r][c];
+
const idx = r * COLS + c;
+
cell.className = 'cell';
+
cell.dataset.char = char;
+
// Set static content once
+
cell.classList.add('start');
+
} else if (char === '^') {
+
cell.classList.add('splitter');
+
gridEl.appendChild(cell);
+
cellElements[idx] = cell;
const stages = getStages();
const stage = stages[currentStage];
+
let beamPositions = new Map();
for (const [key, count] of Object.entries(stage.positions)) {
···
+
// Only update changed cells
+
const prevBeamKeys = new Set();
+
if (currentStage > 0) {
+
const prevStage = stages[currentStage - 1];
+
Object.keys(prevStage.positions).forEach(k => prevBeamKeys.add(k));
+
prevStage.beams.forEach(([r, c]) => prevBeamKeys.add(r + ',' + c));
+
// Find cells that need updating
+
const toUpdate = new Set();
+
beamPositions.forEach((_, key) => toUpdate.add(key));
+
prevBeamKeys.forEach(key => toUpdate.add(key));
+
// Update only changed cells
+
for (const key of toUpdate) {
+
const [r, c] = key.split(',').map(Number);
+
const idx = r * COLS + c;
+
const cell = cellElements[idx];
+
const char = cell.dataset.char;
+
const count = beamPositions.get(key);
+
// Reset dynamic classes
+
cell.classList.remove('beam', 'beam-trail');
+
if (count !== undefined) {
+
cell.classList.add('beam');
+
// For Part 2, adjust opacity/intensity based on timeline count
+
if (isPart2 && count > 1) {
+
// Logarithmic scale for better visual range
+
const intensity = Math.min(1, Math.log10(count + 1) / 4);
+
cell.style.opacity = 0.3 + (intensity * 0.7);
+
// Only show small counts in upper portion to reduce clutter
+
if (count <= 99 && r < ROWS * 0.6) {
+
cell.textContent = count;
+
cell.style.opacity = '1';
+
} else if (beamTrail.has(key) && currentStage > 0) {
+
cell.classList.add('beam-trail');
+
cell.style.opacity = '1';
+
cell.style.opacity = '1';
+
cell.textContent = (char === 'S' || char === '^') ? char : '';
stepEl.textContent = currentStage;
···
const container = document.querySelector('.grid-container');
+
const containerWidth = container.clientWidth * 0.9; // Account for padding (10px + 10px) + border (2px + 2px)
+
// Calculate cell size based on width to fill the container
+
const cellWidth = Math.floor(containerWidth / COLS);
+
// Set a reasonable max size for readability
+
cellSize = Math.max(2, Math.min(cellWidth, 15));
+
const fontSize = Math.max(8, Math.min(cellSize * 0.7, 12));
+
for (let i = 0; i < cellElements.length; i++) {
+
cellElements[i].style.width = cellSize + 'px';
+
cellElements[i].style.height = cellSize + 'px';
+
cellElements[i].style.fontSize = fontSize + 'px';
···
+
if (animationFrameId) {
+
cancelAnimationFrame(animationFrameId);
+
animationFrameId = null;
+
playBtn.textContent = '▶ Play';
···
+
// Full re-render on mode change - reset all cells
+
for (let i = 0; i < cellElements.length; i++) {
+
const cell = cellElements[i];
+
const char = cell.dataset.char;
+
cell.classList.remove('beam', 'beam-trail');
+
cell.style.opacity = '1';
+
cell.textContent = (char === 'S' || char === '^') ? char : '';
···
nextBtn.addEventListener('click', () => goToStage(currentStage + 1));
resetBtn.addEventListener('click', resetAnimation);
+
let animationFrameId = null;
+
function animate(timestamp) {
+
const stages = getStages();
+
const speed = 1100 - parseInt(speedSlider.value);
+
if (timestamp - lastFrameTime >= speed) {
+
if (currentStage < stages.length - 1) {
+
goToStage(currentStage + 1);
+
lastFrameTime = timestamp;
+
playBtn.textContent = '▶ Play';
+
animationFrameId = null;
+
animationFrameId = requestAnimationFrame(animate);
playBtn.addEventListener('click', () => {
+
if (animationFrameId) {
+
cancelAnimationFrame(animationFrameId);
+
animationFrameId = null;
playBtn.textContent = '▶ Play';
const stages = getStages();
···
playBtn.textContent = '⏸ Pause';
+
animationFrameId = requestAnimationFrame(animate);
speedSlider.addEventListener('input', () => {
+
// Speed change is handled in the animate loop
document.addEventListener('keydown', (e) => {
···
+
window.addEventListener('resize', () => {
+
clearTimeout(resizeTimeout);
+
resizeTimeout = setTimeout(scaleGrid, 100);
···
await Bun.write(`${scriptDir}/index.html`, html);
console.log("Generated index.html");
+
stages1[stages1.length - 1]?.beams.length ?? 0,