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

html5 canvas 애니메이션 9종 — 물방울·블랙홀 특수효과

html5 canvas 애니메이션은 물방울이 글자를 부풀려 보이게 하고 블랙홀이 화면을 빨아들이는 것처럼, 사진 보정 없이 픽셀을 직접 움직이는 특수효과입니다. 이 아홉 종을 Three.js 없이 캔버스 한 장으로 만들었고, 아래 격자에서 지금 돌아갑니다.

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

순서는 화려한 순서가 아니라 왜곡이 만들어지는 순서입니다. 먼저 픽셀을 꺼내 다시 놓는 굴절이 생기고(01), 깊이의 착시가 생기고(02·03·04), 화면 전체가 흐르는 되먹임이 생기고(05·06), 마지막에 빛과 안개가 장면을 통째로 입힙니다(07·08·09). 알갱이를 흩뿌리는 캔버스 파티클 엔진과 스프링·중력을 다루는 물리 인터랙션이 이 편의 이웃입니다.

01물방울 렌즈

배경을 한 번 그려 둔 다음 방울이 덮은 원 안쪽 픽셀만 꺼내 구면 굴절 식으로 다시 배치하는, 제품 사진 위를 굴러다니는 물방울 히어로용 부품입니다.

물방울 렌즈
새 창에서 열기

01 · 물방울 렌즈 — 측정 누적면적 32.968% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

getImageDataputImageDatacreateRadialGradient

