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

캔버스 번개 효과 그리기 9종 — 프랙탈 중점변위로 치는 낙뢰

번개 효과 그리기는 번개 사진을 붙이는 일이 아니라 곧은 선 하나를 반씩 쪼개는 일입니다. 라이브러리·WebGL 없이 선분을 재귀로 쪼개는 중점변위 하나로 번개 가지가 스스로 갈라집니다.

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

선분 하나를 쪼개는 규칙이 아홉 번개로 갈라지는 이유

순서를 정한 축은 화려함이 아니라 재귀가 퍼져 나가는 방향입니다. 01 기본 낙뢰가 중점변위 한 겹으로 줄기를 세우는 원형이고, 02는 그 분할 과정에 가지 확률을 얹고, 03은 줄기 여러 개에 지수분포 리듬을 입힙니다. 04가 줄기를 지운 뒤 남는 빛으로 화면을 옮기면 05·06은 경로를 매 프레임 다시 계산하는 지속형(전극 사이 아크 · 커서 추적)으로 넘어가고, 07은 타점 다섯 개를 잇는 연쇄로, 08은 줄기보다 화면 전체의 플래시 감쇠로, 09는 줄기마저 버린 구름 속 발광으로 끝납니다. 같은 씨앗 난수로 지형을 쌓는 노이즈 지형이나 연료를 태우는 불꽃 시뮬레이션과 다르게, 이 아홉 판을 관통하는 숫자는 선분의 길이에 비례해 줄어드는 변위폭 하나입니다.

번개는 왜 프랙탈로 그리나? — 중점변위와 재귀 가지치기의 셈

중점변위는 선분이 짧아질수록 변위도 작아진다

off = (rand() * 2 - 1) * len * ROUGH * 0.5 에서 변위폭이 남은 선분 길이 len 에 비례하는 것이 이 기법의 심장입니다. 재귀 한 겹마다 선분이 절반이 되므로 변위도 절반에 가까워지고, 굵은 꺾임 위에 잔굴곡이 얹히는 자기 닮음 구조가 여기서 나옵니다. 지형의 고도를 같은 규칙으로 흔드는 다이아몬드-스퀘어 알고리즘과 같은 족보라, 이 한 함수로 산맥도 번개도 그릴 수 있는 것입니다. 변위를 고정 픽셀로 주면 큰 구간은 매끈하고 작은 구간은 부서진, 위아래 규모가 어긋난 줄기가 됩니다.

가지 확률과 세대 감쇠가 프랙탈 나무를 만든다

번개의 곁가지는 줄기의 복제품이 아니라 줄기의 축소판이어야 읽힙니다. 02는 가지를 칠 확률 BRANCH_P * (1.15 - gen / (MAX_GEN + 1)) 을 세대가 깊어질수록 낮추고, 변위폭 ROUGH * Math.pow(DECAY, gen) 과 두께·밝기까지 같은 DECAY 로 감쇠시켜 세대가 내려갈수록 가늘고 어두운 가지가 되게 합니다. 확률만 있고 감쇠가 없으면 모든 가지가 원줄기와 같은 존재감을 가져 번개가 들판의 검은 나무처럼 되고, 감쇠만 있고 확률 조건이 없으면 가지가 줄기의 일정 지점에 기계적으로 몰립니다.

지수분포 대기가 하늘의 리듬을 결정한다

실제 낙뢰는 일정한 주기로 떨어지지 않고 짧게 몰아치다 길게 쉬는데, 이 리듬은 균등 난수가 아니라 지수분포 -Math.log(1 - rand()) * mean 에서 나옵니다. 균등 분포는 최소·최대 사이가 고르게 펴져 하늘이 시계처럼 되고, 지수분포는 짧은 대기가 자주 뽑히면서도 아주 긴 대기가 드물게 나와 몰아침과 고요의 대비를 만듭니다. 03은 폭풍 강도 storm 이 몰아치는 구간에서 평균을 MEAN * (1.65 - storm * 1.1) 로 짧게 조여, 하늘 상태가 스스로 리듬을 바꾸게 합니다.

