CSS 3D动画 旋转木马示例(带弧度支持手动拖动)

一、示例图

二、源代码

c 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>CSS3 3D Carousel</title>
    <style>
        :root {
            --scene-cyan: #36f6ff;
            --scene-pink: #ff4d9d;
            --scene-red: #ff3a2d;
            --scene-orange: #ff7a1a;
            --scene-deep: #08111f;
        }

        * {
            box-sizing: border-box;
        }

        body {
            margin: 0;
            min-height: 100vh;
            display: grid;
            place-items: center;
            overflow: hidden;
            perspective: 5000px;
            background:
                radial-gradient(circle at 50% 24%, #ffffff 0%, #f7f9fd 38%, #e8eef6 72%, #d8e1ec 100%);
            font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
        }

        .scene {
            position: relative;
            width: min(900px, 100vw);
            height: min(720px, 100vh);
            isolation: isolate;
        }

        .scene::before {
            content: "";
            position: absolute;
            inset: 16% 18%;
            border-radius: 50%;
            background: radial-gradient(circle, rgba(54, 246, 255, 0.18), rgba(54, 246, 255, 0) 64%);
            filter: blur(48px);
            opacity: 0.8;
            pointer-events: none;
            z-index: 0;
        }

        .cyber-disk {
            position: absolute;
            left: 50%;
            top: 57%;
            width: min(573px, 82vw);
            aspect-ratio: 1;
            transform: translate(-50%, -50%) rotateX(39deg);
            border-radius: 50%;
            border: 1px solid rgba(115, 247, 255, 0.24);
            background:
                radial-gradient(circle at center,
                    rgba(8, 17, 31, 0) 0 18%,
                    rgba(54, 246, 255, 0.22) 18% 19%,
                    rgba(8, 17, 31, 0) 19% 31%,
                    rgba(255, 77, 157, 0.16) 31% 32%,
                    rgba(8, 17, 31, 0) 32% 45%,
                    rgba(54, 246, 255, 0.2) 45% 46%,
                    rgba(8, 17, 31, 0) 46% 58%,
                    rgba(54, 246, 255, 0.14) 58% 59%,
                    rgba(8, 17, 31, 0) 59% 100%),
                repeating-conic-gradient(from 0deg,
                    rgba(54, 246, 255, 0.18) 0deg 5deg,
                    rgba(255, 77, 157, 0) 5deg 16deg),
                radial-gradient(circle at center,
                    rgba(9, 18, 36, 0.92) 0%,
                    rgba(9, 18, 36, 0.82) 48%,
                    rgba(9, 18, 36, 0.24) 70%,
                    rgba(9, 18, 36, 0) 100%);
            box-shadow:
                0 34px 84px rgba(4, 11, 33, 0.34),
                inset 0 0 46px rgba(54, 246, 255, 0.15),
                0 0 26px rgba(54, 246, 255, 0.12);
            pointer-events: none;
            z-index: 1;
        }

        .cyber-disk::before,
        .cyber-disk::after {
            content: "";
            position: absolute;
            border-radius: 50%;
            inset: 0;
        }

        .cyber-disk::before {
            inset: 13%;
            border: 1px solid rgba(54, 246, 255, 0.34);
            box-shadow:
                0 0 20px rgba(54, 246, 255, 0.2),
                inset 0 0 20px rgba(54, 246, 255, 0.16);
        }

        .cyber-disk::after {
            inset: 28%;
            border: 1px solid rgba(255, 77, 157, 0.32);
            box-shadow:
                0 0 16px rgba(255, 77, 157, 0.16),
                inset 0 0 18px rgba(255, 77, 157, 0.16);
        }

        .center-core {
            position: absolute;
            left: 50%;
            top: 50%;
            width: min(290px, 42vw);
            height: min(186px, 27vw);
            transform: translate(-50%, -50%);
            clip-path: polygon(14% 16%, 86% 8%, 84% 92%, 12% 84%);
            background:
                linear-gradient(135deg,
                    #ff1f1f 0%,
                    var(--scene-red) 42%,
                    #ff5a1f 74%,
                    var(--scene-orange) 100%);
            box-shadow:
                inset 0 0 0 1px rgba(255, 255, 255, 0.18),
                inset 0 0 36px rgba(255, 255, 255, 0.08),
                0 16px 44px rgba(255, 61, 43, 0.32),
                0 0 42px rgba(255, 57, 45, 0.24);
            pointer-events: none;
            z-index: 2;
        }

        .center-core::before,
        .center-core::after {
            content: "";
            position: absolute;
            inset: 0;
            clip-path: inherit;
        }

        .center-core::before {
            inset: 10%;
            background:
                linear-gradient(90deg,
                    transparent 0 14%,
                    rgba(255, 255, 255, 0.16) 14% 15%,
                    transparent 15% 34%,
                    rgba(255, 255, 255, 0.16) 34% 35%,
                    transparent 35% 62%,
                    rgba(255, 255, 255, 0.14) 62% 63%,
                    transparent 63% 100%),
                linear-gradient(180deg,
                    rgba(255, 255, 255, 0.12),
                    rgba(255, 255, 255, 0));
            opacity: 0.7;
        }

        .center-core::after {
            inset: -14%;
            background: radial-gradient(circle, rgba(255, 96, 70, 0.42), rgba(255, 96, 70, 0) 66%);
            filter: blur(18px);
            z-index: -1;
        }

        .carousel-shell {
            position: absolute;
            inset: 0;
            display: grid;
            place-items: center;
            z-index: 3;
        }

        .carousel {
            position: relative;
            width: 300px;
            height: 200px;
            margin: 0;
            transform-style: preserve-3d;
            transform: rotateX(-45deg);
            cursor: grab;
            z-index: 3;
        }

        .card {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            font-size: 60px;
            color: transparent;
            transform-style: preserve-3d;
            user-select: none;
            --face-spin: 0deg;
            --image-spin: 0deg;
        }

        .card-face {
            position: absolute;
            inset: 0;
            display: block;
            -webkit-clip-path: polygon(0% 92%, 8% 84%, 20% 76%, 34% 69%, 50% 64%, 66% 69%, 80% 76%, 92% 84%, 100% 92%, 100% 20%, 92% 12%, 80% 6%, 66% 2%, 50% 0%, 34% 2%, 20% 6%, 8% 12%, 0% 20%);
            clip-path: polygon(0% 92%, 8% 84%, 20% 76%, 34% 69%, 50% 64%, 66% 69%, 80% 76%, 92% 84%, 100% 92%, 100% 20%, 92% 12%, 80% 6%, 66% 2%, 50% 0%, 34% 2%, 20% 6%, 8% 12%, 0% 20%);
            backface-visibility: hidden;
            -webkit-backface-visibility: hidden;
            transition: transform 180ms ease-out;
            overflow: hidden;
            z-index: 0;
        }

        .card-face--front {
            transform: rotateZ(var(--face-spin)) translateZ(0.5px);
        }

        .card-face--back {
            transform: rotateY(180deg) rotateZ(var(--face-spin)) translateZ(0.5px);
        }

        .card-image {
            position: absolute;
            inset: 0;
            background-image: var(--card-bg);
            background-size: cover;
            background-position: center;
            transform: rotateZ(var(--image-spin));
            transform-origin: center;
        }

        .card-label {
            position: absolute;
            inset: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #fff;
            font-size: 60px;
            font-weight: 700;
            line-height: 1;
            text-shadow:
                0 4px 12px rgba(0, 0, 0, 0.35),
                0 0 18px rgba(255, 255, 255, 0.18);
            backface-visibility: hidden;
            -webkit-backface-visibility: hidden;
            pointer-events: none;
            z-index: 1;
        }

        .card-label--front {
            transform: translateZ(2px);
        }

        .card-label--back {
            transform: rotateY(180deg) translateZ(2px);
        }

        .card:nth-child(1) {
            transform: rotateY(0deg) translateZ(300px);
            --card-bg: url("https://picsum.photos/seed/hoyu-card-1/600/400");
        }

        .card:nth-child(2) {
            transform: rotateY(90deg) translateZ(300px);
            --card-bg: url("https://picsum.photos/seed/hoyu-card-2/600/400");
        }

        .card:nth-child(3) {
            transform: rotateY(180deg) translateZ(300px);
            --card-bg: url("https://picsum.photos/seed/hoyu-card-3/600/400");
        }

        .card:nth-child(4) {
            transform: rotateY(270deg) translateZ(300px);
            --card-bg: url("https://picsum.photos/seed/hoyu-card-4/600/400");
        }

        @media (max-width: 820px) {
            .scene {
                width: 100vw;
                height: 100vh;
            }

            .cyber-disk {
                top: 61%;
                width: min(560px, 88vw);
            }

            .center-core {
                width: min(250px, 52vw);
                height: min(160px, 34vw);
            }
        }
    </style>
</head>

<body>
    <div class="scene">
        <div class="cyber-disk"></div>
        <div class="center-core"></div>
        <div class="carousel-shell">
            <section class="carousel">
                <div class="card" data-label="1">
                    <span class="card-face card-face--front"><span class="card-image"></span></span>
                    <span class="card-face card-face--back"><span class="card-image"></span></span>
                    <span class="card-label card-label--front">1</span>
                    <span class="card-label card-label--back">1</span>
                </div>
                <div class="card" data-label="2">
                    <span class="card-face card-face--front"><span class="card-image"></span></span>
                    <span class="card-face card-face--back"><span class="card-image"></span></span>
                    <span class="card-label card-label--front">2</span>
                    <span class="card-label card-label--back">2</span>
                </div>
                <div class="card" data-label="3">
                    <span class="card-face card-face--front"><span class="card-image"></span></span>
                    <span class="card-face card-face--back"><span class="card-image"></span></span>
                    <span class="card-label card-label--front">3</span>
                    <span class="card-label card-label--back">3</span>
                </div>
                <div class="card" data-label="4">
                    <span class="card-face card-face--front"><span class="card-image"></span></span>
                    <span class="card-face card-face--back"><span class="card-image"></span></span>
                    <span class="card-label card-label--front">4</span>
                    <span class="card-label card-label--back">4</span>
                </div>
            </section>
        </div>
    </div>

    <script>
        const carousel = document.querySelector(".carousel");
        const cards = Array.from(carousel.children);
        const baseAngles = [0, 90, 180, 270];
        const tiltX = -45;
        let rotateY = 0;
        let dragging = false;
        let startX = 0;
        let startRotateY = 0;

        function normalizeAngle(angle) {
            return ((angle % 360) + 360) % 360;
        }

        function updateCardFaceDirection() {
            cards.forEach((card, index) => {
                const currentAngle = normalizeAngle(baseAngles[index] + rotateY);
                const faceSpin = currentAngle < 90 || currentAngle >= 270 ? 180 : 0;
                card.style.setProperty("--face-spin", `${faceSpin}deg`);
                card.style.setProperty("--image-spin", `${-faceSpin}deg`);
            });
        }

        function applyTransform() {
            carousel.style.transform = `rotateX(${tiltX}deg) rotateY(${rotateY}deg)`;
            updateCardFaceDirection();
        }

        carousel.addEventListener("pointerdown", (e) => {
            dragging = true;
            startX = e.clientX;
            startRotateY = rotateY;
            carousel.style.cursor = "grabbing";
            carousel.setPointerCapture(e.pointerId);
        });

        carousel.addEventListener("pointermove", (e) => {
            if (!dragging) return;
            const deltaX = e.clientX - startX;
            rotateY = startRotateY + deltaX * 0.35;
            applyTransform();
        });

        function endDrag(e) {
            if (!dragging) return;
            dragging = false;
            carousel.style.cursor = "grab";
            if (e && e.pointerId !== undefined) {
                carousel.releasePointerCapture(e.pointerId);
            }
        }

        carousel.addEventListener("pointerup", endDrag);
        carousel.addEventListener("pointercancel", endDrag);
        carousel.addEventListener("pointerleave", endDrag);

        applyTransform();
    </script>
</body>

</html>
相关推荐
Armouy2 小时前
Electron:核心概念、性能优化与兼容问题
前端·javascript·electron
F2E_Zhangmo2 小时前
react native如何发送蓝牙命令
javascript·react native·react.js
博主花神2 小时前
【TypeScript】梳理
javascript·ubuntu·typescript
淡笑沐白2 小时前
ECharts入门指南:数据可视化实战
前端·javascript·echarts
非科班Java出身GISer2 小时前
ArcGIS JS 基础教程(1):地图初始化(含AMD/ESM两种引入方式)
javascript·arcgis·arcgis js·arcgis js 初始化·arcgis js 地图初始化
天宝耐特2 小时前
L2pro+P1搭配LCC-3DGS,实现施工过程“可测量、可漫游、可追溯”的3D永久存档
3d·三维数字化·数字化存档·手持扫描仪·灵光l2pro·施工数字化·p1空间相机
前端摸鱼匠3 小时前
Vue 3 的defineProps编译器宏:详解<script setup>中defineProps的使用
前端·javascript·vue.js·前端框架·ecmascript
天外天-亮3 小时前
Vue2.0 + jsmind:开发思维导图
javascript·vue.js·jsmind
蜡台3 小时前
JavaScript async和awiat 使用
开发语言·前端·javascript·async·await