GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

보로노이 다이어그램 그리는 법 9가지 — Canvas 코드

보로노이 다이어그램 그리는 법은 손으로 선을 잇는 게 아니라 계산이 대신 긋는 것이다. 보로노이 다이어그램은 선을 긋는 그림이 아니다 — 모든 점이 가장 가까운 시드 좌표 값을 찾아 칠해진 결과다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 무작위처럼 보여도 순서가 있다 — 셀을 채우는 가장 단순한 보로노이 패턴에서 시작해 01번이 기본 모자이크를 보여주고, 경계 두께(02)·빛 투과(03)·정점(04)·포인터(05)·경계만 남기기(06)·완화(07)·노이즈(08) 순으로 계산 층을 하나씩 얹다가, 09번에서 표류·포인터 반발·완화를 한 화면에 모아 쓴다.

시드 좌표 값 하나가 왜 셀 전체를 다시 계산하게 만드는가

보로노이 다이어그램은 셀마다 선을 따로 그리는 게 아니라, 오프스크린 160×100 픽셀 하나하나가 nearestTwo 함수로 자신과 가장 가까운 시드·두 번째로 가까운 시드까지 거리를 재서 스스로 소속 셀과 경계 여부를 정하는 방식으로 만들어진다. 계산이 끝나면 putImageData 로 그 결과를 오프스크린 캔버스에 찍고 화면 크기로 늘려 그린다. 시드 좌표 하나만 옮겨도 그 시드에 가까웠던 모든 픽셀이 재계산 대상이 되므로, CSS 트랜지션처럼 속성 하나를 보간하는 게 아니라 16,000개 픽셀 전부가 프레임마다 거리 비교를 다시 하는 계산이다.

01시드 개수 값만큼 색이 채워지는 모자이크 셀

VORONOI_SEED_COUNT 값(24개)만큼 뿌린 시드 좌표가 프레임마다 sin·cos 로 살짝 흔들리고, 오프스크린의 모든 픽셀이 nearestTwo 로 가장 가까운 시드와 두 번째로 가까운 시드까지 거리를 비교해 자기 색을 고른다. 두 거리 차(edge = ND2 - ND)가 1.5보다 작아지는 픽셀만 어둡게 눌러 셀 경계선을 만든다.

시드 개수 값만큼 색이 채워지는 모자이크 셀
새 창에서 열기

01 · 시드 개수 값만큼 색이 채워지는 모자이크 셀 — 측정 누적면적 60.149% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

