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

css 3d 회전 장면 9종 — 마우스를 따라 도는 입체 씬 (CSS+캔버스)

css 3d 회전 장면은 마우스 위치를 각도로 바꿔 회전목마·지형·다리 같은 미니어처를 돌리는 연출입니다. Three.js 없이 캔버스 2D 위에서 좌표를 직접 계산해 원근 투영으로 그렸고, 실측에서 가장 넓게 움직인 05 토러스 격자가 누적면적 29.176퍼센트, 02 지형 씬이

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

아홉 개의 순서는 화려한 정도가 아니라 원근 계산이 쌓이는 순서입니다. 먼저 점 하나를 회전시켜 화면에 투영하는 가장 단순한 형태가 바탕이 되고(01·02), 그 위에 회전 행렬과 군집 규칙이 겹쳐 여러 점이 함께 움직입니다(03·04). 이어서 격자와 나선처럼 점이 곡면 위 규칙을 따르고(05·06), 마지막에 물리와 파동이 장면 전체의 흐름을 만듭니다(07~09). css 3d perspective 를 흉내 내는 계산이라는 점은 아홉 항목 모두 같습니다.

01관람차 씬

무엇이 보이나

놀이공원·이벤트 소개 랜딩 히어로에 쓰는, 바퀴살 16개가 원근으로 다가오는 회전 관람차 미니어처입니다.

관람차 씬 — 바퀴살 16개가 원근으로 다가오는 미니어처
새 창에서 열기

01 · 관람차 씬 — 바퀴살 16개가 원근으로 다가오는 미니어처 — 측정 누적면적 23.298% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotate3dsortrequestAnimationFrame

두 번 회전해야 입체가 된다

function wheelPoint(theta: number, radius: number): Pt3 {
  const x = Math.cos(theta) * radius;
  const y = Math.sin(theta) * radius;
  const ct = Math.cos(TILT), st = Math.sin(TILT);          // x축 기울기 — 바퀴를 뒤로 눕힌다
  const y1 = y * ct, z1 = y * st;
  const ca = Math.cos(YAW), sa = Math.sin(YAW);            // y축 틀기 — 원판이 화면을 향하지 않는다
  return { x: x * ca + z1 * sa, y: y1, z: -x * sa + z1 * ca };
}

function project(p: Pt3): Proj {                          // 원근 투영 — 이 한 줄이 전부다
  const size = sizeRef.current;
  const s = focalRef.current / (focalRef.current + p.z);
  return { sx: size.w * 0.5 + p.x * s, sy: size.h * 0.56 - p.y * s, s };
}

원 위의 점을 x축으로 한 번 눕히고 y축으로 한 번 더 틀어야 평평한 원판이 비로소 바퀴처럼 보입니다. 초점 거리를 z 값에 나누는 한 줄이 가까운 곤돌라를 크게 그리는 식입니다(실측: 누적면적 23.298퍼센트, 최대 프레임 변화 11.328퍼센트, 23/24 프레임 움직임).

02지형 높이맵 씬

무엇이 보이나

게임·지도 서비스 소개 페이지에 쓰는, 격자 점의 높이를 사인파로 계산해 마우스 각도로 도는 잔물결 구릉입니다.

지형 높이맵 씬 — 마우스로 도는 잔물결 구릉
새 창에서 열기

02 · 지형 높이맵 씬 — 마우스로 도는 잔물결 구릉 — 측정 누적면적 28.832% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

focalsinbeginPath

사인파 세 겹이 봉우리를 만든다

function height(i: number, j: number, t: number): number {
  const x = (i / (COLS - 1)) * TAU;      // 0..TAU
  const y = (j / (ROWS - 1)) * TAU;
  return Math.sin(x * F1 + t) * 0.62 + Math.sin(y * F2 + t * 0.8) * 0.38
       + Math.sin((x + y) * 0.9 + t * 1.3) * 0.18;
}

