这个 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">
8°</div>
<div class="low-temp text-xs opacity-70">2°</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">
7°</div>
<div class="low-temp text-xs opacity-70">1°</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">
6°</div>
<div class="low-temp text-xs opacity-70">0°</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">
9°</div>
<div class="low-temp text-xs opacity-70">2°</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 计算位移),模拟真实云朵的飘动。
雨滴动画:
- 雨滴使用圆柱体几何,随机位置生成,向下移动超出屏幕后重置位置,形成循环下落效果。
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!