lighter 합성이 빛의 겹침을 계산한다

번개는 선이 아니라 빛이라, 그린 선 위에 반투명 색을 겹쳐 덧칠하는 일반 합성으로는 발광의 존재감이 안 납니다. 02·03·05·06·07·08·09가 줄기나 광원을 globalCompositeOperation = 'lighter' 로 그리는 이유는 픽셀 값을 서로 더하는 이 합성이 겹친 빛을 실제 빛처럼 누적시키기 때문입니다. 08의 플래시가 하늘과 실루엣을 한꺼번에 밝히는 것도, 09의 펄스가 구름 위에 쌓이는 것도 같은 계산의 다른 표현입니다.

항목별 코드

01기본 낙뢰 — 선분을 반으로 쪼개는 중점변위 한 겹

히어로 배경에 순간적으로 내리치는 낙뢰로 쓰는, 시작점과 끝점을 잇는 선분을 DEPTH 6단계로 재귀 분할하는 가장 작은 판입니다 — 누적면적 37.683%, 강도 평균 46.0, 24프레임 중 23프레임이 움직였습니다.

기본 낙뢰 — 선분을 반으로 쪼개는 중점변위 한 겹
새 창에서 열기

01 · 기본 낙뢰 — 선분을 반으로 쪼개는 중점변위 한 겹 — 측정 누적면적 37.683% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

moveTolineTorequestAnimationFrame

법선 방향으로 밀린 중점이 줄기를 만든다

// ── 중점변위 — 번개의 전부 ──
function displace(ax, ay, bx, by, depth, out) {
  if (depth === 0) { out.push([bx, by]); return; }
  var mx = (ax + bx) / 2, my = (ay + by) / 2;
  var dx = bx - ax, dy = by - ay;
  var len = Math.hypot(dx, dy) || 1;
  var nx = -dy / len, ny = dx / len;              // 선분에 수직인 법선
  var off = (rand() * 2 - 1) * len * ROUGH * 0.5; // 선분 길이에 비례하는 변위
  mx += nx * off; my += ny * off;
  displace(ax, ay, mx, my, depth - 1, out);
  displace(mx, my, bx, by, depth - 1, out);
}

중점을 선분 방향이 아니라 법선 -dy, dx 쪽으로 미는 것이 들쭉날쭉한 꺾임의 출발점이고, 변위폭이 len * ROUGH(0.42) * 0.5 로 길이에 묶여 있어 재귀가 깊어질수록 굵은 꺾임에서 잔굴곡으로 저절로 내려갑니다. 낙뢰는 시드 0x316A01 의 mulberry32 순서대로 ANCHORS 8개 지점을 돌며 떨어지고, 밝기 봉투가 진행률 0.3~0.37 구간에서 0.82까지 다시 올라가 실제 번개의 두 번 점등을 흉내 냅니다.

02재귀 가지치기 — 깊이마다 얇아지고 어두워지는 곁가지

게임 스킬 이펙트나 배너 장식에 어울리는, 분할된 줄기 위에서 낮은 확률로 곁가지를 다시 재귀로 그리는 판입니다 — 누적면적 31.275%, 강도 평균 41.3, 23/24 프레임.

재귀 가지치기 — 깊이마다 얇아지고 어두워지는 곁가지
새 창에서 열기

02 · 재귀 가지치기 — 깊이마다 얇아지고 어두워지는 곁가지 — 측정 누적면적 31.275% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

lineToglobalCompositeOperationrequestAnimationFrame

세대가 내려갈수록 잔잔해지는 가지 확률

