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

튜링 패턴 시뮬레이션 9종 — 라이브러리 없이 두 물질의 반응 격자만으로

튜링 패턴 시뮬레이션은 두 물질이 반응하며 퍼지는 격자를 프레임마다 계산해 점·줄무늬·미로·산호 같은 무늬가 저절로 생기는 과정을 보여 줍니다. 아홉 종을 Gray-Scott 근사로 캔버스 2D 에 직접 구현했고, 시드와 사료율 f 슬라이더로 무늬가 다시 태어납니다.

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

순서는 화려한 순서가 아니라 파라미터가 무늬를 갈라놓는 순서입니다. 먼저 반응이 이기는 자리에 점이 돋고(01·04·05), 사료율이 낮아지며 점이 줄로 이어지고(02·06), 점과 줄 사이에서 줄이 꺾여 미로가 되고(03·07), 제거율이 오르면 무늬가 쪼그라들어 방이 남고(08), 마지막에 아주 낮은 사료율에서 끝없이 갈라지는 성장이 화면을 채웁니다(09). 누적면적으로 가장 넓게 움직인 판은 01 점 무늬(52.878퍼센트)이고, 한 프레임에서 가장 넓게 뒤덮은 순간 최대 면적은 03 미로(50.879퍼센트)입니다. 시드 하나로 무늬를 다시 뽑는 CSS 배경 패턴과 픽셀을 직접 계산하는 캔버스 특수효과, 노이즈 지형이 이 편의 이웃입니다.

01점 무늬 — 반응이 이기는 자리마다 점이 돋는다

히어로 배경의 살아 있는 점 무늬나 표지 일러스트 씨앗에 쓰는 판으로, 균일한 V 밭에 뿌린 U 씨앗 몇 개가 자라 규칙적인 점 배열을 이룹니다.

점 무늬 — 반응이 이기는 자리마다 점이 돋는다
새 창에서 열기

01 · 점 무늬 — 반응이 이기는 자리마다 점이 돋는다 — 측정 누적면적 52.878% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

점은 왜 균일한 간격으로 서는가

// 한 셀의 Gray-Scott 갱신 — 확산(라플라스) + 반응(uvv) + 사료·제거
var lapU = u[yC + xL] + u[yC + xR] + u[yT + x] + u[yB + x] - 4 * uc;
var lapV = v[yC + xL] + v[yC + xR] + v[yT + x] + v[yB + x] - 4 * vc;
var uvv = uc * vc * vc;
var nu = uc + DU * lapU - uvv + F * (1 - uc);
var nv = vc + DV * lapV + uvv - (F + K) * vc;
u2[i] = nu < 0 ? 0 : (nu > 1 ? 1 : nu);   // 포화 클램프 — 없으면 발산한다
v2[i] = nv < 0 ? 0 : (nv > 1 ? 1 : nv);

U 가 V 를 만나면 둘이 결합해 V 를 더 만들고(uvv) V 는 스스로 없어지며((F+K)*v), 점이 하나 자라면 그 주변의 U 를 빨아들여 이웃한 점은 굶어 죽고 살아남은 점끼리는 영향권 경계에서 균형을 이뤄 같은 간격의 배열로 앉습니다. 이 판은 64×40 셀에서 시작 전 1,500스텝 미리 성숙해 점 배열이 완성된 상태로 움직입니다(실측: 누적면적 52.88퍼센트, 강도 99.3, 23/23 프레임).

02줄무늬 — 파라미터 하나로 점이 줄로 이어진다

동물 무늬 컨셉 아트나 스트라이프 패턴 생성기 씨앗에 쓰는 판입니다. 사료율만 낮추면 점이 줄로 이어집니다.

줄무늬 — 파라미터 하나로 점이 줄로 이어진다
새 창에서 열기

02 · 줄무늬 — 파라미터 하나로 점이 줄로 이어진다 — 측정 누적면적 49.959% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

줄은 점이 늘어난 끝이다

