← 포트폴리오

3D 기하 모션 레퍼런스 · Pure CSS

🧊 3D Geometry회전 다면체 · 의존성 0

외부 라이브러리 없이 CSS transform-style:preserve-3d 하나로 만드는 회전 입체 도형. 정육면체부터 팔면체·기둥까지 5종을 라이브 컨트롤로 돌려보고, 그대로 복사해 쓸 수 있는 좌표·각도 유도 과정을 함께 싣습니다. 중간에 제네시스 스포츠카드 덱의 홀로포일·틸트 질감을 3D로 재현하고, 마지막에는 프리미엄 세단 전륜 더블위시본의 4절 링크 기구학을 실제로 풀어 그리는 인터랙티브 다이어그램을 싣습니다.

목차

  1. 원리 — perspective · preserve-3d · translateZ
  2. 회전 다면체 5종 (라이브 컨트롤)
  3. 회전축과 이징 — 같은 도형, 다른 인상
  4. 스포츠카드 질감 — 홀로포일 · 마우스 틸트
  5. 실전 응용 — 로딩 큐브 · 3D 캐러셀
  6. 더블위시본 서스펜션 지오메트리 (인터랙티브)
  7. 차량 실루엣 — G80 · GV80 · GV90
  8. 스탯 젬 — 육각 레이더의 3D 회전
  9. 성능 · 접근성 체크리스트

01 — 원리 속성 3개면 끝난다

CSS 3D는 속성 3개가 전부입니다. ① 부모에 perspective(원근 거리) ② 회전체에 transform-style:preserve-3d(자식을 3D 공간에 유지) ③ 각 면에 rotate + translateZ(방향 잡고 밖으로 밀기). 나머지는 도형별 좌표 계산일 뿐입니다.

① perspective 없음

원근이 없으면 회전해도 납작하게 뭉갠 사각형으로 보입니다.

② preserve-3d 없음

자식이 부모 평면에 눌려서 겹칩니다. 입체가 무너지는 대표 증상.

③ 둘 다 적용 ✓

perspective 600px + preserve-3d. 같은 마크업인데 입체가 섭니다.

기본 골격 — 모든 도형이 이 구조를 공유한다
/* ① 무대 — 원근 거리. 값이 작을수록 왜곡이 강하다 (600~1200px 권장) */
.stage{ perspective: 900px; }

/* ② 회전체 — 자식을 3D 공간에 유지. 이 한 줄이 빠지면 전부 납작해진다 */
.obj{
  transform-style: preserve-3d;
  animation: spinY 14s linear infinite;
}
@keyframes spinY{ to{ transform: rotateX(-14deg) rotateY(360deg) } }

/* ③ 면 — 방향(rotate) 잡고 중심에서 밖으로(translateZ) 민다. 순서가 중요 */
.f{
  position: absolute; left:50%; top:50%;
  transform: var(--t);        /* 면마다 --t 만 다르게 */
}
순서 주의: rotateY(90deg) translateZ(60px)translateZ(60px) rotateY(90deg)는 결과가 다릅니다. transform은 왼쪽부터 차례로 좌표계를 바꾸므로, 반드시 회전 → 이동 순서로 써야 "방향을 잡고 그 방향으로 민다"가 됩니다.

02 — 회전 다면체 5종 · 라이브 컨트롤

14s
정육면체 Cube6면 · 반변 60px
사각뿔 Pyramid4면+밑면 · 높이 90px
팔면체 Octahedron8면 · 사각뿔 2개 맞붙임
삼각기둥 Triangular Prism옆면 3 + 마구리 2
육각기둥 Hexagonal Prism옆면 6 + 마구리 2
와이어프레임면 배경만 투명 처리
정육면체 — 6면 · 가장 먼저 익힐 것
/* 한 변 120px → 중심에서 각 면까지 = 반변 60px = translateZ 값 */
.cube .f{ width:120px; height:120px; margin:-60px 0 0 -60px; }

앞  --t: rotateY(0deg)   translateZ(60px)
오른 --t: rotateY(90deg)  translateZ(60px)
뒤  --t: rotateY(180deg) translateZ(60px)
왼  --t: rotateY(270deg) translateZ(60px)
위  --t: rotateX(90deg)  translateZ(60px)
아래 --t: rotateX(-90deg) translateZ(60px)