function grow(ax, ay, bx, by, gen, angle, depth, segments) {
  var pts = [[ax, ay]];
  displace(ax, ay, bx, by, depth, ROUGH * Math.pow(DECAY, gen), pts);
  segments.push({ pts: pts, gen: gen });
  if (gen >= MAX_GEN) return;
  // 줄기를 따라 가지 후보를 넓게 검사한다
  var baseA = Math.atan2(by - ay, bx - ax);
  var k, idx, ba, bl, p;
  for (k = 1; k <= 5; k++) {
    idx = Math.floor(pts.length * k / 6);
    if (idx < 1 || idx >= pts.length) continue;
    if (rand() < BRANCH_P * (1.15 - gen / (MAX_GEN + 1))) {
      p = pts[idx];
      ba = baseA + (rand() < 0.5 ? -1 : 1) * (0.35 + rand() * 0.6);
      bl = Math.hypot(bx - ax, by - ay) * (0.5 + rand() * 0.28);
      grow(p[0], p[1], p[0] + Math.cos(ba) * bl, p[1] + Math.sin(ba) * bl,
           gen + 1, ba, depth - 1, segments);
    }
  }
}

줄기를 6등분한 위치 k=1..5마다 BRANCH_P(0.15) 의 주사위를 굴려 가지를 치되, 세대 gen 이 깊어지면 확률·변위·두께·밝기가 전부 DECAY(0.66) 의 지수로 줄어 MAX_GEN 5세대를 넘지 않습니다. 가지가 원줄기와 같은 굵기로 자라는 순간 프랙탈이 나무가 아니라 잡초로 보이는데, 이 감쇠 한 줄이 그 경계를 지킵니다.

03폭풍 하늘 — 무작위 간격으로 겹쳐 치는 여러 낙뢰

다크 모드 히어로의 배경 신에 쓰는, 다음 낙뢰까지의 대기를 지수분포로 뽑아 몰아치다 쉬는 하늘의 리듬을 만드는 판입니다 — 누적면적 52.833%, 강도 평균 28.6, 20/24 프레임.

폭풍 하늘 — 무작위 간격으로 겹쳐 치는 여러 낙뢰
새 창에서 열기

03 · 폭풍 하늘 — 무작위 간격으로 겹쳐 치는 여러 낙뢰 — 측정 누적면적 52.833% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

lineToglobalCompositeOperationrequestAnimationFrame

몰아침과 고요를 번갈아 뽑는 지수분포 대기

if (t >= nextAt) {
  spawn(t, storm);
  // 지수분포 대기 — 다음 낙뢰까지. 몰아칠 때는 평균이 짧아진다
  var mean = MEAN * (1.65 - storm * 1.1);
  nextAt = t + -Math.log(1 - rand()) * mean;
}
function spawn(t, storm) {
  // 거리 — 폭풍이 몰아칠 때는 가까운 번개의 비중이 커진다
  var dist = Math.pow(rand(), 1.6 - storm * 0.8) * FARMAX;
  var ax = ANCHORS[cycle % ANCHORS.length] + (rand() - 0.5) * 0.12;
  cycle++;
  var sx = (ax + (dist * 0.5 - 0.25) * 0.2) * W;   // 먼 번개는 중앙 쪽으로 몰림
  var ex = sx + (rand() - 0.5) * W * 0.2 * (1 - dist * 0.5);
  var pts = null;
  if (dist < 0.55) {
    pts = [[sx, -6]];
    displace(sx, -6, ex, H * (0.82 - dist * 0.1), DEPTH - (dist > 0.3 ? 1 : 0), pts);
  }
  bolts.push({ pts: pts, born: t, life: 0.45 + rand() * 0.25, dist: dist, ex: ex, flickAt: t, sx: sx });
}

고정 간격으로 떨어뜨리면 낙뢰가 메트로놈처럼 기계적이 되는데, -Math.log(1 - rand()) * mean 이 짧은 대기와 긴 대기를 같은 분포에서 뽑아 실제 하늘의 불규칙함을 만듭니다. 거리 dist 가 0.55보다 멀면 줄기를 아예 그리지 않고 하늘빛 반응만 남기는 것도 원근의 핵심이라, 먼 번개는 소리 없이 구름만 밝아집니다.

