Update: more science and kid friendly
This commit is contained in:
+91
-46
@@ -1,60 +1,105 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Conway's Game of Life</title>
|
<title>Life Lab: Conway's Game of Life</title>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<main class="page">
|
||||||
<header>
|
<nav class="topbar" aria-label="Life Lab navigation">
|
||||||
<h1>Conway's Game of Life - for Sami </h1>
|
<div class="brand">
|
||||||
<p>A cellular automaton simulation with WebHook</p>
|
<div class="mark" aria-hidden="true">L</div>
|
||||||
</header>
|
<div>
|
||||||
|
<strong>Life Lab</strong>
|
||||||
|
<span>Conway's Game of Life for curious families</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-pill"><i class="dot" aria-hidden="true"></i><span id="runStatus">Paused - ready to experiment</span></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="stats-panel">
|
<section class="hero" aria-labelledby="title">
|
||||||
<div class="stat">
|
<div class="intro">
|
||||||
<span class="stat-label">Generation:</span>
|
<div>
|
||||||
<span id="generation">0</span>
|
<div class="eyebrow">Family science-project showcase</div>
|
||||||
</div>
|
<h1 id="title">A tiny universe where patterns wake up.</h1>
|
||||||
<div class="stat">
|
<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>
|
||||||
<span class="stat-label">Population:</span>
|
</div>
|
||||||
<span id="population">0</span>
|
<ul class="rules" aria-label="Game of Life rules">
|
||||||
</div>
|
<li><b>2-3</b><span><strong>Living cells stay alive</strong> when they have two or three neighbors nearby.</span></li>
|
||||||
<div class="stat">
|
<li><b>3</b><span><strong>Empty spots are born</strong> when exactly three neighbors gather around them.</span></li>
|
||||||
<span class="stat-label">Births:</span>
|
<li><b>0-1</b><span><strong>Lonely cells fade</strong>; crowded cells fade too. Balance makes the best patterns.</span></li>
|
||||||
<span id="births">0</span>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
|
||||||
<span class="stat-label">Deaths:</span>
|
<section class="lab-card" aria-label="Interactive Game of Life board">
|
||||||
<span id="deaths">0</span>
|
<div class="board-shell">
|
||||||
</div>
|
<div class="board-header">
|
||||||
|
<p class="board-title">LIVE CELL GRID - TAP OR CLICK TO DRAW</p>
|
||||||
|
<button class="button primary" id="randomBtn" type="button">Seed a tiny universe</button>
|
||||||
|
</div>
|
||||||
|
<div class="canvas-wrap">
|
||||||
|
<canvas id="lifeCanvas" width="960" height="720" aria-label="Conway's Game of Life cell board"></canvas>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dashboard" aria-label="Simulation controls and learning prompts">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Mission control</h2>
|
||||||
|
<p class="caption">Designed for fast experimenting: change one thing, run it, then compare what happens.</p>
|
||||||
|
<div class="control-grid">
|
||||||
|
<button class="button primary" id="playBtn" type="button">Play</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="sparkBtn" type="button">Make sparkles</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="slider-row">
|
||||||
<button id="startBtn">Start</button>
|
<label for="speed"><span>Universe speed</span><strong><span id="speedLabel">8</span>/12</strong></label>
|
||||||
<button id="pauseBtn">Pause</button>
|
<input id="speed" type="range" min="1" max="12" value="8">
|
||||||
<button id="resetBtn">Reset</button>
|
|
||||||
<button id="randomBtn">Random</button>
|
|
||||||
<div class="speed-control">
|
|
||||||
<label for="speed">Speed:</label>
|
|
||||||
<input type="range" id="speed" min="1" max="20" value="10">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="canvas-container">
|
<div class="theme-row" aria-label="Cell color themes">
|
||||||
<canvas id="gameCanvas"></canvas>
|
<button class="theme" type="button" data-theme="sprout" aria-pressed="true">
|
||||||
|
<span class="swatches"><i class="swatch-sprout-a"></i><i class="swatch-sprout-b"></i></span>
|
||||||
|
<strong>Sprout</strong><span>science green</span>
|
||||||
|
</button>
|
||||||
|
<button class="theme" type="button" data-theme="nebula" aria-pressed="false">
|
||||||
|
<span class="swatches"><i class="swatch-nebula-a"></i><i class="swatch-nebula-b"></i></span>
|
||||||
|
<strong>Nebula</strong><span>arcade space</span>
|
||||||
|
</button>
|
||||||
|
<button class="theme" type="button" data-theme="sunny" aria-pressed="false">
|
||||||
|
<span class="swatches"><i class="swatch-sunny-a"></i><i class="swatch-sunny-b"></i></span>
|
||||||
|
<strong>Sunny</strong><span>friendly sparks</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="presets">
|
<p class="explain" id="lessonText">Start with random cells, then watch for shapes that travel, blink, or disappear. Ask: did the universe get calmer or wilder?</p>
|
||||||
<button data-preset="glider">Glider</button>
|
</div>
|
||||||
<button data-preset="spaceship">Spaceship</button>
|
|
||||||
<button data-preset="pulsar">Pulsar</button>
|
|
||||||
<button data-preset="gosper">Gosper Glider Gun</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<div>
|
||||||
|
<div class="stats-grid" aria-label="Live simulation statistics">
|
||||||
|
<div class="stat"><span>Generation</span><strong id="generation">0</strong></div>
|
||||||
|
<div class="stat"><span>Population</span><strong id="population">0</strong></div>
|
||||||
|
<div class="stat"><span>Births</span><strong id="births">0</strong></div>
|
||||||
|
<div class="stat"><span>Fades</span><strong id="deaths">0</strong></div>
|
||||||
|
</div>
|
||||||
|
<section class="prompt-card">
|
||||||
|
<h2>Try this together</h2>
|
||||||
|
<ul class="prompt-list">
|
||||||
|
<li><strong>Prediction round:</strong> draw ten cells and guess if the population will grow or shrink after five steps.</li>
|
||||||
|
<li><strong>Pattern hunt:</strong> look for blinkers, tiny spaceships, and stable islands that refuse to move.</li>
|
||||||
|
<li><strong>Color lab:</strong> switch themes and ask which one makes movement easiest to spot from across the room.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,299 +1,209 @@
|
|||||||
// Initialize the game when the page loads
|
const canvas = document.getElementById('lifeCanvas');
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
const ctx = canvas.getContext('2d');
|
||||||
// Create game with 80x60 grid and 10px cells
|
const cols = 80;
|
||||||
const game = new GameOfLife(80, 60, 10);
|
const rows = 60;
|
||||||
|
const cellSize = canvas.width / cols;
|
||||||
|
let grid = makeGrid();
|
||||||
|
let running = false;
|
||||||
|
let generation = 0;
|
||||||
|
let births = 0;
|
||||||
|
let deaths = 0;
|
||||||
|
let timer = null;
|
||||||
|
let speed = 8;
|
||||||
|
|
||||||
|
const themeTokens = {
|
||||||
|
sprout: ['oklch(58% 0.16 145)', 'oklch(72% 0.14 170)'],
|
||||||
|
nebula: ['oklch(60% 0.18 285)', 'oklch(70% 0.14 235)'],
|
||||||
|
sunny: ['oklch(70% 0.15 65)', 'oklch(74% 0.12 35)']
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeGrid() {
|
||||||
|
return Array.from({ length: rows }, () => Array(cols).fill(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomize(chance = 0.28) {
|
||||||
|
grid = grid.map(row => row.map(() => (Math.random() < chance ? 1 : 0)));
|
||||||
|
generation = 0;
|
||||||
|
births = 0;
|
||||||
|
deaths = 0;
|
||||||
|
updateLesson('A fresh universe appeared. Press Play and watch which clusters survive the first few generations.');
|
||||||
|
updateStats();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sparkles() {
|
||||||
|
grid = makeGrid();
|
||||||
|
const centerX = Math.floor(cols / 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]];
|
||||||
|
points.forEach(([x, y]) => { grid[centerY + y][centerX + x] = 1; });
|
||||||
|
generation = 0;
|
||||||
|
births = 0;
|
||||||
|
deaths = 0;
|
||||||
|
updateLesson('Sparkles loaded. Step slowly and look for parts that blink while other parts float away.');
|
||||||
|
updateStats();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function neighbors(x, y) {
|
||||||
|
let total = 0;
|
||||||
|
for (let dy = -1; dy <= 1; dy++) {
|
||||||
|
for (let dx = -1; dx <= 1; dx++) {
|
||||||
|
if (dx === 0 && dy === 0) continue;
|
||||||
|
const nx = (x + dx + cols) % cols;
|
||||||
|
const ny = (y + dy + rows) % rows;
|
||||||
|
total += grid[ny][nx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function step() {
|
||||||
|
const next = makeGrid();
|
||||||
|
births = 0;
|
||||||
|
deaths = 0;
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const n = neighbors(x, y);
|
||||||
|
const alive = grid[y][x] === 1;
|
||||||
|
const nextAlive = alive ? (n === 2 || n === 3) : n === 3;
|
||||||
|
next[y][x] = nextAlive ? 1 : 0;
|
||||||
|
if (!alive && nextAlive) births++;
|
||||||
|
if (alive && !nextAlive) deaths++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grid = next;
|
||||||
|
generation++;
|
||||||
|
updateStats();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
const styles = getComputedStyle(document.documentElement);
|
||||||
|
const cellA = styles.getPropertyValue('--cell-a').trim();
|
||||||
|
const cellB = styles.getPropertyValue('--cell-b').trim();
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.fillStyle = styles.getPropertyValue('--surface').trim();
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
ctx.strokeStyle = styles.getPropertyValue('--grid').trim();
|
||||||
|
ctx.lineWidth = 0.7;
|
||||||
|
for (let x = 0; x <= cols; x++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x * cellSize, 0);
|
||||||
|
ctx.lineTo(x * cellSize, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y <= rows; y++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y * cellSize);
|
||||||
|
ctx.lineTo(canvas.width, y * cellSize);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
|
||||||
|
gradient.addColorStop(0, cellA);
|
||||||
|
gradient.addColorStop(1, cellB);
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.shadowColor = cellA;
|
||||||
|
ctx.shadowBlur = 8;
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
if (grid[y][x]) {
|
||||||
|
const inset = 1.4;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(x * cellSize + inset, y * cellSize + inset, cellSize - inset * 2, cellSize - inset * 2, 3);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function population() {
|
||||||
|
return grid.reduce((sum, row) => sum + row.reduce((rowSum, cell) => rowSum + cell, 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats() {
|
||||||
|
document.getElementById('generation').textContent = generation;
|
||||||
|
document.getElementById('population').textContent = population();
|
||||||
|
document.getElementById('births').textContent = births;
|
||||||
|
document.getElementById('deaths').textContent = deaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLesson(text) {
|
||||||
|
document.getElementById('lessonText').textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function play() {
|
||||||
|
running = true;
|
||||||
|
document.getElementById('playBtn').textContent = 'Pause';
|
||||||
|
document.getElementById('runStatus').textContent = 'Running - universe is alive';
|
||||||
|
loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pause() {
|
||||||
|
running = false;
|
||||||
|
document.getElementById('playBtn').textContent = 'Play';
|
||||||
|
document.getElementById('runStatus').textContent = 'Paused - ready to experiment';
|
||||||
|
if (timer) window.clearTimeout(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
if (!running) return;
|
||||||
|
step();
|
||||||
|
timer = window.setTimeout(() => requestAnimationFrame(loop), 620 - speed * 45);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme(name) {
|
||||||
|
const [a, b] = themeTokens[name];
|
||||||
|
document.documentElement.style.setProperty('--cell-a', a);
|
||||||
|
document.documentElement.style.setProperty('--cell-b', b);
|
||||||
|
document.documentElement.style.setProperty('--accent', a);
|
||||||
|
document.querySelectorAll('.theme').forEach(button => {
|
||||||
|
button.setAttribute('aria-pressed', String(button.dataset.theme === name));
|
||||||
|
});
|
||||||
|
updateLesson(name === 'sprout' ? 'Sprout mode keeps the science-lab look: calm, clear, and easy to read.' : name === 'nebula' ? 'Nebula mode feels more arcade-like. It is great for spotting moving patterns.' : 'Sunny mode is warm and playful for younger scientists presenting to family.');
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.addEventListener('pointerdown', event => {
|
||||||
|
pause();
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
const x = Math.floor((event.clientX - rect.left) * scaleX / cellSize);
|
||||||
|
const y = Math.floor((event.clientY - rect.top) * scaleY / cellSize);
|
||||||
|
if (x >= 0 && x < cols && y >= 0 && y < rows) {
|
||||||
|
grid[y][x] = grid[y][x] ? 0 : 1;
|
||||||
|
births = 0;
|
||||||
|
deaths = 0;
|
||||||
|
updateLesson('You drew on the board. Press Step once to test your prediction before running it fast.');
|
||||||
|
updateStats();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
class GameOfLife {
|
document.getElementById('playBtn').addEventListener('click', () => running ? pause() : play());
|
||||||
constructor(width, height, cellSize = 10) {
|
document.getElementById('stepBtn').addEventListener('click', () => { pause(); step(); updateLesson('One generation advanced. Did the population go up, down, or stay balanced?'); });
|
||||||
this.width = width;
|
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(); });
|
||||||
this.height = height;
|
document.getElementById('randomBtn').addEventListener('click', () => randomize(0.28));
|
||||||
this.cellSize = cellSize;
|
document.getElementById('sparkBtn').addEventListener('click', sparkles);
|
||||||
this.grid = this.createGrid();
|
document.getElementById('speed').addEventListener('input', event => {
|
||||||
this.nextGrid = this.createGrid();
|
speed = Number(event.target.value);
|
||||||
this.running = false;
|
document.getElementById('speedLabel').textContent = speed;
|
||||||
this.generation = 0;
|
});
|
||||||
this.births = 0;
|
document.querySelectorAll('.theme').forEach(button => button.addEventListener('click', () => setTheme(button.dataset.theme)));
|
||||||
this.deaths = 0;
|
|
||||||
this.animationId = null;
|
|
||||||
this.speed = 10;
|
|
||||||
|
|
||||||
// Canvas setup
|
if (!CanvasRenderingContext2D.prototype.roundRect) {
|
||||||
this.canvas = document.getElementById('gameCanvas');
|
CanvasRenderingContext2D.prototype.roundRect = function roundRect(x, y, width, height, radius) {
|
||||||
this.ctx = this.canvas.getContext('2d');
|
this.moveTo(x + radius, y);
|
||||||
this.canvas.width = width * cellSize;
|
this.arcTo(x + width, y, x + width, y + height, radius);
|
||||||
this.canvas.height = height * cellSize;
|
this.arcTo(x + width, y + height, x, y + height, radius);
|
||||||
|
this.arcTo(x, y + height, x, y, radius);
|
||||||
// Event listeners
|
this.arcTo(x, y, x + width, y, radius);
|
||||||
this.setupEventListeners();
|
return this;
|
||||||
this.setupPresets();
|
};
|
||||||
|
|
||||||
// Initial draw
|
|
||||||
this.draw();
|
|
||||||
this.updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
createGrid() {
|
|
||||||
return Array(this.height).fill().map(() => Array(this.width).fill(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
randomize() {
|
|
||||||
for (let y = 0; y < this.height; y++) {
|
|
||||||
for (let x = 0; x < this.width; x++) {
|
|
||||||
this.grid[y][x] = Math.random() > 0.7 ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.generation = 0;
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
// Start/Pause/Reset buttons
|
|
||||||
document.getElementById('startBtn').addEventListener('click', () => this.start());
|
|
||||||
document.getElementById('pauseBtn').addEventListener('click', () => this.pause());
|
|
||||||
document.getElementById('resetBtn').addEventListener('click', () => this.reset());
|
|
||||||
document.getElementById('randomBtn').addEventListener('click', () => this.randomize());
|
|
||||||
|
|
||||||
// Speed control
|
|
||||||
document.getElementById('speed').addEventListener('input', (e) => {
|
|
||||||
this.speed = parseInt(e.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Canvas click to toggle cells
|
|
||||||
this.canvas.addEventListener('click', (e) => {
|
|
||||||
if (this.running) return;
|
|
||||||
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
const x = Math.floor((e.clientX - rect.left) / this.cellSize);
|
|
||||||
const y = Math.floor((e.clientY - rect.top) / this.cellSize);
|
|
||||||
|
|
||||||
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
|
||||||
this.grid[y][x] = this.grid[y][x] ? 0 : 1;
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupPresets() {
|
|
||||||
document.querySelector('[data-preset="glider"]').addEventListener('click', () => {
|
|
||||||
this.reset();
|
|
||||||
// Glider pattern
|
|
||||||
this.grid[5][5] = 1;
|
|
||||||
this.grid[6][6] = 1;
|
|
||||||
this.grid[6][7] = 1;
|
|
||||||
this.grid[5][7] = 1;
|
|
||||||
this.grid[4][7] = 1;
|
|
||||||
this.draw();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('[data-preset="spaceship"]').addEventListener('click', () => {
|
|
||||||
this.reset();
|
|
||||||
// Lightweight spaceship
|
|
||||||
this.grid[10][10] = 1;
|
|
||||||
this.grid[10][13] = 1;
|
|
||||||
this.grid[11][9] = 1;
|
|
||||||
this.grid[11][13] = 1;
|
|
||||||
this.grid[12][9] = 1;
|
|
||||||
this.grid[12][10] = 1;
|
|
||||||
this.grid[12][11] = 1;
|
|
||||||
this.grid[12][12] = 1;
|
|
||||||
this.draw();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('[data-preset="pulsar"]').addEventListener('click', () => {
|
|
||||||
this.reset();
|
|
||||||
// Pulsar pattern
|
|
||||||
const pulsarPoints = [
|
|
||||||
[2,4],[2,5],[2,6],[2,10],[2,11],[2,12],
|
|
||||||
[4,2],[4,7],[4,9],[4,14],
|
|
||||||
[5,2],[5,7],[5,9],[5,14],
|
|
||||||
[6,2],[6,7],[6,9],[6,14],
|
|
||||||
[7,4],[7,5],[7,6],[7,10],[7,11],[7,12],
|
|
||||||
[9,4],[9,5],[9,6],[9,10],[9,11],[9,12],
|
|
||||||
[10,2],[10,7],[10,9],[10,14],
|
|
||||||
[11,2],[11,7],[11,9],[11,14],
|
|
||||||
[12,2],[12,7],[12,9],[12,14],
|
|
||||||
[14,4],[14,5],[14,6],[14,10],[14,11],[14,12]
|
|
||||||
];
|
|
||||||
|
|
||||||
pulsarPoints.forEach(([y, x]) => {
|
|
||||||
this.grid[y][x] = 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.draw();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('[data-preset="gosper"]').addEventListener('click', () => {
|
|
||||||
this.reset();
|
|
||||||
// Gosper glider gun
|
|
||||||
const gunPoints = [
|
|
||||||
[5,1],[5,2],[6,1],[6,2],
|
|
||||||
[5,11],[6,11],[7,11],
|
|
||||||
[4,12],[8,12],
|
|
||||||
[3,13],[9,13],
|
|
||||||
[3,14],[9,14],
|
|
||||||
[6,15],
|
|
||||||
[4,16],[8,16],
|
|
||||||
[5,17],[6,17],[7,17],
|
|
||||||
[6,18],
|
|
||||||
[3,21],[4,21],[5,21],
|
|
||||||
[3,22],[4,22],[5,22],
|
|
||||||
[2,23],[6,23],
|
|
||||||
[1,25],[2,25],[6,25],[7,25],
|
|
||||||
[3,35],[4,35],[3,36],[4,36]
|
|
||||||
];
|
|
||||||
|
|
||||||
gunPoints.forEach(([y, x]) => {
|
|
||||||
this.grid[y][x] = 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.draw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
countNeighbors(x, y) {
|
|
||||||
let count = 0;
|
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
|
||||||
for (let dx = -1; dx <= 1; dx++) {
|
|
||||||
if (dx === 0 && dy === 0) continue;
|
|
||||||
const nx = (x + dx + this.width) % this.width;
|
|
||||||
const ny = (y + dy + this.height) % this.height;
|
|
||||||
count += this.grid[ny][nx];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
// Reset counters
|
|
||||||
this.births = 0;
|
|
||||||
this.deaths = 0;
|
|
||||||
|
|
||||||
// Compute next generation
|
|
||||||
for (let y = 0; y < this.height; y++) {
|
|
||||||
for (let x = 0; x < this.width; x++) {
|
|
||||||
const neighbors = this.countNeighbors(x, y);
|
|
||||||
const cell = this.grid[y][x];
|
|
||||||
let nextState;
|
|
||||||
|
|
||||||
// Apply Conway's Game of Life rules
|
|
||||||
if (cell === 1) {
|
|
||||||
nextState = (neighbors === 2 || neighbors === 3) ? 1 : 0;
|
|
||||||
if (nextState === 0) this.deaths++;
|
|
||||||
} else {
|
|
||||||
nextState = (neighbors === 3) ? 1 : 0;
|
|
||||||
if (nextState === 1) this.births++;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.nextGrid[y][x] = nextState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap grids
|
|
||||||
[this.grid, this.nextGrid] = [this.nextGrid, this.grid];
|
|
||||||
this.generation++;
|
|
||||||
|
|
||||||
// Update stats display
|
|
||||||
this.updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
draw() {
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
|
|
||||||
// Draw live cells with gradient
|
|
||||||
const gradient = this.ctx.createLinearGradient(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
gradient.addColorStop(0, '#4facfe');
|
|
||||||
gradient.addColorStop(1, '#00f2fe');
|
|
||||||
|
|
||||||
this.ctx.fillStyle = gradient;
|
|
||||||
|
|
||||||
for (let y = 0; y < this.height; y++) {
|
|
||||||
for (let x = 0; x < this.width; x++) {
|
|
||||||
if (this.grid[y][x] === 1) {
|
|
||||||
this.ctx.fillRect(
|
|
||||||
x * this.cellSize,
|
|
||||||
y * this.cellSize,
|
|
||||||
this.cellSize - 1,
|
|
||||||
this.cellSize - 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw grid lines
|
|
||||||
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
|
|
||||||
this.ctx.lineWidth = 0.5;
|
|
||||||
|
|
||||||
// Vertical lines
|
|
||||||
for (let x = 0; x <= this.width; x++) {
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(x * this.cellSize, 0);
|
|
||||||
this.ctx.lineTo(x * this.cellSize, this.canvas.height);
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Horizontal lines
|
|
||||||
for (let y = 0; y <= this.height; y++) {
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(0, y * this.cellSize);
|
|
||||||
this.ctx.lineTo(this.canvas.width, y * this.cellSize);
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats() {
|
|
||||||
document.getElementById('generation').textContent = this.generation;
|
|
||||||
document.getElementById('population').textContent = this.getPopulation();
|
|
||||||
document.getElementById('births').textContent = this.births;
|
|
||||||
document.getElementById('deaths').textContent = this.deaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPopulation() {
|
|
||||||
let count = 0;
|
|
||||||
for (let y = 0; y < this.height; y++) {
|
|
||||||
for (let x = 0; x < this.width; x++) {
|
|
||||||
count += this.grid[y][x];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
gameLoop() {
|
|
||||||
this.update();
|
|
||||||
this.draw();
|
|
||||||
|
|
||||||
// Adjust speed (higher value = faster)
|
|
||||||
const speedFactor = (21 - this.speed) * 20;
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.running) {
|
|
||||||
this.animationId = requestAnimationFrame(() => this.gameLoop());
|
|
||||||
}
|
|
||||||
}, speedFactor);
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if (!this.running) {
|
|
||||||
this.running = true;
|
|
||||||
this.gameLoop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pause() {
|
|
||||||
this.running = false;
|
|
||||||
if (this.animationId) {
|
|
||||||
cancelAnimationFrame(this.animationId);
|
|
||||||
this.animationId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.pause();
|
|
||||||
this.grid = this.createGrid();
|
|
||||||
this.nextGrid = this.createGrid();
|
|
||||||
this.generation = 0;
|
|
||||||
this.births = 0;
|
|
||||||
this.deaths = 0;
|
|
||||||
this.updateStats();
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
randomize(0.18);
|
||||||
|
|||||||
+457
-126
@@ -1,164 +1,495 @@
|
|||||||
* {
|
:root {
|
||||||
margin: 0;
|
--bg: oklch(98% 0.005 250);
|
||||||
padding: 0;
|
--surface: oklch(100% 0 0);
|
||||||
box-sizing: border-box;
|
--fg: oklch(22% 0.02 240);
|
||||||
|
--muted: oklch(50% 0.018 240);
|
||||||
|
--border: oklch(90% 0.008 240);
|
||||||
|
--accent: oklch(58% 0.16 145);
|
||||||
|
|
||||||
|
--font-display: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
|
||||||
|
--font-body: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, Menlo, monospace;
|
||||||
|
|
||||||
|
--cell-a: oklch(58% 0.16 145);
|
||||||
|
--cell-b: oklch(72% 0.14 170);
|
||||||
|
--cell-glow: color-mix(in oklch, var(--cell-a), white 38%);
|
||||||
|
--grid: color-mix(in oklch, var(--border), var(--fg) 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
margin: 0;
|
||||||
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
|
min-height: 100vh;
|
||||||
color: white;
|
background:
|
||||||
min-height: 100vh;
|
radial-gradient(circle at 16% 12%, color-mix(in oklch, var(--accent), transparent 84%), transparent 30rem),
|
||||||
padding: 20px;
|
linear-gradient(135deg, color-mix(in oklch, var(--bg), white 60%), var(--bg));
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
button, input { font: inherit; }
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
.page {
|
||||||
text-align: center;
|
width: min(1440px, calc(100% - 32px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px 0 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
.topbar {
|
||||||
margin-bottom: 20px;
|
display: flex;
|
||||||
padding: 20px;
|
align-items: center;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
justify-content: space-between;
|
||||||
border-radius: 15px;
|
gap: 16px;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
padding: 14px 16px;
|
||||||
backdrop-filter: blur(10px);
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: color-mix(in oklch, var(--surface), transparent 12%);
|
||||||
|
box-shadow: 0 18px 50px color-mix(in oklch, var(--fg), transparent 92%);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border: 1px solid color-mix(in oklch, var(--accent), var(--border) 45%);
|
||||||
|
border-radius: 13px;
|
||||||
|
background: color-mix(in oklch, var(--accent), white 86%);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 800;
|
||||||
|
box-shadow: 0 0 0 5px color-mix(in oklch, var(--accent), transparent 88%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand strong {
|
||||||
|
display: block;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 17px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand span {
|
||||||
|
display: block;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 0 5px color-mix(in oklch, var(--accent), transparent 82%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 0.95fr) minmax(390px, 1.35fr);
|
||||||
|
gap: 22px;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro,
|
||||||
|
.lab-card,
|
||||||
|
.panel,
|
||||||
|
.prompt-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: color-mix(in oklch, var(--surface), transparent 4%);
|
||||||
|
box-shadow: 0 24px 60px color-mix(in oklch, var(--fg), transparent 93%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 580px;
|
||||||
|
padding: clamp(24px, 4vw, 42px);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-end: -110px;
|
||||||
|
inset-block-end: -120px;
|
||||||
|
width: 290px;
|
||||||
|
height: 290px;
|
||||||
|
border: 1px dashed color-mix(in oklch, var(--accent), transparent 40%);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid color-mix(in oklch, var(--accent), var(--border) 45%);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: color-mix(in oklch, var(--accent), var(--fg) 18%);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2.5rem;
|
max-width: 780px;
|
||||||
margin-bottom: 10px;
|
margin: 24px 0 18px;
|
||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(42px, 7vw, 86px);
|
||||||
|
line-height: 0.96;
|
||||||
|
letter-spacing: -0.045em;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.lede {
|
||||||
font-size: 1.1rem;
|
max-width: 54ch;
|
||||||
opacity: 0.9;
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: clamp(17px, 1.5vw, 20px);
|
||||||
|
line-height: 1.55;
|
||||||
|
text-wrap: pretty;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-panel {
|
.rules {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: center;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
margin-top: 32px;
|
||||||
gap: 20px;
|
padding: 0;
|
||||||
margin-bottom: 20px;
|
list-style: none;
|
||||||
padding: 15px;
|
color: var(--fg);
|
||||||
background: rgba(0, 0, 0, 0.2);
|
position: relative;
|
||||||
border-radius: 15px;
|
z-index: 1;
|
||||||
font-size: 1.1rem;
|
}
|
||||||
|
|
||||||
|
.rules li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 36px 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: color-mix(in oklch, var(--bg), var(--surface) 48%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules b {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: color-mix(in oklch, var(--accent), white 84%);
|
||||||
|
color: color-mix(in oklch, var(--accent), black 30%);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules span {
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-card {
|
||||||
|
padding: clamp(14px, 2vw, 22px);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-shell {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-wrap {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-height: 450px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, color-mix(in oklch, var(--grid), transparent 45%) 1px, transparent 1px),
|
||||||
|
linear-gradient(color-mix(in oklch, var(--grid), transparent 45%) 1px, transparent 1px),
|
||||||
|
color-mix(in oklch, var(--surface), var(--bg) 34%);
|
||||||
|
background-size: 28px 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-wrap::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 10px;
|
||||||
|
border: 1px dashed color-mix(in oklch, var(--accent), transparent 70%);
|
||||||
|
border-radius: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: min(100%, 820px);
|
||||||
|
height: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
aspect-ratio: 1.333;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: color-mix(in oklch, var(--surface), var(--bg) 26%);
|
||||||
|
cursor: crosshair;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.15fr;
|
||||||
|
gap: 22px;
|
||||||
|
margin-top: 22px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h2,
|
||||||
|
.prompt-card h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: clamp(22px, 2.4vw, 34px);
|
||||||
|
line-height: 1.06;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--fg);
|
||||||
|
font-weight: 750;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover { transform: translateY(-1px); border-color: color-mix(in oklch, var(--accent), var(--border) 40%); }
|
||||||
|
.button:active { transform: translateY(0); }
|
||||||
|
|
||||||
|
.button.primary {
|
||||||
|
background: color-mix(in oklch, var(--accent), white 82%);
|
||||||
|
border-color: color-mix(in oklch, var(--accent), var(--border) 22%);
|
||||||
|
color: color-mix(in oklch, var(--fg), var(--accent) 22%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme {
|
||||||
|
min-height: 76px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--surface);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme[aria-pressed="true"] {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatches {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatches i {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid color-mix(in oklch, var(--border), var(--fg) 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch-sprout-a { background: oklch(58% 0.16 145); }
|
||||||
|
.swatch-sprout-b { background: oklch(72% 0.14 170); }
|
||||||
|
.swatch-nebula-a { background: oklch(60% 0.18 285); }
|
||||||
|
.swatch-nebula-b { background: oklch(70% 0.14 235); }
|
||||||
|
.swatch-sunny-a { background: oklch(70% 0.15 65); }
|
||||||
|
.swatch-sunny-b { background: oklch(74% 0.12 35); }
|
||||||
|
|
||||||
|
.theme strong { display: block; font-size: 13px; }
|
||||||
|
.theme span { display: block; color: var(--muted); font-size: 12px; margin-top: 3px; }
|
||||||
|
|
||||||
|
.slider-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: color-mix(in oklch, var(--bg), var(--surface) 56%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-row label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat {
|
.stat {
|
||||||
display: flex;
|
min-height: 98px;
|
||||||
flex-direction: column;
|
padding: 14px;
|
||||||
align-items: center;
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat span {
|
||||||
font-weight: 600;
|
display: block;
|
||||||
margin-bottom: 5px;
|
color: var(--muted);
|
||||||
color: #4facfe;
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.stat strong {
|
||||||
display: flex;
|
display: block;
|
||||||
justify-content: center;
|
margin-top: 12px;
|
||||||
flex-wrap: wrap;
|
font-family: var(--font-mono);
|
||||||
gap: 15px;
|
font-size: clamp(24px, 3vw, 40px);
|
||||||
margin-bottom: 30px;
|
line-height: 1;
|
||||||
padding: 20px;
|
letter-spacing: -0.04em;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.prompt-card {
|
||||||
padding: 12px 24px;
|
margin-top: 10px;
|
||||||
font-size: 1rem;
|
padding: 18px;
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
color: white;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 50px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
.prompt-list {
|
||||||
background: rgba(255, 255, 255, 0.25);
|
display: grid;
|
||||||
transform: translateY(-2px);
|
gap: 10px;
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
padding: 0;
|
||||||
|
margin: 14px 0 0;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active {
|
.prompt-list li {
|
||||||
transform: translateY(0);
|
padding: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: color-mix(in oklch, var(--bg), white 50%);
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.speed-control {
|
.prompt-list strong {
|
||||||
display: flex;
|
color: var(--fg);
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.speed-control label {
|
.explain {
|
||||||
font-weight: 500;
|
margin: 14px 0 0;
|
||||||
|
padding: 14px;
|
||||||
|
border-inline-start: 4px solid var(--accent);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: color-mix(in oklch, var(--accent), white 90%);
|
||||||
|
color: color-mix(in oklch, var(--fg), var(--accent) 15%);
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.speed-control input {
|
@media (max-width: 1020px) {
|
||||||
width: 150px;
|
.hero,
|
||||||
|
.dashboard { grid-template-columns: 1fr; }
|
||||||
|
.intro { min-height: auto; }
|
||||||
|
.canvas-wrap { min-height: 360px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-container {
|
@media (max-width: 680px) {
|
||||||
display: flex;
|
.page { width: min(100% - 20px, 1440px); padding-top: 10px; }
|
||||||
justify-content: center;
|
.topbar { align-items: flex-start; border-radius: 16px; }
|
||||||
margin-bottom: 30px;
|
.status-pill { display: none; }
|
||||||
|
.intro, .lab-card, .panel, .prompt-card { border-radius: 18px; }
|
||||||
|
.control-grid, .theme-row, .stats-grid { grid-template-columns: 1fr; }
|
||||||
|
.canvas-wrap { min-height: 260px; }
|
||||||
|
h1 { font-size: clamp(38px, 13vw, 58px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
#gameCanvas {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
*, *::before, *::after { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.presets {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 15px;
|
|
||||||
padding: 20px;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presets button {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.presets button:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.controls, .presets {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speed-control {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-panel {
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user