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

푸리에 변환으로 그림 그리기 9종 — 원이 도는 방식으로 별·하트를 되살리는 캔버스

푸리에 변환은 선의 점들을 주파수별 원운동의 계수로 바꾸는 계산입니다. 라이브러리·WebGL 없이 그리고 싶은 선의 점들을 복소평면에 올리고 사인·코사인 적분 한 번으로 원 위에 원이 얹혀 도는 에피사이클을 만들면, 맨 끝 원의 붓끝이 지나간 자리가 처음 그린 그림을 되살립니다.

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

데모

아홉 판 모두 HTML 파일 하나로 자립해 브라우저에서 바로 돌아갑니다. 24프레임을 녹화하는 동안 아홉 판 모두 23프레임이 움직였고(움직인프레임 23), 밑바탕의 극격자·레이더 스윕까지 매 프레임 다시 계산해 화면 어느 구석도 정지하지 않습니다. 05번은 마우스로 직접 선을 그을 수 있고 08번은 슬라이더를 잡을 수 있으니 첫 장부터 만져 보시길 권합니다.

항목 초점 누적면적(%) 강도 평균
01 기본 에피사이클 체인 계수 다섯 개로 도는 최소 구조 36.176 33.1
02 구형파 합성 홀수 배음 원이 각진 파형을 만드는 순간 36.880 27.4
03 별 그리기 경로 256점의 계수 전부로 별 재현 45.551 26.4
04 하트 그리기 상위 60개 원의 압축 재합성 46.641 27.7
05 내 선 그리기 그은 점이 즉시 계수로 바뀌는 재계산 42.997 23.7
06 스펙트럼 막대 계수 크기와 위상의 별↔낙서 보간 36.266 33.1
07 위상 모프 크기는 얼리고 위상만 돌리는 실험 40.690 24.6
08 배음 슬라이더 원 개수를 1부터 64까지 움직이는 압축 체험 41.044 31.8
09 로고 트레이스 씬 잔상이 가장 느리게 지워지는 마무리 장면 47.314 24.2

09가 누적면적 47.314%로 아홉 판 중 가장 넓게 채웠는데, 트레일 감쇠를 α=0.012로 가장 느리게 잡아 잔상이 오래 머물기 때문입니다. 강도는 01·06이 33.1로 가장 밝았고, 07은 한 프레임이 가장 크게 변했습니다(프레임최대 15.098%).

손으로 정한 계수에서 이산 푸리에 변환까지, 아홉 걸음

순서는 계산을 어디까지 직접 하는가로 정했습니다. 01은 계수를 손으로 다섯 개 정의해 원 체인의 구조만 보여 주고, 02는 푸리에 급수의 고전 장면인 홀수 배음 합성을 각진 파형과 나란히 놓습니다. 03에서 처음으로 실제 경로 256점에 이산 푸리에 변환을 돌려 별을 되살리고, 04는 같은 파이프라인의 하트에서 계수 상위 60개만 남겨 압축의 예고편을 깔고, 05는 그 계산을 포인터 입력에 붙여 그은 즉시 재계산합니다. 06은 계수의 크기와 위상을 막대로 세워 두 그림 사이의 보간을 보여 주고, 07은 진폭을 얼리고 위상에만 오프셋을 더해 무늬가 변신하는 걸 확인합니다. 08은 04의 하트 계수를 슬라이더로 1부터 64까지 줄였다 늘려 압축 실험을 손으로 만지게 하고, 09는 같은 계산을 어두운 무대 위 로고 공개 장면으로 마무리합니다. 별 그리기로 찾아오신 분께는 03이, 원리가 궁금하신 분께는 06·07이 곧장 답이 되는 순서입니다.

항목별 코드

01기본 에피사이클 체인 — 원 위에 원이 얹혀 맴돌며 점을 찍는 구조

기본 에피사이클 체인 — 원 위에 원이 얹혀 맴돌며 점을 찍는 구조
새 창에서 열기

01 · 기본 에피사이클 체인 — 원 위에 원이 얹혀 맴돌며 점을 찍는 구조 — 측정 누적면적 36.176% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DarcrequestAnimationFrame

주파수 2·3·5·7·9, 반지름 0.34·0.18·0.12·0.07·0.05의 계수 다섯 개를 손으로 정의해 원들을 차례로 이어 붙이면 맨 끝점이 꽃잎 같은 대칭 무늬를 그리며 도는데, 이것이 재합성 구조의 최소 단위로서 이후 모든 판의 원형입니다. 밑바탕엔 스포크 16개와 숨쉬는 동심원 4개의 극격자가 매 프레임 다시 그려지고 측정은 누적면적 36.176%·강도평균 33.1입니다.

계수 다섯 개에서 체인 좌표까지

  var COEF = [
    { k: 2, r: 0.34, p: 0.0 },
    { k: 3, r: 0.18, p: 1.1 },
    { k: 5, r: 0.12, p: 2.4 },
    { k: 7, r: 0.07, p: 0.6 },
    { k: 9, r: 0.05, p: 3.6 }
  ];

  function computeChain(t) {
    var cx = W * 0.5, cy = H * 0.5;
    var base = Math.min(W, H) * 0.48;
    var pts = [{ x: cx, y: cy }];
    for (var i = 0; i < TERMS && i < COEF.length; i++) {
      var c = COEF[i];
      var a = c.k * t + c.p;
      var px = pts[i].x + Math.cos(a) * c.r * base;
      var py = pts[i].y + Math.sin(a) * c.r * base;
      pts.push({ x: px, y: py });
    }
    return pts;
  }

02구형파 합성 — 홀수 배음 원 몇 개로 각진 파형 만들기

구형파 합성 — 홀수 배음 원 몇 개로 각진 파형 만들기
새 창에서 열기

02 · 구형파 합성 — 홀수 배음 원 몇 개로 각진 파형 만들기 — 측정 누적면적 36.88% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DlineTorequestAnimationFrame

각진 파형은 홀수 배음만 남긴 급수로 분해되고 각 항은 반지름이 1/k인 원이 k 배속으로 도는 것과 같은데, 이 판은 그 원 체인을 왼쪽에 세로로 쌓고 맨 끝점의 높이를 오른쪽 파형 영역에 시간 축으로 펼쳐 쌓습니다. 부드러운 원운동의 합이 뾰족한 계단이 되는 순간이 매 프레임 그려지고 측정은 누적면적 36.880%·프레임최대 9.004%입니다.

세로 스택의 끝점 높이가 파형이 된다

  function computeChain(t) {
    // 세로 스택: 원 k 반지름 = AMP/k, 각속도 = k
    var pts = [];
    var cx = W * 0.24, cy = H * 0.5;
    var base = H * 0.36;
    pts.push({ x: cx, y: cy });
    for (var k = 1; k <= TERMS * 2; k += 2) {
      var y = pts[pts.length - 1].y + Math.sin(k * t) * (AMP / k) * base;
      pts.push({ x: cx, y: y });
    }
    return pts;
  }

03별 그리기 — 계수 순 정렬로 붓끝이 별을 한 획에 그리기

별 그리기 — 계수 순 정렬로 붓끝이 별을 한 획에 그리기
새 창에서 열기

03 · 별 그리기 — 계수 순 정렬로 붓끝이 별을 한 획에 그리기 — 측정 누적면적 45.551% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DlineTorequestAnimationFrame

다섯 포인트 별의 닫힌 경로를 256점으로 샘플링해 이산 푸리에 변환을 돌리면 계수 256개가 나오는데, 크기 순으로 정렬해 원 체인을 세우면 붓끝이 별의 윤곽을 한 바퀴 돌아 되살립니다. 체인에 얹는 계수는 큰 것부터 14개(SHOW=14)라 가장자리가 아주 미세하게 부드러워지지만 상위 14개만으로 별의 형태는 사실상 완성되며, 밑바탕의 레이더 스윕은 22단 계단 알파의 부채꼴로 그라디언트 없이 옅어지는 데다 누적면적 45.551%까지 채워 별이 화면의 절반가량을 삼킵니다.