VORONOI_SEED_COUNTdriftSeedsmosaicPalette
  function renderField(t) {
    driftSeeds(t);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        nearestTwo(x + 0.5, y + 0.5, seeds);
        var rgb = paletteRgb[seeds[NEI].c];
        var edge = ND2 - ND;
        var dark = edge < 1.5 ? 0.5 + edge / 3 : 1;
        offBuf[idx] = rgb[0] * dark;
        offBuf[idx + 1] = rgb[1] * dark;
        offBuf[idx + 2] = rgb[2] * dark;
        offBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

측정: 누적면적 60.1% · 강도 평균 84.0 · 23/23 프레임에서 움직였다.

02경계선 두께 값이 마른 땅처럼 갈라지는 셀

CRACK_WIDTH_VALUE(1.6)보다 두 시드 사이 거리 차가 좁은 픽셀만 잉크색(INK_RGB)으로 끌어당기고, 그 혼합 비율은 sin 파동(crackPulsePhase)에 따라 시간마다 오르내려 균열이 숨 쉬듯 짙어졌다 옅어졌다 한다.

경계선 두께 값이 마른 땅처럼 갈라지는 셀
새 창에서 열기

02 · 경계선 두께 값이 마른 땅처럼 갈라지는 셀 — 측정 누적면적 73.787% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CRACK_WIDTH_VALUEcrackPulsePhasedriftSeeds
  function renderField(t) {
    driftSeeds(t);
    var crackPulsePhase = 0.5 + 0.5 * Math.sin(t * PULSE_FREQ);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        nearestTwo(x + 0.5, y + 0.5, seeds);
        var rgb = paletteRgb[seeds[NEI].c];
        var band = 0.85 + 0.15 * Math.sin(ND * 0.4 + NEI);
        var edge = ND2 - ND;
        var r, g, b;
        if (edge < CRACK_WIDTH_VALUE) {
          var m = 0.15 + 0.55 * crackPulsePhase;
          r = rgb[0] + (INK_RGB[0] - rgb[0]) * m;
          g = rgb[1] + (INK_RGB[1] - rgb[1]) * m;
          b = rgb[2] + (INK_RGB[2] - rgb[2]) * m;
        } else {
          r = rgb[0] * band; g = rgb[1] * band; b = rgb[2] * band;
        }
        offBuf[idx] = r; offBuf[idx + 1] = g; offBuf[idx + 2] = b; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

측정: 누적면적 73.8% · 강도 평균 47.9 · 23/23 프레임에서 움직였다.

03셀마다 빛 투과 값이 다른 스테인드글라스 패널

cosA·sinA 로 만든 방향값에 픽셀 좌표를 투영(proj)해 시간에 따라 이동하는 광선 띠(bandCenter)를 만들고, 그 띠에 가까운 픽셀일수록 가우시안 값(glow)이 커져 셀이 유리처럼 밝아진다 — 경계 픽셀은 GLASS_BORDER_PX 안쪽에서 따로 어두워진다.

셀마다 빛 투과 값이 다른 스테인드글라스 패널
새 창에서 열기

03 · 셀마다 빛 투과 값이 다른 스테인드글라스 패널 — 측정 누적면적 72.633% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLASS_BORDER_PXlightSweepAnglestainedPalette
  function renderField(t) {
    var bandCenter = ((t / LOOP_MS) * (projSpan * 1.6)) - projSpan * 0.3;
    var idx4 = 0, idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var ni = cellIdx[idx];
        var rgb = paletteRgb[seeds[ni].c];
        var proj = x * cosA + y * sinA;
        var glow = Math.exp(-Math.pow((proj - bandCenter) / 14, 2)) * 0.6;
        var e = edgeVal[idx];
        var borderMix = e < GLASS_BORDER_PX ? 0.35 : 0;
        var r = rgb[0] * (1 - borderMix) * (1 + glow);
        var g = rgb[1] * (1 - borderMix) * (1 + glow);
        var b = rgb[2] * (1 - borderMix) * (1 + glow);
        offBuf[idx4] = Math.min(255, r);
        offBuf[idx4 + 1] = Math.min(255, g);
        offBuf[idx4 + 2] = Math.min(255, b);
        offBuf[idx4 + 3] = 255;
        idx += 1; idx4 += 4;
      }
    }
  }

측정: 누적면적 72.6% · 강도 평균 42.4 · 20/23 프레임에서 움직였다.

04정점 흔들림 값으로 모핑하는 들로네 삼각 메쉬

VERTEX_JITTER_PX(10)만큼 매 프레임 sin·cos 로 흔들린 정점 좌표를 delaunayTriangulate 에 다시 넣어, 외접원 안에 다른 점이 들어오는 삼각형만 골라 갈아엎는 방식으로 그물 전체를 다시 짠다. 이 삼각 그물은 같은 시드로 그리는 보로노이 다이어그램 들로네 삼각분할의 짝이다 — 외접원 안에 다른 점이 없어야 한다는 조건만으로 선이 정해진다.

정점 흔들림 값으로 모핑하는 들로네 삼각 메쉬
새 창에서 열기

04 · 정점 흔들림 값으로 모핑하는 들로네 삼각 메쉬 — 측정 누적면적 66.146% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

VERTEX_JITTER_PXdelaunayTriangulatecircumcircle
    var jittered = points.map(function (p) {
      return [
        p.bx + Math.sin(elapsed * JITTER_FREQ + p.phaseX) * VERTEX_JITTER_PX,
        p.by + Math.cos(elapsed * JITTER_FREQ * 0.85 + p.phaseY) * VERTEX_JITTER_PX
      ];
    });
    var tris = delaunayTriangulate(jittered);

