HTML&CSS:前端 3D 黑科技:云层交互与天气展示

424 阅读7分钟

这个 HTML 文件是一个结合 3D 动画 和 天气信息展示 的网页,主要使用 Three.js 实现 3D 云层效果,并通过 Tailwind CSS 完成响应式布局和视觉设计。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

HTML&CSS


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
    <title>公众号:前端Hardy</title>
    <style>
        .body-wrapper {
            font-family: 'Outfit', sans-serif;
            background: linear-gradient(135deg, #3a3897 0%, #2c2a72 40%, #1a1a4e 100%);
        }

        .weather-widget::before {
            content: '';
            position: absolute;
            top: -50%;
            left: -50%;
            width: 200%;
            height: 200%;
            background: radial-gradient(circle, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0) 70%);
            animation: shimmer 15s infinite linear;
            pointer-events: none;
            z-index: 1;
        }

        @keyframes shimmer {
            0% {
                transform: rotate(0deg);
            }

            100% {
                transform: rotate(360deg);
            }
        }

        .weather-icon-main {
            filter: drop-shadow(0 0 12px rgba(220, 220, 255, 0.5));
            animation: floating 3.5s ease-in-out infinite;
        }

        @keyframes floating {
            0% {
                transform: translateY(0px);
            }

            50% {
                transform: translateY(-8px);
            }

            100% {
                transform: translateY(0px);
            }
        }

        #cloud-container {
            pointer-events: auto;
        }

        @keyframes fadeInUp {
            from {
                opacity: 0;
                transform: translateY(15px);
            }

            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .animate-fadeInUp {
            opacity: 0;
            animation: fadeInUp 0.6s ease-out forwards;
        }

        @keyframes fadeInScaleUp {
            from {
                opacity: 0;
                transform: translateY(10px) scale(0.95);
            }

            to {
                opacity: 1;
                transform: translateY(0) scale(1);
            }
        }

        .animate-fadeInScaleUp {
            opacity: 0;
            animation: fadeInScaleUp 0.7s ease-out forwards;
        }

        @keyframes gentleBob {

            0%,
            100% {
                transform: translateY(0);
            }

            50% {
                transform: translateY(-4px);
            }
        }

        .animate-gentleBob {
            animation: gentleBob 2.5s ease-in-out infinite;
        }

        .sun-info .sunrise:hover .sun-icon,
        .sun-info .sunset:hover .sun-icon {
            transform: rotate(15deg) scale(1.15);
        }

        .sun-icon {
            transition: transform 0.3s ease-in-out;
        }

        .forecast-day:hover {
            box-shadow: 0 0 25px rgba(255, 255, 255, 0.1), 0 4px 10px rgba(0, 0, 0, 0.2);
        }

        .forecast-day:hover .forecast-icon {
            transform: scale(1.2) translateY(-2px);
            transition: transform 0.3s ease-out;
        }

        .forecast-day .forecast-icon {
            transition: transform 0.3s ease-out;
        }

        .delay-100 {
            animation-delay: 0.1s !important;
        }

        .delay-200 {
            animation-delay: 0.2s !important;
        }

        .delay-300 {
            animation-delay: 0.3s !important;
        }

        .delay-400 {
            animation-delay: 0.4s !important;
        }

        .delay-500 {
            animation-delay: 0.5s !important;
        }

        .delay-600 {
            animation-delay: 0.6s !important;
        }

        .delay-700 {
            animation-delay: 0.7s !important;
        }
    </style>
</head>

<body>
    <div class="body-wrapper min-h-screen flex justify-center items-center p-4">
        <div class="weather-widget-container relative w-full max-w-sm sm:max-w-md">
            <div
                class="weather-widget relative text-white bg-gradient-to-br from-purple-700/70 via-indigo-800/60 to-blue-900/70 backdrop-blur-lg shadow-2xl rounded-3xl p-6 overflow-hidden border border-white/10">
                <div id="cloud-container"
                    class="absolute top-0 right-0 w-36 h-36 sm:w-40 sm:h-40 z-30 cursor-pointer rounded-tr-3xl overflow-hidden">
                    <div id="cloud-tooltip"
                        class="tooltip absolute top-20 right-2 sm:top-24 sm:right-4 bg-black/70 text-white px-3 py-1.5 rounded-md text-xs opacity-0 transition-opacity duration-300 pointer-events-none z-40 shadow-lg">
                        Click clouds for a surprise!
                        <div class="absolute -top-1 right-3 w-3 h-3 bg-black/70 transform rotate-45"></div>
                    </div>
                </div>

                <div class="relative z-20">
                    <div class="date-time text-sm font-light opacity-80 mb-1 tracking-wide animate-fadeInUp"
                        id="dateTime">Thursday, 09:24</div>

                    <div class="current-weather flex items-center mb-2">
                        <div class="weather-icon-main text-5xl mr-3"></div>
                        <div class="temp text-5xl font-semibold">
                            <span
                                class="bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-300 animate-fadeInScaleUp delay-100"
                                id="temperature">8°C</span>
                        </div>
                    </div>

                    <div class="location text-lg opacity-90 mb-4 tracking-wide animate-fadeInUp delay-200"
                        id="location">New York, USA</div>

                    <div
                        class="sun-info bg-black/30 backdrop-blur-sm rounded-2xl p-3 sm:p-4 flex justify-between items-center mb-4 border border-white/10 shadow-md animate-fadeInUp delay-300">
                        <div class="sunrise text-center">
                            <div class="sun-icon text-xl mb-1">☀️</div>
                            <div class="text-xs opacity-80" id="sunriseTime">6:14 am</div>
                        </div>
                        <div class="day-length text-center text-sm opacity-90" id="dayLength">11 h 42 m</div>
                        <div class="sunset text-center">
                            <div class="sun-icon text-xl mb-1">🌙</div>
                            <div class="text-xs opacity-80" id="sunsetTime">5:56 pm</div>
                        </div>
                    </div>

                    <div
                        class="precipitation bg-white/10 backdrop-blur-sm rounded-xl p-3 flex items-center mb-4 border border-white/5 shadow-sm animate-fadeInUp delay-400">
                        <div class="precip-icon text-2xl mr-2 text-blue-300 drop-shadow-lg animate-gentleBob">🌧️</div>
                        <div class="text-sm opacity-90" id="precipitationChance">Rain 85%</div>
                    </div>

                    <div class="humidity-wind flex justify-between text-sm opacity-90 mb-5 animate-fadeInUp delay-500">
                        <div id="humidity">Humidity: 68%</div>
                        <div id="windSpeed">Wind: 12 km/h</div>
                    </div>

                    <div class="forecast flex flex-nowrap justify-between pb-2">
                        <div
                            class="forecast-day bg-white/5 backdrop-blur-sm rounded-xl p-3 w-20 text-center border border-white/10 shadow-sm hover:bg-white/10 transition-all duration-200 cursor-pointer transform hover:-translate-y-1 animate-fadeInUp delay-500">
                            <div class="day-name text-xs font-medium mb-1 opacity-80">Today</div>
                            <div class="forecast-icon text-2xl my-1 drop-shadow-md"></div>
                            <div
                                class="high-temp text-sm font-semibold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-300"></div>
                            <div class="low-temp text-xs opacity-70"></div>
                        </div>
                        <div
                            class="forecast-day bg-white/5 backdrop-blur-sm rounded-xl p-3 w-20 text-center border border-white/10 shadow-sm hover:bg-white/10 transition-all duration-200 cursor-pointer transform hover:-translate-y-1 animate-fadeInUp delay-600">
                            <div class="day-name text-xs font-medium mb-1 opacity-80">Fri</div>
                            <div class="forecast-icon text-2xl my-1 drop-shadow-md">🌧️</div>
                            <div
                                class="high-temp text-sm font-semibold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-300"></div>
                            <div class="low-temp text-xs opacity-70"></div>
                        </div>
                        <div
                            class="forecast-day bg-white/5 backdrop-blur-sm rounded-xl p-3 w-20 text-center border border-white/10 shadow-sm hover:bg-white/10 transition-all duration-200 cursor-pointer transform hover:-translate-y-1 animate-fadeInUp delay-700">
                            <div class="day-name text-xs font-medium mb-1 opacity-80">Sat</div>
                            <div class="forecast-icon text-2xl my-1 drop-shadow-md">🌧️</div>
                            <div
                                class="high-temp text-sm font-semibold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-300"></div>
                            <div class="low-temp text-xs opacity-70"></div>
                        </div>
                        <div
                            class="forecast-day bg-white/5 backdrop-blur-sm rounded-xl p-3 w-20 text-center border border-white/10 shadow-sm hover:bg-white/10 transition-all duration-200 cursor-pointer transform hover:-translate-y-1 animate-fadeInUp delay-[0.8s]">
                            <div class="day-name text-xs font-medium mb-1 opacity-80">Sun</div>
                            <div class="forecast-icon text-2xl my-1 drop-shadow-md">☀️</div>
                            <div
                                class="high-temp text-sm font-semibold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-300"></div>
                            <div class="low-temp text-xs opacity-70"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script type="module">
        import * as THREE from "https://esm.sh/three";
        import { OrbitControls } from "https://esm.sh/three/examples/jsm/controls/OrbitControls.js";

        const dateTimeEl = document.getElementById('dateTime');

        function updateDateTime() {
            const now = new Date();
            const optionsDate = { weekday: 'long' };
            const optionsTime = { hour: '2-digit', minute: '2-digit', hour12: false };
            if (dateTimeEl) {
                dateTimeEl.textContent = `${now.toLocaleDateString(undefined, optionsDate)}, ${now.toLocaleTimeString([], optionsTime)}`;
            }
        }
        updateDateTime();
        setInterval(updateDateTime, 60000);

        const container = document.getElementById('cloud-container');

        if (container) {
            const containerRect = container.getBoundingClientRect();
            const scene = new THREE.Scene();
            const cameraAspect = (containerRect.width > 0 && containerRect.height > 0) ? containerRect.width / containerRect.height : 1;
            const camera = new THREE.PerspectiveCamera(60, cameraAspect, 0.1, 1000);
            const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });

            renderer.setSize(containerRect.width, containerRect.height);
            renderer.setClearColor(0x000000, 0);
            container.appendChild(renderer.domElement);

            camera.position.set(0, 0.5, 4.5);

            const ambientLight = new THREE.AmbientLight(0xffffff, 1.2);
            scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 2.0);
            directionalLight.position.set(2, 3, 2);
            scene.add(directionalLight);
            const pointLight = new THREE.PointLight(0xaabbee, 0.8, 15);
            pointLight.position.set(-1, 1, 3);
            scene.add(pointLight);

            const controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.dampingFactor = 0.07;
            controls.rotateSpeed = 0.8;
            controls.enableZoom = false;
            controls.enablePan = false;
            controls.minPolarAngle = Math.PI / 3;
            controls.maxPolarAngle = Math.PI / 1.8;
            controls.target.set(0, 0, 0);

            const cloudGroup = new THREE.Group();
            scene.add(cloudGroup);

            const cloudMaterial = new THREE.MeshPhysicalMaterial({
                color: 0xf0f8ff,
                transparent: true, opacity: 0.85, roughness: 0.6, metalness: 0.0,
                transmission: 0.1,
                ior: 1.3,
                specularIntensity: 0.2,
                sheen: 0.2, sheenColor: 0xffffff, sheenRoughness: 0.5,
                clearcoat: 0.05, clearcoatRoughness: 0.3,
            });

            function createCloudPart(radius, position) {
                const geometry = new THREE.SphereGeometry(radius, 20, 20);
                const mesh = new THREE.Mesh(geometry, cloudMaterial);
                mesh.position.copy(position);
                return mesh;
            }

            function createDetailedCloud(x, y, z, scale) {
                const singleCloudGroup = new THREE.Group();
                singleCloudGroup.position.set(x, y, z);
                singleCloudGroup.scale.set(scale, scale, scale);
                const parts = [
                    { radius: 0.8, position: new THREE.Vector3(0, 0, 0) }, { radius: 0.6, position: new THREE.Vector3(0.7, 0.2, 0.1) },
                    { radius: 0.55, position: new THREE.Vector3(-0.6, 0.1, -0.2) }, { radius: 0.7, position: new THREE.Vector3(0.1, 0.4, -0.3) },
                    { radius: 0.5, position: new THREE.Vector3(0.3, -0.3, 0.2) }, { radius: 0.6, position: new THREE.Vector3(-0.4, -0.2, 0.3) },
                    { radius: 0.45, position: new THREE.Vector3(0.8, -0.1, -0.2) }, { radius: 0.5, position: new THREE.Vector3(-0.7, 0.3, 0.3) },
                ];
                parts.forEach(part => singleCloudGroup.add(createCloudPart(part.radius, part.position)));
                singleCloudGroup.userData = {
                    isRaining: false, rainColor: Math.random() > 0.5 ? 0x87CEFA : 0xB0E0E6,
                    originalPosition: singleCloudGroup.position.clone(), bobOffset: Math.random() * Math.PI * 2,
                    bobSpeed: 0.0005 + Math.random() * 0.0003, bobAmount: 0.15 + Math.random() * 0.1,
                };
                return singleCloudGroup;
            }

            const cloud1 = createDetailedCloud(-0.7, 0.2, 0, 1.0);
            const cloud2 = createDetailedCloud(0.7, -0.1, 0.3, 0.9);
            cloudGroup.add(cloud1, cloud2);
            cloudGroup.position.y = -0.2;
            let autoRotateSpeed = 0.002;

            function createRaindropsForCloud(cloud) {
                const rainGroup = new THREE.Group();
                cloud.add(rainGroup);
                cloud.userData.rainGroup = rainGroup;
                const raindropMaterial = new THREE.MeshBasicMaterial({ color: cloud.userData.rainColor, transparent: true, opacity: 0.7 });
                const localRaindrops = [];
                for (let i = 0; i < 30; i++) {
                    const raindropGeom = new THREE.CylinderGeometry(0.015, 0.015, 0.25, 6);
                    const raindrop = new THREE.Mesh(raindropGeom, raindropMaterial);
                    raindrop.position.set((Math.random() - 0.5) * 1.8, -0.8 - Math.random() * 1.5, (Math.random() - 0.5) * 1.8);
                    raindrop.userData = { originalY: raindrop.position.y - Math.random() * 0.5, speed: 0.08 + Math.random() * 0.05 };
                    localRaindrops.push(raindrop);
                    rainGroup.add(raindrop);
                }
                rainGroup.visible = false;
                return localRaindrops;
            }

            const raindrops1 = createRaindropsForCloud(cloud1);
            const raindrops2 = createRaindropsForCloud(cloud2);

            const raycaster = new THREE.Raycaster();
            const mouse = new THREE.Vector2();

            renderer.domElement.addEventListener('click', (event) => {
                const rect = renderer.domElement.getBoundingClientRect();
                mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
                mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
                raycaster.setFromCamera(mouse, camera);
                const intersects = raycaster.intersectObjects(cloudGroup.children, true);

                if (intersects.length > 0) {
                    let clickedObj = intersects[0].object;
                    let physicallyClickedCloud = null;
                    while (clickedObj.parent && clickedObj.parent !== cloudGroup) {
                        clickedObj = clickedObj.parent;
                    }

                    if (clickedObj.parent === cloudGroup) {
                        physicallyClickedCloud = clickedObj;

                        const isCloud1Raining = cloud1.userData.isRaining;
                        const isCloud2Raining = cloud2.userData.isRaining;

                        let newGlobalRainState;
                        if (isCloud1Raining && isCloud2Raining) {
                            newGlobalRainState = false;
                        } else {
                            newGlobalRainState = true;
                        }

                        cloud1.userData.isRaining = newGlobalRainState;
                        if (cloud1.userData.rainGroup) {
                            cloud1.userData.rainGroup.visible = newGlobalRainState;
                        }

                        cloud2.userData.isRaining = newGlobalRainState;
                        if (cloud2.userData.rainGroup) {
                            cloud2.userData.rainGroup.visible = newGlobalRainState;
                        }

                        if (physicallyClickedCloud) {
                            const originalScale = physicallyClickedCloud.scale.clone();
                            physicallyClickedCloud.scale.multiplyScalar(1.15);
                            setTimeout(() => {
                                physicallyClickedCloud.scale.copy(originalScale);
                            }, 150);
                        }
                    }
                }
            });

            const tooltip = document.getElementById('cloud-tooltip');
            setTimeout(() => {
                if (tooltip) tooltip.classList.add('opacity-100');
                setTimeout(() => { if (tooltip) tooltip.classList.remove('opacity-100'); }, 3500);
            }, 1500);

            function animate() {
                requestAnimationFrame(animate);
                const time = Date.now();
                cloudGroup.rotation.y += autoRotateSpeed;

                [cloud1, cloud2].forEach(cloud => {
                    if (cloud) {
                        cloud.position.y = cloud.userData.originalPosition.y + Math.sin(time * cloud.userData.bobSpeed + cloud.userData.bobOffset) * cloud.userData.bobAmount;

                        if (cloud.userData.isRaining && cloud.userData.rainGroup) {
                            const currentRaindrops = cloud === cloud1 ? raindrops1 : raindrops2;
                            currentRaindrops.forEach(raindrop => {
                                raindrop.position.y -= raindrop.userData.speed;
                                if (raindrop.position.y < -5) {
                                    raindrop.position.y = -0.8;
                                    raindrop.position.x = (Math.random() - 0.5) * 1.8 * cloud.scale.x;
                                    raindrop.position.z = (Math.random() - 0.5) * 1.8 * cloud.scale.z;
                                }
                            });
                        }
                    }
                });
                controls.update();
                renderer.render(scene, camera);
            }

            window.addEventListener('resize', () => {
                const newRect = container.getBoundingClientRect();
                if (newRect.width > 0 && newRect.height > 0) {
                    camera.aspect = newRect.width / newRect.height;
                    camera.updateProjectionMatrix();
                    renderer.setSize(newRect.width, newRect.height);
                }
            });

            animate();

        } else {
            console.error("Cloud container (id: 'cloud-container') not found! 3D cloud animation will not be initialized.");
        }
    </script>
