☰
CSS 3D 立体走马灯环形展厅(Carousel 3D):三角函数圆环布局与惯性旋转阻尼
2026/10/11 21:33:07 网站建设 项目流程

在 3D 数字艺术馆、英雄角色轮播选择界面、以及高端品牌的产品矩阵展示中,**三维环形走马灯(3D Cylindrical Carousel)**始终是最具空间仪式感的交互形态之一。多张卡片如同环形柱面上的多面体切片,围绕着中央虚拟轴心等距排布。当用户用鼠标水平轻拨,整个三维展厅如同一座精密的机械转盘缓缓旋转,前景卡片迎面放大、侧翼卡片带着强烈的景深视差向深空后撤,带来无与伦比的空间沉浸感。

然而,在很多前端团队尝试纯手工实现 3D 走马灯时,代码往往写得极其狼狈:

  • 很多开发者凭感觉随意拍脑袋写一个translateZ(300px),结果卡片不是在旋转时发生穿模重叠,就是在某些角度下产生巨大的镂空破绽;
  • 当卡片数量从 6 张动态增加到 10 张时,整个环形结构瞬间崩塌变形;
  • 拖拽手势松开后,转盘不知道该如何平滑“吸附(Snap)”到离正面最近的卡片,经常停在半截角度,破坏了视觉展示的庄严感。

真正的 3D 环形展厅,其底层是一套严格自洽的正多边形外接圆三角几何学。

本文将从严格的正多边形内切半径数学推导切入,结合现代 CSS 3D Transform 与生产级 TypeScript 惯性吸附控制器,手写一套兼具数学美感与丝滑触感的 3D 环形展厅系统。


几何机理:正 $N$ 边形内切圆半径推导

要让 $N$ 张卡片完美围成一个既不重叠、又无缝衔接的正多棱柱,核心在于精准求解旋转半径(Translate Z Distance)。

【俯视几何投影图】 ▲ Z (视线观察轴) │ Card 0 (正面) ┌──────────┐ / │ 宽 w │ \ / └──────────┘ \ / │ r \ / │ \ Card N-1 ─────────O (旋转轴心) ─── Card 1 \ / \ / \ / Card ...

数学推导

设走马灯总共有 $N$ 张卡片,每张卡片的 CSS 宽度为 $w$。

  1. **单张卡片所占的中心角(Central Angle)**为:

$$\theta = \frac{360^\circ}{N} = \frac{2\pi}{N}\text{ (弧度)}$$

  1. 观察以圆心 $O$ 与单张卡片构成的等腰三角形,底边长为 $w$。从圆心向卡片作垂直平分线,得到一个直角三角形:
    • 对边长度为 $\frac{w}{2}$;
    • 顶角的一半为 $\frac{\theta}{2} = \frac{\pi}{N}$;
    • 邻边长度即为卡片需要沿 Z 轴推开的内切物理半径 $r$。

根据正切三角函数定义:

$$\tan\left(\frac{\theta}{2}\right) = \frac{w / 2}{r} \implies r = \frac{w}{2 \cdot \tan\left(\frac{\pi}{N}\right)}$$

黄金几何结论

  • 如果 $N = 6$ 张卡片,卡片宽 $w = 260\text{px}$:
    $\theta = 60^\circ$,$\tan(30^\circ) = \frac{\sqrt{3}}{3} \approx 0.57735$;
    $r = \frac{130}{0.57735} \approx \mathbf{225.16\text{px}}$;
  • 无论卡片宽度是多少、卡片数量有多少,只要代入该公式,卡片便能在空间中形成绝对严格相切、无一丝缝隙的正多棱柱空间环!

CSS 3D 空间架构:单变量驱动的舞台装配

有了几何半径 $r$,空间布局变得极其清晰:

  • 卡片分布:每一张卡片只需在初始化时,先绕 Y 轴旋转对应的角度 $i \cdot \theta$,再沿着自身的局部 Z 轴向外推开距离 $r$;
  • 转盘旋转:整个 3D 舞台只需声明一个 CSS 自定义属性--carousel-angle,转动父容器即可,子卡片完全无需在动画中重复计算坐标!
