Custom Code Experiment •
Faction Mosaic 1
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Faction Mosaic 1</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
:root {
color-scheme: light dark;
background-color: light-dark(#eaeef3, #0a0c12);
}
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: inherit;
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
}
canvas {
display: block;
cursor: pointer;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
</head>
<body>
<script>
(function(){
let FactionCount = 4;
const simSize = 100;
const stability = 12;
const mutations = 3;
const mutationRate = 60;
const maxFPS = 24;
const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
const idx2coord = (index, width = simSize) => [index % width, Math.floor(index / width)];
const initPalette = (length, hueOffset = randomInt(0, 240)) => {
return Array.from({ length }, (_, i) => {
const lightness = Math.round(48 + (i / length) * 28);
const chroma = 62 + Math.floor(Math.random() * 12);
const hue = (hueOffset + (360 / length) * i) % 360;
return `oklch(${lightness}% ${chroma}% ${hue}deg / 0.85)`;
});
};
const initWins = (length) => Array.from({ length }, (_, i) => (i - 1 + length) % length);
const NeighborOffsets = [
[-1, -1], [0, -1], [1, -1],
[-1, 0], [1, 0],
[-1, 1], [0, 1], [1, 1]
];
const OffsetToIndex = [
-1 - simSize, 0 - simSize, 1 - simSize,
-1, 1,
-1 + simSize, simSize, 1 + simSize
];
const getNeighbors = (index, array) => {
const [u, v] = idx2coord(index);
const neighbors = new Array(NeighborOffsets.length);
for (let k = 0; k < NeighborOffsets.length; k++) {
const dx = NeighborOffsets[k][0];
const dy = NeighborOffsets[k][1];
const nx = (u + dx + simSize) % simSize;
const ny = (v + dy + simSize) % simSize;
neighbors[k] = array[nx + ny * simSize];
}
return neighbors;
};
let grid = Array.from({ length: simSize ** 2 }, () => randomInt(0, FactionCount - 1));
let Palette = initPalette(FactionCount);
let Wins = initWins(FactionCount);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
function resizeCanvasToDisplay() {
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w;
canvas.height = h;
draw();
}
window.addEventListener('resize', () => {
resizeCanvasToDisplay();
});
const randomize = () => {
FactionCount = randomInt(3, 5);
grid = Array.from({ length: simSize ** 2 }, () => randomInt(0, FactionCount - 1));
Palette = initPalette(FactionCount);
Wins = initWins(FactionCount);
draw();
};
let interval = setInterval(() => randomize(), 10000);
canvas.addEventListener('click', () => {
randomize();
if (interval) {
clearInterval(interval);
interval = null;
}
});
const step = () => {
const scores = new Array(grid.length);
for (let i = 0; i < grid.length; i++) {
const cell = grid[i];
const neighbors = getNeighbors(i, grid);
let total = 0;
for (let n = 0; n < neighbors.length; n++) {
const nb = neighbors[n];
if (cell === nb) {
if (randomInt(0, stability) === 0) {
total += randomInt(-4, 6);
}
} else {
total += (Wins[cell] === nb) ? 1 : -1;
}
}
scores[i] = total;
}
const newGrid = new Array(grid.length);
for (let i = 0; i < grid.length; i++) {
if (scores[i] >= 0) {
newGrid[i] = grid[i];
} else {
const neighborScores = getNeighbors(i, scores);
let bestScore = -Infinity;
let bestIdx = 0;
for (let k = 0; k < neighborScores.length; k++) {
if (neighborScores[k] > bestScore) {
bestScore = neighborScores[k];
bestIdx = k;
}
}
const neighborIndex = (i + OffsetToIndex[bestIdx] + grid.length) % grid.length;
newGrid[i] = grid[neighborIndex];
}
}
grid = newGrid;
if (randomInt(0, mutationRate) === 0) {
for (let m = 0; m < mutations; m++) {
const center = randomInt(0, grid.length - 1);
const newColor = randomInt(0, FactionCount - 1);
for (let k = 0; k < OffsetToIndex.length; k++) {
const idx = (grid.length + center + OffsetToIndex[k]) % grid.length;
grid[idx] = newColor;
}
grid[center] = newColor;
}
}
};
const draw = () => {
if (!ctx) return;
const w = canvas.width;
const h = canvas.height;
if (w === 0 || h === 0) return;
const cellW = w / simSize;
const cellH = h / simSize;
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < grid.length; i++) {
const faction = grid[i];
const [col, row] = idx2coord(i);
const x = col * cellW;
const y = row * cellH;
const baseRadius = Math.min(cellW, cellH) * 0.48;
const radiusJitter = (Math.random() * 4) - 1.2;
const radius = Math.max(2.5, baseRadius + radiusJitter);
const offsetX = Math.random() * cellW;
const offsetY = Math.random() * cellH;
ctx.fillStyle = Palette[faction];
ctx.beginPath();
ctx.arc(x + offsetX, y + offsetY, radius, 0, Math.PI * 2);
ctx.fill();
}
};
let lastTimestamp = 0;
const animationLoop = (timestamp) => {
requestAnimationFrame(animationLoop);
if (timestamp - lastTimestamp >= 1000 / maxFPS) {
lastTimestamp = timestamp;
step();
draw();
}
};
resizeCanvasToDisplay();
draw();
animationLoop(-999);
window.addEventListener('orientationchange', () => {
setTimeout(() => {
resizeCanvasToDisplay();
draw();
}, 30);
});
})();
</script>
</body>
</html>