</body>

</html>

HTML 与 CSS

  • body-wrapper: 使用 min-h-screen 和 Flex 布局,使内容垂直居中,背景为深蓝色渐变,营造科技感;
  • weather-widget-container:限制最大宽度(max-w-sm/max-w-md),内部 .weather-widget 使用渐变背景和毛玻璃效果(backdrop-blur-lg),搭配阴影(shadow-2xl)和圆角(rounded-3xl),模拟立体卡片。
  • .weather-widget::before 通过径向渐变和旋转动画模拟光效流动,增强页面动态感;
  • .weather-icon-main 使用 floating 动画实现上下浮动,搭配投影(drop-shadow)模拟立体感;
  • animate-fadeInUp、animate-fadeInScaleUp 等类名,结合 animation-delay 实现元素分批入场动画,提升用户体验。

核心逻辑

1. 场景搭建

容器与渲染器:

  • 通过 #cloud-container 包裹 3D 画布,使用 Three.js 的 WebGLRenderer 渲染场景,背景透明(alpha: true),确保与页面背景融合:
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setClearColor(0x000000, 0); // 透明背景

相机与灯光:

  • 透视相机(PerspectiveCamera):视角 60°,宽高比自适应容器,模拟人眼视角。
  • 环境光(AmbientLight)、平行光(DirectionalLight)、点光源(PointLight):组合使用以照亮云层,增强立体感和细节。

2. 云层模型构建

材质与几何:

  • 使用 MeshPhysicalMaterial 模拟云层的半透明、光泽感,通过 roughness、specularIntensity 等参数调整质感。
  • 云层由多个球体组合而成(createDetailedCloud 函数),通过不同半径和位置的球体堆叠,形成自然的云朵形态。

交互逻辑:

  • 点击云层触发下雨效果:通过射线投射器(Raycaster)检测点击事件,切换云层的 isRaining 状态,显示 / 隐藏雨滴粒子。
  • 点击时云层缩放反馈:点击后短暂放大云层(scale.multiplyScalar(1.15)),增强交互反馈。

3. 动态效果

自动旋转与浮动:

  • 云层整体绕 Y 轴缓慢旋转(cloudGroup.rotation.y += autoRotateSpeed)。
  • 单个云层基于正弦函数实现上下浮动(Math.sin 计算位移),模拟真实云朵的飘动。

雨滴动画:

  • 雨滴使用圆柱体几何,随机位置生成,向下移动超出屏幕后重置位置,形成循环下落效果。

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!