측정: 누적면적 66.1% · 강도 평균 75.1 · 23/23 프레임에서 움직였다.

05포인터가 하나의 시드가 되어 경계를 미는 셀

포인터 좌표(없으면 virtualPointerOrbit 가 만든 가상 궤도)를 기준으로 SEED_REPEL_RADIUS_PX(120) 안의 시드만 거리 비율의 제곱(push*push)만큼 바깥으로 밀어내고, 반경 밖 시드는 원래 자리(bx, by)로 그대로 둔다.

포인터가 하나의 시드가 되어 경계를 미는 셀
새 창에서 열기

05 · 포인터가 하나의 시드가 되어 경계를 미는 셀 — 측정 누적면적 54.09% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SEED_REPEL_RADIUS_PXrepelSeedsvirtualPointerOrbit
  function repelSeeds(pointerCalc) {
    var radiusCalc = SEED_REPEL_RADIUS_PX * (CALC_W / cw);
    seeds.forEach(function (s) {
      var dx = s.bx - pointerCalc.x, dy = s.by - pointerCalc.y;
      var dist = Math.sqrt(dx * dx + dy * dy) || 0.0001;
      if (dist < radiusCalc) {
        var push = (radiusCalc - dist) / radiusCalc;
        var k = push * push * 0.95;
        s.x = s.bx + (dx / dist) * k * radiusCalc;
        s.y = s.by + (dy / dist) * k * radiusCalc;
      } else {
        s.x = s.bx; s.y = s.by;
      }
    });
  }

측정: 누적면적 54.1% · 강도 평균 103.4 · 23/23 프레임에서 움직였다.

06경계선만 남아 빛나는 네트워크 셀

셀 채우기는 버리고 두 시드 사이 거리 차가 EDGE_GLOW_WIDTH(3.4) 안에 들 때만 배경색(BG_RGB)에서 셀 색으로 glowT 비율만큼 섞어, 남는 건 경계선의 빛뿐인 네트워크 장면을 만든다.

경계선만 남아 빛나는 네트워크 셀
새 창에서 열기

06 · 경계선만 남아 빛나는 네트워크 셀 — 측정 누적면적 63.17% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EDGE_GLOW_WIDTHdriftSeedsglowPalette
  function renderField(t) {
    driftSeeds(t);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        nearestTwo(x + 0.5, y + 0.5, seeds);
        var edge = ND2 - ND;
        var r, g, b;
        if (edge < EDGE_GLOW_WIDTH) {
          var glowT = 1 - edge / EDGE_GLOW_WIDTH;
          var rgb = paletteRgb[seeds[NEI].c];
          r = BG_RGB[0] + (rgb[0] - BG_RGB[0]) * glowT;
          g = BG_RGB[1] + (rgb[1] - BG_RGB[1]) * glowT;
          b = BG_RGB[2] + (rgb[2] - BG_RGB[2]) * glowT;
        } else {
          r = BG_RGB[0]; g = BG_RGB[1]; b = BG_RGB[2];
        }
        offBuf[idx] = r; offBuf[idx + 1] = g; offBuf[idx + 2] = b; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

측정: 누적면적 63.2% · 강도 평균 84.7 · 23/23 프레임에서 움직였다.

07반복 횟수 값이 늘수록 고르게 정돈되는 셀

computeCentroids 가 현재 시드 배치로 만든 셀마다 그 안의 모든 픽셀 좌표를 더해 평균(중심)을 구하고, 그 중심을 다음 시드 위치로 삼는 계산을 LLOYD_RELAXATION_STEPS(6)번 미리 돌려 저장해 둔 뒤 프레임마다 그 사이를 보간해 재생한다.

반복 횟수 값이 늘수록 고르게 정돈되는 셀
새 창에서 열기

07 · 반복 횟수 값이 늘수록 고르게 정돈되는 셀 — 측정 누적면적 36.754% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LLOYD_RELAXATION_STEPScomputeCentroidsrelaxationStates
  function computeCentroids(positions) {
    var sumX = new Array(positions.length).fill(0);
    var sumY = new Array(positions.length).fill(0);
    var cnt = new Array(positions.length).fill(0);
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        nearestTwo(x + 0.5, y + 0.5, positions);
        sumX[NEI] += x + 0.5; sumY[NEI] += y + 0.5; cnt[NEI] += 1;
      }
    }
    return positions.map(function (p, i) {
      if (!cnt[i]) return { x: p.x, y: p.y, c: p.c };
      return { x: sumX[i] / cnt[i], y: sumY[i] / cnt[i], c: p.c };
    });
  }