// 시드 — 사인 곡선 가로 줄 3개. 위상·진폭을 시드가 뽑는다
function seedField(sd) {
  var rng = mulberry32(sd >>> 0);
  u.fill(1); v.fill(0);
  var n = 3;
  for (var s = 0; s < n; s++) {
    var cy = (s + 0.5) * GH / n;
    var ph = rng() * 6.28;
    var amp = 1 + rng() * 2;
    var th = 2 + rng();
    for (var y = 0; y < GH; y++) {
      for (var x = 0; x < GW; x++) {
        var line = cy + Math.sin(x * 0.22 + ph) * amp;
        if (Math.abs(y - line) <= th) { v[y * GW + x] = 0.5; u[y * GW + x] = 0.25; }
      }
    }
  }
}

사료율이 높으면 U 가 넉넉해 점마다 제 영역을 지키지만, 낮아지면 점의 가장자리가 U 부족으로 한 방향으로 뻗고 뻗은 끝끼리 만나 긴 줄이 됩니다(실측: 누적면적 49.96퍼센트, 강도 102.3으로 이 편에서 가장 강한 대비, 23/23 프레임).

03미로 — 줄이 꺾여 길을 내는 중간 지점

게임 맵 텍스처나 정적 배경 패턴에 쓰는 판입니다. 점과 줄 사이의 파라미터에서는 줄이 마구 꺾입니다.

미로 — 줄이 꺾여 길을 내는 중간 지점
새 창에서 열기

03 · 미로 — 줄이 꺾여 길을 내는 중간 지점 — 측정 누적면적 52.86% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

미로는 점과 줄의 중간 지점이다

// 시드 — 작은 U 조각 스무 개를 시드가 뽑은 자리에 흩뿌린다
function seedField(sd) {
  var rng = mulberry32(sd >>> 0);
  u.fill(1); v.fill(0);
  var n = 20;
  for (var s = 0; s < n; s++) {
    var cx = 2 + Math.floor(rng() * (GW - 6));
    var cy = 1 + Math.floor(rng() * (GH - 4));
    var w = 1 + Math.floor(rng() * 4);
    var h = 1 + Math.floor(rng() * 3);
    for (var y = cy; y < Math.min(GH, cy + h); y++) {
      for (var x = cx; x < Math.min(GW, cx + w); x++) {
        v[y * GW + x] = 0.5; u[y * GW + x] = 0.25;
      }
    }
  }
}

사료율이 점 구간과 줄 구간 사이에 있으면 줄의 끝이 닫히지 않고 계속 달아나다가 이웃 줄에 닿아 꺾이고, 꺾인 자리에서 다시 갈라지길 반복해 통로가 남는 미로가 됩니다(실측: 누적면적 52.86퍼센트로 01 에 이어 두 번째, 순간 최대 면적 50.879퍼센트로는 이 편 최고, 23/23 프레임).

04산호 성장 — 씨앗에서 밖으로 뻗는 반응 전선

로딩 연출이나 유기적 성장 인트로에 쓰는 판입니다. 중앙 씨앗 하나에서 반응 전선이 밖으로 뻗습니다.

산호 성장 — 씨앗에서 밖으로 뻗는 반응 전선
새 창에서 열기

04 · 산호 성장 — 씨앗에서 밖으로 뻗는 반응 전선 — 측정 누적면적 41.844% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

성장 전선은 왜 밖으로만 뻗나

// 시드 — 중앙 한 덩어리. 가장자리를 노이즈로 울퉁불퉁하게
function seedField(sd) {
  var rng = mulberry32(sd >>> 0);
  u.fill(1); v.fill(0);
  var cx = GW / 2 + (rng() - 0.5) * 6, cy = GH / 2 + (rng() - 0.5) * 4;
  var rr = 5.5 + rng() * 2;
  for (var y = 0; y < GH; y++) {
    for (var x = 0; x < GW; x++) {
      var dx = x - cx, dy = (y - cy) * 1.15;
      var d = Math.sqrt(dx * dx + dy * dy);
      var wob = rr * (0.7 + 0.5 * rng());
      if (d < wob) { v[y * GW + x] = 0.5; u[y * GW + x] = 0.25; }
    }
  }
}