04잔광 확산 — 픽셀 버퍼로 번지는 번개의 헤일로

제품 소개 페이지의 강조 배경이나 다크 카드의 발광 테두리에 쓰는, 줄기가 지나간 자리의 밝기를 이웃 픽셀과 섞어 번지게 하는 판입니다 — 누적면적 33.303%, 강도 평균 40.8, 23/24 프레임.

잔광 확산 — 픽셀 버퍼로 번지는 번개의 헤일로
새 창에서 열기

04 · 잔광 확산 — 픽셀 버퍼로 번지는 번개의 헤일로 — 측정 누적면적 33.303% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

이웃 평균으로 번지고 냉각으로 식는 이중 버퍼

// 확산 — 각 픽셀을 이웃 4개 평균 쪽으로 SPREAD 만큼 끌고 COOL 로 식힌다
function diffuse() {
  for (var y = 0; y < H; y++) {
    var row = y * W;
    var up = (y > 0 ? y - 1 : y) * W;
    var dn = (y < H - 1 ? y + 1 : y) * W;
    for (var x = 0; x < W; x++) {
      var i = row + x;
      var l = lum[i];
      var av = (lum[row + (x > 0 ? x - 1 : x)] + lum[row + (x < W - 1 ? x + 1 : x)] +
                lum[up + x] + lum[dn + x]) * 0.25;
      lum2[i] = (l + SPREAD * (av - l)) * COOL;
    }
  }
  var tmp = lum; lum = lum2; lum2 = tmp;
}

밝기를 색이 아니라 Float32Array 한 장에 담아 매 프레임 이웃 4픽셀 평균 쪽으로 SPREAD(0.44) 만큼 끌고 COOL(0.986) 로 식힌 뒤 두 버퍼를 교대로 스왑하는데, 화면 재구성은 밝기에서 색을 매기는 LUT(잉크-진보라-라일락-코어) 조회와 putImageData 한 장으로 끝납니다. 낙뢰는 평균 GAP_MS 430ms 간격으로 떨어지고, 줄기가 사라진 뒤에도 한동안 남는 헤일로는 정적 소재가 필요할 때 뽑아 쓰기 좋은 번개 효과 png 재료가 됩니다.

05테슬라 아크 — 두 전극 사이를 매 프레임 다시 긋는 지속 방전

SF 테마 사이트의 장식 요소에 어울리는, 고정된 두 전극 사이의 경로를 매 프레임 새 난수로 다시 계산해 떨리게 하는 지속 방전 판입니다 — 누적면적 32.122%, 강도 평균 46.5, 23/24 프레임.

테슬라 아크 — 두 전극 사이를 매 프레임 다시 긋는 지속 방전
새 창에서 열기

05 · 테슬라 아크 — 두 전극 사이를 매 프레임 다시 긋는 지속 방전 — 측정 누적면적 32.122% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

lineToglobalCompositeOperationrequestAnimationFrame

끝점은 고정하고 경로만 떨리게 하는 호흡

var throb0 = 0.62 + 0.38 * Math.abs(Math.sin(tSec * 2.1));
var roomA = 0.04 + 0.42 * throb0;   // 잉크 위에 깔리는 라일락 라이팅의 알파
var throb = throb0;
var mag = H * 0.38 * throb;

var main = genBolt(elL.x, elL.y, elR.x, elR.y, mag, JITTER);

// 잔광 확산 버퍼 — 이번 프레임 아크를 도장한 뒤 번지게 하고 무대 위에 깐다
glowStampPath(main, 14);
glowDiffuse();
glowCompose();