측정: 누적면적 36.8% · 강도 평균 57.1 · 23/23 프레임에서 움직였다.

08셀 크기 편차 값으로 만드는 위장 무늬

valueNoise2D 가 좌표 그리드를 이중선형보간해 만든 노이즈 값 두 개로 픽셀 좌표 자체를 NOISE_WARP_PX(13)만큼 뒤틀어(warpCoord) 놓고 나서 nearestTwo 를 부르므로, 시드는 그대로인데 왜곡된 좌표가 찾는 셀 경계만 구불구불해진다.

셀 크기 편차 값으로 만드는 위장 무늬
새 창에서 열기

08 · 셀 크기 편차 값으로 만드는 위장 무늬 — 측정 누적면적 27.205% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

NOISE_WARP_PXvalueNoise2DwarpCoord
  function valueNoise2D(gx, gy, arr) {
    var x0 = Math.floor(gx), y0 = Math.floor(gy);
    var x1 = Math.min(NOISE_GRID_W - 1, x0 + 1), y1 = Math.min(NOISE_GRID_H - 1, y0 + 1);
    var tx = gx - x0, ty = gy - y0;
    var v00 = arr[y0 * NOISE_GRID_W + x0], v10 = arr[y0 * NOISE_GRID_W + x1];
    var v01 = arr[y1 * NOISE_GRID_W + x0], v11 = arr[y1 * NOISE_GRID_W + x1];
    var top = v00 + (v10 - v00) * tx, bot = v01 + (v11 - v01) * tx;
    return top + (bot - top) * ty;
  }
  function warpCoord(x, y, breathe) {
    var gx = x / CALC_W * (NOISE_GRID_W - 1), gy = y / CALC_H * (NOISE_GRID_H - 1);
    var n1 = valueNoise2D(gx, gy, noiseGridX), n2 = valueNoise2D(gx, gy, noiseGridY);
    return { x: x + (n1 - 0.5) * 2 * NOISE_WARP_PX * breathe, y: y + (n2 - 0.5) * 2 * NOISE_WARP_PX * breathe };
  }

측정: 누적면적 27.2% · 강도 평균 37.7 · 23/23 프레임에서 움직였다.

09여러 값이 한 화면에서 함께 반응하는 히어로 장면

01의 표류·05의 포인터 반발·07의 로이드 완화를 한 renderField 안에서 순서대로 더한다 — relaxNudge(t) 가 만든 블렌드 값으로 표류 좌표와 정리된 좌표를 섞고, 그 위에 applyPointerRepel 로 다시 한 번 미는 3단 계산이 겹친다.

여러 값이 한 화면에서 함께 반응하는 히어로 장면
새 창에서 열기

09 · 여러 값이 한 화면에서 함께 반응하는 히어로 장면 — 측정 누적면적 72.469% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HERO_SEED_COUNTapplyPointerRepelrelaxNudge
  function applyPointerRepel(pointerCalc) {
    var radiusCalc = HERO_REPEL_RADIUS_PX * (CALC_W / cw);
    seeds.forEach(function (s) {
      var dx = s.x - pointerCalc.x, dy = s.y - pointerCalc.y;
      var dist = Math.sqrt(dx * dx + dy * dy) || 0.0001;
      if (dist < radiusCalc) {
        var push = (radiusCalc - dist) / radiusCalc;
        var k = push * push * 0.85;
        s.x += (dx / dist) * k * radiusCalc;
        s.y += (dy / dist) * k * radiusCalc;
      }
    });
  }

측정: 누적면적 72.5% · 강도 평균 89.4 · 23/23 프레임에서 움직였다.

아홉 장면이 픽셀 단위로 얼마나 움직였는가 — 측정값 모음

