//-
  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
  
  -
    const shadow = [
      [0, 320],
      [200, 260],
      [400, 320],
      [200, 380],
    ];
  polygon(
    id="shadow"
    points=interpolate(shadow)
  )

  -
    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
View Compiled
: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;
  padding: 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;
}

#shadow {
  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;
  padding: 8px 16px;
  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();

resetBtn.addEventListener('click', e => {
  faces = generateFaces();
})

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.