경로 256점을 복소 계수로 바꾸는 변환

  function dft(path) {
    var M = path.length;
    var out = [];
    for (var k = 0; k < M; k++) {
      var freq = (k <= M / 2) ? k : k - M;   // 음수 주파수 접기
      var re = 0, im = 0;
      for (var n = 0; n < M; n++) {
        var a = -TAU * k * n / M;
        var c = Math.cos(a), s = Math.sin(a);
        re += path[n].x * c - path[n].y * s;
        im += path[n].x * s + path[n].y * c;
      }
      re /= M; im /= M;
      out.push({ freq: freq, re: re, im: im, amp: Math.sqrt(re * re + im * im) });
    }
    out.sort(function (a, b) { return b.amp - a.amp; });   // ③ 크기 순
    return out;
  }

04하트 그리기 — 닫힌 곡선의 계수로 하트를 되살리기

하트 그리기 — 닫힌 곡선의 계수로 하트를 되살리기
새 창에서 열기

04 · 하트 그리기 — 닫힌 곡선의 계수로 하트를 되살리기 — 측정 누적면적 46.641% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DlineTorequestAnimationFrame

하트 매개변수 곡선을 같은 256점 변환으로 보내 상위 60개 계수만 그리면, 체인이 꼬리의 오목한 꺾임에서 시작해 하트를 정확히 되짚으며 계수를 버려도 본떼가 남는다는 압축의 성질을 미리 봅니다. 누적면적 46.641%로 03보다 조금 더 넓게 채웠고 프레임최대는 5.647%입니다.

계수에서 체인 좌표를 다시 더하는 재합성

  function chainAt(u, show) {
    var base = Math.min(W, H) * 0.42;
    var cx = W * 0.5, cy = H * 0.48;
    var pts = [{ x: cx, y: cy }];
    var n = show || COEF.length;
    for (var i = 0; i < n && i < COEF.length; i++) {
      var c = COEF[i];
      var a = c.freq * u * TAU;
      var ph = Math.atan2(c.im, c.re);
      var ang = a + ph;
      var px = pts[i].x + Math.cos(ang) * c.amp * base;
      var py = pts[i].y + Math.sin(ang) * c.amp * base;
      pts.push({ x: px, y: py });
    }
    return pts;
  }

05내 선 그리기 — 마우스로 긋자마자 원이 되살리는 에피사이클

내 선 그리기 — 마우스로 긋자마자 원이 되살리는 에피사이클
새 창에서 열기

05 · 내 선 그리기 — 마우스로 긋자마자 원이 되살리는 에피사이클 — 측정 누적면적 42.997% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointerdownpointermoverequestAnimationFrame

캔버스에 pointerdown~move로 선을 그으면 점들이 중심·폭으로 정규화된 뒤 즉시 복소 계수로 바뀌어, 방금 손으로 그은 서명·낙서를 에피사이클이 한 획씩 다시 그립니다. 계산은 O(N²)이지만 점을 512개로 제한해 순식간에 끝나고, 입력이 없을 땐 에피사이클이 물결 서명 예시를 반복해서 되그리며 옅은 잔상을 남기고, 누적면적은 42.997%입니다.

그은 점의 정규화와 즉시 재계산

  function normalize() {
    // 그은 점을 무대 중심·단위반지름으로 정규화
    var minx = 1e9, maxx = -1e9, miny = 1e9, maxy = -1e9;
    for (var i = 0; i < PATH.length; i++) {
      if (PATH[i].x < minx) minx = PATH[i].x;
      if (PATH[i].x > maxx) maxx = PATH[i].x;
      if (PATH[i].y < miny) miny = PATH[i].y;
      if (PATH[i].y > maxy) maxy = PATH[i].y;
    }
    var cx = (minx + maxx) / 2, cy = (miny + maxy) / 2;
    var span = Math.max(maxx - minx, maxy - miny) || 1;
    normPath = [];
    for (var j = 0; j < PATH.length; j++) {
      normPath.push({ x: (PATH[j].x - cx) / span, y: (PATH[j].y - cy) / span });
    }
    COEF = dft(normPath);
  }

포인터 이동 한 점이 곧 계수 갱신 한 번

  function moveDraw(e) {
    if (!drawing) return;
    var p = toField(e);
    if (Math.abs(p.x - lastPt.x) + Math.abs(p.y - lastPt.y) < 4) return;
    PATH.push(p);
    if (PATH.length > MAXPTS) PATH.shift();
    handCtx.strokeStyle = rgba(C_HOT, 0.28);
    handCtx.lineWidth = 2;
    handCtx.lineCap = 'round';
    handCtx.beginPath();
    handCtx.moveTo(lastPt.x, lastPt.y);
    handCtx.lineTo(p.x, p.y);
    handCtx.stroke();
    lastPt = p;
    normalize();
    prevPen = null;
    e.preventDefault();
  }

