Skip to content

Instantly share code, notes, and snippets.

@primaryobjects
Last active March 1, 2025 03:02
Show Gist options
  • Save primaryobjects/f478815aaba6ac1207a10abf85692548 to your computer and use it in GitHub Desktop.
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

Conway's Game of Life

A demo of cellular automata.

cap1a cap2a

<!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>
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);
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);
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);
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);
@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