전극 좌표 elL·elR 를 프레임 사이에 절대 움직이지 않게 두고 genBolt 의 변위폭 mag 만 H * 0.38 * throb 로 숨쉬게 하면, 위치는 붙박이인데 경로만 끊임없이 재계산되는 아크가 됩니다. 무대 전체가 roomA 로 함께 밝아지는 것은 방 안에 켠 번개 효과 조명이 방전 세기를 그대로 받는 셈이고, 04의 확산 기법을 전극 사이 무대에 이식한 잔광 버퍼가 아크 뒤에 광륜을 깔아 VOLT 240kV · 곁가지 3개짜리 방전 장면을 완성합니다.

06포인터 추적 낙뢰 — 커서를 향해 다시 그려지는 경로

인터랙티브 히어로의 마우스 트레일에 쓰는, 고정 시작점에서 포인터 좌표까지의 구간을 매 프레임 새로 쪼개는 판입니다 — 누적면적 31.824%, 강도 평균 71.6, 23/24 프레임.

포인터 추적 낙뢰 — 커서를 향해 다시 그려지는 경로
새 창에서 열기

06 · 포인터 추적 낙뢰 — 커서를 향해 다시 그려지는 경로 — 측정 누적면적 31.824% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointermovelineTorequestAnimationFrame

목표를 쫓는 lerp 와 매 프레임 새로 긋는 줄기

// 목표 갱신 — 포인터가 없으면 리사주 8자 자동 시연 경로
if (!hasPointer) {
  var a = tSec * AUTOSPD;
  ptrX = W * 0.5 + Math.sin(a * 1.3 - 1.1) * W * 0.40;
  ptrY = H * 0.55 + Math.sin(a * 2.6) * H * 0.36;
}
// 표시 위치 lerp — 목표를 부드럽게 쫓는다
curX += (ptrX - curX) * 0.22;
curY += (ptrY - curY) * 0.22;

// 줄기 — 시작점에서 표시 위치까지 새 경로
var main = genBolt(origin.x, origin.y, curX, curY, MAG_PX, 6);

포인터 좌표를 그대로 쓰면 번개가 커서에 붙어 떨리기만 하는데, 매 프레임 목표와의 차이에 0.22를 곱해 다가가는 lerp 가 추적에 미끄러움을 더해 번개가 커서를 쫓아 늘어졌다 줄어드는 인상을 만듭니다. pointermove 를 한 번도 받지 못한 환경에서는 8자 리사주 경로가 시연용 목표가 되어 화면 구석까지 쓰고, 변위폭은 MAG_PX 26px로 줄기를 잔잔하게 유지합니다.

07체인 라이트닝 — 타점에서 다음 타점으로 튀는 연쇄

게임 UI 스킬 프리뷰에 쓰는, 다섯 타점을 가까운 순서로 잇고 구간마다 점등 지연을 늘려 튀어가는 연쇄 판입니다 — 누적면적 52.858%, 강도 평균 28.5, 23/24 프레임.

체인 라이트닝 — 타점에서 다음 타점으로 튀는 연쇄
새 창에서 열기

07 · 체인 라이트닝 — 타점에서 다음 타점으로 튀는 연쇄 — 측정 누적면적 52.858% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

lineToglobalCompositeOperationrequestAnimationFrame

그리디 최근접 정렬과 구간별 점등 지연

// 구간 e 의 밝기 봉투(envelope) — 점등 지연 → 유지 → 페이드 → 휴지
var N_EDGES = 4;
var FLASH_IN = 40, HOLD_AFTER = 60, FADE_MS = 330;
function edgeAlpha(e, t) {
  var start = e * CHAIN_GAP_MS;
  var fadeStart = (N_EDGES - 1) * CHAIN_GAP_MS + HOLD_AFTER;
  if (t < start) return 0;
  if (t < start + FLASH_IN) return (t - start) / FLASH_IN;
  if (t < fadeStart) return 1;
  if (t < fadeStart + FADE_MS) return 1 - (t - fadeStart) / FADE_MS;
  return 0;
}