06스펙트럼 막대 — 계수 크기가 알려주는 '어떤 원이 일하는가'

스펙트럼 막대 — 계수 크기가 알려주는 '어떤 원이 일하는가'
새 창에서 열기

06 · 스펙트럼 막대 — 계수 크기가 알려주는 '어떤 원이 일하는가' — 측정 누적면적 36.266% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DfillRectrequestAnimationFrame

별과 여러 주파수가 비슷한 크기로 섞인 물결 낙서, 두 그림의 계수를 주파수 슬롯(±12)에 막대로 세우고 진폭·위상을 2.6초 주기로 왕복 보간하면 별에서 낙서로 그림의 지문이 연속 변형됩니다. 막대 영역이 화면의 좁은 띠를 차지하는 성격 덕에 프레임최대는 2.672%로 아홉 판 중 가장 작지만 강도평균은 33.1로 가장 밝은 축입니다.

두 그림의 계수를 섞는 진폭·위상 보간

  function coefAt(mix) {
    var used = [];
    for (var fi = -NBARS / 2; fi <= NBARS / 2; fi++) {
      if (fi === 0 && mix < 0.5) continue;
      var s = slotStar[fi], d = slotDoodle[fi];
      if (!s || !d) continue;
      used.push({
        freq: fi,
        amp: s.amp + (d.amp - s.amp) * mix,
        ph: s.ph !== undefined ? s.ph + (d.ph - s.ph) * mix : 0
      });
    }
    // 위상 채우기(첫 계산 후 고정)
    for (var i = 0; i < used.length; i++) {
      var s2 = slotStar[used[i].freq], d2 = slotDoodle[used[i].freq];
      used[i].ph = Math.atan2(s2.im, s2.re) + (Math.atan2(d2.im, d2.re) - Math.atan2(s2.im, s2.re)) * mix;
    }
    return used;
  }

07위상 모프 — 계수는 그대로 두고 위상만 돌리면 무늬가 변신하기

위상 모프 — 계수는 그대로 두고 위상만 돌리면 무늬가 변신하기
새 창에서 열기

07 · 위상 모프 — 계수는 그대로 두고 위상만 돌리면 무늬가 변신하기 — 측정 누적면적 40.69% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CanvasRenderingContext2DglobalAlpharequestAnimationFrame

별 경로 계수의 진폭은 얼리고 위상에만 주파수 비례 오프셋을 더하면 붓끝 궤적이 별에서 꽃잎·소용돌이로 연속 변신하는데, 크기가 "얼마나" 위상이 "어디서"를 담당하는지를 분리해서 보여 주는 실험입니다. 오프셋은 2.4초 주기의 사인파로 -0.8~0.8을 왕복하고 측정은 누적면적 40.690%·프레임최대 15.098%로 한 프레임 변화가 아홉 판 중 가장 큽니다.

위상에 주파수 비례 오프셋을 더하는 한 줄

  function chainAt(u, d, show) {
    var base = Math.min(W, H) * 0.36;
    var cx = W * 0.32, cy = H * 0.5;
    var pts = [{ x: cx, y: cy }];
    for (var i = 0; i < show && i < COEF.length; i++) {
      var c = COEF[i];
      var ph = Math.atan2(c.im, c.re) + c.freq * d;
      var ang = c.freq * u * TAU + ph;
      pts.push({
        x: pts[i].x + Math.cos(ang) * c.amp * base,
        y: pts[i].y + Math.sin(ang) * c.amp * base
      });
    }
    return pts;
  }

08배음 슬라이더 — 원 개수를 줄이면 그림이 어떻게 흐려지나

배음 슬라이더 — 원 개수를 줄이면 그림이 어떻게 흐려지나
새 창에서 열기

08 · 배음 슬라이더 — 원 개수를 줄이면 그림이 어떻게 흐려지나 — 측정 누적면적 41.044% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