U 를 중앙에 크게 부어두면 그 가장자리에서만 반응이 살아남으니 전선은 반드시 U 가 있는 바깥으로 뻗고, 지나간 자리에는 V 흔적이 남아 산호·이끼의 몸통이 됩니다(실측: 누적면적 41.84퍼센트, 17/23 프레임 — 성장이 일정 속도로 밖으로 향하는 특성상 프레임 사이 변화가 전선 띠에 집중됩니다).

05양 털 — 씨앗 여러 개가 부딪히며 섞인다

텍스처 배경이나 일러스트 채움 무늬에 쓰는 판입니다. 씨앗 열넷이 자라 서로 부딪히며 덩어리진 질감을 만듭니다.

양 털 — 씨앗 여러 개가 부딪히며 섞인다
새 창에서 열기

05 · 양 털 — 씨앗 여러 개가 부딪히며 섞인다 — 측정 누적면적 50.932% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

덩어리는 충돌의 흔적이다

// 시드 — 화면 곳곳에 점 14개를 넓게. 시드가 좌표를 결정
function seedField(sd) {
  var rng = mulberry32(sd >>> 0);
  u.fill(1); v.fill(0);
  for (var n = 0; n < 14; n++) {
    var cx = 5 + rng() * (GW - 10), cy = 4 + rng() * (GH - 8);
    var rr = 2 + rng() * 1.6;
    for (var y = Math.max(0, cy - rr | 0); y <= Math.min(GH - 1, cy + rr + 1); y++) {
      for (var x = Math.max(0, cx - rr | 0); x <= Math.min(GW - 1, cx + rr + 1); x++) {
        var dx = x - cx, dy = y - cy;
        if (dx * dx + dy * dy < rr * rr) { v[y * GW + x] = 0.5; u[y * GW + x] = 0.25; }
      }
    }
  }
}

씨앗을 여럿 뿌리면 각자 자란 무늬가 만나는 자리에서 서로를 밀어내며 경계를 만들고, 그 경계망이 양 털·구름 뭉치처럼 불규칙하면서 덩어리진 질감을 만듭니다(실측: 누적면적 50.93퍼센트, 23/23 프레임).

06얼룩말 줄 — 좁은 격자에 세로 방향 이완을 준다

브랜드 세로 스트라이프나 캐릭터 무늬에 쓰는 판으로, 세로로 긴 격자에 가로 확산을 편향시켜 세로 줄이 촘촘히 들어섭니다.

얼룩말 줄 — 좁은 격자에 세로 방향 이완을 준다
새 창에서 열기

06 · 얼룩말 줄 — 좁은 격자에 세로 방향 이완을 준다 — 측정 누적면적 45.926% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

줄의 방향은 확산의 방향을 따른다

// 시드 — 세로 밴드 4~6개를 시드가 뽑은 x 위치에 세운다
function seedField(sd) {
  var rng = mulberry32(sd >>> 0);
  u.fill(0); v.fill(0);
  for (var p = 0; p < GW * GH; p++) u[p] = 0.98 + rng() * 0.04;
  var nbars = 4 + Math.floor(rng() * 3);
  for (var n = 0; n < nbars; n++) {
    var cx = 6 + rng() * (GW - 12);
    var rr = 2.5 + rng() * 1.5;
    for (var x = Math.max(0, cx - rr | 0); x <= Math.min(GW - 1, cx + rr + 1); x++) {
      var dx = x - cx;
      if (dx * dx < rr * rr) {
        for (var y = 0; y < GH; y++) v[y * GW + x] = 0.4;
      }
    }
  }
}

확산이 어느 방향으로 더 세게 일어나는지에 따라 무늬의 줄이 그 방향에 직각으로 정렬됩니다. 이 판은 격자를 세로로 길게 잡고 라플라스의 가로 항에 WX 1.2배 가중을 줘 얼룩말 옆구리처럼 세로 줄이 촘촘히 들어서게 했습니다(실측: 누적면적 45.93퍼센트, 23/23 프레임).

