Update: drawing dots, making a better spark
This commit is contained in:
+4
-4
@@ -24,7 +24,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="eyebrow">Family science-project showcase</div>
|
<div class="eyebrow">Family science-project showcase</div>
|
||||||
<h1 id="title">A tiny universe where patterns wake up.</h1>
|
<h1 id="title">A tiny universe where patterns wake up.</h1>
|
||||||
<p class="lede">Click cells to build a creature, press play, and watch simple rules turn into crawlers, fireworks, traffic jams, and quiet little islands.</p>
|
<p class="lede">Click or drag cells to build a creature, press play, and watch simple rules turn into crawlers, fireworks, traffic jams, and quiet little islands.</p>
|
||||||
</div>
|
</div>
|
||||||
<ul class="rules" aria-label="Game of Life rules">
|
<ul class="rules" aria-label="Game of Life rules">
|
||||||
<li><b>2-3</b><span><strong>Living cells stay alive</strong> when they have two or three neighbors nearby.</span></li>
|
<li><b>2-3</b><span><strong>Living cells stay alive</strong> when they have two or three neighbors nearby.</span></li>
|
||||||
@@ -36,13 +36,13 @@
|
|||||||
<section class="lab-card" aria-label="Interactive Game of Life board">
|
<section class="lab-card" aria-label="Interactive Game of Life board">
|
||||||
<div class="board-shell">
|
<div class="board-shell">
|
||||||
<div class="board-header">
|
<div class="board-header">
|
||||||
<p class="board-title">LIVE CELL GRID - TAP OR CLICK TO DRAW</p>
|
<p class="board-title">LIVE CELL GRID - TAP, CLICK, OR DRAG TO DRAW</p>
|
||||||
<button class="button primary" id="randomBtn" type="button">Seed a tiny universe</button>
|
<button class="button primary" id="randomBtn" type="button">Seed a tiny universe</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="canvas-wrap">
|
<div class="canvas-wrap">
|
||||||
<canvas id="lifeCanvas" width="960" height="720" aria-label="Conway's Game of Life cell board"></canvas>
|
<canvas id="lifeCanvas" width="960" height="720" aria-label="Conway's Game of Life cell board"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<p class="caption">Tip: pause the universe, draw a few cells, then press play. The board wraps around at the edges like a donut-shaped planet.</p>
|
<p class="caption">Tip: pause the universe, drag across the grid to draw a shape, then press play. The board wraps around at the edges like a donut-shaped planet.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<button class="button primary" id="playBtn" type="button">Play</button>
|
<button class="button primary" id="playBtn" type="button">Play</button>
|
||||||
<button class="button" id="stepBtn" type="button">Step once</button>
|
<button class="button" id="stepBtn" type="button">Step once</button>
|
||||||
<button class="button" id="clearBtn" type="button">Clear board</button>
|
<button class="button" id="clearBtn" type="button">Clear board</button>
|
||||||
<button class="button" id="sparkBtn" type="button">Make sparkles</button>
|
<button class="button" id="sparkBtn" type="button">Make fireworks</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="slider-row">
|
<div class="slider-row">
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ let births = 0;
|
|||||||
let deaths = 0;
|
let deaths = 0;
|
||||||
let timer = null;
|
let timer = null;
|
||||||
let speed = 8;
|
let speed = 8;
|
||||||
|
let drawing = false;
|
||||||
|
let drawValue = 1;
|
||||||
|
let lastPaintedCell = null;
|
||||||
|
|
||||||
const themeTokens = {
|
const themeTokens = {
|
||||||
sprout: ['oklch(58% 0.16 145)', 'oklch(72% 0.14 170)'],
|
sprout: ['oklch(58% 0.16 145)', 'oklch(72% 0.14 170)'],
|
||||||
@@ -31,20 +34,43 @@ function randomize(chance = 0.28) {
|
|||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sparkles() {
|
function fireworks() {
|
||||||
grid = makeGrid();
|
grid = makeGrid();
|
||||||
const centerX = Math.floor(cols / 2);
|
const centerX = Math.floor(cols / 2);
|
||||||
const centerY = Math.floor(rows / 2);
|
const centerY = Math.floor(rows / 2);
|
||||||
const points = [[0,0], [1,0], [-1,0], [0,1], [0,-1], [2,1], [-2,-1], [4,0], [4,1], [4,-1], [-4,0], [-4,1], [-4,-1], [7,2], [8,2], [7,3], [8,3]];
|
const gliderGun = [
|
||||||
points.forEach(([x, y]) => { grid[centerY + y][centerX + x] = 1; });
|
[0,4], [1,4], [0,5], [1,5],
|
||||||
|
[10,4], [10,5], [10,6], [11,3], [11,7], [12,2], [12,8], [13,2], [13,8], [14,5], [15,3], [15,7], [16,4], [16,5], [16,6], [17,5],
|
||||||
|
[20,2], [20,3], [20,4], [21,2], [21,3], [21,4], [22,1], [22,5], [24,0], [24,1], [24,5], [24,6],
|
||||||
|
[34,2], [35,2], [34,3], [35,3]
|
||||||
|
];
|
||||||
|
const pulsar = [
|
||||||
|
[2,0], [3,0], [4,0], [8,0], [9,0], [10,0], [0,2], [5,2], [7,2], [12,2], [0,3], [5,3], [7,3], [12,3], [0,4], [5,4], [7,4], [12,4], [2,5], [3,5], [4,5], [8,5], [9,5], [10,5],
|
||||||
|
[2,7], [3,7], [4,7], [8,7], [9,7], [10,7], [0,8], [5,8], [7,8], [12,8], [0,9], [5,9], [7,9], [12,9], [0,10], [5,10], [7,10], [12,10], [2,12], [3,12], [4,12], [8,12], [9,12], [10,12]
|
||||||
|
];
|
||||||
|
const blinkers = [[0,0], [1,0], [2,0]];
|
||||||
|
addPattern(gliderGun, centerX - 18, centerY - 22);
|
||||||
|
addPattern(pulsar, centerX - 6, centerY + 8);
|
||||||
|
addPattern(blinkers, centerX - 28, centerY + 16);
|
||||||
|
addPattern(blinkers, centerX + 24, centerY + 16);
|
||||||
generation = 0;
|
generation = 0;
|
||||||
births = 0;
|
births = 0;
|
||||||
deaths = 0;
|
deaths = 0;
|
||||||
updateLesson('Sparkles loaded. Step slowly and look for parts that blink while other parts float away.');
|
updateLesson('Fireworks loaded. Press Play and watch the launcher keep sending tiny spaceships across the board.');
|
||||||
updateStats();
|
updateStats();
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addPattern(points, originX, originY) {
|
||||||
|
points.forEach(([x, y]) => {
|
||||||
|
const gridX = originX + x;
|
||||||
|
const gridY = originY + y;
|
||||||
|
if (gridX >= 0 && gridX < cols && gridY >= 0 && gridY < rows) {
|
||||||
|
grid[gridY][gridX] = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function neighbors(x, y) {
|
function neighbors(x, y) {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
for (let dy = -1; dy <= 1; dy++) {
|
||||||
@@ -167,28 +193,68 @@ function setTheme(name) {
|
|||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.addEventListener('pointerdown', event => {
|
function cellFromPointer(event) {
|
||||||
pause();
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const scaleX = canvas.width / rect.width;
|
const scaleX = canvas.width / rect.width;
|
||||||
const scaleY = canvas.height / rect.height;
|
const scaleY = canvas.height / rect.height;
|
||||||
const x = Math.floor((event.clientX - rect.left) * scaleX / cellSize);
|
const x = Math.floor((event.clientX - rect.left) * scaleX / cellSize);
|
||||||
const y = Math.floor((event.clientY - rect.top) * scaleY / cellSize);
|
const y = Math.floor((event.clientY - rect.top) * scaleY / cellSize);
|
||||||
if (x >= 0 && x < cols && y >= 0 && y < rows) {
|
if (x < 0 || x >= cols || y < 0 || y >= rows) return null;
|
||||||
grid[y][x] = grid[y][x] ? 0 : 1;
|
return { x, y };
|
||||||
births = 0;
|
}
|
||||||
deaths = 0;
|
|
||||||
updateLesson('You drew on the board. Press Step once to test your prediction before running it fast.');
|
function paintCell(event) {
|
||||||
updateStats();
|
const cell = cellFromPointer(event);
|
||||||
draw();
|
if (!cell) return;
|
||||||
}
|
const key = `${cell.x},${cell.y}`;
|
||||||
|
if (key === lastPaintedCell || grid[cell.y][cell.x] === drawValue) return;
|
||||||
|
lastPaintedCell = key;
|
||||||
|
grid[cell.y][cell.x] = drawValue;
|
||||||
|
births = 0;
|
||||||
|
deaths = 0;
|
||||||
|
updateLesson(drawValue ? 'You are painting a shape. Release, then press Step once to test your prediction.' : 'You are erasing cells. Release, then redraw any shape you want to test.');
|
||||||
|
updateStats();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrawing() {
|
||||||
|
drawing = false;
|
||||||
|
lastPaintedCell = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.addEventListener('pointerdown', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
pause();
|
||||||
|
const cell = cellFromPointer(event);
|
||||||
|
if (!cell) return;
|
||||||
|
drawing = true;
|
||||||
|
drawValue = grid[cell.y][cell.x] ? 0 : 1;
|
||||||
|
lastPaintedCell = null;
|
||||||
|
canvas.setPointerCapture(event.pointerId);
|
||||||
|
paintCell(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('pointermove', event => {
|
||||||
|
if (!drawing) return;
|
||||||
|
event.preventDefault();
|
||||||
|
paintCell(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('pointerup', event => {
|
||||||
|
if (canvas.hasPointerCapture(event.pointerId)) {
|
||||||
|
canvas.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
stopDrawing();
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('pointercancel', stopDrawing);
|
||||||
|
canvas.addEventListener('pointerleave', stopDrawing);
|
||||||
|
|
||||||
document.getElementById('playBtn').addEventListener('click', () => running ? pause() : play());
|
document.getElementById('playBtn').addEventListener('click', () => running ? pause() : play());
|
||||||
document.getElementById('stepBtn').addEventListener('click', () => { pause(); step(); updateLesson('One generation advanced. Did the population go up, down, or stay balanced?'); });
|
document.getElementById('stepBtn').addEventListener('click', () => { pause(); step(); updateLesson('One generation advanced. Did the population go up, down, or stay balanced?'); });
|
||||||
document.getElementById('clearBtn').addEventListener('click', () => { pause(); grid = makeGrid(); generation = 0; births = 0; deaths = 0; updateLesson('The board is clear. Draw a creature, then test if it survives.'); updateStats(); draw(); });
|
document.getElementById('clearBtn').addEventListener('click', () => { pause(); grid = makeGrid(); generation = 0; births = 0; deaths = 0; updateLesson('The board is clear. Draw a creature, then test if it survives.'); updateStats(); draw(); });
|
||||||
document.getElementById('randomBtn').addEventListener('click', () => randomize(0.28));
|
document.getElementById('randomBtn').addEventListener('click', () => randomize(0.28));
|
||||||
document.getElementById('sparkBtn').addEventListener('click', sparkles);
|
document.getElementById('sparkBtn').addEventListener('click', fireworks);
|
||||||
document.getElementById('speed').addEventListener('input', event => {
|
document.getElementById('speed').addEventListener('input', event => {
|
||||||
speed = Number(event.target.value);
|
speed = Number(event.target.value);
|
||||||
document.getElementById('speedLabel').textContent = speed;
|
document.getElementById('speedLabel').textContent = speed;
|
||||||
|
|||||||
+1
-1
@@ -279,7 +279,7 @@ canvas {
|
|||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: color-mix(in oklch, var(--surface), var(--bg) 26%);
|
background: color-mix(in oklch, var(--surface), var(--bg) 26%);
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
touch-action: manipulation;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.caption {
|
.caption {
|
||||||
|
|||||||
Reference in New Issue
Block a user