function corner(i: number, j: number, t: number): Corner {
  const size = sizeRef.current;
  const heading = headingRef.current, pitch = pitchRef.current, amp = ampRef.current;
  const wx = (i / (COLS - 1) - 0.5) * 430;
  const wz = (j / (ROWS - 1) - 0.5) * 340;
  const wy = height(i, j, t) * amp;
  const ch = Math.cos(heading), sh = Math.sin(heading);
  const x1 = wx * ch - wz * sh;
  const z1 = wx * sh + wz * ch;
  const a = ALT + pitch;
  const ca = Math.cos(a), sa = Math.sin(a);
  const y2 = wy * ca - z1 * sa;
  const z2 = wy * sa + z1 * ca;
  const s = FOCAL / (FOCAL + z2 + 300);
  return { sx: size.w * 0.5 + x1 * s, sy: size.h * 0.62 - y2 * s, s, h: wy };
}

격자 점마다 세 사인파를 더해 높이를 정하고, 그 높이를 카메라 고도각으로 한 번 더 굴려야 파도가 옆으로 흐르는 게 아니라 산등성이가 돌아눕는 것처럼 보입니다(실측: 누적면적 28.832퍼센트, 23/24 프레임 움직임).

03분자 모형 씬

무엇이 보이나

과학·교육 서비스 소개에 쓰는, 원자 15개가 회전 행렬로 돌며 결합을 맺는 분자 모형입니다.

분자 모형 씬 — 원자들이 모여 결합을 맺는 장면
새 창에서 열기

03 · 분자 모형 씬 — 원자들이 모여 결합을 맺는 장면 — 측정 누적면적 10.773% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

projectsortlineTo

회전 행렬은 두 줄로 충분하다

function rot(p: Pt3): Pt3 {
  const cy = Math.cos(angYRef.current), sy = Math.sin(angYRef.current);
  const x1 = p.x * cy + p.z * sy;
  const z1 = -p.x * sy + p.z * cy;
  const cx = Math.cos(angXRef.current), sx = Math.sin(angXRef.current);
  const y2 = p.y * cx - z1 * sx;
  const z2 = p.y * sx + z1 * cx;
  return { x: x1, y: y2, z: z2 };
}

function project(p: Pt3): Proj {
  const size = sizeRef.current;
  const s = FOCAL / (FOCAL + p.z + 150);      // 원근 투영 — 이 한 줄이 전부다(가까이 붙어 크게)
  return { sx: size.w * 0.5 + p.x * s, sy: size.h * 0.5 - p.y * s, s };
}

y축 회전 한 번, x축 회전 한 번을 이어 붙이면 임의의 방향으로 돌릴 수 있습니다. 원자마다 반지름을 ±5퍼센트 펄스로 흔들어 결합이 숨 쉬는 듯한 움직임을 냈습니다(실측: 누적면적 10.773퍼센트, 23/24 프레임 움직임).

04새떼 보이드 씬

무엇이 보이나

동물·환경 캠페인 페이지에 쓰는, 분리·정렬·결합 세 규칙으로 40마리가 무리를 유지하는 씬입니다.

새떼 보이드 씬 — 세 규칙으로 유지되는 무리 이동
새 창에서 열기

04 · 새떼 보이드 씬 — 세 규칙으로 유지되는 무리 이동 — 측정 누적면적 28.538% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

requestAnimationFramesqrtsort

세 규칙이 한 프레임 안에서 섞인다

if (cn > 0) {
  ax += (cxs / cn - b.x) * coh * 1.1; ay += (cys / cn - b.y) * coh * 1.1; az += (czs / cn - b.z) * coh * 1.1;  // 결합
  ax += (vxs / cn - b.vx) * 1.1; ay += (vys / cn - b.vy) * 1.1; az += (vzs / cn - b.vz) * 1.1;                  // 정렬
}
ax += sxs * 260; ay += sys * 260; az += szs * 260;                                                              // 분리
ax += (st.food.x - b.x) * FOOD_GAIN * coh; ay += (st.food.y - b.y) * FOOD_GAIN * coh; az += (st.food.z - b.z) * FOOD_GAIN * coh;
b.vx += ax * dt; b.vy += ay * dt; b.vz += az * dt;
const sp = Math.sqrt(b.vx * b.vx + b.vy * b.vy + b.vz * b.vz) || 1;
const cl = sp > SPD_MAX ? SPD_MAX / sp : (sp < SPD_MIN ? SPD_MIN / sp : 1);
b.vx *= cl; b.vy *= cl; b.vz *= cl;