07물결 — 무늬 전체가 천천히 흐른다

명상·음악 서비스 배경에 쓰는 판으로, 정착한 줄무늬에 미세 주입을 계속해 무늬 전체가 물결치듯 흐릅니다.

물결 — 무늬 전체가 천천히 흐른다
새 창에서 열기

07 · 물결 — 무늬 전체가 천천히 흐른다 — 측정 누적면적 36.247% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

정지한 무늬를 흐르게 하려면

// 미세 주입 — 여덟 주입원이 리사주 궤적을 돌며 U 를 조금씩 붓는다
function ripple() {
  for (var j = 0; j < SRC_N; j++) {
    var ph = 1.7 + j * 2.4;                       // 주입원마다 다른 위상
    var cx = (GW / 2 + Math.cos(t * ph) * GW * 0.36) | 0;
    var cy = (GH / 2 + Math.sin(t * (0.9 + j * 0.8)) * GH * 0.36) | 0;
    for (var dy = -SRC_RAD; dy <= SRC_RAD; dy++) {
      for (var dx = -SRC_RAD; dx <= SRC_RAD; dx++) {
        if (dx * dx + dy * dy > SRC_RAD * SRC_RAD) continue;
        var i = ((cy + dy + GH) % GH) * GW + ((cx + dx + GW) % GW);
        U[i] = 1;
        var nv = V[i] + SRC_BOOST;
        V[i] = nv > 1 ? 1 : nv;                   // 1 넘게 부어도 클램프가 막는다
      }
    }
  }
}

성숙한 줄무늬는 그대로 두면 아무 일도 일어나지 않습니다. 주입 지점의 줄이 밀리고 그 밀림이 줄을 따라 전달되어 화면 전체의 무늬가 느린 물결처럼 흐릅니다(실측: 누적면적 36.25퍼센트, 강도 30.4, 23/23 프레임).

08세포막 — 경계가 두꺼워지고 떨림이 남는다

과학 콘텐츠 일러스트나 세포 컨셉 UI 에 쓰는 판으로, 제거율을 올려 무늬가 방으로 쪼그라듭니다.

세포막 — 경계가 두꺼워지고 떨림이 남는다
새 창에서 열기

08 · 세포막 — 경계가 두꺼워지고 떨림이 남는다 — 측정 누적면적 31.474% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

방은 무늬가 죽은 자리가 아니라 균형점이다

// 떨림 — 열넷 주입원이 방 경계 근처를 돌며 미세 자극을 준다
function quiver() {
  for (var j = 0; j < SRC_N; j++) {
    var ph = 2.3 + j * 1.9;
    var cx = (GW / 2 + Math.cos(t * ph) * GW * 0.34) | 0;
    var cy = (GH / 2 + Math.sin(t * (1.3 + j * 0.9)) * GH * 0.32) | 0;
    for (var dy = -SRC_RAD; dy <= SRC_RAD; dy++) {
      for (var dx = -SRC_RAD; dx <= SRC_RAD; dx++) {
        if (dx * dx + dy * dy > SRC_RAD * SRC_RAD) continue;
        var i = ((cy + dy + GH) % GH) * GW + ((cx + dx + GW) % GW);
        U[i] = 1;
        var nv = V[i] + SRC_BOOST;
        V[i] = nv > 1 ? 1 : nv;
      }
    }
  }
}

제거율 k 가 오르면 V 가 빨리 사라지니 무늬는 스스로 최소 크기로 쪼그라들고 동그란 방들만 남습니다. 방의 경계는 반응과 제거가 맞서는 균형점이라 완전히 멈추지 않고 미세하게 떨리는데, 이 떨림이 반응이 살아 있다는 증거라 같은 모양의 정적 그림과 구별됩니다(실측: 누적면적 31.47퍼센트, 23/23 프레임).

