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

리사주 곡선 만들기 9종 — 파동 간섭으로 그리는 캔버스

리사주 곡선은 서로 다른 주파수의 두 사인파를 X·Y 축에 각각 걸었을 때 펜이 그리는 폐곡선으로, 오실로스코프 화면에서 위상차를 눈으로 확인하는 데 쓰이는 그림입니다. 그 원리를 Math.sin 과 캔버스만으로 아홉 장면에 펼쳤고, 전부 마이크 없이도 돌아갑니다.

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

순서는 난이도 순이 아니라 파동이 겹치는 방식이 점점 복잡해지는 순서입니다. 앞의 셋은 파 하나의 모양을 그리는 기본(01 두 축의 합성, 02 두 점파원의 합, 03 반사로 생기는 정재파), 가운데 셋은 겹침의 결과를 무늬로 보는 계열(04 마디선에 모래가 쌓이는 클라드니, 05 주파수 차가 만드는 맥놀이, 06 같은 수식을 극좌표로 돌린 로즈), 뒤의 셋은 이런 파동 무늬를 화면 곳곳에 옮겨 쓰는 응용입니다(07 격자 두 장이 만드는 무아레, 08 위상차 슬라이더, 09 마이크 소리를 얹는 멤브레인). 물결 파 간섭 시뮬레이션을 찾아 왔다면 02 를, 화면에 남을 장면을 고른다면 04 나 07 이 눈에 먼저 들어옵니다. 캔버스 픽셀을 통째로 굴리는 장면이 갖고 싶다면 HTML5 Canvas 애니메이션 9종 — 물방울·블랙홀 특수효과가 이웃이고, 입자로 흩어지는 쪽은 캔버스 파티클 효과 9종이, 소리를 시각화하는 축은 오디오 비주얼라이저 만들기 9종이 잇습니다.

01리사주 XY 오실로스코프

두 사인파를 X·Y 에 걸어 펜이 그리는 자취를 잔상과 함께 보여주는, 오실로스코프 화면을 그대로 옮긴 장면입니다.

리사주 XY 오실로스코프
새 창에서 열기

01 · 리사주 XY 오실로스코프 — 측정 누적면적 16.829% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

requestAnimationFrameMath.singlobalCompositeOperation

펜 좌표는 Math.sin 두 줄

var FX = 3, FY = 2;          // X·Y 주파수 비 — 3:2 로 두면 곡선이 닫히는 데 2 초가 걸린다
function point(th) {
  var a = Math.min(W, H) * 0.36;
  return {
    x: W * 0.5 + Math.sin(th * FX) * a,
    y: H * 0.5 + Math.sin(th * FY + phase) * a
  };
}

잔상은 destination-out 로 지우며 남긴다

ctx.globalCompositeOperation = 'destination-out';
ctx.fillStyle = 'rgba(0,0,0,' + TRAIL + ')';
ctx.fillRect(0, 0, W, H);
ctx.globalCompositeOperation = 'source-over';

X 는 sin(3θ), Y 는 sin(2θ+위상) 이라 3:2 비율의 폐곡선이 위상에 따라 기울어지며 살아 움직이고, MDN globalCompositeOperation 문서가 설명하는 destination-out 이 매 프레임 알파 0.10 씩 지워 잔상을 남깁니다.

02이중 점파원 간섭 필드

두 점에서 퍼지는 원형 파동이 만나 보강·상쇄되는 무늬를, 픽셀마다 거리를 재어 직접 계산해 그리는 물결 간섭 장면입니다.

이중 점파원 간섭 필드
새 창에서 열기

02 · 이중 점파원 간섭 필드 — 측정 누적면적 54.069% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createImageDataputImageDataMath.hypot

픽셀마다 Math.hypot 두 번

var d1 = Math.hypot(x - cx1, y - cy1);
var d2 = Math.hypot(x - cx2, y - cy2);
var a1 = Math.sin(k * d1 - t);
var a2 = Math.sin(k * d2 - t);
var v = (a1 + a2) * 0.25 + 0.5;           // 0..1