표본 좌표를 옮기는 것이 확대다

  for (var y = 0; y < sh; y++) {
    for (var x = 0; x < sw; x++) {
      var i = (y * sw + x) * 4;
      var dx = x - ox, dy = y - oy;
      var dist = Math.sqrt(dx * dx + dy * dy);
      var rn = dist / pr;
      var j = i;
      if (rn < 1 && dist > 0) {
        var inc = Math.asin(rn);                 // 입사각
        var ref = Math.asin(rn / IOR);           // 굴절각 (스넬 법칙)
        var shift = depth * Math.tan(inc - ref); // 안쪽으로 당겨 읽을 거리
        var k = (dist - shift) / dist;
        var mx = Math.round(ox + dx * k), my = Math.round(oy + dy * k);
        if (mx < 0) mx = 0; else if (mx >= sw) mx = sw - 1;
        if (my < 0) my = 0; else if (my >= sh) my = sh - 1;
        j = (my * sw + mx) * 4;
      }

그릴 것은 그대로 두고 읽는 곳만 방울 중심 쪽으로 당겨 오니 격자와 글자가 렌즈 아래에서 부풉니다. 물의 굴절률 1.34 로 스넬 법칙을 그대로 풀었고, 방울을 끌면 잡은 위치로 스르르 따라옵니다(실측: 드래그 위치로 방울 중심 이동, 알파 픽셀 14,273개 변화).

02떠오르는 3D 볼 필드

구슬 마흔 개에 z 값을 주고 초점거리로 한 번 나눠 원근을 만드는, 요금제 섹션 뒤로 떠오르는 배경용 부품입니다.

떠오르는 3D 볼 필드
새 창에서 열기

02 · 떠오르는 3D 볼 필드 — 측정 누적면적 31.135% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createRadialGradientctx.filterglobalAlpha

원근은 나누기 한 줄이다

  // 화가 알고리즘 — z 내림차순으로 먼 것부터. 깊이 버퍼가 없는 2D 에서 겹침 순서를 만드는 유일한 방법이다
  order.sort(function (a, b) { return b.z - a.z; });

  var blurNow = -1;
  for (var i = 0; i < COUNT; i++) {
    var o = order[i];
    var s = focal / (focal + o.z);                 // 원근 투영 — 이 한 줄이 전부다
    var sx = W * 0.5 + (o.x * spanX * 0.5 - camX) * s;
    var sy = H * 0.5 + (o.y * spanY * 0.5 - camY) * s;
    var r = o.r * s;
    if (r < 0.8) continue;
    // 흐림은 정수 단계로 끊는다 — z 로 정렬돼 있어 ctx.filter 를 다섯 번쯤만 갈아 끼우면 된다.
    var blur = Math.round((o.z - Z_NEAR) / (Z_FAR - Z_NEAR) * MAX_BLUR * 1.2);

스케일 s 를 좌표와 반경에 곱하면 같은 크기의 구슬이 먼 것은 작게, 가까운 것은 크게 그려집니다. 먼 것은 ctx.filter 흐림과 낮은 알파로 더 멀어 보이게 하고, 포인터를 움직이면 카메라가 살짝 치우쳐 그 시차가 공간이 기울었다는 단서를 줍니다.

03노이즈 쉐이더 재현

160×100 짜리 작은 버퍼에 값 노이즈를 네 겹 쌓아 픽셀을 직접 쓰고 확대하는, 섹션 사이를 메우는 흐르는 구름 배경용 부품입니다.

노이즈 쉐이더 재현
새 창에서 열기

03 · 노이즈 쉐이더 재현 — 측정 누적면적 44.889% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createImageDataimageSmoothingEnableddrawImage

노이즈를 주파수 2배로 겹쳐 쌓는다

// fbm — 같은 노이즈를 주파수 2배·진폭 절반으로 겹쳐 쌓는다. 겹 수가 곧 결의 고움이다
function fbm(x, y, z, oct) {
  var sum = 0, amp = 0.5, norm = 0, f = 1;
  for (var i = 0; i < oct; i++) {
    sum += vnoise(x * f, y * f, z * f) * amp;
    norm += amp;
    amp *= 0.5;
    f *= 2;
  }
  return sum / norm;
}

셰이더의 프래그먼트 루프를 흉내 내되 해상도를 160×100 으로 낮춰 매 프레임 픽셀 계산을 화면의 약 100분의 1로 줄였습니다. 시간을 셋째 값으로 넣어 구름이 흐르고, 겹 수를 4에서 2로 줄이면 결이 뭉툭해집니다(실측: 슬라이더 겹 2, 알파 픽셀 69,182개 변화).

04흔들리는 3D 텍스트

글자를 보이지 않는 캔버스에 한 번만 그린 뒤 가로 한 줄씩 잘라 사인파로 밀어 붙이는, 행사 페이지의 출렁이는 헤드라인용 부품입니다.

흔들리는 3D 텍스트
새 창에서 열기

04 · 흔들리는 3D 텍스트 — 측정 누적면적 13.245% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillTextmeasureTextdrawImage

줄마다 사인파 위상만큼 민다

function pass(img, drop, t) {
  for (var y = 0; y < H; y += ROW) {
    var sx = 7 * Math.sin(0.055 * y + 2.4 * t) + 3 * Math.sin(0.021 * y - 1.5 * t + 1.4);
    var w = 1 + 0.05 * Math.sin(0.026 * y - 1.9 * t + 0.7);
    ctx.drawImage(img, 0, y * dpr, img.width, ROW * dpr, (W - W * w) * 0.5 + sx, y + drop, W * w, ROW);
  }
}

두 개의 사인이 서로 반대 방향으로 달리는 행파라 줄 사이에 위상차가 생기고, 글자가 깃발처럼 출렁입니다. 아래에 깔아 둔 어두운 복사층이 두께를 만들고, 입력칸에 직접 문구를 넣으면 measureText 가 다시 중앙을 잡습니다(실측: 문구 교체에 알파 픽셀 14,779개 변화).

05블랙홀 터널

직전 프레임을 중심 기준으로 살짝 키워 자기 위에 다시 그리는 되먹임 확대 하나로 끝없이 빨려드는 터널을 만드는, 페이지 전환 직전의 몰입 배경용 부품입니다.

블랙홀 터널
새 창에서 열기

05 · 블랙홀 터널 — 측정 누적면적 52.193% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

drawImageglobalCompositeOperationcreateRadialGradient

되먹임 확대 한 번이 터널 전부다

function frame(now) {
  var w = cv.width, h = cv.height;
  // ① 직전 프레임을 보조 캔버스에 복사
  actx.clearRect(0, 0, w, h);
  actx.drawImage(cv, 0, 0);
  // ② 중심 기준 확대해 다시 깐다 — 이 한 번의 drawImage 가 터널 전부다
  var dw = w * zoom, dh = h * zoom;
  ctx.globalCompositeOperation = 'source-over';
  ctx.drawImage(aux, 0, 0, w, h, (w - dw) * 0.5, (h - dh) * 0.5, dw, dh);
  // ③ 새 얼룩을 더한다 — 겹치는 곳이 밝아지는 lighter 합성
  ctx.globalCompositeOperation = 'lighter';
  for (var i = 0; i < SEEDS; i++) speck();
  // ④ 지평선과 광자 구
  horizon(now);
  raf = requestAnimationFrame(frame);
}

직전 그림을 매 프레임 1.06배로 키우니 얼룩이 바깥으로 흘러 나가며 깊이가 무한인 터널이 됩니다. 확대율을 1.02 로 낮추면 빨려드는 속도가 눈에 띄게 느려집니다(실측: 같은 0.4초에 변한 픽셀 63,768개에서 57,853개로).

06은하 회오리

별 육백 개의 반지름과 시작 각도를 한 번만 정해 두고 자리를 식으로 계산하는, 야간 모드 대시보드의 잔잔한 배경용 부품입니다.

은하 회오리
새 창에서 열기

06 · 은하 회오리 — 측정 누적면적 30.838% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

globalCompositeOperationdestination-outMath.cos

위치는 매 프레임 식으로 계산한다

function draw(t) {
  // 꼬리 — 완전히 지우지 않고 낮은 알파로 바른다
  ctx.globalCompositeOperation = 'destination-out';
  ctx.fillStyle = 'rgba(0, 0, 0, ' + FADE + ')';
  ctx.fillRect(0, 0, W, H);
  // 별과 허브 빛은 겹치면 밝아지는 lighter 로 얹는다
  ctx.globalCompositeOperation = 'lighter';
  for (var i = 0; i < COUNT; i++) {
    var st = stars[i];
    var R = st.r * rmax + 8;                       // 아주 중심으로 모두 몰리지 않게 바닥 반지름 8
    var a = st.a0 + t * K / (Math.pow(R, 0.8) + 20);
    var ex = Math.cos(a) * R;
    var ey = Math.sin(a) * R * FLAT;
    // 계산식의 포인터 항 — (px, py) 쪽으로 좌표 전체가 inf 만큼 기운다(밀도 중심이 잠깐 그 자리로)
    var x = cx + ex * cosT - ey * sinT + (px - cx) * inf;
    var y = cy + ex * sinT + ey * cosT + (py - cy) * inf;

각속도가 바깥으로 갈수록 줄어들어 안쪽이 빨리 돌고 바깥이 느리게 도니 차등 회전이 쌓여 나선팔이 저절로 생깁니다. 속도나 수명을 저장하는 배열이 없어 한참 뒤에도 같은 시각에는 같은 그림이고, destination-out 의 낮은 알파 지우기가 궤적을 남깁니다.

07유체 물결 표면

높이 격자 두 장을 번갈아 쓰며 파동식을 풀고 이웃 칸의 높이 차이로 아래 그림의 픽셀을 끌어오는, 손대면 반응하는 배너 물 표면용 부품입니다.

유체 물결 표면
새 창에서 열기

07 · 유체 물결 표면 — 측정 누적면적 57.639% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Float32ArraygetImageDataputImageData

이웃 네 칸의 평균에서 직전 값을 뺀다

// 파동식 — 이웃 네 칸 평균의 두 배에서 직전 값을 빼면 다음 높이다. 두 장을 교환하며 푼다
function sim(steps) {
  for (var s = 0; s < steps; s++) {
    for (var y = 1; y < ROWS - 1; y++) {
      var row = y * COLS;
      for (var x = 1; x < COLS - 1; x++) {
        var i = row + x;
        prev[i] = ((curr[i - 1] + curr[i + 1] + curr[i - COLS] + curr[i + COLS]) * 0.5 - prev[i]) * DAMP;
      }
    }
    swap = prev; prev = curr; curr = swap;
  }
}
    for (var x = 0; x < RW; x++) {
      var cx = Math.round(x * kx);
      var cl = Math.max(0, cx - 1), cr = Math.min(COLS - 1, cx + 1);
      var hL = curr[rc + cl], hR = curr[rc + cr];
      var hU = curr[ru + cx], hD = curr[rd + cx];
      var sxp = x + (hL - hR) * REFR;

물결은 물리 현상이지만 물결이 보이는 것은 굴절입니다 — 좌우·위아래 높이 차를 오프셋으로 원본 픽셀을 당겨 옵니다. 판을 누르거나 지나가면 코사인 낙차로 물이 움푹 들어갔다가 동그랗게 퍼지고 벽에서 되돌아옵니다(실측: 드래그에 알파 픽셀 79,762개 변화).

08빛 산란 광선

실루엣을 그린 그림을 광원 기준으로 조금씩 키우며 열두 번 겹쳐 더하는, 숲·창문 사진 위에 얹는 빛줄기용 부품입니다.

빛 산란 광선
새 창에서 열기

08 · 빛 산란 광선 — 측정 누적면적 46.398% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

globalCompositeOperationlightercreateLinearGradient

겹칠수록 밝아지는 합성으로 광선이 자란다

  ctx.globalAlpha = 1;
  ctx.drawImage(scene, 0, 0, W, H);
  // ② 광원 기준 1.03배씩 커지는 겹 12장 — 겹칠수록 밝아지는 lighter 합성으로 광선이 자란다
  var s = 1;
  for (var i = 1; i <= STACK; i++) {
    s *= GROW;
    ctx.globalCompositeOperation = 'lighter';
    ctx.globalAlpha = 0.55 * (1 - i / (STACK + 1));
    ctx.drawImage(veil, lx * (1 - s), ly * (1 - s), W * s, H * s);
  }

실루엣의 어두운 덮개를 광원에서 벗어나는 방향으로 열두 장 겹치면 나무 사이 틈만 밝게 누적되어 부챗살 광선이 됩니다. 광원은 포인터를 천천히 따라오고(실측: 이동에 알파 픽셀 77,145개 변화), 키보드 방향키로도 움직일 수 있습니다.

09깊이 안개 씬

능선 여섯 겹을 거리순으로 세우고 거리마다 안개 색을 지수로 섞는, 여행 랜딩의 조용한 원경 배경용 부품입니다.

깊이 안개 씬
새 창에서 열기

09 · 깊이 안개 씬 — 측정 누적면적 1.434% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createLinearGradientfillRectglobalAlpha

남을 대비는 지수로 녹는다

  // 능선 — 뒤에서 앞으로. 거리마다 안개 색을 지수로 섞는다: t = exp(-거리/밀도)
  for (var i = 0; i < LAYERS; i++) {
    var d = LAYERS - i;              // 거리 — 앞층이 1
    var t = Math.exp(-d / density);  // 그 층이 남길 대비
    var off = (sec * meta[i].speed) % W;
    ctx.fillStyle = css3(mix3(SKY1C, INK, t));
    ctx.save();
    ctx.translate(-off, 0);
    ctx.fill(paths[i]);
    ctx.translate(W, 0);
    ctx.fill(paths[i]);

지수 혼합은 먼 층일수록 하늘색에 급격히 녹아 들게 해, 같은 잉크색 능선 여섯 개가 단계적인 원경으로 읽힙니다. 각 층이 서로 다른 속도로 가로 흐르는 시차가 깊이를 두 배로 확실히 하고, 밀도를 ×2.4 에서 ×5.0 으로 올리면 안개가 짙어져 맨 앞 능선만 남습니다(실측: 알파 픽셀 46,650개 변화).

html5 canvas 2d 는 왜곡을 어떻게 만드나?

html5 canvas 2d 가 왜곡을 만드는 방법은 그리기가 아니라 다시 읽기 하나입니다. 이미 그려진 픽셀을 getImageData 로 꺼내 표본 좌표만 옮겨 되돌리면(01), 이웃의 높이차로 당겨 오면(07), 직전 프레임을 키워 얹으면(05) 같은 그림이 각각 렌즈·물결·터널로 보입니다. 화가 알고리즘과 지수 혼합(02·09)처럼 그리는 순서와 색만 바꾸는 것도 픽셀을 다시 읽는 같은 계열입니다.

이 아홉 개는 전부 requestAnimationFrame 이 넘겨주는 시간을 시계로 삼고, 프레임 사이 간격을 직접 재는 02 는 그 간격이 0.05초를 넘지 못하게 자릅니다. 실측에서 60프레임의 평균 간격은 항목마다 16.4~16.6밀리초로 60fps 에 붙어 있었고, 움직인 프레임은 스물세 칸 전부, 누적면적은 09 의 1.4퍼센트부터 07 의 57.6퍼센트까지 항목마다 다릅니다. html5 canvas api 의 이 세 함수 — 읽기·쓰기·합성 — 가 물방울과 블랙홀의 전부입니다.

어디서 깨지는가 — 포스터에서 유리가 회색 공이 되다

첫 판의 물방울은 렌즈가 아니라 반투명 회색 공으로 읽혔습니다. 굴절을 계산하면서 물층 두께를 반지름의 0.5배로 잡았더니 확대가 너무 약해서 배경이 거의 그대로 보였고, 그 위에 겹친 가장자리 그늘이 주인공이 되어 버린 것입니다. 두께를 0.9배까지 올려서야 격자가 눈에 띄게 휘었고, 1.0 을 넘기면 가장자리의 표본 좌표가 음수로 넘어가 상이 뒤집히는 경계도 소스에 남겨 두었습니다.

재는 쪽의 함정도 있었습니다. 굴절 검증을 처음에 파랑 채널로 잰 것이 잘못이었습니다 — 파란 격자와 크림 배경이 파랑 채널에서는 5밖에 차이가 나지 않아, 멀쩡히 휘고 있는 격자가 빈 화면으로 오판되었습니다. 빨강 채널로 다시 재고 나서야 원 안 격자 간격이 원 밖의 약 1.3배로 벌어져 있다는 사실이 잡혔습니다. 아홉 개의 완성된 특수효과 소스와 측정값 전체는 글 아래 암호 zip 안에 들어 있습니다. 여는 암호는 w2k393bp 인데, 세 로케일 판의 받기 단추가 모두 이 한 암호를 공유합니다.

변형 세 가지와 접근성

이름 바꾼 값 느낌 한 줄
되먹임 배율 05 확대 1.06 → 1.02 빨려드는 속도가 느려지고 터널이 길어진다
노이즈 겹 03 octaves 4 → 2 구름이 뭉툭해지고 결이 거칠어진다
색 $subject-blue → $subject-mint 차가운 파랑에서 상큼한 민트로

접근성은 아홉 판이 한 가지 원칙을 공유합니다. prefers-reduced-motion: reduce 를 켠 사용자에게는 rAF 루프를 아예 시작하지 않고, 각 판이 가장 잘 읽히는 정적 한 장만 그립니다 — 01 은 방울이 배경 위에 얹힌 상태, 05 는 터널이 완성된 상태, 09 는 새가 하늘에 떠 있는 순간입니다(실측: 세 항목 모두 알파 픽셀 수가 0 이 아닌 정지판). 여섯 판(01·02·03·06·07·08)에는 tabindex 와 role 이 있어 Enter·Space 로 물결을 일으키고(07), 방향키로 방울·카메라·노이즈 판·은하 중심·물결 자리·광원을 움직일 수 있습니다. 08 의 커서는 가늘은 십자라 화면을 가리지 않습니다.

FAQ

자바스크립트 애니메이션 효과를 CSS 대신 캔버스로 만들어야 할 때는 언제인가요?

실제 픽셀 값을 읽어야 할 때입니다. CSS 변형은 요소를 통째로 움직이지만 방울 아래 글자를 부풀리거나(01) 물결 아래 그림을 굴절시키는(07) 것은 픽셀 단위의 재배치라 CSS 로는 표현할 수 없습니다. 반면 단순 이동·페이드라면 CSS 가 배터리와 성능 면에서 낫습니다.

캔버스 데모가 휴대폰에서 느려지지 않나요?

계산하는 픽셀 수를 화면보다 작게 잡는 것이 이 편의 정석입니다. 노이즈는 160×100 버퍼에서 계산해 확대하고(03), 물 시뮬레이션은 격자 96×47 에서 풀고 렌더 해상도도 그 격자의 3배에서 그립니다(07). 블랙홀 터널은 매 프레임 그리는 것이 확대 한 번과 얼룩 몇 개뿐이라 가장 가볍습니다.

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

됩니다. 글 아래 암호 zip 에 아홉 개의 vanilla 판과 React 판이 통째로 들어 있어 붙여 넣으면 바로 돌아갑니다. 화면 문구는 세 언어로 분리돼 있고, 격자 색과 굵기는 각 판 맨 위의 변수와 토큰으로 바꾸면 됩니다.

더 읽을거리

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

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