input[type=range]CanvasRenderingContext2DrequestAnimationFrame

하트 계수를 크기 순으로 준비해 두고 원 개수를 1부터 64까지 슬라이더로 움직이면 하트가 물방울에서 점점 또렷한 하트로 되살아납니다. 자동 시연은 3.2초 주기의 삼각파로 개수를 왕복하고 슬라이더를 직접 잡으면 자동 모드가 꺼지며, 누적면적 41.044%·프레임최대 12.360%입니다.

자동 왕복과 사람의 손이 만나는 프레임

    var show;
    if (autoMode) {
      // 자동 왕복 — 1→64→1. 사람이 잡으면 멈춘다
      var tri = 0.5 - 0.5 * Math.cos(lt * TAU / SWEEP);
      show = 1 + Math.round(tri * (MAXSHOW - 1));
      if (slider && String(slider.value) !== String(show)) slider.value = String(show);
    } else {
      show = slider ? parseInt(slider.value, 10) : MAXSHOW;
    }

    decayTrail();
    var pen = draw(u, pulse, show, lt * TAU);
    stampTrail(pen);
    if (nOut) nOut.textContent = String(show);
    raf = requestAnimationFrame(frame);

슬라이더를 잡는 순간 자동 모드가 풀린다

  if (slider) {
    slider.addEventListener('pointerdown', function () { autoMode = false; });
    slider.addEventListener('input', function () {
      autoMode = false;
      prevPen = null;
      if (nOut) nOut.textContent = slider.value;
    });
    slider.addEventListener('keydown', function () { autoMode = false; });
  }

09로고 트레이스 씬 — 어두운 무대 위 한 획 로고 공개 장면

로고 트레이스 씬 — 어두운 무대 위 한 획 로고 공개 장면
새 창에서 열기

09 · 로고 트레이스 씬 — 어두운 무대 위 한 획 로고 공개 장면 — 측정 누적면적 47.314% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createRadialGradientCanvasRenderingContext2DrequestAnimationFrame

"G" 한 획 로고의 윤곽을 03·04와 같은 변환·재합성으로 되그리되 어두운 무대에 중앙 조명을 깔고 트레일 감쇠를 α=0.012로 아홉 판 중 가장 느리게 잡으면, 원들의 회전이 멈춰도 잔상이 오래 머무는 한 장면이 됩니다. 누적면적 47.314%는 이 느린 감쇠가 만든 아홉 판 중 최대치입니다.

원호와 가로획이 이어지는 한 획 경로

  function logoPath(u) {
    // 0~0.72: 원호(각도 -60도에서 240도 방향으로), 0.72~1: 가로획
    var ARC = 0.72;
    var R = 1;
    if (u < ARC) {
      var ang = (-Math.PI / 6) + (u / ARC) * (TAU * 300 / 360) * -1;
      return { x: Math.cos(ang) * R * 0.62, y: Math.sin(ang) * R * 0.62 };
    }
    var f = (u - ARC) / (1 - ARC);
    var sx = Math.cos(-Math.PI / 6) * 0.62, sy = Math.sin(-Math.PI / 6) * 0.62;
    var ex = 0.42, ey = 0.02;
    var cx2 = sx + (ex - sx) * f, cy2 = sy + (ey - sy) * f;
    return { x: cx2, y: cy2 };
  }

로고 공개 장면과 손글씨 데모 — 이 아홉 개를 어디에 붙이나

브랜드 인트로나 로딩 화면이라면 09 로고 트레이스 씬을 그대로 쓰는 것이 가장 빠릅니다. 어두운 무대 위에서 한 획이 완성되는 연출이 이미 잡혀 있어 텍스트만 로고 경로로 바꾸면 됩니다. 방문자가 직접 참여하길 바라는 이벤트 페이지나 방명록에는 05 내 선 그리기를 앉히면, 마우스로 그은 낙서가 그 자리에서 에피사이클로 되살아나는 체험이 됩니다.

오디오·신호 처리를 설명하는 교육 콘텐츠라면 06 스펙트럼 막대가 맞습니다. 계수 크기가 어떤 원이 일하는지를 막대로 보여주므로 푸리에 변환의 추상적인 개념이 눈에 보이는 그래프로 바뀝니다. 세 판 모두 파일 하나로 완결돼 있어 별도 프레임워크 없이 정적 섹션에 바로 끼워 넣을 수 있습니다.