결과는 putImageData 로 한 번에

var i = (y * W + x) * 4;
buf[i]     = Math.round(C1[0] + (C2[0] - C1[0]) * v);
buf[i + 1] = Math.round(C1[1] + (C2[1] - C1[1]) * v);
buf[i + 2] = Math.round(C1[2] + (C2[2] - C1[2]) * v);
buf[i + 3] = 255;
ctx.putImageData(img, 0, 0);

거리 d 에서 파의 위상은 sin(d/λ·2π−t) 이고 두 파를 더한 진폭을 밝기로 눕히면, MDN Math.hypot 문서의 거리 함수 하나로 물리 교과서의 간섭무늬가 그대로 나옵니다. 누적면적 54.07%·23/23 프레임으로 아홉 중 두 번째로 넓게 살아 움직입니다.

03정상파 줄넘기 하모닉

고정된 양 끝의 줄이 1차·2차·3차 하모닉으로 차례로 진동하며 마디가 늘어나는, 기타 줄의 원리를 보여주는 장면입니다.

정상파 줄넘기 하모닉
새 창에서 열기

03 · 정상파 줄넘기 하모닉 — 측정 누적면적 10.665% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

requestAnimationFrameMath.sinlineTo

줄 모양은 sin(nπs)·cos(ωt)

for (var i = 0; i <= SEG; i++) {
  var s = i / SEG;
  var x = x0 + (x1 - x0) * s;
  var y = mid - Math.sin(Math.PI * n * s) * amp * Math.cos(t);
  if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}

마디는 절대 움직이지 않는 점

ctx.fillStyle = NODE;
for (var k = 1; k < n; k++) {
  var nx = x0 + (x1 - x0) * (k / n);
  ctx.beginPath();
  ctx.arc(nx, mid, 4, 0, Math.PI * 2);
  ctx.fill();
}

정재파는 공간 부분 sin(nπs) 와 시간 부분 cos(ωt) 의 곱이라 줄 전체가 같은 시계로 진동하고, sin(nπs)=0 인 지점 n−1 개는 진폭이 0 이라 흔들려도 제자리입니다.

04클라드니 모래무늬 패턴

진동판 위 모래가 마디선을 따라 쌓이는 클라드니 실험을, 판 전체 픽셀에 정상파 식을 대입해 재현한 장면입니다.

클라드니 모래무늬 패턴
새 창에서 열기

04 · 클라드니 모래무늬 패턴 — 측정 누적면적 32.851% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sincreateImageDataMath.random

마디선은 두 사인곱의 차이가 0 인 곳

function chladni(x, y, n, m) {
  return Math.sin(n * Math.PI * x) * Math.sin(m * Math.PI * y) -
         Math.sin(m * Math.PI * x) * Math.sin(n * Math.PI * y);
}

모래는 값이 0 근처일 때만 진하게

var v = Math.abs(chladni(nx, ny, n, m));
if (v < 0.05) {
  var jitter = Math.random() * 0.35;
  var t = Math.min(1, (0.05 - v) / 0.05 + jitter);
  buf[i]     = Math.round(PLATEC[0] + (SANDC[0] - PLATEC[0]) * t);
  buf[i + 1] = Math.round(PLATEC[1] + (SANDC[1] - PLATEC[1]) * t);
  buf[i + 2] = Math.round(PLATEC[2] + (SANDC[2] - PLATEC[2]) * t);
}

(n, m) 쌍이 (2,3)→(3,4)→(4,2)→(5,3) 으로 넘어갈 때 두 모드를 진행률로 섞어 부드럽게 이어지고, Math.random 의 흩뿌림이 실제 알갱이처럼 보이게 합니다. 강도 평균 106 의 진한 대비로 마디선이 또렷하게 드러납니다.

05맥놀이 진폭 엔벨로프