/* margin 은 left/top:50% 로 잡은 기준점을 면의 중심으로 되돌리는 보정 */
사각뿔 — 기울기 각도를 유도하는 법
/* 밑변 120(반변 a=60), 높이 h=90 으로 정하면
     빗면 slant = √(h² + a²) = √(8100+3600) = 108.17px
     기울기 A   = asin(a / slant) = asin(0.5547) = 33.69°
   검산: slant × cos(A) = 108.17 × 0.8321 = 90.00 = h  ✓          */

.pyr .side{
  width:120px; height:108.17px; margin:-63.17px 0 0 -60px;
  transform-origin: 50% 100%;               /* 밑변을 축으로 눕힌다 */
  clip-path: polygon(50% 0, 100% 100%, 0 100%); /* 삼각형 */
}
옆면 ×4  --t: rotateY(0|90|180|270deg) translateZ(60px) rotateX(33.69deg)
밑면     --t: translateY(45px) rotateX(90deg)

/* 팔면체는 이 사각뿔을 그대로 두 번 쓴다 — 아래쪽 묶음에 rotateX(180deg) */
도형면 구성핵심 수치난이도
정육면체정사각형 6translateZ = 반변 (120px → 60px)★☆☆
사각뿔삼각형 4 + 밑면 1빗면 108.17 · 기울기 33.69°★★☆
팔면체삼각형 8사각뿔 2개 · 아래쪽 rotateX(180deg)★★☆
삼각기둥사각형 3 + 삼각형 2정삼각형 내접반지름 34.64 · 120°씩★★☆
육각기둥사각형 6 + 육각형 2내접반지름 51.96 · 60°씩★★☆
정이십면체삼각형 20CSS로는 비권장 — Canvas 투영이 낫다★★★
기둥류 공식: 정n각기둥의 옆면은 rotateY(k·360/n) translateZ(내접반지름) 반복입니다. 내접반지름 = 변길이 × 1/(2·tan(π/n)). 육각형(변 60)이면 51.96, 팔각형(변 60)이면 72.43.

03 — 회전축과 이징 같은 큐브, 다른 인상

도형이 같아도 어느 축으로 어떤 속도 곡선을 타느냐에 따라 성격이 완전히 달라집니다. 로고·로딩·배경 중 어디에 쓸지에 따라 골라 쓰세요.

linear — 기계적

속도가 일정. 배경 오브젝트·무한 로딩에 적합. 시선을 끌지 않습니다.

ease-in-out — 호흡

가속·감속이 붙어 살아있는 느낌. 히어로 로고·브랜드 심볼용.

back(오버슈트) — 장난기

끝에서 살짝 튕깁니다. 게임·카드 뒤집기 등 캐주얼한 맥락에만.

축 · 이징 — 상황별 프리셋
/* 축 — 무엇을 보여주고 싶은가 */
rotateY(360deg)                 /* 좌우 회전. 기본값. 면을 차례로 보여준다 */
rotateX(360deg)                 /* 앞뒤로 구르기. 상하 면이 강조된다 */
rotateZ(360deg)                 /* 화면 안에서 뱅글. 사실상 2D — 입체감 없음 */
rotateX(360deg) rotateY(720deg) /* 배수를 다르게 = 불규칙 텀블링 */

/* 이징 — 어떤 성격으로 보이고 싶은가 */
linear                          /* 배경·로딩. 존재감을 낮춘다 */
cubic-bezier(.65,0,.35,1)       /* 히어로 로고. 호흡감 */
cubic-bezier(.68,-.6,.32,1.6)   /* 카드 뒤집기. 끝에서 오버슈트 */

/* ⚠ 무한 회전에 오버슛 이징을 쓰면 매 바퀴 덜컥거린다.
   무한 루프는 linear, 1회성 트리거는 곡선 이징이 원칙. */

04 — 스포츠카드 질감 제네시스 덱 연계

제네시스 팀 카드 덱에서 쓴 홀로포일·네온 엣지는 정지 이미지였습니다. 여기에 3D 회전을 붙이면 실제 트레이딩 카드를 손에 쥔 감각이 됩니다. 왼쪽은 자동 회전(backface-visibility로 앞뒷면 전환), 오른쪽은 마우스를 올려보세요.

홀로포일 회전 카드

Y축 360° 회전 + 포일 그라디언트 스윕. 뒷면은 rotateY(180deg)로 겹쳐 두고 backface-visibility:hidden으로 서로를 가립니다.