왜 원만 돌면 그림이 나오나? — 복소 계수와 주파수 분해의 셈

점 하나는 복소수 하나, 경로는 복소평면 위의 닫힌 선

평면의 점 (x, y)를 x + iy로 보면 그리고 싶은 선은 복소수의 시퀀스가 되고, 닫힌 경로라면 이 시퀀스는 주기를 한 바퀴로 하는 주기함수로 취급할 수 있습니다. 주기함수는 주파수 서로 다른 원운동의 합으로 분해된다는 것이 푸리에 급수의 출발점이고, 그 분해의 계산이 이 글 03~05·07~09 일곱 판에 들어 있는 이산 변환입니다(02는 손으로 쓴 사인 항, 01은 계수 직접 정의).

겹셈 두 줄이 계수 하나를 만든다

변환의 한 항은 길이 6줄 안팎의 겹셈입니다. 모든 점에 같은 각도의 회전(코사인·사인 짝)을 곱해 더한 뒤 점 수로 나누면 그 주파수의 계수가 실수부·허수부로 나오고, 크기는 두 부분의 피타고라스 합, 위상은 arctan 비율로 읽습니다. 256점이면 겹셈을 256번, 주파수도 256개라 65,536번의 곱셈인데 요즘 브라우저에서는 한 프레임에 수십 번 돌려도 여유이므로 빠른 알고리즘을 가져올 필요가 없습니다.

재합성은 앞 원의 끝점을 다음 원의 중심으로 옮기는 것

계수를 크기 순으로 세운 뒤 시각 u에서 각 원의 각도를 freq·u·2π + 위상으로 잡고 앞 점에서 cos·sin으로 다음 중심을 더하면 체인의 끝점이 원래 경로를 따라 움직입니다. 큰 원부터 얹는 정렬은 체인이 화면 안에서 안정적으로 수렴하는 조건이자 04·08의 "상위 k개만 남긴다"는 압축의 전제입니다. k가 절반을 넘는 주파수는 음의 주파수(k - N)로 접어야 실제 회전 방향이 맞는데, 접지 않으면 닫힌 경로가 똑바로 되살아나지 않습니다.

크기는 얼마나, 위상은 어디서

계수의 두 성질이 그림에서 역할을 나눠 가집니다. 크기는 어떤 주파수가 얼마나 일하는지를 정해 그림의 지문이 되고(06의 막대), 위상은 각 원이 어디서 시작하는지를 정해 무늬의 배열을 바꿉니다(07의 모프). 진폭을 지키고 위상만 돌려도 별이 꽃잎으로 변하는 것도, 위상을 버리고 진폭만 쓰면 원들이 돌아도 붓끝이 그림을 찾지 못하는 것도 푸리에 변환 원리를 이 한 쌍의 분업으로 요약할 수 있는 증거입니다.

회전 속도와 정렬 기준을 바꾸는 세 변형과 접근성

값 하나를 바꾼 세 변형

첫째, 03의 표시 원 개수를 전체에서 상위 5개로 줄이면 꼭짓점이 뭉개져 둥근 별이 됩니다. 둘째, 01의 회전 속도를 1배에서 3배로 올리면 트레일이 길게 늘어나 빗금 무늬가 됩니다. 셋째, 03의 정렬을 크기 순에서 주파수 순으로 바꾸면 붓끝 궤적은 같아도 원 배열의 모양이 완전히 달라집니다. 세 변형 모두 규칙은 건드리지 않고 값 하나만 바꾼다는 점이 이 계산의 성격을 잘 보여줍니다.

움직임을 줄인 사용자에게 남기는 것

아홉 판 모두 matchMedia('(prefers-reduced-motion: reduce)')로 이 설정을 감지해, 켜져 있으면 프레임을 계속 흘리는 대신 궤적을 미리 한 바퀴 쌓은 뒤 마지막 완성 장면을 고정하고 캔버스에 정지 클래스를 붙입니다. 05의 예시 낙서와 09의 완성된 로고 잔상도 같은 방식으로 정지 장면에 남으므로 움직임을 줄인 사용자에게도 그림은 그대로 전달됩니다.

손으로 만지는 두 판과 나머지 일곱 판

05는 포인터 입력을, 08은 키보드 접근이 가능한 슬라이더를 받고 나머지 일곟 판은 손을 대지 않아도 완결됩니다. 05의 캔버스에는 실험판이라는 알 수 있는 이름과 탭 포커스가 붙어 있고 08의 슬라이더에도 원 개수라는 이름이 붙어 있어 화면 낭독기로 조작 흐름을 따라갈 수 있습니다.

계수를 크기 순이 아니라 주파수 순으로 세우면 체인이 흔들리는 함정

계수를 주파수 순으로 세우면 체인이 화면 밖으로 흔들린다

재합성의 수학은 정렬과 무관하게 항상 같은 끝점을 내놓지만 큰 원이 나중에 얹히면 체인의 중심이 크게 흔들려 실제로는 쓸 수 없는 장면이 됩니다. 크기 순 정렬은 안정성의 문제이지 정확도의 문제가 아니라는 점을 03을 고쳐 보며 확인했습니다. 정렬 기준을 바꾸는 변형이 붓끝 궤적은 같아도 원 배열의 모양을 완전히 바꾸는 것도 같은 이유입니다.

05는 점이 늘 때마다 계산을 다시 돌린다

포인터로 그은 선은 점이 하나 늘 때마다 전체 계수를 다시 계산하므로 긴 낙서를 천천히 그으면 재계산이 수백 번 일어납니다. 점을 512개로 잘라 두었기에 체감 흐름은 유지되지만 제한을 크게 올리면 그리는 동안 프레임이 떨리기 시작하니, 촘촘한 선을 원하면 점 수를 늘리기보다 표시할 원의 수를 줄이는 편이 가볍습니다.

감쇠를 0에 가깝게 내리면 잔상이 그림을 지운다

트레일을 지우는 α를 지나치게 낮추면 오래된 궤적이 화면에 남아 새로 그리는 붓끝과 뒤섞여 그림의 윤곽이 흐려집니다. 09가 α=0.012까지 내려 느린 잔상을 연출로 쓸 수 있었던 건 어두운 무대 위 한 획 짜리 단순한 경로였기 때문이고, 별·하트처럼 궤적이 겹치는 그림에서는 03·04의 α=0.03 수준이 잔상과 선명함의 균형점이었습니다.

이 아홉 장면의 소스 전부와 항목별 html·css 는 암호 zip 으로 묶여 있고 비번은 tjy7w4m5 입니다. 풀면 각 판의 index.html 이 그대로 들어 있어 본문의 코드 조각과 줄 단위로 나란히 대조해 보실 수 있습니다.

에피사이클 체인을 붙이기 전 자주 나오는 질문

푸리에 변환 공식을 몰라도 이 데모를 쓸 수 있나요?

가능합니다. zip 을 풀면 아홉 항목의 폴더가 각자 들어 있어 원하는 판 하나만 골라 그 파일을 페이지에 올리면 그대로 움직입니다. 색은 파일 머리의 팔레트 토큰을, 크기는 캔버스 폭을 몇 개만 바꿔 조정하며, 각 판은 서로를 참조하지 않는 단일 파일이라 하나만 써도 나머지와 무관합니다.

복소수를 안 쓰고도 에피사이클을 만들 수 있나요?

됩니다. 복소수 겹셈은 결국 코사인·사인 두 줄의 실수 계산과 같아서, 이 글의 코드도 실수부·허수부를 각각 곱해 더하는 방식으로 쓰여 있습니다. 02의 구형파 합성은 아예 사인 합만으로 같은 원리를 보여 주는 판입니다.

푸리에 급수와 푸리에 변환은 무엇이 다른가요?

급수는 닫힌 주기 신호를 주파수별 항의 무한 합으로 펼치는 것이고 변환은 일반 신호에서 각 주파수 성분의 크기를 뽑아 내는 것으로, 이 글의 데모처럼 경로가 닫혀 있고 점이 유한하면 두 관점이 이산 계수라는 같은 결과로 만납니다. 02가 급수의 장면이고 03·04가 변환의 장면에 가깝습니다.

더 읽을거리

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

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