주파수가 1Hz 만 다른 두 파를 더해 우르르 커졌다 사라지는 맥놀이를, 합성파 위에 점선 포락선까지 그려 보여주는 장면입니다.

맥놀이 진폭 엔벨로프
새 창에서 열기

05 · 맥놀이 진폭 엔벨로프 — 측정 누적면적 42.926% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sinrequestAnimationFramestrokeStyle

합성파는 두 사인의 합

var F1 = 9, F2 = 10;          // 두 파동의 주파수(Hz) — 차이 1Hz 가 맥놀이 주기를 만든다
var WINDOW = 1.2;             // 화면에 보이는 시간창(초)
var SCROLL = 0.6;             // 파형이 흘러가는 속도(초/초)
ctx.strokeStyle = WAVE;
ctx.lineWidth = 2;
ctx.beginPath();
for (var x = 0; x <= W; x += 2) {
  var t = t0 + (x / W) * WINDOW;
  var v = Math.sin(2 * Math.PI * F1 * t) + Math.sin(2 * Math.PI * F2 * t);
  var y = mid - v * scale;
  if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();

포락선은 |cos(π·Δf·t)|·2

ctx.strokeStyle = ENV;
ctx.lineWidth = 1.6;
ctx.setLineDash([5, 4]);
ctx.beginPath();
for (var xi = 0; xi <= W; xi += 2) {
  var ti = t0 + (xi / W) * WINDOW;
  var env = Math.abs(Math.cos(Math.PI * (F1 - F2) * ti)) * 2;
  var ye = mid - env * scale;
  if (xi === 0) ctx.moveTo(xi, ye); else ctx.lineTo(xi, ye);
}
ctx.stroke();
ctx.setLineDash([]);

두 사인의 합은 cos(π·Δf·t) 를 포락선으로 갖는다는 삼각함수 항등식 그대로 1초 주기의 맥놀이가 나오고, 점선 포락선이 그 순간 최대 진폭을 위아래로 감쌉니다.

06극좌표 로즈 리사주

같은 사인·코사인 수식을 직교좌표 대신 극좌표에 넣어 회전하는 꽃잎 무늬가 나오는, 01 번의 극좌표 버전입니다.

극좌표 로즈 리사주
새 창에서 열기

06 · 극좌표 로즈 리사주 — 측정 누적면적 17.09% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.cosMath.sinrequestAnimationFrame

반지름은 cos(K·θ)

var K = 5 / 4;                // 꽃잎 개수를 정하는 주파수 비율 n/d
function petalPoint(th, spin, R) {
  var r = Math.cos(K * th) * R;
  var a = th + spin;
  return {
    x: W * 0.5 + Math.sin(a) * r,
    y: H * 0.5 + Math.cos(a) * r
  };
}

그리는 범위는 θ 0→8π

var N = 480;                 // K 가 분수라 한 바퀴를 다 그리려면 표본이 많이 필요하다
for (var i = 0; i <= N; i++) {
  var p = petalPoint(i / N * Math.PI * 8, spin, R);
  if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y);
}

K=5/4 처럼 분수면 곡선이 닫히는 데 8π 가 필요하고, spin 이 전체를 천천히 돌려 만다라형 로딩 화면에 바로 쓸 수 있는 무늬를 만듭니다.

07무아레 격자 간섭

줄무늬 두 장을 7도 어긋나게 겹쳐 훨씬 굵은 무아레 줄무늬가 흐르는, 간섭의 화면 인쇄 버전입니다.

무아레 격자 간섭
새 창에서 열기

07 · 무아레 격자 간섭 — 측정 누적면적 60.433% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createImageDataputImageDataMath.sin

격자 한 장은 회전축 사인 줄무늬