캡처 사이 23구간마다 인접한 두 장의 픽셀 차이를 비교한 값이다. 누적면적은 그 23번 비교마다 바뀐 화면 비율을 전부 더한 값이라 100%를 넘을 수 있다.

항목 누적면적 강도 평균 움직인 프레임
01 모자이크 셀 60.1% 84.0 23/23
02 균열 텍스처 73.8% 47.9 23/23
03 스테인드글라스 72.6% 42.4 20/23
04 들로네 메쉬 66.1% 75.1 23/23
05 포인터 반발 54.1% 103.4 23/23
06 엣지 글로우 63.2% 84.7 23/23
07 로이드 완화 36.8% 57.1 23/23
08 위장 무늬 27.2% 37.7 23/23
09 히어로 장면 72.5% 89.4 23/23

05 포인터 반발은 강도 평균이 103.4로 아홉 중 가장 높지만 누적면적(54.1%)은 중간이다 — 시드가 반경 안에서 한 번에 크게 튕겨 나가면서 그 경계 픽셀의 색 차이가 크게 벌어지기 때문이다. 반대로 08 위장 무늬는 누적면적(27.2%)과 강도 평균(37.7) 모두 가장 낮다 — 노이즈로 좌표만 살짝 뒤트는 정도라 픽셀 변화가 가장 잔잔하다.

숫자 하나로 셀 배치가 달라지는 변형 세 가지

변형 바꾼 값 느낌
시드 개수 값 조정 01 VORONOI_SEED_COUNT 24 → 60 24개일 때는 큰 조각 몇 개로 성기게 보이고, 60개로 올리면 같은 화면이 작은 타일 수십 개로 빽빽하게 쪼개진 모자이크처럼 바뀐다
포인터 반발 반경 값 조정 05 SEED_REPEL_RADIUS_PX 120 → 240 120px 에서는 커서 바로 옆 셀만 살짝 물러나고, 240px 로 늘리면 캔버스 절반 가까이의 셀이 통째로 밀려나는 느낌을 준다
로이드 완화 반복 횟수 값 조정 07 LLOYD_RELAXATION_STEPS 6 → 20 6번만 돌리면 정리 도중 상태라 셀 크기가 여전히 들쭉날쭉하지만, 20번까지 밀어붙이면 벌집처럼 고른 크기로 수렴한 모습을 볼 수 있다

이 아홉 장면을 실제로 쓰는 화면

모자이크(01)·균열(02)·스테인드글라스(03) 는 그 자체로 안정된 배경이라 랜딩 페이지 히어로나 브랜드 인트로 로딩 화면, 프리미엄 카드 패널의 뒷배경으로 바로 쓸 수 있다. 데이터 시각화 팀이라면 04 들로네 메쉬를 대시보드 배경 장식으로, 기술 소개 섹션이라면 06 엣지 글로우를 네트워크 다이어그램 느낌으로 붙인다. 05 포인터 반발과 09 히어로 장면은 커서가 지나갈 때 반응이 필요한 인터랙티브 랜딩에, 07 로이드 완화는 로딩이 끝나고 화면이 정돈되는 전환 연출에, 08 위장 무늬는 아웃도어·패션 제품 배경처럼 유기적인 질감이 필요한 자리에 맞는다.

보로노이 다이어그램 원리는 이 데모에서 어떻게 보이나 — 293·395·400편과 다른 점

293번 CSS 배경 패턴 9종 은 시드 하나로 반복 타일을 다시 그리는 배경 패턴이고, 395번 캔버스 로딩 애니메이션 효과 9가지 는 rAF 로 물리량을 흘려보내는 로더, 400번 생명게임 시뮬레이터 9가지 는 셀마다 이웃 수를 세는 규칙 기반 오토마타다. 이번 9종은 그 세 갈래와 달리 매 픽셀이 "누가 나와 가장 가까운가"라는 최근접 탐색 하나로 소속을 정한다 — 규칙표도, 이웃 세기도 없이 거리 비교만으로 경계가 생기는 게 보로노이 다이어그램 원리다.

오프스크린 갱신을 건너뛰면 깨지는 함정

