07Honeycomb-Grid Automaton Whose State Splits on a Six-Neighbor Count Value
Cells are offset into a hexagonal layout so each has exactly six neighbors, and the count of live cells among those six is compared against the rule table to decide the next generation, spreading patterns in directions a square grid cannot.
Best forBoard-game-style backgrounds, honeycomb-themed decorative sections
OFF_EVENOFF_ODDhexPath

<main class="stage">
<div class="rv is-demo" tabindex="0" role="button" aria-label="Honeycomb grid running -- click or press space to pause, R to reseed">
<canvas class="rv__cv" width="480" height="300"></canvas>
<p class="rv__hint">Click or press space to pause, R to reseed</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-orange);
.rv {
position: relative;
width: min(92vw, calc(160vh - 12.8vw));
aspect-ratio: 480 / 300;
margin: 0 auto;
border-radius: $r-card;
overflow: hidden;
cursor: pointer;
box-shadow: $shadow-raised;
outline-offset: 4px;
&:focus-visible {
outline: 3px solid $subject-cream;
}
}
.rv__cv {
display: block;
width: 100%;
height: 100%;
}
.rv__hint {
position: absolute;
left: $sp-3;
bottom: $sp-3;
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
font-size: 12px;
line-height: 1.4;
font-weight: 500;
background: rgba(255, 247, 230, .85);
color: $subject-ink;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.rv {
cursor: pointer;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.rv {
position: relative;
width: min(92vw, 160vh - 12.8vw);
aspect-ratio: 480/300;
margin: 0 auto;
border-radius: 16px;
overflow: hidden;
cursor: pointer;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
outline-offset: 4px;
}
.rv:focus-visible {
outline: 3px solid #fff7e6;
}
.rv__cv {
display: block;
width: 100%;
height: 100%;
}
.rv__hint {
position: absolute;
left: 12px;
bottom: 12px;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
font-size: 12px;
line-height: 1.4;
font-weight: 500;
background: rgba(255, 247, 230, 0.85);
color: #17141a;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.rv {
cursor: pointer;
}
}(function(){
'use strict';
// 395-09(진행률 채움 로더)와 다른 축: 육각 이웃 수 값이 생사를 가르는 계산이 주인공.
// odd-q 어긋난 육각 좌표계에서 매 세대 여섯 이웃 수를 세어 전체를 다시 계산하고 beginPath·fill 로 그린다.
function makeLcg(seed){
let s = seed >>> 0;
return function(){
s = (s * 1664525 + 1013904223) >>> 0;
return s / 4294967296;
};
}
const W = 480, H = 300;
const SIZE = 16;
const HORIZ = SIZE * 1.5;
const VERT = SIZE * Math.sqrt(3);
const COLS = 18, ROWS = 9;
const OX = 30, OY = 28;
const ORANGE = '#c89a8f';
const ALIVE_COLOR = '#93a4ca';
let SEED = 70701;
let rnd = makeLcg(SEED);
const SURVIVE_MIN = 2, SURVIVE_MAX = 3, BIRTH_COUNT = 2; // 육각 6이웃 기준 생존·탄생 문턱
let grid = new Uint8Array(COLS * ROWS);
let next = new Uint8Array(COLS * ROWS);
function idx(x, y){ return y * COLS + x; }
// odd-q 어긋난 육각 좌표계 이웃 여섯 개 — 열이 짝수냐 홀수냐에 따라 표가 다르다.
const OFF_EVEN = [[-1,-1],[-1,0],[0,-1],[0,1],[1,-1],[1,0]];
const OFF_ODD = [[-1,0],[-1,1],[0,-1],[0,1],[1,0],[1,1]];
function seedGrid(){
for (let i = 0; i < grid.length; i++) grid[i] = rnd() < 0.33 ? 1 : 0;
}
function neighborCount(x, y){
const offs = (x & 1) ? OFF_ODD : OFF_EVEN;
let n = 0;
for (let i = 0; i < 6; i++){
const nx = x + offs[i][0], ny = y + offs[i][1];
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
n += grid[idx(nx, ny)];
}
return n;
}
function step(){
for (let y = 0; y < ROWS; y++){
for (let x = 0; x < COLS; x++){
const alive = grid[idx(x, y)] === 1;
const n = neighborCount(x, y);
let willLive = 0;
if (alive){
willLive = (n >= SURVIVE_MIN && n <= SURVIVE_MAX) ? 1 : 0;
} else {
willLive = (n === BIRTH_COUNT) ? 1 : 0;
}
next[idx(x, y)] = willLive;
}
}
const tmp = grid; grid = next; next = tmp;
}
const cvEl = document.querySelector('.rv__cv');
const ctx = cvEl.getContext('2d');
const rootEl = document.querySelector('.rv');
function hexPath(cx, cy){
ctx.beginPath();
for (let i = 0; i < 6; i++){
const a = Math.PI / 180 * (60 * i);
const vx = cx + (SIZE - 1.2) * Math.cos(a);
const vy = cy + (SIZE - 1.2) * Math.sin(a);
if (i === 0) ctx.moveTo(vx, vy); else ctx.lineTo(vx, vy);
}
ctx.closePath();
}
function render(){
ctx.fillStyle = ORANGE;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = ALIVE_COLOR;
for (let y = 0; y < ROWS; y++){
for (let x = 0; x < COLS; x++){
if (grid[idx(x, y)] === 1){
const cx = OX + x * HORIZ;
const cy = OY + VERT * (y + 0.5 * (x & 1));
hexPath(cx, cy);
ctx.fill();
}
}
}
}
const ARIA_PLAY = 'Honeycomb grid running -- click or press space to pause, R to reseed';
const ARIA_PAUSE = 'Honeycomb grid paused -- click or press space to resume, R to reseed';
function updateAria(){
rootEl.setAttribute('aria-label', paused ? ARIA_PAUSE : ARIA_PLAY);
}
let paused = false;
const reduceMQ = window.matchMedia('(prefers-reduced-motion: reduce)');
function human(){ rootEl.classList.remove('is-demo'); }
function reseed(){
SEED = (SEED * 1103515245 + 12345) >>> 0;
rnd = makeLcg(SEED || 1);
seedGrid();
}
function togglePause(){
paused = !paused;
updateAria();
}
function onActivate(e){
if (e.type === 'keydown'){
if (e.key === 'r' || e.key === 'R'){
human();
reseed();
render();
return;
}
if (e.key !== 'Enter' && e.key !== ' ') return;
e.preventDefault();
}
human();
if (reduceMQ.matches){
step();
render();
} else {
togglePause();
}
}
function onKey(e){
if (e.key === 'r' || e.key === 'R' || e.key === 'Enter' || e.key === ' '){
onActivate(e);
}
}
const GEN_INTERVAL_MS = 85;
let acc = 0, last = 0;
function frame(now){
if (!reduceMQ.matches){
if (!last) last = now;
const dt = now - last;
last = now;
if (!paused){
acc += dt;
while (acc >= GEN_INTERVAL_MS){
step();
acc -= GEN_INTERVAL_MS;
}
render();
}
requestAnimationFrame(frame);
}
}
function onResize(){ render(); }
rootEl.addEventListener('click', onActivate);
rootEl.addEventListener('keydown', onKey);
window.addEventListener('resize', onResize);
seedGrid();
updateAria();
render();
if (!reduceMQ.matches){
requestAnimationFrame(frame);
}
})();import { useEffect, useRef } from 'react';
import styles from './HexGridAutomaton.module.scss';
function ctx2d(cv: HTMLCanvasElement): CanvasRenderingContext2D {
const c = cv.getContext('2d');
if (!c) throw new Error('no 2d context');
return c;
}
function makeLcg(seed: number): () => number {
let s = seed >>> 0;
return function(){
s = (s * 1664525 + 1013904223) >>> 0;
return s / 4294967296;
};
}
const W = 480, H = 300;
const SIZE = 16;
const HORIZ = SIZE * 1.5;
const VERT = SIZE * Math.sqrt(3);
const COLS = 18, ROWS = 9;
const OX = 30, OY = 28;
const ORANGE = '#c89a8f';
const ALIVE_COLOR = '#93a4ca';
let SEED = 70701;
let rnd = makeLcg(SEED);
const SURVIVE_MIN = 2, SURVIVE_MAX = 3, BIRTH_COUNT = 2; // 육각 6이웃 기준 생존·탄생 문턱
let grid = new Uint8Array(COLS * ROWS);
let next = new Uint8Array(COLS * ROWS);
function idx(x: number, y: number): number { return y * COLS + x; }
// odd-q 어긋난 육각 좌표계 이웃 여섯 개 — 열이 짝수냐 홀수냐에 따라 표가 다르다.
const OFF_EVEN = [[-1,-1],[-1,0],[0,-1],[0,1],[1,-1],[1,0]];
const OFF_ODD = [[-1,0],[-1,1],[0,-1],[0,1],[1,0],[1,1]];
function seedGrid(): void {
for (let i = 0; i < grid.length; i++) grid[i] = rnd() < 0.33 ? 1 : 0;
}
function neighborCount(x: number, y: number): number {
const offs = (x & 1) ? OFF_ODD : OFF_EVEN;
let n = 0;
for (let i = 0; i < 6; i++){
const nx = x + offs[i][0], ny = y + offs[i][1];
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
n += grid[idx(nx, ny)];
}
return n;
}
function step(): void {
for (let y = 0; y < ROWS; y++){
for (let x = 0; x < COLS; x++){
const alive = grid[idx(x, y)] === 1;
const n = neighborCount(x, y);
let willLive = 0;
if (alive){
willLive = (n >= SURVIVE_MIN && n <= SURVIVE_MAX) ? 1 : 0;
} else {
willLive = (n === BIRTH_COUNT) ? 1 : 0;
}
next[idx(x, y)] = willLive;
}
}
const tmp = grid; grid = next; next = tmp;
}
function hexPath(ctx: CanvasRenderingContext2D, cx: number, cy: number): void {
ctx.beginPath();
for (let i = 0; i < 6; i++){
const a = Math.PI / 180 * (60 * i);
const vx = cx + (SIZE - 1.2) * Math.cos(a);
const vy = cy + (SIZE - 1.2) * Math.sin(a);
if (i === 0) ctx.moveTo(vx, vy); else ctx.lineTo(vx, vy);
}
ctx.closePath();
}
function render(ctx: CanvasRenderingContext2D): void {
ctx.fillStyle = ORANGE;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = ALIVE_COLOR;
for (let y = 0; y < ROWS; y++){
for (let x = 0; x < COLS; x++){
if (grid[idx(x, y)] === 1){
const cx = OX + x * HORIZ;
const cy = OY + VERT * (y + 0.5 * (x & 1));
hexPath(ctx, cx, cy);
ctx.fill();
}
}
}
}
const ARIA_PLAY = '벌집 격자 진행 중 — 클릭하거나 스페이스바로 정지, R 로 재시드';
const ARIA_PAUSE = '벌집 격자 정지됨 — 클릭하거나 스페이스바로 재생, R 로 재시드';
export default function HexGridAutomaton(){
const rootRef = useRef<HTMLDivElement | null>(null);
const cvRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const root = rootRef.current;
const cv = cvRef.current;
if (!root || !cv) return;
const rootEl: HTMLDivElement = root;
const cvEl: HTMLCanvasElement = cv;
const ctx = ctx2d(cvEl);
function updateAria(): void {
rootEl.setAttribute('aria-label', paused ? ARIA_PAUSE : ARIA_PLAY);
}
let paused = false;
const reduceMQ = window.matchMedia('(prefers-reduced-motion: reduce)');
function human(): void { rootEl.classList.remove('is-demo'); }
function reseed(): void {
SEED = (SEED * 1103515245 + 12345) >>> 0;
rnd = makeLcg(SEED || 1);
seedGrid();
}
function togglePause(): void {
paused = !paused;
updateAria();
}
function onActivate(e: Event): void {
if (e.type === 'keydown'){
const ke = e as KeyboardEvent;
if (ke.key === 'r' || ke.key === 'R'){
human();
reseed();
render(ctx);
return;
}
if (ke.key !== 'Enter' && ke.key !== ' ') return;
ke.preventDefault();
}
human();
if (reduceMQ.matches){
step();
render(ctx);
} else {
togglePause();
}
}
const GEN_INTERVAL_MS = 85;
let acc = 0, last = 0, rafId = 0;
function frame(now: number): void {
if (!reduceMQ.matches){
if (!last) last = now;
const dt = now - last;
last = now;
if (!paused){
acc += dt;
while (acc >= GEN_INTERVAL_MS){
step();
acc -= GEN_INTERVAL_MS;
}
render(ctx);
}
rafId = requestAnimationFrame(frame);
}
}
function onResize(): void { render(ctx); }
rootEl.addEventListener('click', onActivate);
rootEl.addEventListener('keydown', onActivate);
window.addEventListener('resize', onResize);
seedGrid();
updateAria();
render(ctx);
if (!reduceMQ.matches){
rafId = requestAnimationFrame(frame);
}
return () => {
rootEl.removeEventListener('click', onActivate);
rootEl.removeEventListener('keydown', onActivate);
window.removeEventListener('resize', onResize);
if (rafId) cancelAnimationFrame(rafId);
};
}, []);
return (
<main className={styles.stage}>
<div ref={rootRef} className={`${styles.rv} is-demo`} tabIndex={0} role="button" aria-label="벌집 격자 진행 중 — 클릭하거나 스페이스바로 정지, R 로 재시드">
<canvas ref={cvRef} className={styles.rv__cv} width={480} height={300} />
<p className={styles.rv__hint}>클릭·스페이스바로 정지, R 로 재시드</p>
</div>
</main>
);
}07 Honeycomb-Grid Automaton Whose State Splits on a Six-Neighbor Count Value — Measured area 49.486% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Conway Game of Life in Canvas, 9 Patterns
The post this demo lives in
Conway Game of Life in Canvas, 9 PatternsConway game of life: The grid looks random, but a live-neighbor value decides life or death, ages into fading color, and See all demos in this post


