A demo of cellular automata.
Last active
March 1, 2025 03:02
-
-
Save primaryobjects/f478815aaba6ac1207a10abf85692548 to your computer and use it in GitHub Desktop.
Conway's Game of Life Cellular Automata, 3D in JavaScript with Three.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Cellular Automata in Three.js</title> | |
<!-- Compiled and minified CSS --> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"> | |
<!-- Compiled and minified JavaScript --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> | |
<link rel="stylesheet" href="style.css"> | |
</head> | |
<body> | |
<div id="ui"> | |
<label for="stayAlive">Stay Alive (comma-separated):</label> | |
<input type="text" id="stayAlive" value="2,3"> | |
<label for="becomeAlive">Become Alive (comma-separated):</label> | |
<input type="text" id="becomeAlive" value="3"> | |
<a class="waves-effect waves-light btn-small" onclick="setAutomataRules()">Set Rules</a> | |
<a class="waves-effect waves-light btn-small" onclick="document.getElementById('ui').style.display = 'none';">Hide Panel</a> | |
<div class="input-field col s12"> | |
<select id="presetDropdown" onchange="applyPreset()"> | |
<option value="custom">Custom</option> | |
<option value="gameOfLife">Game of Life</option> | |
<option value="highLife">HighLife</option> | |
<option value="dayAndNight">Day & Night</option> | |
<option value="diamoeba">Diamoeba</option> | |
<option value="2x2">2x2</option> | |
</select> | |
<label for="presetDropdown">Choose a preset:</label> | |
</div> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/three/examples/js/controls/OrbitControls.js"></script> | |
<script src="script.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const gridSize = 50; | |
const cellSize = 1; | |
let cells = []; | |
let stayAlive = [2, 3]; | |
let becomeAlive = [3]; | |
const aspectRatioOffset = 1; | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000); | |
const renderer = new THREE.WebGLRenderer(); | |
const controls = new THREE.OrbitControls(camera, renderer.domElement); | |
const init = () => { | |
// Scene, camera, and renderer setup | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.appendChild(renderer.domElement); | |
resetGrid(); | |
// Adjust camera aspect ratio and renderer size when the window is resized | |
window.addEventListener('resize', onWindowResize); | |
// Set up the OrbitControls | |
controls.enablePan = false; | |
controls.enableZoom = true; // Enable zoom | |
controls.minDistance = 10; // Minimum zoom distance | |
controls.maxDistance = 200; // Maximum zoom distance | |
// Enable damping (inertia) for smoother controls | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
var elems = document.querySelectorAll('select'); | |
var instances = M.FormSelect.init(elems); | |
// Add an event listener for keydown events | |
document.addEventListener('keydown', function(event) { | |
// Check if the pressed key is 'Escape' | |
if (event.key === 'Escape' || event.key === 'Esc') { | |
// Get the element with the ID 'ui' | |
var uiElement = document.getElementById('ui'); | |
// Toggle the display property | |
if (uiElement.style.display === 'none') { | |
uiElement.style.display = 'block'; | |
} else { | |
uiElement.style.display = 'none'; | |
} | |
} | |
}); | |
animate(); | |
}; | |
const resetGrid = () => { | |
// Clear the previous grid | |
while (scene.children.length > 0) { | |
scene.remove(scene.children[0]); | |
} | |
// Create new grid of cubes | |
for (let x = 0; x < gridSize; x++) { | |
cells[x] = []; | |
for (let y = 0; y < gridSize; y++) { | |
const geometry = new THREE.BoxGeometry(cellSize, cellSize, cellSize); | |
const material = new THREE.MeshBasicMaterial({ color: Math.random() < 0.5 ? 0x0000ff : 0xff0000 }); | |
const cube = new THREE.Mesh(geometry, material); | |
cube.position.set(x * cellSize - 20, y * cellSize - 20, 0); | |
cells[x][y] = { cube, alive: Math.random() < 0.5 }; | |
scene.add(cube); | |
} | |
} | |
updateCameraPosition(); | |
}; | |
const updateCameraPosition = () => { | |
const centerX = (gridSize * cellSize) / 2; | |
const centerY = (gridSize * cellSize) / 2; | |
const aspectRatio = window.innerWidth / window.innerHeight; | |
const zoomOffset = gridSize * cellSize * (aspectRatioOffset / aspectRatio); | |
camera.position.set(centerX, centerY, zoomOffset); | |
camera.lookAt(new THREE.Vector3(centerX, centerY, 0)); | |
}; | |
const onWindowResize = () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
updateCameraPosition(); | |
}; | |
const updateCells = () => { | |
let newCells = JSON.parse(JSON.stringify(cells)); | |
for (let x = 0; x < gridSize; x++) { | |
for (let y = 0; y < gridSize; y++) { | |
const neighbors = countAliveNeighbors(x, y); | |
if (cells[x][y].alive) { | |
newCells[x][y].alive = stayAlive.includes(neighbors); | |
} else { | |
newCells[x][y].alive = becomeAlive.includes(neighbors); | |
} | |
} | |
} | |
for (let x = 0; x < gridSize; x++) { | |
for (let y = 0; y < gridSize; y++) { | |
cells[x][y].alive = newCells[x][y].alive; | |
cells[x][y].cube.material.color.set(cells[x][y].alive ? 0xff0000 : 0x0000ff); | |
} | |
} | |
}; | |
const countAliveNeighbors = (x, y) => { | |
let count = 0; | |
for (let dx = -1; dx <= 1; dx++) { | |
for (let dy = -1; dy <= 1; dy++) { | |
if (dx === 0 && dy === 0) continue; | |
const nx = (x + dx + gridSize) % gridSize; | |
const ny = (y + dy + gridSize) % gridSize; | |
if (cells[nx][ny].alive) count++; | |
} | |
} | |
return count; | |
}; | |
// Function to set the automata rules from user input | |
const setAutomataRules = () => { | |
const stayAliveInput = document.getElementById('stayAlive').value.split(',').map(Number); | |
const becomeAliveInput = document.getElementById('becomeAlive').value.split(',').map(Number); | |
stayAlive = stayAliveInput.filter(num => !isNaN(num)); | |
becomeAlive = becomeAliveInput.filter(num => !isNaN(num)); | |
resetGrid(); // Reset the grid when a preset is selected | |
}; | |
// Function to apply a preset from the dropdown | |
const applyPreset = () => { | |
const preset = document.getElementById('presetDropdown').value; | |
switch (preset) { | |
case 'gameOfLife': | |
stayAlive = [2, 3]; | |
becomeAlive = [3]; | |
break; | |
case 'highLife': | |
stayAlive = [2, 3]; | |
becomeAlive = [3, 6]; | |
break; | |
case 'dayAndNight': | |
stayAlive = [3, 4, 6, 7, 8]; | |
becomeAlive = [3, 6, 7, 8]; | |
break; | |
case 'diamoeba': | |
stayAlive = [5, 6, 7, 8]; | |
becomeAlive = [3, 5, 6, 7, 8]; | |
break; | |
case '2x2': | |
stayAlive = [1, 2, 5]; | |
becomeAlive = [3, 6]; | |
break; | |
default: | |
stayAlive = [2, 3]; | |
becomeAlive = [3]; | |
break; | |
} | |
// Update the input fields to reflect the selected preset | |
document.getElementById('stayAlive').value = stayAlive.join(','); | |
document.getElementById('becomeAlive').value = becomeAlive.join(','); | |
setAutomataRules(); | |
}; | |
// Animation loop | |
const animate = () => { | |
requestAnimationFrame(animate); | |
updateCells(); | |
controls.update(); // Update controls | |
renderer.render(scene, camera); | |
}; | |
document.addEventListener("DOMContentLoaded", init); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const gridSize = 50; | |
const cellSize = 1; | |
let cells = []; | |
let stayAlive = [2, 3]; | |
let becomeAlive = [3]; | |
const aspectRatioOffset = 1; | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000); | |
const renderer = new THREE.WebGLRenderer(); | |
const controls = new THREE.OrbitControls(camera, renderer.domElement); | |
const init = () => { | |
// Scene, camera, and renderer setup | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.appendChild(renderer.domElement); | |
resetGrid(); | |
// Adjust camera aspect ratio and renderer size when the window is resized | |
window.addEventListener('resize', onWindowResize); | |
// Set up the OrbitControls | |
controls.enablePan = false; | |
controls.enableZoom = true; // Enable zoom | |
controls.minDistance = 10; // Minimum zoom distance | |
controls.maxDistance = 200; // Maximum zoom distance | |
// Enable damping (inertia) for smoother controls | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
var elems = document.querySelectorAll('select'); | |
var instances = M.FormSelect.init(elems); | |
// Add an event listener for keydown events | |
document.addEventListener('keydown', function(event) { | |
// Check if the pressed key is 'Escape' | |
if (event.key === 'Escape' || event.key === 'Esc') { | |
// Get the element with the ID 'ui' | |
var uiElement = document.getElementById('ui'); | |
// Toggle the display property | |
if (uiElement.style.display === 'none') { | |
uiElement.style.display = 'block'; | |
} else { | |
uiElement.style.display = 'none'; | |
} | |
} | |
}); | |
animate(); | |
}; | |
const resetGrid = () => { | |
// Clear the previous grid | |
while (scene.children.length > 0) { | |
scene.remove(scene.children[0]); | |
} | |
// Add a light source for 3D effect | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Ambient light | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); // Directional light for 3D shading | |
directionalLight.position.set(gridSize * cellSize / 2, gridSize * cellSize / 2, gridSize); | |
scene.add(directionalLight); | |
const pointLight = new THREE.PointLight(0xffffff, 0.5); // Point light for gradient effect | |
pointLight.position.set(gridSize * cellSize / 2, gridSize * cellSize / 2, gridSize * 2); | |
scene.add(pointLight); | |
// Create new grid of cubes | |
for (let x = 0; x < gridSize; x++) { | |
cells[x] = []; | |
for (let y = 0; y < gridSize; y++) { | |
const geometry = new THREE.BoxGeometry(cellSize, cellSize, cellSize); | |
// Generate gradient colors | |
const colors = new Float32Array(geometry.attributes.position.count * 3); | |
const startColor = new THREE.Color(0xff0000); // Start color: red | |
const endColor = new THREE.Color(0x0000ff); // End color: blue | |
for (let i = 0; i < geometry.attributes.position.count; i++) { | |
const t = i / geometry.attributes.position.count; | |
const color = startColor.clone().lerp(endColor, t); | |
colors[i * 3] = color.r; | |
colors[i * 3 + 1] = color.g; | |
colors[i * 3 + 2] = color.b; | |
} | |
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
const material = new THREE.MeshPhongMaterial({ | |
vertexColors: true, | |
flatShading: true | |
}); | |
const cube = new THREE.Mesh(geometry, material); | |
cube.position.set(x * cellSize - 20, y * cellSize - 20, 0); | |
cells[x][y] = { cube, alive: Math.random() < 0.5 }; | |
scene.add(cube); | |
} | |
} | |
updateCameraPosition(); | |
}; | |
const updateCells = () => { | |
let newCells = JSON.parse(JSON.stringify(cells)); | |
for (let x = 0; x < gridSize; x++) { | |
for (let y = 0; y < gridSize; y++) { | |
const neighbors = countAliveNeighbors(x, y); | |
if (cells[x][y].alive) { | |
newCells[x][y].alive = stayAlive.includes(neighbors); | |
} else { | |
newCells[x][y].alive = becomeAlive.includes(neighbors); | |
} | |
} | |
} | |
for (let x = 0; x < gridSize; x++) { | |
for (let y = 0; y < gridSize; y++) { | |
cells[x][y].alive = newCells[x][y].alive; | |
const cube = cells[x][y].cube; | |
if (cube) { | |
const aliveColor = new THREE.Color(0xff0000); // Bright red color | |
const dyingColor = new THREE.Color(0xffffff); // Fully transparent | |
if (cells[x][y].alive) { | |
cube.material.color.set(aliveColor); | |
cube.material.opacity = 1.0; | |
cube.material.transparent = false; | |
} else { | |
cube.material.transparent = true; | |
const fadingSpeed = 0.05; // Adjust this value to control the fading speed | |
if (cube.material.opacity > 0) { | |
cube.material.opacity -= fadingSpeed; | |
} else { | |
scene.remove(cube); | |
cells[x][y].cube = null; // Remove reference to the cube | |
} | |
} | |
} | |
} | |
} | |
}; | |
const updateCameraPosition = () => { | |
const centerX = (gridSize * cellSize) / 2; | |
const centerY = (gridSize * cellSize) / 2; | |
const aspectRatio = window.innerWidth / window.innerHeight; | |
const zoomOffset = gridSize * cellSize * (aspectRatioOffset / aspectRatio); | |
camera.position.set(centerX, centerY, zoomOffset); | |
camera.lookAt(new THREE.Vector3(centerX, centerY, 0)); | |
}; | |
const onWindowResize = () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
updateCameraPosition(); | |
}; | |
const countAliveNeighbors = (x, y) => { | |
let count = 0; | |
for (let dx = -1; dx <= 1; dx++) { | |
for (let dy = -1; dy <= 1; dy++) { | |
if (dx === 0 && dy === 0) continue; | |
const nx = (x + dx + gridSize) % gridSize; | |
const ny = (y + dy + gridSize) % gridSize; | |
if (cells[nx][ny].alive) count++; | |
} | |
} | |
return count; | |
}; | |
// Function to set the automata rules from user input | |
const setAutomataRules = () => { | |
const stayAliveInput = document.getElementById('stayAlive').value.split(',').map(Number); | |
const becomeAliveInput = document.getElementById('becomeAlive').value.split(',').map(Number); | |
stayAlive = stayAliveInput.filter(num => !isNaN(num)); | |
becomeAlive = becomeAliveInput.filter(num => !isNaN(num)); | |
}; | |
// Function to apply a preset from the dropdown | |
const applyPreset = () => { | |
const preset = document.getElementById('presetDropdown').value; | |
switch (preset) { | |
case 'gameOfLife': | |
stayAlive = [2, 3]; | |
becomeAlive = [3]; | |
break; | |
case 'highLife': | |
stayAlive = [2, 3]; | |
becomeAlive = [3, 6]; | |
break; | |
case 'dayAndNight': | |
stayAlive = [3, 4, 6, 7, 8]; | |
becomeAlive = [3, 6, 7, 8]; | |
break; | |
case 'diamoeba': | |
stayAlive = [5, 6, 7, 8]; | |
becomeAlive = [3, 5, 6, 7, 8]; | |
break; | |
case '2x2': | |
stayAlive = [1, 2, 5]; | |
becomeAlive = [3, 6]; | |
break; | |
default: | |
stayAlive = [2, 3]; | |
becomeAlive = [3]; | |
break; | |
} | |
// Update the input fields to reflect the selected preset | |
document.getElementById('stayAlive').value = stayAlive.join(','); | |
document.getElementById('becomeAlive').value = becomeAlive.join(','); | |
setAutomataRules(); | |
resetGrid(); // Reset the grid when a preset is selected | |
}; | |
// Animation loop | |
const animate = () => { | |
requestAnimationFrame(animate); | |
updateCells(); | |
controls.update(); // Update controls | |
renderer.render(scene, camera); | |
}; | |
document.addEventListener("DOMContentLoaded", init); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const gridSize = 50; | |
const cellSize = 1; | |
let cells = []; | |
let stayAlive = [2, 3]; | |
let becomeAlive = [3]; | |
const aspectRatioOffset = 1; | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000); | |
const renderer = new THREE.WebGLRenderer(); | |
const controls = new THREE.OrbitControls(camera, renderer.domElement); | |
const init = () => { | |
// Scene, camera, and renderer setup | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.appendChild(renderer.domElement); | |
resetGrid(); | |
// Adjust camera aspect ratio and renderer size when the window is resized | |
window.addEventListener('resize', onWindowResize); | |
// Set up the OrbitControls | |
controls.enablePan = false; | |
controls.enableZoom = true; // Enable zoom | |
controls.minDistance = 10; // Minimum zoom distance | |
controls.maxDistance = 200; // Maximum zoom distance | |
// Enable damping (inertia) for smoother controls | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
var elems = document.querySelectorAll('select'); | |
var instances = M.FormSelect.init(elems); | |
// Add an event listener for keydown events | |
document.addEventListener('keydown', function(event) { | |
// Check if the pressed key is 'Escape' | |
if (event.key === 'Escape' || event.key === 'Esc') { | |
// Get the element with the ID 'ui' | |
var uiElement = document.getElementById('ui'); | |
// Toggle the display property | |
if (uiElement.style.display === 'none') { | |
uiElement.style.display = 'block'; | |
} else { | |
uiElement.style.display = 'none'; | |
} | |
} | |
}); | |
animate(); | |
}; | |
const initializeGliders = () => { | |
const gliderPattern = [ | |
[0, 1, 0], | |
[0, 0, 1], | |
[1, 1, 1] | |
]; | |
for (let x = 0; x < gridSize; x += 5) { // Adjust spacing as needed | |
for (let y = 0; y < gridSize; y += 5) { | |
placePattern(x, y, gliderPattern); | |
} | |
} | |
}; | |
const placePattern = (startX, startY, pattern) => { | |
for (let x = 0; x < pattern.length; x++) { | |
for (let y = 0; y < pattern[x].length; y++) { | |
if (pattern[x][y] === 1) { | |
cells[startX + x][startY + y].alive = true; | |
} | |
} | |
} | |
}; | |
const resetGrid = () => { | |
// Clear the previous grid | |
while (scene.children.length > 0) { | |
scene.remove(scene.children[0]); | |
} | |
// Add a light source for 3D effect | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Ambient light | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); // Directional light for 3D shading | |
directionalLight.position.set(gridSize * cellSize / 2, gridSize * cellSize / 2, gridSize); | |
scene.add(directionalLight); | |
const pointLight = new THREE.PointLight(0xffffff, 0.5); // Point light for gradient effect | |
pointLight.position.set(gridSize * cellSize / 2, gridSize * cellSize / 2, gridSize * 2); | |
scene.add(pointLight); | |
// Create new grid of cubes | |
for (let x = 0; x < gridSize; x++) { | |
cells[x] = []; | |
for (let y = 0; y < gridSize; y++) { | |
const geometry = new THREE.BoxGeometry(cellSize, cellSize, cellSize); | |
// Generate gradient colors | |
const colors = new Float32Array(geometry.attributes.position.count * 3); | |
const startColor = new THREE.Color(0xff0000); // Start color: red | |
const endColor = new THREE.Color(0x0000ff); // End color: blue | |
for (let i = 0; i < geometry.attributes.position.count; i++) { | |
const t = i / geometry.attributes.position.count; | |
const color = startColor.clone().lerp(endColor, t); | |
colors[i * 3] = color.r; | |
colors[i * 3 + 1] = color.g; | |
colors[i * 3 + 2] = color.b; | |
} | |
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
const material = new THREE.MeshPhongMaterial({ | |
vertexColors: true, | |
flatShading: true | |
}); | |
const cube = new THREE.Mesh(geometry, material); | |
cube.position.set(x * cellSize - 20, y * cellSize - 20, 0); | |
cells[x][y] = { cube, alive: false }; | |
scene.add(cube); | |
} | |
} | |
// Initialize the grid with gliders | |
initializeGliders(); | |
updateCameraPosition(); | |
}; | |
const updateCells = () => { | |
let newCells = JSON.parse(JSON.stringify(cells)); | |
for (let x = 0; x < gridSize; x++) { | |
for (let y = 0; y < gridSize; y++) { | |
const neighbors = countAliveNeighbors(x, y); | |
if (cells[x][y].alive) { | |
newCells[x][y].alive = stayAlive.includes(neighbors); | |
} else { | |
newCells[x][y].alive = becomeAlive.includes(neighbors); | |
} | |
} | |
} | |
for (let x = 0; x < gridSize; x++) { | |
for (let y = 0; y < gridSize; y++) { | |
cells[x][y].alive = newCells[x][y].alive; | |
const cube = cells[x][y].cube; | |
if (cube) { | |
const aliveColor = new THREE.Color(0xff0000); // Bright red color | |
const dyingColor = new THREE.Color(0xffffff); // Fully transparent | |
if (cells[x][y].alive) { | |
cube.material.color.set(aliveColor); | |
cube.material.opacity = 1.0; | |
cube.material.transparent = false; | |
} else { | |
cube.material.transparent = true; | |
const fadingSpeed = 0.06; // Adjust this value to control the fading speed | |
if (cube.material.opacity > 0) { | |
cube.material.opacity -= fadingSpeed; | |
} else { | |
scene.remove(cube); | |
cells[x][y].cube = null; // Remove reference to the cube | |
} | |
} | |
} | |
} | |
} | |
}; | |
const updateCameraPosition = () => { | |
const centerX = (gridSize * cellSize) / 2; | |
const centerY = (gridSize * cellSize) / 2; | |
const aspectRatio = window.innerWidth / window.innerHeight; | |
const zoomOffset = gridSize * cellSize * (aspectRatioOffset / aspectRatio); | |
camera.position.set(centerX, centerY, zoomOffset); | |
camera.lookAt(new THREE.Vector3(centerX, centerY, 0)); | |
}; | |
const onWindowResize = () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
updateCameraPosition(); | |
}; | |
const countAliveNeighbors = (x, y) => { | |
let count = 0; | |
for (let dx = -1; dx <= 1; dx++) { | |
for (let dy = -1; dy <= 1; dy++) { | |
if (dx === 0 && dy === 0) continue; | |
const nx = (x + dx + gridSize) % gridSize; | |
const ny = (y + dy + gridSize) % gridSize; | |
if (cells[nx][ny].alive) count++; | |
} | |
} | |
return count; | |
}; | |
// Function to set the automata rules from user input | |
const setAutomataRules = () => { | |
const stayAliveInput = document.getElementById('stayAlive').value.split(',').map(Number); | |
const becomeAliveInput = document.getElementById('becomeAlive').value.split(',').map(Number); | |
stayAlive = stayAliveInput.filter(num => !isNaN(num)); | |
becomeAlive = becomeAliveInput.filter(num => !isNaN(num)); | |
}; | |
// Function to apply a preset from the dropdown | |
const applyPreset = () => { | |
const preset = document.getElementById('presetDropdown').value; | |
switch (preset) { | |
case 'gameOfLife': | |
stayAlive = [2, 3]; | |
becomeAlive = [3]; | |
break; | |
case 'highLife': | |
stayAlive = [2, 3]; | |
becomeAlive = [3, 6]; | |
break; | |
case 'dayAndNight': | |
stayAlive = [3, 4, 6, 7, 8]; | |
becomeAlive = [3, 6, 7, 8]; | |
break; | |
case 'diamoeba': | |
stayAlive = [5, 6, 7, 8]; | |
becomeAlive = [3, 5, 6, 7, 8]; | |
break; | |
case '2x2': | |
stayAlive = [1, 2, 5]; | |
becomeAlive = [3, 6]; | |
break; | |
default: | |
stayAlive = [2, 3]; | |
becomeAlive = [3]; | |
break; | |
} | |
// Update the input fields to reflect the selected preset | |
document.getElementById('stayAlive').value = stayAlive.join(','); | |
document.getElementById('becomeAlive').value = becomeAlive.join(','); | |
setAutomataRules(); | |
resetGrid(); // Reset the grid when a preset is selected | |
}; | |
// Animation loop | |
const animate = () => { | |
requestAnimationFrame(animate); | |
updateCells(); | |
controls.update(); // Update controls | |
renderer.render(scene, camera); | |
}; | |
document.addEventListener("DOMContentLoaded", init); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const gridSize = 50; | |
const cellSize = 1; | |
let cells = []; | |
let stayAlive = [2, 3]; | |
let becomeAlive = [3]; | |
const aspectRatioOffset = 1; | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000); | |
const renderer = new THREE.WebGLRenderer(); | |
const controls = new THREE.OrbitControls(camera, renderer.domElement); | |
const init = () => { | |
// Scene, camera, and renderer setup | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.appendChild(renderer.domElement); | |
resetGrid(); | |
// Adjust camera aspect ratio and renderer size when the window is resized | |
window.addEventListener('resize', onWindowResize); | |
// Set up the OrbitControls | |
controls.enablePan = false; | |
controls.enableZoom = true; // Enable zoom | |
controls.minDistance = 10; // Minimum zoom distance | |
controls.maxDistance = 200; // Maximum zoom distance | |
// Enable damping (inertia) for smoother controls | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
var elems = document.querySelectorAll('select'); | |
var instances = M.FormSelect.init(elems); | |
// Add an event listener for keydown events | |
document.addEventListener('keydown', function(event) { | |
// Check if the pressed key is 'Escape' | |
if (event.key === 'Escape' || event.key === 'Esc') { | |
// Get the element with the ID 'ui' | |
var uiElement = document.getElementById('ui'); | |
// Toggle the display property | |
if (uiElement.style.display === 'none') { | |
uiElement.style.display = 'block'; | |
} else { | |
uiElement.style.display = 'none'; | |
} | |
} | |
}); | |
animate(); | |
}; | |
const initializeGliders = () => { | |
const gliderPattern = [ | |
[0, 1, 0], | |
[0, 0, 1], | |
[1, 1, 1] | |
]; | |
for (let x = 0; x < gridSize; x += 5) { // Adjust spacing as needed | |
for (let y = 0; y < gridSize; y += 5) { | |
placePattern(x, y, gliderPattern); | |
} | |
} | |
}; | |
const placePattern = (startX, startY, pattern) => { | |
for (let x = 0; x < pattern.length; x++) { | |
for (let y = 0; y < pattern[x].length; y++) { | |
if (pattern[x][y] === 1) { | |
cells[startX + x][startY + y].alive = true; | |
} | |
} | |
} | |
}; | |
const resetGrid = () => { | |
// Clear the previous grid | |
while (scene.children.length > 0) { | |
scene.remove(scene.children[0]); | |
} | |
// Add a light source for 3D effect | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Ambient light | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); // Directional light for 3D shading | |
directionalLight.position.set(gridSize * cellSize / 2, gridSize * cellSize / 2, gridSize); | |
scene.add(directionalLight); | |
const pointLight = new THREE.PointLight(0xffffff, 0.5); // Point light for gradient effect | |
pointLight.position.set(gridSize * cellSize / 2, gridSize * cellSize / 2, gridSize * 2); | |
scene.add(pointLight); | |
// Create new grid of cubes | |
for (let x = 0; x < gridSize; x++) { | |
cells[x] = []; | |
for (let y = 0; y < gridSize; y++) { | |
const geometry = new THREE.BoxGeometry(cellSize, cellSize, cellSize); | |
// Generate gradient colors | |
const colors = new Float32Array(geometry.attributes.position.count * 3); | |
const startColor = new THREE.Color(0x00ffff); // Start color: red | |
const endColor = new THREE.Color(0x0000ff); // End color: blue | |
for (let i = 0; i < geometry.attributes.position.count; i++) { | |
const t = i / geometry.attributes.position.count; | |
const color = startColor.clone().lerp(endColor, t); | |
colors[i * 3] = color.r; | |
colors[i * 3 + 1] = color.g; | |
colors[i * 3 + 2] = color.b; | |
} | |
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
const material = new THREE.MeshPhongMaterial({ | |
vertexColors: true, | |
flatShading: true | |
}); | |
const cube = new THREE.Mesh(geometry, material); | |
cube.position.set(x * cellSize - 20, y * cellSize - 20, 0); | |
cells[x][y] = { cube, alive: false }; | |
scene.add(cube); | |
} | |
} | |
// Initialize the grid with gliders | |
initializeGliders(); | |
updateCameraPosition(); | |
}; | |
const updateCells = () => { | |
let newCells = JSON.parse(JSON.stringify(cells)); | |
for (let x = 0; x < gridSize; x++) { | |
for (let y = 0; y < gridSize; y++) { | |
const neighbors = countAliveNeighbors(x, y); | |
if (cells[x][y].alive) { | |
newCells[x][y].alive = stayAlive.includes(neighbors); | |
} else { | |
newCells[x][y].alive = becomeAlive.includes(neighbors); | |
} | |
} | |
} | |
for (let x = 0; x < gridSize; x++) { | |
for (let y = 0; y < gridSize; y++) { | |
cells[x][y].alive = newCells[x][y].alive; | |
const cube = cells[x][y].cube; | |
if (cube) { | |
const aliveColor = new THREE.Color(0x00ffff); // Bright red color | |
const dyingColor = new THREE.Color(0xffffff); // Fully transparent | |
if (cells[x][y].alive) { | |
cube.material.color.set(aliveColor); | |
cube.material.opacity = 1.0; | |
cube.material.transparent = false; | |
} else { | |
cube.material.transparent = true; | |
const fadingSpeed = 0.06; // Adjust this value to control the fading speed | |
if (cube.material.opacity > 0) { | |
cube.material.opacity -= fadingSpeed; | |
} else { | |
scene.remove(cube); | |
cells[x][y].cube = null; // Remove reference to the cube | |
} | |
} | |
} | |
} | |
} | |
}; | |
const updateCameraPosition = () => { | |
const centerX = (gridSize * cellSize) / 2; | |
const centerY = (gridSize * cellSize) / 2; | |
const aspectRatio = window.innerWidth / window.innerHeight; | |
const zoomOffset = gridSize * cellSize * (aspectRatioOffset / aspectRatio); | |
camera.position.set(centerX, centerY, zoomOffset); | |
camera.lookAt(new THREE.Vector3(centerX, centerY, 0)); | |
}; | |
const onWindowResize = () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
updateCameraPosition(); | |
}; | |
const countAliveNeighbors = (x, y) => { | |
let count = 0; | |
for (let dx = -1; dx <= 1; dx++) { | |
for (let dy = -1; dy <= 1; dy++) { | |
if (dx === 0 && dy === 0) continue; | |
const nx = (x + dx + gridSize) % gridSize; | |
const ny = (y + dy + gridSize) % gridSize; | |
if (cells[nx][ny].alive) count++; | |
} | |
} | |
return count; | |
}; | |
// Function to set the automata rules from user input | |
const setAutomataRules = () => { | |
const stayAliveInput = document.getElementById('stayAlive').value.split(',').map(Number); | |
const becomeAliveInput = document.getElementById('becomeAlive').value.split(',').map(Number); | |
stayAlive = stayAliveInput.filter(num => !isNaN(num)); | |
becomeAlive = becomeAliveInput.filter(num => !isNaN(num)); | |
}; | |
// Function to apply a preset from the dropdown | |
const applyPreset = () => { | |
const preset = document.getElementById('presetDropdown').value; | |
switch (preset) { | |
case 'gameOfLife': | |
stayAlive = [2, 3]; | |
becomeAlive = [3]; | |
break; | |
case 'highLife': | |
stayAlive = [2, 3]; | |
becomeAlive = [3, 6]; | |
break; | |
case 'dayAndNight': | |
stayAlive = [3, 4, 6, 7, 8]; | |
becomeAlive = [3, 6, 7, 8]; | |
break; | |
case 'diamoeba': | |
stayAlive = [5, 6, 7, 8]; | |
becomeAlive = [3, 5, 6, 7, 8]; | |
break; | |
case '2x2': | |
stayAlive = [1, 2, 5]; | |
becomeAlive = [3, 6]; | |
break; | |
default: | |
stayAlive = [2, 3]; | |
becomeAlive = [3]; | |
break; | |
} | |
// Update the input fields to reflect the selected preset | |
document.getElementById('stayAlive').value = stayAlive.join(','); | |
document.getElementById('becomeAlive').value = becomeAlive.join(','); | |
setAutomataRules(); | |
resetGrid(); // Reset the grid when a preset is selected | |
}; | |
// Animation loop | |
const animate = () => { | |
requestAnimationFrame(animate); | |
updateCells(); | |
controls.update(); // Update controls | |
renderer.render(scene, camera); | |
}; | |
document.addEventListener("DOMContentLoaded", init); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); | |
body, html { | |
margin: 0; | |
padding: 0; | |
width: 100%; | |
height: 100%; | |
overflow: hidden; | |
} | |
canvas { | |
display: block; | |
} | |
body, button { | |
font-family: 'Roboto', sans-serif; | |
} | |
#ui, button { | |
transition: transform 0.3s ease, box-shadow 0.3s ease; | |
} | |
button:hover { | |
transform: scale(1.05); | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); | |
} | |
#ui { | |
display: none; | |
} | |
@media (max-width: 768px) { | |
#ui { | |
width: 90%; | |
left: 50%; | |
transform: translateX(-50%); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment