``````//-
Our cube is going to be comprised of 6 faces.
In code, we'll need to refer to these by some index or ID, so let's define that!

3 (top)
+--------+
/|       /|
/ |      / |
+--------+  |
|  |  1  | 4|
|0 +-----|--+
| /   5  | /
|/       |/
+--------+
2 (bot)

Overflow on any edge of a face will go to the adjacent face.
//

//- Pick how many cells we will fit into each row/column of a face
- const CELL_DENSITY = 20;

//- Some computed constants based on CELL_DENSITY
- const CELL_WIDTH = 400 / CELL_DENSITY;
- const CELL_HEIGHT = CELL_WIDTH;
- const CELL_COUNT = CELL_DENSITY * CELL_DENSITY;

- const lerp = (start, end, t) => start * (1 - t) + end * t;
- const interpolate = (...pts) => pts.map(pt => pt.join(',')).join(' ');

//- This mixin is going to help us draw a face filled with a grid of cells
mixin face(id, tl, tr, br, bl)
g(id=id)
//- Let's create a "face" polygon in case we need to style the whole surface
- const points = interpolate(tl, tr, br, bl);
polygon.face(
points=points
)
//- Now, to fill our face with cells
- for (let cell = 0; cell < CELL_COUNT; cell++)
-
// First, let's find out what col and row we are on
const col = cell % CELL_DENSITY;
const row = Math.floor(cell / CELL_DENSITY);

// Next, let's compute the X and Y ranges for the
// top and bottom "rails" of the row
const topXRange = [
lerp(tl[0], bl[0], row / CELL_DENSITY),
lerp(tr[0], br[0], row / CELL_DENSITY)
];
const topYRange = [
lerp(tl[1], bl[1], row / CELL_DENSITY),
lerp(tr[1], br[1], row / CELL_DENSITY)
];
const botXRange = [
lerp(tl[0], bl[0], (row + 1) / CELL_DENSITY),
lerp(tr[0], br[0], (row + 1) / CELL_DENSITY),
]
const botYRange = [
lerp(tl[1], bl[1], (row + 1) / CELL_DENSITY),
lerp(tr[1], br[1], (row + 1) / CELL_DENSITY),
]

// Then, let's compute the four corners of the polygon
// that will make up the cell
const cell_tl = [
lerp(...topXRange, col / CELL_DENSITY),
lerp(...topYRange, col / CELL_DENSITY),
];
const cell_tr = [
lerp(...topXRange, (col + 1) / CELL_DENSITY),
lerp(...topYRange, (col + 1) / CELL_DENSITY),
];
const cell_br = [
lerp(...botXRange, (col + 1) / CELL_DENSITY),
lerp(...botYRange, (col + 1) / CELL_DENSITY),
];
const cell_bl = [
lerp(...botXRange, col / CELL_DENSITY),
lerp(...botYRange, col / CELL_DENSITY)
];

// Finally, interpolate it into a string format for SVG
const points = interpolate(cell_tl, cell_tr, cell_br, cell_bl);

polygon.cell(
points=points
)

svg(xmlns="http://www.w3.org/2000/svg", viewBox="0 0 400 400")
//- Now the fun part! Let's draw our cube's faces

-
[0, 320],
[200, 260],
[400, 320],
[200, 380],
];
polygon(
)

-
const pts0 = [
[200, 20],
[0, 80],
[0, 320],
[200, 260],
];
+face('face-0', ...pts0)

-
const pts1 = [
[400, 80],
[200, 20],
[200, 260],
[400, 320],
]
+face('face-1', ...pts1)

-
const pts2 = [
[0, 320],
[200, 260],
[400, 320],
[200, 380],
];
+face('face-2', ...pts2)

-
const pts3 = [
[0, 80],
[200, 20],
[400, 80],
[200, 140],
];
+face('face-3', ...pts3)

-
const pts4 = [
[200, 140],
[400, 80],
[400, 320],
[200, 380],
]
+face('face-4', ...pts4)

-
const pts5 = [
[0, 80],
[200, 140],
[200, 380],
[0, 320],
]
+face('face-5', ...pts5)

button.reset Regenerate``````
``````:root {
--ease: cubic-bezier(0.17, 0.67, 0.54, 1);
--color-bg: rgb(14, 39, 60);

--offset: 8px;
--min-offset: calc(var(--offset) * -1);
--close-scale: 0.7;
--open-scale: 0.9;
}

html,
body {
height: 100%;
}

body {
margin: 0;
height: 100vh;
background-color: var(--color-bg);
display: flex;
justify-content: center;
flex-direction: column;
}

svg {
width: 90vw;
max-width: 620px;
margin: 0 auto;
overflow: visible;
}

rect,
g {
transform-box: fill-box;
transform-origin: center;
}

.face {
stroke: none;
fill: var(--color-bg);
transform-box: fill-box;
transform-origin: center;
}

transform: translateY(20px);
fill: rgba(0, 0, 0, 0.1);
filter: blur(8px);
}

.cell {
transform-box: fill-box;
transform-origin: center;
transition: all 80ms var(--ease) 180ms;
transform: scale(var(--close-scale));
stroke: var(--color-inactive);
stroke-width: 4px;
fill: none;
opacity: 1;
}

#face-0,
#face-1,
#face-2 {
display: none;
}

#face-3 {
--color-active: rgba(208, 57, 112, 1);
--color-inactive: rgba(23, 65, 99, 0.2);
}

#face-4 {
--color-active: rgba(182, 43, 94, 1);
--color-inactive: rgba(19, 54, 83, 0.2);
}

#face-5 {
--color-active: rgba(212, 73, 124, 1);
--color-inactive: rgba(31, 87, 132, 0.2);
}

.cell[data-alive="1"] {
stroke: var(--color-active);
}

#face-3 .cell[data-alive="1"] {
transform: scale(var(--open-scale)) translate(0px, var(--min-offset));
}

#face-4 .cell[data-alive="1"] {
transform: scale(var(--open-scale)) translate(var(--offset), var(--offset));
}

#face-5 .cell[data-alive="1"] {
transform: scale(var(--open-scale))
translate(var(--min-offset), var(--offset));
}

button.reset {
position: fixed;
right: 24px;
bottom: 24px;
cursor: pointer;
-webkit-appearance: none;
transition: all 0.3s ease;
border: none;
background: rgba(211, 188, 204, 0.08);
color: hsl(100, 0%, 78%);
width: auto;
margin: 48px auto 0;
font-size: 1rem;
font-family: "Helvetica Neue", "Helvetica", "Roboto", "Segoe UI", "Arial", sans-serif;
text-transform: uppercase;
letter-spacing: 1px;
outline: none;
}

button.reset:disabled {
opacity: 0.5;
}

button.reset:hover:not(:disabled) {
background: rgba(211, 188, 204, 0.2);
}

button.reset:active:not(:disabled) {
background: rgba(211, 188, 204, 0.3);
}``````
``````const resetBtn = document.querySelector('.reset');
const svg = document.querySelector("svg");
const groups = svg.querySelectorAll("g[id*=face]");
const faceNodes = Array.from(groups).map((f) => f.querySelectorAll(".cell"));
const density = Math.sqrt(faceNodes[0].length);
const lastIndex = density - 1;

function toRowCol(index) {
const row = Math.floor(index / density);
const col = index % density;

return [row, col];
}

function toIndex(row, col) {
return row * density + col;
}

function generateFaces() {
return faceNodes.map((f, fi) =>
Array.from(f).map((c, i) => {
if ([0, 1, 2].includes(fi)) return 0;

return Math.random() > 0.85 ? 1 : 0;
})
);
}

let faces = generateFaces();

const edgeFuncs = [
// 0
(row, col) => {
if (col >= density) {
return getCell(5, row, 0);
} else if (col < 0) {
return getCell(1, row, lastIndex);
} else if (row >= density) {
return getCell(2, 0, lastIndex - col);
} else if (row < 0) {
return getCell(3, 0, lastIndex - col);
}
},
// 1
(row, col) => {
if (col >= density) {
return getCell(0, row, 0);
} else if (col < 0) {
return getCell(4, row, lastIndex);
} else if (row >= density) {
return getCell(2, lastIndex - col, lastIndex);
} else if (row < 0) {
return getCell(3, lastIndex - col, lastIndex);
}
},
// 2
(row, col) => {
if (col >= density) {
return getCell(1, lastIndex, lastIndex - row);
} else if (col < 0) {
return getCell(5, lastIndex, row);
} else if (row >= density) {
return getCell(4, lastIndex, col);
} else if (row < 0) {
return getCell(0, lastIndex, lastIndex - col);
}
},
// 3
(row, col) => {
if (col >= density) {
return getCell(1, 0, lastIndex - row);
} else if (col < 0) {
return getCell(5, 0, row);
} else if (row >= density) {
return getCell(4, 0, col);
} else if (row < 0) {
return getCell(0, 0, lastIndex - col);
}
},
// 4
(row, col) => {
if (col >= density) {
return getCell(1, row, 0);
} else if (col < 0) {
return getCell(5, row, lastIndex);
} else if (row >= density) {
return getCell(2, lastIndex, col);
} else if (row < 0) {
return getCell(3, lastIndex, col);
}
},
// 5
(row, col) => {
if (col >= density) {
return getCell(4, row, 0);
} else if (col < 0) {
return getCell(0, row, lastIndex);
} else if (row >= density) {
return getCell(2, col, 0);
} else if (row < 0) {
return getCell(3, col, 0);
}
}
];

function getCell(faceIndex, row, col) {
const getPossibleEdgeCell = edgeFuncs[faceIndex];
const overflowRow = row >= density || row < 0;
const overflowCol = col >= density || col < 0;

if (overflowRow && overflowCol) {
return 1;
}

return getPossibleEdgeCell(row, col) ?? faces[faceIndex][toIndex(row, col)];
}

function getCellNeighbors(faceIndex, cellIndex) {
const face = faces[faceIndex];
const [row, col] = toRowCol(cellIndex);

return [
getCell(faceIndex, row - 1, col - 1),
getCell(faceIndex, row - 1, col),
getCell(faceIndex, row - 1, col + 1),
getCell(faceIndex, row, col - 1),
getCell(faceIndex, row, col + 1),
getCell(faceIndex, row + 1, col - 1),
getCell(faceIndex, row + 1, col),
getCell(faceIndex, row + 1, col + 1)
];
}

function nextCellState(cell, neighbors) {
const liveNeighbors = neighbors.reduce((acc, curr) =>
curr ? acc + curr : acc
);

// Game of Life
if (!cell && liveNeighbors === 3) {
return 1;
} else if (cell && (liveNeighbors === 2 || liveNeighbors === 3)) {
return 1;
} else {
return 0;
}

// Seeds
return liveNeighbors === 2 ? 1 : 0;
}

function nextFaceState(cells, faceIndex) {
const newState = [...cells];

for (let [i, cell] of Object.entries(cells)) {
const neighbors = getCellNeighbors(faceIndex, i);
const newCellState = nextCellState(cell, neighbors);
newState[i] = newCellState;
}

return newState;
}

function nextGameState() {
faces = faces.map((face, i) => nextFaceState(face, i));
}

function render() {
faces.forEach((face, faceIndex) => {
face.forEach((cell, cellIndex) => {
const node = faceNodes[faceIndex][cellIndex];
const curr = parseInt(node.dataset.alive ?? 0);
if (curr !== cell) {
node.dataset.alive = cell ? "1" : "0";
}
});
});
}

setInterval(() => {
requestAnimationFrame(() => {
nextGameState();
render();
});
}, 124);

render();