09갈라짐 — 성장 끝이 두 갈래로 계속 갈라진다

인포그래픽 배경이나 성장 컨셉 연출에 쓰는 판으로, 아주 낮은 사료율에서 끝이 갈라지며 화면을 채웁니다.

갈라짐 — 성장 끝이 두 갈래로 계속 갈라진다
새 창에서 열기

09 · 갈라짐 — 성장 끝이 두 갈래로 계속 갈라진다 — 측정 누적면적 34.187% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

갈라짐은 저항에 대한 답이다

// 시드 — 하단 가로 줄(틈새 있는 마디 줄)에 V 를 심는다. 위로 뻗는 성장의 씨앗
function build(seed) {
  var rng = mulberry32(seed >>> 0);
  for (var i = 0; i < GW * GH; i++) { U[i] = 1; V[i] = 0; }
  var y0 = GH - 6;
  for (var x = 2; x < GW - 2; x++) {
    // 마디 줄 — 이어진 부분과 끊긴 부분이 섞여 시작점이 시드마다 달라진다
    var gap = rng() < 0.30;
    if (gap) continue;
    V[y0 * GW + x] = 1;
    if (rng() < 0.6) V[(y0 + 1) * GW + x] = 1;
    if (rng() < 0.3) V[(y0 - 1) * GW + x] = 1;
  }
  for (var m = 0; m < RIP_STEPS; m++) step();
}

사료율이 아주 낮으면 성장 끝이 이웃 줄의 영역에 닿을 때 그 안으로 못 들어가고 두 갈래로 갈라져 우회하고, 갈라진 끝이 또 닿으면 또 갈라지길 반복합니다. 04 산호와 달리 끝이 죽지 않으므로 화면을 채울 때까지 갈라짐이 계속됩니다(실측: 누적면적 34.19퍼센트, 23/23 프레임).

반응 확산 모델은 어떻게 무늬가 되나?

이 편 전체가 하나의 Gray-Scott 격자를 파라미터로 갈라 본 결과물입니다. Gray-Scott 반응 확산 모델은 사료 U 와 촉매 V 두 물질을 격자 위에 두고, 매 순간 확산(이웃으로 퍼짐)과 반응(U+2V→3V)과 공급·제거(f·k)를 동시에 계산합니다. 튜링 패턴 뜻 자체가 여기서 나옵니다 — 어느 물질도 스스로 무늬를 만들지 않지만, 두 물질의 확산 속도 차이가 있으면 평평한 상태가 불안정해져 작은 요동이 자라며 주기적인 무늬로 정착합니다. 사료율 f 하나를 움직이는 것만으로 점·줄·미로·방·성장이 차례로 나오는 것은 이 불안정성의 모양이 파라미터를 따라 갈리기 때문이고, 동물의 피부 무늬도 같은 규칙의 다른 파라미터로 설명됩니다. 격자는 64×40(세로 판은 48×72, 07·08·09 는 96×60)의 낮은 해상도로 고정하고 화면 크기는 확대 보간에 맡기니 계산량이 창 크기와 무관하고, 시드가 순서표를 다시 뽑으니 같은 시드는 같은 무늬를, 다른 시드는 다른 무늬를 재생합니다 — 모든 판에 붙은 시드 입력과 사료율 슬라이더가 이 결정성의 증명입니다.

어디서 깨지는가 — 성숙한 격자가 화면을 죽이던 첫 판