var ANGLE = 7 * Math.PI / 180;   // 두 층의 각도차 — 작을수록 무늬 간격이 넓어진다
var PITCH = 6;                    // 줄무늬 한 칸(px)
function stripe(x, y, angle, phase) {
  var rx = x * Math.cos(angle) + y * Math.sin(angle);
  var v = Math.sin(rx / PITCH * Math.PI * 2 + phase) * 0.5 + 0.5;
  return v;
}

겹침은 두 밝기의 곱

var a = stripe(x, y, 0, phase);
var b = stripe(x, y, ANGLE, -phase * 0.6);
var v = a * b;                                 // 두 층의 밝기를 곱해 겹침(무아레)을 만든다

줄무늬의 각도차가 작을수록 무아레 간격이 넓어지는 주기 관계가 그대로 나오고, 위상을 서로 반대 방향으로 밀어 굵은 줄이 물결처럼 흐릅니다. 누적면적 60.43% 로 아홉 중 가장 넓게 움직입니다.

08위상차 비교 패널

위상 δ 슬라이더를 밀면 같은 두 파가 직선→타원→원으로 바뀌는 과정을 세 칸으로 나눠 보여주는, 리사주 곡선 실험 도구입니다.

위상차 비교 패널
새 창에서 열기

08 · 위상차 비교 패널 — 측정 누적면적 27.153% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

::-webkit-slider-thumbMath.sinrequestAnimationFrame

X=sin(ωt), Y=sin(ωt+δ)

var N = 120;
for (var i = 0; i <= N; i++) {
  var th = i / N * Math.PI * 2;
  var px = cxr + Math.sin(th) * R;
  var py = mid - Math.sin(th + delta) * R;
  if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}

슬라이더를 잡으면 자동 루프를 넘겨준다

if (manual) {
  delta = slider.value / 100 * Math.PI * 2;
} else {
  var u = (sec % LOOP) / LOOP;
  delta = (u < 0.5 ? u * 2 : (1 - u) * 2) * Math.PI * 2;
  if (slider) slider.value = String(Math.round(delta / (Math.PI * 2) * 100));
}

δ=0 이면 대각선 직선, π/2 면 정원, π 면 반대 대각선이 되는 위상차의 정의를 input[type=range] 하나로 만질 수 있고, 손을 대는 순간부터 자동 왕복이 멈춥니다.

09원형 막 진동 모드 (마이크 입력)

원형 진동판이 모드를 바꿔 울리는 장면에 마이크 버튼을 얹어, 실제 소리의 진폭이 판을 움직이게 하는 장면입니다.

원형 막 진동 모드 (마이크 입력)
새 창에서 열기

09 · 원형 막 진동 모드 (마이크 입력) — 측정 누적면적 18.58% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

getUserMediaAnalyserNoderequestAnimationFrame

합성 신호는 시드 고정 난수로 시작한다

var SEED = 7;                // 합성 신호 시드 — 고정해야 24프레임 촬영이 매번 같은 무늬를 준다
function seeded(i) {
  var x = Math.sin(i * 127.1 + SEED * 311.7) * 43758.5453;
  return x - Math.floor(x);
}
function synthLevel(sec) {
  var u = (sec % LOOP) / LOOP * Math.PI * 2;
  return 0.38 + 0.30 * Math.sin(u) * Math.sin(u * 0.5 + 0.7) + 0.10 * Math.sin(u * 3.1);
}

마이크는 getUserMedia 거부 시 합성으로 돌아온다

navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
  var src = audioCtx.createMediaStreamSource(stream);
  analyser = audioCtx.createAnalyser();
  analyser.fftSize = 1024;
  audioBuf = new Uint8Array(analyser.fftSize);
  src.connect(analyser);         // 분석만 — 스피커로 재생하지 않는다(하울링 방지)
  micOn = true;
}).catch(function () {
  micOn = false;
  stateEl.textContent = '권한 거부 — 합성 신호로 재생합니다';
});

MDN getUserMedia 문서로 스트림을 얻어 AnalyserNode 의 RMS 를 읽고, 권한을 거부하면 안전하게 시드 고정 합성 신호로 되돌아갑니다.