/* carousel-3d.css */ .carousel-viewport { position: relative; width: 100vw; height: 500px; display: flex; align-items: center; justify-content: center; /* 摄像机距离:带来宏大的空间张力 */ perspective: 1200px; perspective-origin: 50% 45%; overflow: hidden; background-color: #080a10; cursor: grab; } .carousel-viewport:active { cursor: grabbing; } /* 核心 3D 转盘容器 */ .carousel-stage { position: relative; width: var(--card-w, 260px); height: var(--card-h, 360px); transform-style: preserve-3d; /* 核心:单变量驱动整个大转盘旋转 */ transform: translateZ(calc(var(--radius, 225px) * -1)) rotateY(var(--carousel-angle, 0deg)); will-change: transform; } /* 单个卡片面片 */ .carousel-card { position: absolute; inset: 0; border-radius: 16px; background: linear-gradient(145deg, #1e2433 0%, #111520 100%); border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 15px 35px -5px rgba(0, 0, 0, 0.6); transform-style: preserve-3d; /* 自动根据 CSS 变量计算几何变换 */ transform: rotateY(calc(var(--index) * var(--theta))) translateZ(var(--radius)); user-select: none; transition: border-color 0.3s ease, box-shadow 0.3s ease; } .carousel-card:hover { border-color: #38bdf8; box-shadow: 0 0 30px rgba(56, 189, 248, 0.3); } .card-inner { padding: 24px; color: #f1f5f9; display: flex; flex-direction: column; height: 100%; box-sizing: border-box; }

交互驱动:TypeScript 惯性阻尼与磁吸对齐(Snap to Nearest)

用户在松手后,转盘不仅要有顺滑的物理滑行,还必须在减速末端自动吸附到离当前视线正前方最近的卡片中心:

// Carousel3DEngine.ts export class Carousel3DEngine { private stage: HTMLElement; private cardCount: number; private cardWidth: number; private theta: number; private radius: number; private currentAngle = 0; private targetAngle = 0; private isDragging = false; private startX = 0; private startAngle = 0; private velocity = 0; private lastMoveTime = 0; private lastMoveX = 0; private rafId: number | null = null; constructor(stageEl: HTMLElement, cardCount: number = 6, cardWidth: number = 260) { this.stage = stageEl; this.cardCount = cardCount; this.cardWidth = cardWidth; // 1. 精确三角几何推导 this.theta = 360 / cardCount; const rad = (Math.PI / cardCount); this.radius = Math.round((cardWidth / 2) / Math.tan(rad)); this.applyLayoutVariables(); this.bindEvents(); this.startLoop(); } private applyLayoutVariables() { this.stage.style.setProperty('--card-w', `${this.cardWidth}px`); this.stage.style.setProperty('--theta', `${this.theta}deg`); this.stage.style.setProperty('--radius', `${this.radius}px`); // 为每个卡片挂载一维下标 const cards = Array.from(this.stage.querySelectorAll<HTMLElement>('.carousel-card')); cards.forEach((card, idx) => { card.style.setProperty('--index', String(idx)); }); } private bindEvents() { const vp = this.stage.parentElement!; vp.addEventListener('pointerdown', (e) => { this.isDragging = true; this.startX = e.clientX; this.startAngle = this.currentAngle; this.lastMoveX = e.clientX; this.lastMoveTime = performance.now(); this.velocity = 0; }); window.addEventListener('pointermove', (e) => { if (!this.isDragging) return; const now = performance.now(); const dt = Math.max(1, now - this.lastMoveTime); const dx = e.clientX - this.lastMoveX; // 跟踪瞬时角速度 (px -> deg 灵敏度换算) this.velocity = (dx / dt) * 12.0; this.lastMoveX = e.clientX; this.lastMoveTime = now; // 实时累加拖拽角 const totalDx = e.clientX - this.startX; this.currentAngle = this.startAngle + totalDx * 0.25; this.targetAngle = this.currentAngle; }); window.addEventListener('pointerup', () => { if (!this.isDragging) return; this.isDragging = false; // 结合初速度惯性规划目标角 const projectedAngle = this.currentAngle + this.velocity * 15.0; // 磁吸对齐:锁定到最近的单张卡片中心角 (整数倍的 theta) const nearestStep = Math.round(projectedAngle / this.theta); this.targetAngle = nearestStep * this.theta; }); } private startLoop() { const tick = () => { if (!this.isDragging) { // 二阶弹簧平滑插值贴合目标角 const delta = this.targetAngle - this.currentAngle; this.currentAngle += delta * 0.08; } // 注入 CSS 单变量 this.stage.style.setProperty('--carousel-angle', `${this.currentAngle.toFixed(2)}deg`); this.rafId = requestAnimationFrame(tick); }; this.rafId = requestAnimationFrame(tick); } }

性能优化:背面剔除(Backface Culling)与 GPU 遮挡剪裁

当走马灯包含 12 张以上的复杂卡片时,有一半的卡片在几何上处于背对观察者的后半圆柱面上。

如果继续对背面不可见的卡片进行渲染,会浪费宝贵的 GPU 片段光栅化带宽。
为.carousel-card加上一行核心样式:

.carousel-card { /* 开启硬件背面剔除:法线朝向屏幕深处时直接跳过光栅化 */ backface-visibility: hidden; }

开启此属性后,GPU 硬件图元阶段会自动执行法线点积判定 $\vec{N} \cdot \vec{V}$,瞬间削减 50% 的像素过度绘制(Overdraw),即使在低配设备上也能确保 3D 展厅丝滑如飞。

通过严格的正多边形三角函数推导,与现代 CSS 变量和弹性吸附算法的深层缝合,我们用不到 200 行代码,在二维浏览器中搭建起了一座恢宏大气、触感真实的立体环形数字展厅。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询