···
···
border: 2px solid #313244;
···
270
-
transition: background 0.15s ease;
268
+
transition: background 0.1s ease;
···
284
-
box-shadow: 0 0 8px #a6e3a1, 0 0 16px #a6e3a1;
285
-
animation: pulse 0.5s ease-in-out infinite alternate;
background: rgba(166, 227, 161, 0.25);
295
-
.cell.beam .count {
296
-
text-shadow: 0 0 2px #cdd6f4;
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();
429
+
let cellElements = [];
const gridEl = document.getElementById('grid');
const stepEl = document.getElementById('step');
···
totalEl.textContent = getStages().length - 1;
468
+
function initGrid() {
469
+
gridEl.style.gridTemplateColumns = \`repeat(\${COLS}, 1fr)\`;
470
+
cellElements = new Array(ROWS * COLS);
472
+
for (let r = 0; r < ROWS; r++) {
473
+
for (let c = 0; c < COLS; c++) {
474
+
const cell = document.createElement('div');
475
+
const char = grid[r][c];
476
+
const idx = r * COLS + c;
478
+
cell.className = 'cell';
479
+
cell.dataset.row = r;
480
+
cell.dataset.col = c;
481
+
cell.dataset.char = char;
483
+
// Set static content once
484
+
if (char === 'S') {
485
+
cell.classList.add('start');
486
+
} else if (char === '^') {
487
+
cell.classList.add('splitter');
490
+
gridEl.appendChild(cell);
491
+
cellElements[idx] = cell;
const stages = getStages();
const stage = stages[currentStage];
478
-
let beamPositions = new Map(); // key -> count (for part 2) or 1 (for part 1)
502
+
let beamPositions = new Map();
for (const [key, count] of Object.entries(stage.positions)) {
···
493
-
if (gridEl.children.length === 0) {
494
-
gridEl.style.gridTemplateColumns = \`repeat(\${COLS}, 1fr)\`;
495
-
for (let r = 0; r < ROWS; r++) {
496
-
for (let c = 0; c < COLS; c++) {
497
-
const cell = document.createElement('div');
498
-
cell.className = 'cell';
499
-
cell.dataset.row = r;
500
-
cell.dataset.col = c;
501
-
gridEl.appendChild(cell);
517
+
// Only update changed cells
518
+
const prevBeamKeys = new Set();
519
+
if (currentStage > 0) {
520
+
const prevStage = stages[currentStage - 1];
522
+
Object.keys(prevStage.positions).forEach(k => prevBeamKeys.add(k));
524
+
prevStage.beams.forEach(([r, c]) => prevBeamKeys.add(r + ',' + c));
506
-
const cells = gridEl.children;
507
-
for (let r = 0; r < ROWS; r++) {
508
-
for (let c = 0; c < COLS; c++) {
509
-
const idx = r * COLS + c;
510
-
const cell = cells[idx];
511
-
const char = grid[r][c];
512
-
const key = r + ',' + c;
528
+
// Find cells that need updating
529
+
const toUpdate = new Set();
530
+
beamPositions.forEach((_, key) => toUpdate.add(key));
531
+
prevBeamKeys.forEach(key => toUpdate.add(key));
514
-
cell.className = 'cell';
515
-
cell.textContent = '';
533
+
// Update only changed cells
534
+
for (const key of toUpdate) {
535
+
const [r, c] = key.split(',').map(Number);
536
+
const idx = r * COLS + c;
537
+
const cell = cellElements[idx];
538
+
const char = cell.dataset.char;
539
+
const count = beamPositions.get(key);
517
-
const count = beamPositions.get(key);
518
-
if (count !== undefined) {
519
-
cell.classList.add('beam');
520
-
if (isPart2 && count > 1) {
521
-
const countSpan = document.createElement('span');
522
-
countSpan.className = 'count';
523
-
countSpan.textContent = count > 999 ? '999+' : count;
524
-
cell.appendChild(countSpan);
541
+
// Reset dynamic classes
542
+
cell.classList.remove('beam', 'beam-trail');
544
+
if (count !== undefined) {
545
+
cell.classList.add('beam');
547
+
// For Part 2, adjust opacity/intensity based on timeline count
548
+
if (isPart2 && count > 1) {
549
+
// Logarithmic scale for better visual range
550
+
const intensity = Math.min(1, Math.log10(count + 1) / 4);
551
+
cell.style.opacity = 0.3 + (intensity * 0.7);
553
+
// Only show small counts in upper portion to reduce clutter
554
+
if (count <= 99 && r < ROWS * 0.6) {
555
+
cell.textContent = count;
557
+
cell.textContent = '';
526
-
} else if (beamTrail.has(key) && currentStage > 0) {
527
-
cell.classList.add('beam-trail');
530
-
if (char === 'S') {
531
-
cell.classList.add('start');
532
-
if (!count) cell.textContent = 'S';
533
-
} else if (char === '^') {
534
-
cell.classList.add('splitter');
535
-
if (!count) cell.textContent = '^';
560
+
cell.style.opacity = '1';
561
+
cell.textContent = '';
563
+
} else if (beamTrail.has(key) && currentStage > 0) {
564
+
cell.classList.add('beam-trail');
565
+
cell.style.opacity = '1';
566
+
cell.textContent = '';
568
+
// Show static char
569
+
cell.style.opacity = '1';
570
+
cell.textContent = (char === 'S' || char === '^') ? char : '';
stepEl.textContent = currentStage;
···
const container = document.querySelector('.grid-container');
558
-
const containerWidth = container.clientWidth;
559
-
const containerHeight = container.clientHeight;
590
+
const containerWidth = container.clientWidth * 0.9; // Account for padding (10px + 10px) + border (2px + 2px)
561
-
const cellWidth = Math.floor((containerWidth - COLS) / COLS);
562
-
const cellHeight = Math.floor((containerHeight - ROWS) / ROWS);
592
+
// Calculate cell size based on width to fill the container
593
+
const cellWidth = Math.floor(containerWidth / COLS);
564
-
const cellSize = Math.max(1, Math.min(cellWidth, cellHeight, 20));
595
+
// Set a reasonable max size for readability
596
+
cellSize = Math.max(2, Math.min(cellWidth, 15));
566
-
const cells = gridEl.children;
567
-
for (let i = 0; i < cells.length; i++) {
568
-
cells[i].style.width = cellSize + 'px';
569
-
cells[i].style.height = cellSize + 'px';
570
-
cells[i].style.fontSize = Math.max(10, cellSize * 0.5) + 'px';
598
+
const fontSize = Math.max(8, Math.min(cellSize * 0.7, 12));
600
+
for (let i = 0; i < cellElements.length; i++) {
601
+
cellElements[i].style.width = cellSize + 'px';
602
+
cellElements[i].style.height = cellSize + 'px';
603
+
cellElements[i].style.fontSize = fontSize + 'px';
···
630
+
if (playInterval) {
631
+
playInterval = null;
632
+
if (animationFrameId) {
633
+
cancelAnimationFrame(animationFrameId);
634
+
animationFrameId = null;
636
+
playBtn.textContent = '▶ Play';
···
658
+
// Full re-render on mode change - reset all cells
659
+
for (let i = 0; i < cellElements.length; i++) {
660
+
const cell = cellElements[i];
661
+
const char = cell.dataset.char;
662
+
cell.classList.remove('beam', 'beam-trail');
663
+
cell.style.opacity = '1';
664
+
cell.textContent = (char === 'S' || char === '^') ? char : '';
···
nextBtn.addEventListener('click', () => goToStage(currentStage + 1));
resetBtn.addEventListener('click', resetAnimation);
682
+
let animationFrameId = null;
683
+
let lastFrameTime = 0;
685
+
function animate(timestamp) {
686
+
const stages = getStages();
687
+
const speed = 1100 - parseInt(speedSlider.value);
689
+
if (timestamp - lastFrameTime >= speed) {
690
+
if (currentStage < stages.length - 1) {
691
+
goToStage(currentStage + 1);
692
+
lastFrameTime = timestamp;
694
+
// Animation complete
695
+
playInterval = null;
696
+
playBtn.textContent = '▶ Play';
697
+
animationFrameId = null;
702
+
if (playInterval) {
703
+
animationFrameId = requestAnimationFrame(animate);
playBtn.addEventListener('click', () => {
633
-
clearInterval(playInterval);
710
+
if (animationFrameId) {
711
+
cancelAnimationFrame(animationFrameId);
712
+
animationFrameId = null;
playBtn.textContent = '▶ Play';
const stages = getStages();
···
playBtn.textContent = '⏸ Pause';
642
-
const speed = 1100 - parseInt(speedSlider.value);
643
-
playInterval = setInterval(() => {
644
-
const stages = getStages();
645
-
if (currentStage < stages.length - 1) {
646
-
goToStage(currentStage + 1);
648
-
clearInterval(playInterval);
649
-
playInterval = null;
650
-
playBtn.textContent = '▶ Play';
721
+
playInterval = true;
723
+
animationFrameId = requestAnimationFrame(animate);
speedSlider.addEventListener('input', () => {
657
-
if (playInterval) {
658
-
clearInterval(playInterval);
659
-
const speed = 1100 - parseInt(speedSlider.value);
660
-
playInterval = setInterval(() => {
661
-
const stages = getStages();
662
-
if (currentStage < stages.length - 1) {
663
-
goToStage(currentStage + 1);
665
-
clearInterval(playInterval);
666
-
playInterval = null;
667
-
playBtn.textContent = '▶ Play';
728
+
// Speed change is handled in the animate loop
document.addEventListener('keydown', (e) => {
···
686
-
window.addEventListener('resize', scaleGrid);
745
+
window.addEventListener('resize', () => {
746
+
clearTimeout(resizeTimeout);
747
+
resizeTimeout = setTimeout(scaleGrid, 100);
···
await Bun.write(`${scriptDir}/index.html`, html);
console.log("Generated index.html");
695
-
console.log("Part 1:", stages1.length, "stages,", totalSplits1, "splits,", stages1[stages1.length - 1]?.beams.length ?? 0, "final beams");
696
-
console.log("Part 2:", stages2.length, "stages,", finalTimelines, "final timelines");
765
+
stages1[stages1.length - 1]?.beams.length ?? 0,