이웃 새의 평균 위치·평균 속도·너무 가까운 새를 미는 힘을 매 프레임 더한 뒤 속도를 상하한으로 눌러야 무리가 한 덩어리로 퍼지지도 흩어지지도 않습니다(실측: 누적면적 28.538퍼센트, 최대 프레임 변화 9.444퍼센트).

05토러스 격자 씬

무엇이 보이나

제품·테크 브랜드 히어로에 쓰는, 도넛 격자점 위로 파동 위상이 흐르는 토러스 결정 씬입니다.

토러스 결정 씬 — 도넛 격자 위를 흐르는 빛
새 창에서 열기

05 · 토러스 결정 씬 — 도넛 격자 위를 흐르는 빛 — 측정 누적면적 29.176% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

cossinfillStyle

파동이 튜브 반지름 자체를 흔든다

function tubeR(u: number, v: number, tube: number, now: number): number {
  const ph = (u / TAU) * 2 + (v / TAU) * 3 + now / 1300;
  return tube + Math.sin(ph * TAU) * WAVE_GAIN;
}

function torusPoint(u: number, v: number, tube: number, now: number): P3 {
  const rr = tubeR(u, v, tube, now);
  const ring = 112 + Math.cos(v) * rr;
  return { x: ring * Math.cos(u), y: Math.sin(v) * rr, z: ring * Math.sin(u) };
}

튜브 반지름 자체를 시간에 따라 출렁이는 값으로 바꾸면 표면이 고정된 도넛이 아니라 빛이 지나가는 결정처럼 밝기가 옮겨 다닙니다(실측: 누적면적 29.176퍼센트, 최대 프레임 변화 25.398퍼센트 — 아홉 항목 중 가장 넓게 움직였습니다).

06나선 계단 씬

무엇이 보이나

건축·공간 소개 페이지에 쓰는, 계단참 26개를 축 각도와 높이로 쌓아 올려다보는 나선 계단 씬입니다.

나선 계단 씬 — 올라가며 도는 계단참
새 창에서 열기

06 · 나선 계단 씬 — 올라가며 도는 계단참 — 측정 누적면적 24.083% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotate3dsortarc

황금각으로 계단참을 흩는다

function treadCorners(i: number, rise: number): { inA: P3; inB: P3; outA: P3; outB: P3 } {
  const st = stateRef.current;
  const ang = i * GOLD + st.spin;
  const y = i * rise * RISE_K;
  const wide = 0.9 - i / STEPS * 0.42;    // 낮은 참일수록 시야를 넓게 차지하는 각폭
  const a0 = ang - wide, a1 = ang + wide;
  return {
    inA: { x: Math.cos(a0) * RIN, y: y, z: Math.sin(a0) * RIN },
    inB: { x: Math.cos(a1) * RIN, y: y, z: Math.sin(a1) * RIN },
    outA: { x: Math.cos(a0) * ROUT, y: y, z: Math.sin(a0) * ROUT },
    outB: { x: Math.cos(a1) * ROUT, y: y, z: Math.sin(a1) * ROUT }
  };
}

계단참마다 각도를 황금각만큼 어긋나게 쌓으면 층이 겹쳐 보이지 않습니다. 낮은 참일수록 각폭을 넓혀 올려다볼 때 원근이 자연스럽게 좁아지는 효과도 이 규칙에서 나옵니다(실측: 누적면적 24.083퍼센트, 23/24 프레임 움직임).

07롤러코스터 트랙 씬

무엇이 보이나

게임·체험형 캠페인에 쓰는, 트랙 곡선을 따라 빛구슬이 가속·감속하며 달리는 씬입니다.

롤러코스터 트랙 씬 — 굴곡 궤적을 달리는 빛구슬
새 창에서 열기