아홉 장면은 어디서 깨지는가 — 함정

렌더러가 애니메이션 0개로 프레임을 못 찍는다

이 아홉 개는 전부 CSS @keyframes 를 하나도 쓰지 않았습니다. 데모 촬영 도구는 CSS 애니메이션 시계를 직접 놓고 프레임을 뽑는데, keyframes 가 0 개면 그 시계가 잡을 것이 없어 실시간 캔버스를 기다려야 합니다. 그래서 움직임 전부를 requestAnimationFrame 루프 안에 두고 23/23 프레임을 모두 촬영했습니다. CSS 애니메이션 없이 캔버스만 돌리는 장면을 검사 도구에 넣을 때는 이 구조를 꼭 지켜야 합니다.

Math.random 을 쓰면 측정이 매번 달라진다

04 번 모래 입자의 흩뿌림에 Math.random 을 그대로 쓰면 프레임마다 무늬가 달라져 재는 값이 흔들립니다. 모드 진행은 시간 t 로 흔들지 않고, 무늬에 매달 잔떨림만 시드 고정 난수로 물리고, 흩뿌림처럼 본래 무작위여야 하는 부분에만 Math.random 을 남겨 두었습니다.

getUserMedia 는 헤드리스에서 권한 대화상자로 막힌다

09 번을 자동 촬영하면 마이크 권한 대화상자가 뜨지도 않고 그냥 거부됩니다. 그래서 기본을 합성 신호로 두고 버튼을 누를 때만 getUserMedia 를 시도하는 구조로, 미리보기·측정은 합성으로 돌아가고 실제 페이지에서만 소리가 얹힙니다. 위 함정을 다 피해 다닌 아홉 개의 HTML·CSS·JS 원본은 글 아래 암호 zip 에 들어 있고, 여는 암호는 adp2w7yb 입니다.

접근성

prefers-reduced-motion 이 켜진 곳에서는 아홉 개 모두 루프를 돌리지 않고 각자 대표 한 장만 남깁니다. 01 은 위상 0.3 의 곡선과 잔상, 02 는 위상 0.25 의 간섭무늬, 03 은 1차 하모닉 최대 진폭, 04 는 (2,3) 모드의 마디선, 05 는 한 순간의 파형과 포락선, 06 은 회전 0.4 의 꽃잎, 07 은 위상 0.35 의 격자, 08 은 δ=π/2 의 원, 09 는 레벨 0.5 의 진동판 한 장입니다. 어떤 항목도 빈 캔버스로 끝나지 않습니다. 08 의 슬라이더와 09 의 마이크 버튼은 키보드로 도달할 수 있고 MDN prefers-reduced-motion 문서의 권장을 따릅니다.

FAQ

리사주 곡선 그리기를 처음 시작한다면 어디부터 보나요?

08 번 위상차 슬라이더부터 보세요. δ 값을 밀면서 직선이 타원이 되고 다시 원이 되는 과정을 손으로 확인하면, 01 의 3:2 곡선이 왜 저 모양으로 살아 움직이는지가 바로 이해됩니다.

음파 간섭 데모를 수업 자료로 써도 되나요?

02 번이 물리 교과서의 이중 점파원 간섭무늬와 같은 식으로 그려져 있어 스크린으로 띄우기에 가장 적합합니다. 파장 WAVE 값 하나를 바꾸면 무늬 간격이 달라지니, 실험 변인을 보여주기에도 좋습니다.

정재파와 진행파의 차이를 코드로 확인할 수 있나요?

03 번의 sin(nπs)·cos(ωt) 곱 구조가 그 정의 그 자체입니다. 마디 점이 움직이지 않는 것을 화면에서 확인하고, 02 번처럼 sin(k·d−t) 형태의 진행파와 비교하면 시간항의 위치가 어떻게 다른지 눈으로 대비해 볼 수 있습니다.

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

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