첫 판의 05 양털은 잘 자라던 무늬가 어느 순간부터 완전히 멈춰 버려, 2초짜리 기록에 움직인 프레임이 일곱 칸에 그쳤습니다. 원인을 찾아 보니 Gray-Scott 는 파라미터가 정해지면 결국 수학적 정지 상태로 수렴하는 계이기 때문에, 미리 성숙을 크게 잡을수록 첫 프레임부터 이미 죽은 그림이 나오는 것이었습니다 — 누적면적 25퍼센트라는 숫자는 절반쯤 자라다 멈춘 무늬가 처음 한두 프레임만 움직였다는 뜻이었습니다. 더 곤란한 문제는 이를 고치려고 주입을 넣은 직후에 나타났습니다: 주입으로 V 를 1로 밀면 이웃 셀의 라플라스가 U 를 음수로 끌고, 음수 U 에서 uvv 항이 부호를 뒤집어 약 백 스텝 만에 격자 전체가 NaN 으로 발산해 화면이 새까맣게 죽었습니다. 두 문제의 답이 같았습니다 — 갱신 뒤 값을 0 과 1 사이로 되돌리는 클램프 두 줄을 넣고(01 코드블록의 마지막 두 줄), 주입은 한 번에 크게 넣지 않고 여럿이 조금씩 순회하며 붓게 했습니다. 그 뒤로 아홉 판 전부가 23/23 프레임을 움직입니다. 완성된 반응 격자 아홉 판과 React 판 전체는 글 아래의 암호 zip 에 들어 있고, 여는 암호는 54nx2m3f 입니다.

변형 세 가지와 접근성

이름 바꾼 값 느낌 한 줄
사료율 f 02 줄무늬 f 0.045 → 0.030 줄이 끊겨 점 무늬로 되돌아간다
시드 01 점 무늬 시드 7 → 91 같은 규칙, 완전히 다른 점 배치
색 $turing-mint → $turing-peach 차가운 청록 반응장이 따뜻한 복숭아 톤으로

접근성 처리는 아홉 판이 한결같습니다. prefers-reduced-motion 을 reduce 로 켠 사용자에게는 rAF 루프를 돌리지 않고 시드가 정한 정적 한 장만 그립니다 — 01 은 점 배열이 완성된 장면, 02 는 줄무늬가 자리 잡은 장면, 04 는 산호 전선이 절반 뻗은 장면입니다. 이 정적 프레임에서도 시드 입력과 사료율 슬라이더는 살아 있어, 시드를 바꾸면 다른 무늬 한 장이 즉시 다시 그려지고 슬라이더를 움직이면 다른 무늬 계열의 정적 장으로 바뀝니다. 반응 애니메이션이 꺼져도 계산이 뽑아 낸 무늬라는 결과는 남는 것이 이 구조의 요점입니다.

FAQ

반응 확산 시뮬레이션을 라이브러리 없이 돌려도 성능이 될까요?

됩니다. 격자를 64×40(2,560 셀)으로 고정하고 프레임마다 8~14스텝을 밟아도 한 프레임 계산량은 셀 수백 개의 덧셈·곱셈 수준이라 60fps 예산 안에 남고, 화면 크기는 저해상 버퍼를 확대해 그리는 것이니 창을 키워도 계산이 늘지 않습니다. 아홉 판 전부 23/23 프레임이 움직였고(실측), rAF 가 넘겨주는 시간으로 재생되니 느린 기기에서는 느리게 돌릴 뿐 계산이 쌓이지 않습니다.

튜링 패턴 실험은 이 코드로 어디까지 해볼 수 있나요?

사료율 f 슬라이더와 시드 입력이 전부 연결돼 있어, f 를 0.014(09 성장)에서 0.060(08 세포막)까지 밀며 점·줄·미로·방이 차례로 나오는 것을 한 판 안에서 볼 수 있습니다. 두 계수 DU·DV(1.0·0.5)와 제거율 k 는 소스 맨 위 상수로 정리돼 있어 값을 바꾸면 확산 속도 차이가 무늬 굵기를 바꾸는 것까지 실험할 수 있습니다.

소스 코드를 그대로 복사해도 되나요?

됩니다. 글 아래의 암호 zip 을 풀면 바닐라 아홉 판과 React 포트가 통째로 나와 붙여 넣는 즉시 돌아갑니다. 색은 각 판 scss 맨 위의 파스텔 토큰 변수로 갈아끼우면 되고, 시드 입력에 숫자를 넣으면 같은 시드는 같은 무늬를 다시 뽑아 줍니다.

더 읽을거리

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

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