타점 다섯 개를 0번에서 시작해 매번 가장 가까운 미방문 타점을 잇는 그리디 정렬로 순서를 고정하고, 구간 인덱스 e 에 CHAIN_GAP_MS(55ms) 를 곱해 시작 시각을 뒤로 미루면 40ms 점등 · 유지 · 330ms 페이드의 봉투가 순서대로 흘러 연쇄처럼 읽힙니다. 화면은 매 프레임 완전히 지우지 않고 rgba(C_INK, 0.065) 를 덧칠하는 트레일 페이드로 굳혀서 잔광이 한 주기 안에 쭉 이어지고, 구간마다 헤일로 색을 라일락-민트-핑크로 돌려 씁니다.

08스트라이크 플래시 — 타격 직후 화면 전체가 지수로 식는다

풀스크린 인트로나 알림 배너의 주의 환기에 쓰는, 낙뢰 프레임에 하늘 전체를 밝혔다 지수적으로 식히는 판입니다 — 누적면적 52.849%, 강도 평균 30.9, 23/24 프레임.

스트라이크 플래시 — 타격 직후 화면 전체가 지수로 식는다
새 창에서 열기

08 · 스트라이크 플래시 — 타격 직후 화면 전체가 지수로 식는다 — 측정 누적면적 52.849% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillRectglobalCompositeOperationrequestAnimationFrame

이전 값에 감쇠율을 곱하는 지수 냉각

// 플래시 감쇠 — 이전 값에 감쇠율을 곱하는 지수 감쇠가 이 데모의 주제다
flash *= Math.pow(DECAY, dt * 60);
// 낙뢰 순간(주기 시작 0~120ms)엔 플래시가 최대치를 유지한다
if (t < 120) flash = 1;

// 하늘 — 플래시가 켜질수록 깊은 슬레이트가 라일락 광으로 밝아진다
var skyCol = mix(C_SKY, C_LILAC, flash * 0.5);
ctx.fillStyle = rgba(skyCol, 1);
ctx.fillRect(0, 0, W, H);

flash 에 매 프레임 Math.pow(0.95, dt * 60) 을 곱하는 것만으로 사진 플래시 같은 지수 냉각이 나오는데, 주기 CYCLE_MS 1000ms의 첫 120ms 동안은 값을 1로 계속 밀어 올려 스트로크가 확실히 보이는 시간을 확보합니다. 줄기는 주기 시작마다 시드를 되돌려 같은 모양이 반복 떨어지고, 뒷산·언덕 능선·작은 탑의 실루엣이 같은 플래시에 함께 떠오르며, 별빛은 45ms 계단으로 반짝여 하늘이 다 식은 휴지 구간에도 프레임마다 값이 바뀝니다.

09구름 속 발광 — 줄기 없이 안개 속에서 깜빡이는 클라우드투클라우드

먹구름 배경의 대기 연출이나 날씨 위젯의 폭풍 상태에 쓰는, 줄기를 그리는 대신 구름 안에 광원을 짧게 여러 번 켜는 판입니다 — 누적면적 45.795%, 강도 평균 62.6, 23/24 프레임.

구름 속 발광 — 줄기 없이 안개 속에서 깜빡이는 클라우드투클라우드
새 창에서 열기

09 · 구름 속 발광 — 줄기 없이 안개 속에서 깜빡이는 클라우드투클라우드 — 측정 누적면적 45.795% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createRadialGradientglobalCompositeOperationrequestAnimationFrame

줄기 대신 광원을 스케줄로 배치하기

prngState = SEED >>> 0;
pulses = [];
for (var i = 0; i < PULSE_COUNT; i++) {
  pulses.push({
    t0: i * (CYCLE_MS / PULSE_COUNT) + rand() * 18,   // 순차 발생 — 몰아치는 느낌
    dur: 110 + rand() * 120,                           // 짧게 켜졌다 식는 펄스 길이
    x: (0.10 + rand() * 0.80) * W,
    y: (0.16 + rand() * 0.62) * CLOUD_H,
    col: GLOWS[i % GLOWS.length]
  });
}