07 · 롤러코스터 트랙 씬 — 굴곡 궤적을 달리는 빛구슬 — 측정 누적면적 18.396% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

quadraticCurveToarcsort

기울기가 곧 가속도다

function step(dt: number): void {
  const st = stateRef.current;
  const track = trackRef.current, cum = cumRef.current;
  const p0 = track[st.segIdx], p1 = track[(st.segIdx + 1) % SAMPLES];
  const segLen = cum[st.segIdx + 1] - cum[st.segIdx];
  const slope = (p1.y - p0.y) / segLen;              // 내리막(−)에서 가속
  const g = gravValRef.current * 2;                  // 다이얼 % → px/s²
  st.beadV += -g * slope * dt;
  st.beadV *= 1 - 0.05 * dt;
  if (st.beadV < VMIN) st.beadV = VMIN;
  st.beadS += st.beadV * dt;
}

트랙 두 점의 높이 차를 구간 길이로 나눈 기울기에 중력값을 곱해 속도에 더하면, 오르막에서 느려지고 내리막에서 빨라지는 움직임이 애니메이션 없이 계산에서 저절로 나옵니다(실측: 누적면적 18.396퍼센트, 최대 프레임 변화 6.998퍼센트).

08뜬 섬 씬

무엇이 보이나

판타지·게임 랜딩에 쓰는, 섬 7개가 각자 다른 위상으로 파도 위에 떠 있는 씬입니다.

뜬 섬 씬 — 파도 위에 떠 있는 섬 여러 개
새 창에서 열기

08 · 뜬 섬 씬 — 파도 위에 떠 있는 섬 여러 개 — 측정 누적면적 8.134% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translate3dscalesin

섬마다 위상이 달라야 따로 떠 있다

function project(p: P3): Proj {
  const st = stateRef.current;
  const s = 320 / (320 + p.z + 40);   // FOCAL=320, BACK=40
  return { sx: st.W * 0.5 + p.x * s, sy: st.H * 0.52 - p.y * s, s: s };
}

function islandWorld(isl: { baseY: number; phase: number; speedK: number; ang: number; r: number }, now: number): P3 {
  const amp = ampValRef.current;
  const by = isl.baseY + Math.sin(now / 1000 * FLOAT_SPEED * isl.speedK + isl.phase) * amp;
  return { x: Math.cos(isl.ang) * isl.r, y: by, z: Math.sin(isl.ang) * isl.r };
}

극좌표로 배치한 섬마다 서로 다른 위상값을 사인 함수에 넣어야 같은 리듬으로 한꺼번에 오르내리지 않고 섬마다 제각각 떠 있는 것처럼 보입니다(실측: 누적면적 8.134퍼센트, 최대 프레임 변화 1.866퍼센트).

09아치 교량 씬

무엇이 보이나

인프라·도시 소개 페이지에 쓰는, 아치 세그먼트를 사인 곡선으로 계산해 마우스 각도로 훑어보는 다리 씬입니다.

아치 교량 씬 — 마우스 각도로 훑어보는 다리
새 창에서 열기

09 · 아치 교량 씬 — 마우스 각도로 훑어보는 다리 — 측정 누적면적 4.622% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotate3dsinlineTo

사인 하나가 다리 곡선 전부다

function archY(t: number, height: number): number { return Math.sin(t * Math.PI) * height; }
function archX(t: number): number { return (t - 0.5) * SPAN; }

function project(p: P3): Proj {
  const st = stateRef.current;
  const s = 300 / (300 + p.z + 90);   // FOCAL=300, BACK=90
  return { sx: st.W * 0.5 + p.x * s, sy: st.H * 0.62 - p.y * s, s: s };
}

경간의 진행률 t(0에서 1까지)를 사인 곡선 하나에 넣으면 양 끝에서 0, 가운데서 최대 높이가 되는 아치 형태가 그대로 나옵니다. 돌 블록 22개를 이 곡선 위에 z 순으로 쌓기만 하면 다리가 완성됩니다(실측: 누적면적 4.622퍼센트, 최대 프레임 변화 1.909퍼센트로 아홉 항목 중 가장 작게 움직였지만 애니메이션 0개로 정적 카드는 아닙니다).