nearestTwo 는 시드 배열을 처음부터 끝까지 훑는 탐색이라, 픽셀 하나마다 시드 수만큼 거리 계산을 반복한다 — 160×100 오프스크린에서 시드 24개면 프레임마다 384,000번, 변형 1처럼 60개로 늘리면 960,000번까지 뛴다. 화면 해상도를 그대로 계산 해상도로 써버리면(오프스크린 없이 480×300 을 바로 돈다면) 픽셀 수가 9배로 뛰어 저사양 기기에서 프레임이 뚝뚝 끊긴다 — 오프스크린을 160×100 으로 고정해 두고 drawImage 로만 확대하는 이유가 여기 있다. 05·09 의 radiusCalc = SEED_REPEL_RADIUS_PX * (CALC_W / cw) 줄도 놓치기 쉬운 지점이다. 포인터 좌표는 화면 표시 픽셀(cw)로 들어오는데 시드는 160px 짜리 계산 좌표계에 살고 있어서, 이 비율 변환을 빼먹으면 화면을 늘렸을 때 반발 반경이 실제 커서 위치와 어긋난다. 9개 index.html·style.scss 원본에서 이 변환 줄을 직접 보고 대조하고 싶다면 y8nwxyua 으로 zip 을 열면 된다.

동작 줄임 설정에서 아홉 장면이 멈추는 접근성 처리

아홉 파일 모두 스크립트가 시작할 때 matchMedia('(prefers-reduced-motion: reduce)').matches 를 한 번 읽어 playing 을 끄고 elapsed 값을 항목별로 고정한다 — 대부분 LOOP_MS 의 절반 지점에서 멈추지만 02 는 균열 펄스가 가장 옅은 25% 지점, 07 은 완화가 97%까지 끝난 지점을 골라 뒀다. requestAnimationFrame 루프 자체는 멈추지 않고 매 프레임 같은 elapsed 로 renderField 를 다시 불러 같은 픽셀을 다시 그리므로, 계산이 아예 꺼지는 게 아니라 입력값이 고정돼 결과만 정적으로 보이는 쪽이다. 05·09 는 포인터 좌표까지 { x: -999, y: -999 } 로 캔버스 밖에 둬 가상 궤도 자체를 켜지 않고, root.classList.remove('is-demo') 로 CSS 쪽 재생 표시도 같이 끈다. prefers-reduced-motion 을 이렇게 스크립트 시작 시점에 직접 감지해 입력을 고정하는 쪽이, 계산 자체를 통째로 멈추는 것보다 다루기 쉽다.

보로노이 다이어그램 그리는 법을 적용하기 전에 자주 나오는 질문

보로노이 다이어그램을 SVG 로 직접 그릴 수도 있나요?

된다. 이 데모는 매 프레임 다시 계산해야 하는 애니메이션이라 픽셀(Canvas)로 그렸지만, 시드 좌표가 고정된 정적인 그림이라면 좌표만 구해 <path> 폴리곤으로 그리는 쪽이 더 가볍다. 다만 이 zip 은 라이브러리 없이 바닐라 JS 로 픽셀 최근접 탐색만으로 만든 버전이다.

시드 개수를 실제 프로젝트에서 얼마나 늘려도 되나요?

오프스크린이 160×100(16,000픽셀)으로 고정돼 있어서 시드를 60개까지 늘려도(변형 1) 체감 프레임 저하는 크지 않았다. 다만 nearestTwo 가 시드 수에 비례해 느려지는 만큼, 수백 개 단위로 올릴 계획이면 화면을 격자로 나눠 가까운 시드만 비교하는 방식으로 바꾸는 걸 권한다.

데모에서 보이는 셀 색 배치는 실제 방문자 데이터인가요?

아니다. seedRand 라는 결정적 PRNG(항목마다 다른 정수로 초기화)가 만든 좌표라 새로고침해도 똑같은 배치가 나온다. 실제 데이터를 연결하려면 시드 좌표를 API 응답 같은 실제 값으로 바꿔 넣기만 하면 같은 계산 구조를 그대로 재사용할 수 있다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.