발광원 PULSE_COUNT 9개의 시작 시각을 CYCLE_MS(500ms) 를 9로 나눈 간격에 무작위 18ms를 얹어 순차 배치하면 하늘이 한 번에 켜지는 게 아니라 훑고 지나가며 깜빡입니다. 각 펄스는 빠르게 차오르고 지수로 식는 봉투로 반지름 CLOUD_H * GLOW_RADIUS(0.95) 의 방사형 그라디언트를 그리는데, 코어 색 알파가 a * 0.62 여서 줄기가 없어도 구름 속 방전의 위치가 읽히고, lighter 합성으로 구름 정적 층 위에 빛이 쌓입니다.

번개 이펙트를 실제로 쓰는 화면

게임 UI의 피격 연출이나 버튼 클릭 반응에는 06 포인터 추적 낙뢰처럼 입력 좌표를 향해 다시 그려지는 경로가 맞습니다. 타격 직후 화면이 식는 08 스트라이크 플래시는 전환 애니메이션의 트랜지션 효과로도 그대로 옮겨 붙습니다.

배경 연출로는 03 폭풍 하늘처럼 여러 낙뢰가 겹치는 장면이나 09 구름 속 발광처럼 잔잔히 깜빡이는 장면이 어울리는데, 어느 쪽이든 화면 전체를 덮는 히어로 영역에서만 씁니다.

재귀 깊이를 과하게 올리면 번개가 여기서 깨지는 함정

깊이를 올리면 선분 수가 지수로 늘어난다

재귀 깊이를 6에서 8로 올리는 건 두 배가 아니라 네 배입니다 — 분할마다 구간 수가 두 배가 되어 6단계 64개가 8단계 256개가 되고, 02처럼 구간마다 가지 후보를 다섯 곳 검사하는 판에서는 후보 검사 횟수가 그대로 곱해져 프레임이 눈에 띄게 무거워집니다. 소스 주석에 5는 굵직한 굴곡, 7은 잔잔한 지그재그라고 적어 둔 것처럼 깊이는 질감의 문제이기도 해서, 값을 올리기 전에 굵은 꺾임이 먼저 사라지는지부터 보는 것이 순서입니다.

법선 오프셋이 없으면 지그재그가 한 방향으로 쏠린다

중점을 x 축만 흔들면 세로로 내리꽂히는 줄기가 좌우로만 갈라지고, 대각선 구간에서는 꺾임이 뭉개집니다. 변위를 선분에 수직인 법선 (-dy, dx) / len 방향으로 미는 이유가 여기 있는데, 방향과 무관하게 항상 선분을 어기는 힘이 되어야 어떤 각도의 구간에서도 같은 거칠기가 나옵니다. 이 법선 계산을 빼먹은 판은 위에서 보면 그럴듯한데 경로가 눕는 순간 프랙탈이 깨진 것이 바로 드러납니다.

매 프레임 전체를 지우면 잔광이 살아나지 않는다

05·06처럼 경로를 매 프레임 새로 계산하는 판에서 배경을 매번 불투명하게 다시 칠하면 직전 프레임의 빛이 통째로 사라져 아크가 깜빡이는 전등처럼 됩니다. 04와 05는 밝기를 별도 버퍼에 저장해 확산으로 식히고, 07은 rgba(C_INK, 0.065) 의 얇은 덧칠로 이전 그림을 서서히 지우는 트레일 페이드로 이 문제를 풉니다. 번개의 잔상은 줄기를 다시 그려서가 아니라 지우는 속도를 늦춰서 얻는 것이므로, 값을 직접 흔들어 보려면 소스 통째가 든 zip 을 열어야 하고 여는 암호는 gy22a2jj 입니다.

가지 수 하나로 갈리는 세 변형과 접근성 처리

값 하나를 바꾼 세 변형