어디서 깨지는가 — 함정

회전이 한 번이면 납작한 타원이다

가장 흔한 함정은 회전을 한 번만 시키는 것입니다. 01 관람차처럼 원판 하나를 x축으로만 눕히면 화면에서 여전히 납작한 타원으로 보이는데, 여기에 y축 회전을 더해 원판이 카메라를 정면으로 보지 않게 틀어야 비로소 css 3d 효과라 부를 만한 입체감이 생깁니다. 04 새떼처럼 여러 점이 같이 움직이는 씬에서는 순서 문제도 따라옵니다. z 값으로 정렬하지 않고 배열 순서 그대로 그리면 뒤에 있어야 할 새가 앞의 새를 덮어 버립니다.

z 정렬이 없으면 블록이 다리를 뚫는다

09 아치 다리는 처음에는 돌 블록을 z 정렬 없이 앞쪽 배열부터 그렸는데, 마우스로 각도를 돌리면 뒤쪽 블록이 가끔 앞쪽을 뚫고 튀어나왔습니다. 매 프레임 블록의 z 값을 다시 계산해 정렬한 뒤에야 화가 알고리즘대로 먼 것부터 칠해져 겹침이 사라졌습니다.

파동 이득이 크면 표면이 안으로 접힌다

05 토러스처럼 css 3d animation 을 흉내 내는 파동 값이 지나치게 커지면 튜브 반지름이 음수로 뒤집혀 표면이 안쪽으로 접히는 것도 실측 중 본 오류입니다. 완성된 아홉 장면의 소스와 React 이식판은 글 맨 아래의 암호 걸린 zip 에 들어 있습니다. 열쇠가 되는 비밀번호는 afcdc6hj 입니다.

접근성

모든 씬은 role="application"과 tabindex="0"을 가진 캔버스 위에서 동작하고, 포커스가 오면 outline 으로 표시됩니다. prefers-reduced-motion: reduce 가 걸리면 01·05·08·09 는 마지막으로 본 각도에서 한 프레임만 그리고 회전을 멈춥니다. 02 지형은 파도가 가장 잘 읽히는 고정 시각(0.6) 한 장으로 멈추고, 03 분자는 포인터를 움직인 그 순간의 각도로 고정됩니다. 06 계단과 07 트랙, 04 새떼도 같은 방식으로 요청이 오면 requestAnimationFrame 루프를 다시 돌리지 않고 정지 화면 한 장만 그립니다. 자세한 값은 WCAG 동작 축소 안내를 참고하세요.

FAQ

css 3d perspective 없이 이런 입체 장면을 만든 건가요?

네, CSS perspective 속성이 아니라 캔버스 2D 위에서 좌표를 직접 회전·투영하는 계산으로 만들었습니다. focal / (focal + z) 한 줄이 원근 축소를 대신하고, 그 값에 x·y 를 곱해 화면 좌표를 구합니다.

아홉 장면 모두 마우스로만 조작하나요?

네, 캔버스 위에서 포인터의 x·y 비율을 읽어 회전 속도나 카메라 기울기로 바꿉니다. 터치 화면에서는 손가락이 같은 좌표를 넘겨받아 똑같이 동작합니다.

코드를 그대로 복사해서 내 프로젝트에 붙여도 되나요?

네, 라이브러리나 CDN 의존 없이 순수 캔버스 계산만 쓰기 때문에 항목 코드블록을 그대로 옮기면 동작합니다. zip 안에는 index.html·style.scss 원본과 React 포트가 함께 들어 있습니다.

관련해서 캔버스 특수효과 9종도 같은 방식의 픽셀 계산을 다룹니다. DOM 트랜스폼으로 도는 마우스를 따라오는 CSS 3D 회전 장면, 구면 좌표를 다루는 3D 지구본 캔버스 장면도 함께 참고할 만합니다.

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

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