SAMPLE
GENESIS TEST LAB 1
PWR 92SPD 87TEC 95

마우스 틸트 카드

커서 위치를 rotateX/rotateY로 매핑하고 같은 좌표로 radial-gradient 광택을 따라 붙입니다. 실제 카드의 각도별 반사를 흉내내는 방식.

마우스를
올려보세요
마우스 틸트 — JS 12줄
/* CSS — 각도와 광택 위치를 전부 커스텀 속성으로 뺀다 */
.tilt{
  transform: rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));
  transition: transform .12s ease-out;   /* 짧게. 길면 끈적인다 */
}
.tilt .sheen{
  background: radial-gradient(circle at var(--mx,50%) var(--my,50%), #7dd3fc 0%, transparent 55%);
  mix-blend-mode: color-dodge;             /* 광택의 핵심 */
}

// JS — 커서를 -1~1 로 정규화해 최대 기울기(MAX)만큼 곱한다
const MAX = 12;
box.addEventListener('pointermove', e => {
  const r = card.getBoundingClientRect();
  const px = (e.clientX - r.left) / r.width;
  const py = (e.clientY - r.top) / r.height;
  card.style.setProperty('--ry', (px - .5) * 2 * MAX + 'deg');
  card.style.setProperty('--rx', -(py - .5) * 2 * MAX + 'deg'); // 부호 반대
  card.style.setProperty('--mx', px * 100 + '%');
  card.style.setProperty('--my', py * 100 + '%');
});
box.addEventListener('pointerleave', () => { /* 0deg 로 복귀 */ });
rotateX 부호가 반대인 이유: 커서를 위로 올리면 카드 윗변이 뒤로 눕는 것이 자연스럽습니다. 화면 y축은 아래가 +이고 rotateX(+)는 윗변을 앞으로 당기므로, 그대로 쓰면 방향이 뒤집힙니다.

05 — 실전 응용 로딩 · 캐러셀

로딩 큐브 (텀블링)

일정 회전이 아니라 90°씩 끊어 구르는 키프레임. 스피너보다 대기 시간이 짧게 느껴집니다.

3D 캐러셀 (회전목마)

패널 8장을 rotateY(k·45deg) translateZ(R)로 원형 배치. 도형 만들기와 완전히 같은 공식입니다.

01
02
03
04
05
06
07
08
캐러셀 반지름 — 패널이 겹치지 않는 최소 거리
/* n장을 원형 배치할 때 필요한 반지름 R
     R ≥ (패널너비 / 2) / tan(180° / n)
   예) 패널 110px, 8장 → (55) / tan(22.5°) = 55 / 0.4142 = 132.8px
       → 여유를 줘서 150px 사용                                   */

.caro{ transform-style: preserve-3d; animation: spinY 18s linear infinite; }
.caro .p{
  transform: var(--t);
  backface-visibility: hidden;   /* ★ 없으면 뒷면 숫자가 거울상으로 비친다 */
}

01 --t: rotateY(0deg)   translateZ(150px)
02 --t: rotateY(45deg)  translateZ(150px)
…  45° 씩 증가 × 8장

/* 뒷면을 감추는 대신 "뒤에서도 글자를 바로 세우고" 싶다면,
   패널 안쪽에 역회전 래퍼를 하나 더 두고 rotateY(180deg) 를 상쇄시킨다 */

06 — 더블위시본 서스펜션 프리미엄 세단 전륜 지오메트리

지금까지가 보이는 기하였다면, 이것은 움직이는 기하입니다. 더블위시본은 길이가 다른 상·하 컨트롤암이 만드는 4절 링크(차체–UCA–너클–LCA)이고, 휠이 위아래로 움직일 때 캠버·트랙·롤센터가 전부 이 링크의 기구학으로 결정됩니다. 아래 다이어그램은 그림이 아니라 실제로 4절 링크를 풀어서 그립니다 — 슬라이더를 움직이면 원-원 교점을 다시 계산합니다.

0 mm ← 리바운드  |  범프 →
컨트롤암 · 너클 암 연장선 → 순간중심(IC) 접지점–IC 선 → 롤센터(RC) 스프링 · 타이로드 차량 중심선 · 지면
캠버 (deg)
KPI (deg)
롤센터 높이 (mm)
트랙 변화 (mm)
FVSA (mm)
캠버게인 (°/25mm)

캠버 곡선 (트래블 −80 ~ +80mm)

롤센터 이동 (트래블에 따른 높이)

왜 순간중심이 차량 안쪽에 있어야 하나: 휠은 매 순간 IC를 중심으로 회전합니다. IC가 휠보다 안쪽에 있어야 범프에서 휠 상단이 안으로 기울어 네거티브 캠버가 붙습니다. IC가 바깥쪽에 잡히면 부호가 뒤집혀 롤 시 외륜이 포지티브 캠버로 눕고 접지가 무너집니다 — 하드포인트를 잘못 잡았을 때 실제로 나오는 대표적 실패입니다.

조향축을 3D로 — 캐스터 · KPI

캠버·롤센터는 정면도만으로 풀리지만, 조향축(킹핀축)은 정면도의 KPI와 측면도의 캐스터가 합쳐진 3차원 축입니다. 평면도 두 장으로는 안 보이니 돌려서 봅니다.

━ 킹핀축 · KPI 11.1° 캐스터 6.5° 지면 · 타이어 245mm 폭

설계 파라미터 — 이 모델 vs 통상값

위 다이어그램의 하드포인트에서 계산된 값과 프리미엄 세단의 통상 범위 비교. 캐스터·스크럽 반경은 정면도 모델 밖이라 대표값입니다.

파라미터이 모델통상
정적 캠버−0.50°−0.3 ~ −1.2°
캠버 게인−0.41°/25mm−0.3 ~ −0.7
KPI11.1°8 ~ 15°
롤센터 높이50.0mm30 ~ 80mm
FVSA3591mm2000 ~ 4500mm
LCA / UCA425 / 250mmUCA가 짧아야 함
캐스터*6.5°3 ~ 8°
스크럽 반경*+8mm−5 ~ +15mm
4절 링크 풀이 — 이 다이어그램이 실제로 돌리는 계산
/* 하드포인트 (정면도, mm). 원점 = 지면 × 차량중심선, +x 외측, +y 상향 */
Li = [330, 165]   // 로어암 내측 피벗 (차체)
Lo = [755, 157]   // 로어암 외측 볼조인트
Ui = [420, 573]   // 어퍼암 내측 피벗 (차체)
Uo = [668, 600]   // 어퍼암 외측 볼조인트
WC = [800, 340]   // 휠센터,  타이어 반경 R = 340

/* 1) LCA 각도 θ 를 입력으로 두면 하부 볼조인트는 원 위에 있다 */
Lo(θ) = Li + Ll·[cos θ, sin θ]

/* 2) 상부 볼조인트 = 두 원의 교점
      · Ui 중심 반지름 Lu (어퍼암 길이)
      · Lo 중심 반지름 DK (너클 길이 — 두 볼조인트 간 거리는 불변)   */
d  = |Lo − Ui|
a  = (Lu² − DK² + d²) / (2d)
h  = √(Lu² − a²)                  // h² < 0 이면 링크가 성립하지 않는 각도
P  = Ui + a·(Lo−Ui)/d
Uo = P ± h·perp((Lo−Ui)/d)        // 두 해 중 직전 자세에 가까운 쪽을 택한다

/* 3) 너클이 회전한 만큼이 곧 캠버 변화 (강체) */
tilt = atan2(Uo.x − Lo.x, Uo.y − Lo.y)
camber = camber₀ + (tilt − tilt₀)

/* 4) 순간중심 IC = 두 암 연장선의 교점,
      롤센터 RC = 접지점 CP 와 IC 를 이은 선이 차량중심선(x=0)과 만나는 높이 */
IC = line(Li,Lo) ∩ line(Ui,Uo)
RC = CP.y + (0 − CP.x)·(IC.y − CP.y)/(IC.x − CP.x)

/* 5) 휠 트래블 → θ 는 역산이 필요하다. θ 를 훑어 (θ, 휠센터높이) 표를
      만들어 두고 선형보간으로 뒤집는 편이 뉴턴법보다 안정적이다. */

07 — 차량 실루엣 선으로만 그린 G80 · GV80 · GV90

면도 그림자도 없이 선만으로 차를 세웁니다. 세 대를 같은 축척(9.5mm/unit)과 같은 지면선에 올려 비례 차이가 그대로 드러나게 했고, 세 차종을 관통하는 시그니처 두 줄(Two Lines)을 같은 굵기로 강조했습니다. 선은 stroke-dashoffset으로 앞에서 뒤로 그려집니다.

스크롤로 화면에 들어오면 자동 재생됩니다

G80

Sedan
전장 5,005 · 전고 1,465 · 휠베이스 3,010 mm  /  길이÷높이 3.42 — 롱후드 + 패스트백

낮은 지붕(y=61)과 뒤로 길게 흐르는 C필러가 만드는 패스트백. 트렁크 리드가 짧아 옆에서 보면 쿠페에 가깝습니다.

GV80

SUV
전장 4,945 · 전고 1,715 · 휠베이스 2,955 mm  /  길이÷높이 2.88 — 직립 그린하우스

G80보다 250mm 높고 60mm 짧습니다. 지붕이 뒤까지 평평하게 이어지고 D필러가 거의 수직으로 떨어져 적재 공간을 만듭니다.

GV90

Flagship EV SUV
전장 ≈5,340 · 전고 ≈1,790 · 휠베이스 ≈3,200 mm  /  길이÷높이 2.98 — 모노리식 · 코치도어

가장 박스에 가까운 비례. 전기차 전용 플랫폼이라 후드가 짧고 휠베이스가 길어 도어 구간이 넓습니다. 가운데 도어 컷이 B필러 없는 코치도어로 마주 열리는 구간.

세 대를 관통하는 한 가지: 앞뒤에 그은 두 줄입니다. 세단이든 SUV든, 내연이든 전기든 실루엣은 완전히 다른데 램프의 두 줄만은 같은 높이·같은 굵기로 반복됩니다. 브랜드 시그니처를 형태가 아니라 선의 규칙으로 잡으면 차급이 달라져도 살아남는다는 예시입니다.
정확도에 대해. 위 실루엣은 공개된 전장·전고·휠베이스 수치와 비례를 기준으로 손으로 그린 선 해석이지, 실측 도면이나 공식 디자인 자산을 옮긴 것이 아닙니다. 엠블럼·워드마크는 쓰지 않았습니다. GV90은 양산 이전이라 수치를 로 표기했고, 공개된 콘셉트 방향(모노리식 볼륨·코치도어)만 반영했습니다. 정밀 비교가 필요하면 실제 도면 좌표로 path를 교체하세요 — 구조는 그대로 씁니다.
선이 그려지는 모션 — 경로 길이를 재서 dash 로 감췄다가 푼다
/* CSS — 경로 길이만큼 점선을 만들고 그만큼 밀어두면 "안 그려진" 상태가 된다 */
[data-draw]{
  fill: none;
  stroke-dasharray:  var(--len);   /* 점선 1칸 = 경로 전체 길이 */
  stroke-dashoffset: var(--len);   /* 그만큼 밀어 완전히 숨긴다 */
}
.go [data-draw]{ animation: silDraw 1.5s cubic-bezier(.3,.8,.4,1) forwards;
                 animation-delay: var(--d); }
@keyframes silDraw{ to{ stroke-dashoffset: 0 } }   /* 0 이 되면 다 그려진다 */

// JS — 길이는 브라우저가 재준다. 하드코딩하면 경로 수정 때마다 깨진다
svg.querySelectorAll('[data-draw]').forEach((el,i) => {
  el.style.setProperty('--len', el.getTotalLength());
  el.style.setProperty('--d', (i * 0.07) + 's');  // 스태거
});

// 화면에 들어올 때 재생 — 스크롤 위치와 무관하게 처음부터 보이게 한다
new IntersectionObserver((es,ob) => es.forEach(e => {
  if(e.isIntersecting){ e.target.classList.add('go'); ob.unobserve(e.target); }
}), {threshold:.25}).observe(svg);

/* ⚠ getTotalLength() 는 <path>·<circle>·<line> 등 기하 요소에만 있다.
   <rect> 는 지원이 갈리니 path 로 바꿔 두는 편이 안전하다. */

08 — 스탯 젬 카드의 육각 레이더를 3D로 세운다

제네시스 팀 카드 덱 CARD 4의 육각 레이더는 평면입니다. 6개 축을 적도 정점으로 두고 위아래에 꼭짓점을 세우면 그대로 육각 쌍뿔(hexagonal bipyramid)이 되고, 스탯 값이 곧 정점의 반지름이 됩니다 — 숫자가 형상이 되는 셈입니다. 값을 바꾸면 젬의 실루엣이 즉시 달라집니다.

여기서만 Canvas를 씁니다. 면이 12개인데다 전부 불규칙 삼각형이라 CSS 3D로는 면마다 clip-path와 변환을 따로 계산해야 합니다. 02절 표에서 예고한 그 경계 — 면이 10개를 넘고 형상이 데이터로 바뀌면 Canvas 투영이 짧다는 것의 실제 사례입니다.
CARD 04 · VIRTUAL DEV

6축 스탯 — 값이 곧 반지름

95
93
96
94
91
92

표현

균등한 값은 대칭 젬, 한쪽으로 쏠린 값은 찌그러진 젬이 됩니다. 레이더 차트에서는 눈에 덜 띄던 편중이 입체에서는 실루엣으로 드러납니다.

스탯 → 3D 정점 · 깊이 정렬 · 원근 투영
/* 1) 6축을 적도에 배치. 각도는 60°씩, 반지름은 스탯 값에 비례 */
for(let k=0; k<6; k++){
  const a = k*Math.PI/3, r = vals[k]/100;
  verts.push([Math.cos(a)*r, 0, Math.sin(a)*r]);   // y=0 평면 = 레이더 원본
}
const H = avg(vals)/100 * 1.15;                 // 평균값이 젬의 높이
verts.push([0, H, 0], [0, -H, 0]);              // 위·아래 꼭짓점 → 면 12개

/* 2) 회전 + 원근 투영. z 가 클수록(가까울수록) 크게 그린다 */
function project(p, ry, rx){
  const x =  p[0]*cos(ry) + p[2]*sin(ry);
  const z = -p[0]*sin(ry) + p[2]*cos(ry);
  const y2 = p[1]*cos(rx) - z*sin(rx);
  const z2 = p[1]*sin(rx) + z*cos(rx);
  const s = D/(D - z2);                        // D = 시점 거리
  return {x: cx + x*s*S, y: cy - y2*s*S, z: z2};
}

/* 3) 반투명 면은 반드시 깊이 정렬 — 안 하면 뒷면이 앞면을 덮는다 */
faces
  .map(f => ({f, z: avgZ(f)}))
  .sort((a,b) => a.z - b.z)                     // 먼 것부터 그린다 (painter's algorithm)
  .forEach(drawFace);

/* ⚠ Canvas 는 z-버퍼가 없다. 정렬을 빼먹으면 도형이 뒤집혀 보인다.
   면이 서로 관통하는 형상이라면 정렬만으로는 부족하니 그때는 WebGL 로 간다. */

09 — 성능 · 접근성 배포 전 확인

항목어떻게
reduced-motion전정 장애 사용자에게 회전은 어지럼증을 유발@media (prefers-reduced-motion:reduce)에서 animation:none + 보기 좋은 정지 각도 고정 (이 페이지 적용됨)
GPU 합성transform/opacity만 60fps로 합성됨. width·top 애니는 매 프레임 레이아웃 재계산회전은 transform만 쓴다. 이미 지킴
will-change레이어를 미리 승격해 첫 프레임 버벅임 제거 — 단 남발하면 메모리 폭증화면에 상시 보이는 1~2개에만. 목록 전체엔 걸지 않는다
backface-visibility기본값이 visible이라 뒷면 글자가 거울상으로 비침카드 앞뒷면에는 hidden. 반투명 다면체는 visible이 정답(속이 비쳐야 입체로 읽힘)
perspective 값너무 작으면(<400px) 어안렌즈처럼 왜곡도형 크기의 5~8배를 기준으로. 120px 큐브면 600~1000px
z-fighting면이 정확히 겹치면 깜빡임같은 평면에 두 면을 두지 말고 translateZ를 0.1px 어긋내기
모바일 발열반투명 면이 많으면 오버드로 부담작은 화면에서는 면 수를 줄이거나 animation-duration을 늘린다
언제 CSS를 쓰고 언제 쓰지 말아야 하나. 면이 10개 이하인 다면체·카드·캐러셀은 CSS가 압도적으로 간단합니다(의존성 0, 복붙 가능). 정이십면체처럼 면이 20개를 넘거나, 조명·그림자·재질 반사가 필요하면 Canvas 투영이나 three.js로 넘어가는 편이 코드가 짧아집니다.