이름 바꾼 값 느낌
가지 확률 02 재귀 가지치기 0.15 → 0.4 가지가 많아질수록 번개가 나무처럼 빽빽해진다
변위 강도 01 기본 낙뢰 변위폭 20px → 60px 줄기가 더 거칠고 날카롭게 꺾인다
발생 빈도 03 폭풍 하늘 평균 대기 900ms → 2400ms 몰아치던 하늘이 뜸해지며 긴장이 늘어진다

셋 다 소스 맨 위의 상수 한 줄만 고치는 축이라 나머지 계산은 그대로 살아 있습니다.

움직임을 줄인 사용자에게 남는 정지 프레임

움직임을 줄여 달라는 설정(prefers-reduced-motion: reduce)이 켜져 있으면 아홉 판 전부 애니메이션 루프를 돌리지 않고 그 판이 가장 잘 읽히는 한 시각만 계산해 그립니다. 01·02·03은 낙뢰가 하나 잡힌 한 장을, 04는 확산을 여섯 스텝 미리 돌려 헤일로가 자리 잡은 한 장을, 05와 06은 1.6초 시각의 아크와 추적 줄기를, 07은 90ms 시점 세 구간이 다 켜진 순간을, 08은 60ms 시점 플래시가 살짝 남은 낙뢰를, 09는 120ms 시점 발광 두세 개가 켜진 구름을 보여 줍니다. 계산 자체는 애니메이션과 같은 코드를 한 번만 돌리는 것이라 정지 화면의 품질이 낮아지지 않습니다.

아홉 판 전부 초점을 받는 실험판

아홉 판 모두 role="application" 과 tabindex="0" 을 가진 실험판이라 탭으로 초점을 옮겨 그림 영역에 들어갈 수 있고, 포인터 입력을 받는 것은 06뿐입니다. 06은 커서를 옮기면 번개가 그 자리를 따라와 붙잡는 반응이 판의 주제라 reduced-motion 사용자도 손이 닿는 즉시 그 시각 한 장을 다시 계산해 그립니다. 나머지 여덟 판은 자동으로 흐르는 장면이라 포인터가 없어도 완결되어 있습니다.

낙뢰 이펙트를 붙이기 전에 자주 나오는 질문

css 로 번개를 그리는 것과 캔버스 중점변위는 뭐가 다른가요?

CSS 애니메이션은 미리 정해 둔 키프레임 사이를 브라우저가 보간하는 방식이라 번개의 경로 자체를 만들 수 없고, 번개 모양의 이미지나 클립패스를 흔드는 데 그칩니다. 캔버스 중점변위는 매 프레임 선분을 다시 쪼개 경로를 계산하므로 같은 줄기가 두 번 나오지 않고, 06처럼 포인터를 향해 경로를 다시 긋는 상호작용도 같은 규칙 안에서 됩니다.

이 번개 효과를 gif 파일로 뽑아서 다른 곳에 쓸 수 있나요?

가능합니다. 이 글의 실측값도 24프레임 gif 로 렌더한 화면을 프레임별로 비교해 얻은 것이라, 화면 녹화로 gif 를 만들거나 zip 의 바닐라 판을 붙인 뒤 캔버스를 프레임마다 이미지로 저장해 이어 붙이면 됩니다. 다만 03처럼 지수분포 대기가 들어간 판은 몰아침과 고요가 다 나오도록 녹화 폭을 넉넉히 잡는 것이 좋습니다.

03 폭풍 하늘만 왜 20/24 프레임인가요?

먼 번개는 줄기를 그리지 않고 하늘빛 반응만 남기는데, 그 반응이 임곗값 아래로 내려가는 프레임이 네 개 있었기 때문입니다. 줄기가 그려지는 가까운 번개의 프레임은 전부 움직였고, 하늘 전체의 명도를 조금씩 바꾸는 먼 번개의 섬광은 픽셀 변화량이 작아 측정 도구가 잡지 못한 것이라 화면이 죽어 있다는 뜻이 아닙니다.

더 읽을거리

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

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