【圣诞快乐 Merry Christmas】 3D 粒子变形圣诞体验

目录

    • [🎄 技术揭秘:如何用 Three.js 打造 3D 粒子变形圣诞体验](#🎄 技术揭秘:如何用 Three.js 打造 3D 粒子变形圣诞体验)
    • [1. 界面设计与 CSS 美学 (Glassmorphism)](#1. 界面设计与 CSS 美学 (Glassmorphism))
    • [2. 核心技术:Three.js 粒子系统](#2. 核心技术:Three.js 粒子系统)
      • [BufferGeometry 的威力](#BufferGeometry 的威力)
      • 发光材质
    • [3. 数学之美:粒子形状生成算法](#3. 数学之美:粒子形状生成算法)
      • [🎄 螺旋圣诞树](#🎄 螺旋圣诞树)
      • [🎁 礼物盒](#🎁 礼物盒)
      • [🎅 抽象圣诞老人](#🎅 抽象圣诞老人)
    • [4. 灵魂注入:Tween.js 变形动画](#4. 灵魂注入:Tween.js 变形动画)
    • 完整代码
    • [5. 总结](#5. 总结)

专栏导读

🌸 欢迎来到Python办公自动化专栏---Python处理办公问题,解放您的双手
🏳️‍🌈 个人博客主页:请点击------> 个人的博客主页 求收藏
🏳️‍🌈 Github主页:请点击------> Github主页 求Star⭐
🏳️‍🌈 知乎主页:请点击------> 知乎主页 求关注
🏳️‍🌈 CSDN博客主页:请点击------> CSDN的博客主页 求关注
👍 该系列文章专栏:请点击------>Python办公自动化专栏 求订阅
🕷 此外还有爬虫专栏:请点击------>Python爬虫基础专栏 求订阅
📕 此外还有python基础专栏:请点击------>Python基础学习专栏 求订阅
文章作者技术和水平有限,如果文中出现错误,希望大家能指正🙏
❤️ 欢迎各位佬关注! ❤️

🎄 技术揭秘:如何用 Three.js 打造 3D 粒子变形圣诞体验

在这个项目中,我们探索了如何使用 Three.jsTween.js 创建一个充满节日氛围的 3D 互动网页。不仅仅是简单的 3D 模型展示,我们实现了一个由 30,000 个独立粒子组成的系统,这些粒子可以在不同的形态(圣诞树、礼物盒、糖果手杖、圣诞老人)之间平滑流体变形。

本文将详细拆解其背后的技术实现,从 CSS 界面设计到 3D 粒子数学生成算法。

1. 界面设计与 CSS 美学 (Glassmorphism)

虽然核心是 3D,但 UI 的质感决定了第一印象。为了不抢占 3D 场景的视觉焦点,我们采用了 磨砂玻璃 (Glassmorphism) 风格的 UI 设计。

沉浸式背景

首先,我们确保 Canvas 占满全屏,并使用深邃的黑色背景来衬托发光的粒子。

css 复制代码
body {
    margin: 0;
    overflow: hidden;
    background-color: #000; /* 纯黑背景,极致对比 */
    font-family: 'Segoe UI', sans-serif;
}

#canvas-container {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    z-index: 1;
}

磨砂玻璃按钮

控制按钮采用了半透明模糊效果,使其看起来悬浮在 3D 场景之上。

css 复制代码
.btn {
    background: rgba(255, 255, 255, 0.1); /* 极低的透明度 */
    border: 1px solid rgba(255, 255, 255, 0.3); /* 细微的边框 */
    color: white;
    padding: 8px 16px;
    border-radius: 20px;
    backdrop-filter: blur(5px); /* 关键:背景模糊 */
    transition: all 0.3s;
}

.btn:hover, .btn.active {
    background: rgba(255, 255, 255, 0.3); /* 激活状态更亮 */
    border-color: white;
    box-shadow: 0 0 15px rgba(255, 255, 255, 0.3); /* 发光效果 */
}

2. 核心技术:Three.js 粒子系统

我们没有使用传统的 Mesh(网格),而是使用了 THREE.Points。这是因为我们需要控制每一个"点"的移动,从而实现变形效果。

BufferGeometry 的威力

为了渲染 30,000 个粒子并保持 60FPS 的流畅度,必须使用 BufferGeometry。它直接操作 GPU 显存,比普通的 Geometry 快得多。

javascript 复制代码
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3); // x, y, z
const colors = new Float32Array(particleCount * 3);    // r, g, b

// ... 填充数组数据 ...

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

发光材质

为了让粒子看起来像节日彩灯,我们使用了 THREE.AdditiveBlending(叠加混合模式)。当多个粒子重叠时,亮度会叠加,产生耀眼的光晕效果。

javascript 复制代码
const material = new THREE.PointsMaterial({
    size: 4,
    map: texture, // 圆形渐变纹理
    vertexColors: true, // 允许每个粒子有不同颜色
    blending: THREE.AdditiveBlending, // 叠加发光
    depthTest: false, // 关闭深度测试,避免粒子互相遮挡产生的黑边
    transparent: true
});

3. 数学之美:粒子形状生成算法

项目的核心难点在于如何计算出不同形状的粒子坐标。我们预先计算了四种形态的坐标数据。

🎄 螺旋圣诞树

利用圆锥体方程加上螺旋偏移。

javascript 复制代码
// 核心逻辑
const y = (Math.random() * 200) - 100; // 高度
const maxRadius = 80 * (1 - (y + 100) / 200); // 半径随高度减小
const angle = y * 0.1 + Math.random() * Math.PI * 2; // 螺旋角度
const x = maxRadius * Math.cos(angle);
const z = maxRadius * Math.sin(angle);

🎁 礼物盒

在一个立方体的表面随机采样点。

javascript 复制代码
const size = 60;
// 随机选择立方体的 6 个面之一,然后随机生成该面上的 UV 坐标
// 另外,通过坐标判断是否在"丝带"区域(十字交叉),如果是则染成金色。

🎅 抽象圣诞老人

这是最复杂的形状,由多个几何体组合而成:

  • 身体:底部的红色圆柱/球体混合。
  • 腰带:黑色圆环。
  • 头部:皮肤色的球体。
  • 胡子:白色的半球体区域。
  • 帽子:顶部的红色圆锥 + 白色绒球。

通过 if-else 逻辑判断粒子的随机位置属于身体的哪个部分,从而分配坐标和颜色。

4. 灵魂注入:Tween.js 变形动画

有了起始坐标(圣诞树)和目标坐标(例如礼物盒),我们不需要销毁重建粒子,而是让现有的粒子过去。

这里使用了 Tween.js 来驱动一个从 0 到 1 的进度因子 t

javascript 复制代码
const tweenObj = { t: 0 };

new TWEEN.Tween(tweenObj)
    .to({ t: 1 }, 2000) // 2秒动画
    .easing(TWEEN.Easing.Exponential.InOut) // 缓动函数,两头慢中间快
    .onUpdate(() => {
        const t = tweenObj.t;
        // 线性插值 (Lerp)
        currentPos = startPos + (targetPos - startPos) * t;
        currentColor = startColor + (targetColor - startColor) * t;
        
        // 通知 GPU 更新数据
        geometry.attributes.position.needsUpdate = true;
        geometry.attributes.color.needsUpdate = true;
    })
    .start();

完整代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D 粒子圣诞变换</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #000;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        #canvas-container {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 1;
        }

        #overlay-text {
            position: absolute;
            bottom: 40px;
            width: 100%;
            text-align: center;
            z-index: 10;
            pointer-events: none;
            color: rgba(255, 255, 255, 0.8);
            font-weight: 300;
            letter-spacing: 4px;
            font-size: 24px;
            text-shadow: 0 0 10px rgba(255,255,255,0.5);
            transition: opacity 1s;
        }

        #loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: rgba(255,255,255,0.5);
            font-size: 14px;
            pointer-events: none;
            z-index: 5;
            letter-spacing: 2px;
        }

        .controls {
            position: absolute;
            top: 20px;
            right: 20px;
            z-index: 20;
            display: flex;
            gap: 10px;
        }

        .btn {
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.3);
            color: white;
            padding: 8px 16px;
            cursor: pointer;
            border-radius: 20px;
            backdrop-filter: blur(5px);
            transition: all 0.3s;
            font-size: 12px;
        }

        .btn:hover, .btn.active {
            background: rgba(255, 255, 255, 0.3);
            border-color: white;
        }
    </style>
</head>
<body>

    <div id="canvas-container"></div>
    
    <div id="overlay-text">MERRY CHRISTMAS</div>

    <div id="loading">Loading 3D Assets...</div>

    <div class="controls">
        <button class="btn" onclick="manualSwitch(0)">圣诞树</button>
        <button class="btn" onclick="manualSwitch(1)">礼物盒</button>
        <button class="btn" onclick="manualSwitch(2)">糖果手杖</button>
        <button class="btn" onclick="manualSwitch(3)">圣诞老人</button>
    </div>

    <!-- Three.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/examples/js/controls/OrbitControls.js"></script>
    <!-- Tween.js for smoother animations -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>

    <script>
        let scene, camera, renderer, controls;
        let particleSystem;
        let particleCount = 30000;
        let currentShape = 0; // 0: Tree, 1: Gift, 2: Candy, 3: Santa
        let shapes = []; // Stores {positions, colors} for each shape
        let time = 0;
        let autoSwitchTimer;

        // Constants
        const COLOR_GREEN = new THREE.Color(0x0f9b0f);
        const COLOR_LIGHT_GREEN = new THREE.Color(0x37c937);
        const COLOR_RED = new THREE.Color(0xff0000);
        const COLOR_GOLD = new THREE.Color(0xffd700);
        const COLOR_WHITE = new THREE.Color(0xffffff);
        const COLOR_SKIN = new THREE.Color(0xffccaa);
        const COLOR_BLACK = new THREE.Color(0x111111);

        function init() {
            const container = document.getElementById('canvas-container');
            document.getElementById('loading').style.display = 'none';

            scene = new THREE.Scene();
            scene.fog = new THREE.FogExp2(0x000000, 0.0008);

            camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 2, 2000);
            camera.position.set(0, 50, 400);
            camera.lookAt(0, 0, 0);

            renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(window.innerWidth, window.innerHeight);
            container.appendChild(renderer.domElement);

            if (THREE.OrbitControls) {
                controls = new THREE.OrbitControls(camera, renderer.domElement);
                controls.enableDamping = true;
                controls.dampingFactor = 0.05;
                controls.autoRotate = true;
                controls.autoRotateSpeed = 1.0;
                controls.enableZoom = true;
            }

            // 1. Pre-calculate all shapes
            generateShapes();

            // 2. Create Particle System with initial shape (Tree)
            createParticleSystem();

            // 3. Create Snow (Background)
            createSnow();

            window.addEventListener('resize', onWindowResize, false);
            
            animate();

            // Start auto-switch cycle
            startAutoSwitch();
        }

        function generateShapes() {
            shapes.push(generateTreeData());
            shapes.push(generateGiftData());
            shapes.push(generateCandyData());
            shapes.push(generateSantaData());
        }

        // --- Shape Generators ---

        function generateTreeData() {
            const positions = [];
            const colors = [];
            
            for (let i = 0; i < particleCount; i++) {
                const y = (Math.random() * 200) - 100;
                const percent = (y + 100) / 200;
                const maxRadius = 80 * (1 - percent);
                const r = Math.random() * maxRadius; 
                const angle = Math.random() * Math.PI * 2 * 15 + y * 0.1;

                const x = r * Math.cos(angle);
                const z = r * Math.sin(angle);

                positions.push(x, y, z);

                // Colors
                if (Math.random() < 0.05) {
                    const c = Math.random() < 0.5 ? COLOR_RED : COLOR_GOLD;
                    colors.push(c.r, c.g, c.b);
                } else if (y > 95) { // Star at top
                    colors.push(1, 0.9, 0.2);
                } else {
                    const c = COLOR_GREEN.clone().lerp(COLOR_LIGHT_GREEN, Math.random());
                    colors.push(c.r, c.g, c.b);
                }
            }
            return { positions, colors };
        }

        function generateGiftData() {
            const positions = [];
            const colors = [];
            const size = 60; // Box size

            for (let i = 0; i < particleCount; i++) {
                // Random point on cube surface
                let x, y, z;
                const face = Math.floor(Math.random() * 6);
                const u = (Math.random() * 2 - 1) * size;
                const v = (Math.random() * 2 - 1) * size;

                switch(face) {
                    case 0: x = size; y = u; z = v; break;
                    case 1: x = -size; y = u; z = v; break;
                    case 2: y = size; x = u; z = v; break;
                    case 3: y = -size; x = u; z = v; break;
                    case 4: z = size; x = u; y = v; break;
                    case 5: z = -size; x = u; y = v; break;
                }

                positions.push(x, y, z);

                // Ribbon check (Cross shape)
                const ribbonWidth = 15;
                let isRibbon = false;
                if (Math.abs(x) < ribbonWidth || Math.abs(y) < ribbonWidth || Math.abs(z) < ribbonWidth) {
                    isRibbon = true;
                }

                if (isRibbon) {
                    colors.push(COLOR_GOLD.r, COLOR_GOLD.g, COLOR_GOLD.b);
                } else {
                    colors.push(COLOR_RED.r, COLOR_RED.g, COLOR_RED.b);
                }
            }
            return { positions, colors };
        }

        function generateCandyData() {
            const positions = [];
            const colors = [];
            
            for (let i = 0; i < particleCount; i++) {
                const t = i / particleCount; // 0 to 1
                
                // Candy Cane shape: Line up, then curve
                let x, y, z;
                const height = 180;
                const radius = 12;
                
                // We'll distribute particles along the tube volume
                // A cane has a straight part and a curved part
                
                const segment = Math.random();
                let tubeX, tubeY, tubeZ;

                if (segment < 0.7) {
                    // Straight part
                    tubeY = (Math.random() * 140) - 70;
                    tubeX = 0;
                    tubeZ = 0;
                } else {
                    // Curved part (Top hook)
                    const angle = (Math.random() * Math.PI); // 0 to 180 degrees
                    const hookRadius = 35;
                    tubeY = 70 + hookRadius * Math.sin(angle);
                    tubeX = -hookRadius + hookRadius * Math.cos(angle); // Hook to the left
                    tubeZ = 0;
                }

                // Add thickness
                const r = Math.random() * radius;
                const theta = Math.random() * Math.PI * 2;
                
                // Simple tube displacement
                x = tubeX + r * Math.cos(theta);
                y = tubeY + r * Math.sin(theta); // Approximate for hook
                z = tubeZ + r * Math.sin(theta); // Approximate

                // Better orientation for hook needed? 
                // For simplicity, let's just do a spiral stripe coloring on this form
                
                positions.push(x, y, z);

                // Stripe logic based on height/angle
                // Map position to a linear value to create stripes
                const stripeVal = y + x * 0.5;
                if (Math.sin(stripeVal * 0.1) > 0) {
                    colors.push(COLOR_RED.r, COLOR_RED.g, COLOR_RED.b);
                } else {
                    colors.push(COLOR_WHITE.r, COLOR_WHITE.g, COLOR_WHITE.b);
                }
            }
            return { positions, colors };
        }

        function generateSantaData() {
            const positions = [];
            const colors = [];

            for (let i = 0; i < particleCount; i++) {
                let x, y, z, r, g, b;
                const part = Math.random();

                if (part < 0.4) {
                    // Body (Red Coat) - Bottom Sphere/Cylinder
                    const radius = 50 * Math.random();
                    const theta = Math.random() * Math.PI * 2;
                    const height = Math.random() * 80 - 60; // -60 to 20
                    x = radius * Math.cos(theta);
                    z = radius * Math.sin(theta);
                    y = height;
                    
                    // Belt check
                    if (y > -5 && y < 5) {
                        r=0.1; g=0.1; b=0.1; // Black belt
                    } else {
                        r=1; g=0; b=0; // Red coat
                    }
                    
                    // Buttons
                    if (z > radius - 5 && Math.abs(x) < 5 && y > -60 && y < 20) {
                        r=1; g=1; b=1; // White buttons
                    }

                } else if (part < 0.6) {
                    // Head (Skin + Beard)
                    const radius = 25 * Math.random();
                    const theta = Math.random() * Math.PI * 2;
                    const phi = Math.random() * Math.PI;
                    x = radius * Math.sin(phi) * Math.cos(theta);
                    z = radius * Math.sin(phi) * Math.sin(theta);
                    y = radius * Math.cos(phi) + 35; // Center at 35

                    // Face direction is +Z
                    if (z > 5 && y < 35) {
                        r=1; g=1; b=1; // White Beard
                    } else if (z > 5 && y >= 35) {
                        r=1; g=0.8; b=0.6; // Skin
                        // Eyes
                        if (z > 15 && y > 38 && y < 42 && Math.abs(x) > 5 && Math.abs(x) < 12) {
                            r=0; g=0; b=0;
                        }
                    } else {
                        r=1; g=1; b=1; // Hair/Beard back
                    }

                } else {
                    // Hat (Cone)
                    const h = Math.random() * 40; // 0 to 40
                    const maxR = 26 * (1 - h/40);
                    const rad = Math.random() * maxR;
                    const theta = Math.random() * Math.PI * 2;
                    
                    x = rad * Math.cos(theta);
                    z = rad * Math.sin(theta);
                    y = h + 55; // Start at top of head

                    // Pom-pom at tip
                    if (h > 35) {
                        r=1; g=1; b=1;
                    } else if (h < 5) {
                         r=1; g=1; b=1; // Hat rim
                    } else {
                        r=1; g=0; b=0; // Red Hat
                    }
                }

                positions.push(x, y, z);
                colors.push(r, g, b);
            }
            return { positions, colors };
        }

        // --- System Logic ---

        function createParticleSystem() {
            const geometry = new THREE.BufferGeometry();
            
            // Initial Positions (Tree)
            const initialData = shapes[0];
            
            // We need 2 buffers: current position (for render) and target position (for tweening, logically handled)
            // Actually, we can just tween the attribute array values directly.
            
            const positions = new Float32Array(initialData.positions);
            const colors = new Float32Array(initialData.colors);
            
            geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
            geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

            // Texture
            const canvas = document.createElement('canvas');
            canvas.width = 32;
            canvas.height = 32;
            const context = canvas.getContext('2d');
            const gradient = context.createRadialGradient(16, 16, 0, 16, 16, 16);
            gradient.addColorStop(0, 'rgba(255,255,255,1)');
            gradient.addColorStop(0.2, 'rgba(255,255,255,0.8)');
            gradient.addColorStop(0.5, 'rgba(255,255,255,0.2)');
            gradient.addColorStop(1, 'rgba(0,0,0,0)');
            context.fillStyle = gradient;
            context.fillRect(0, 0, 32, 32);
            const texture = new THREE.CanvasTexture(canvas);

            const material = new THREE.PointsMaterial({
                size: 4,
                map: texture,
                vertexColors: true,
                blending: THREE.AdditiveBlending,
                depthTest: false,
                transparent: true
            });

            particleSystem = new THREE.Points(geometry, material);
            scene.add(particleSystem);
        }

        function createSnow() {
            const geometry = new THREE.BufferGeometry();
            const positions = [];
            for (let i = 0; i < 3000; i++) {
                positions.push(
                    Math.random() * 1000 - 500,
                    Math.random() * 1000 - 500,
                    Math.random() * 1000 - 500
                );
            }
            geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));

            const canvas = document.createElement('canvas');
            canvas.width = 16;
            canvas.height = 16;
            const ctx = canvas.getContext('2d');
            ctx.beginPath();
            ctx.arc(8, 8, 8, 0, 2 * Math.PI);
            ctx.fillStyle = 'rgba(255,255,255,0.6)';
            ctx.fill();
            const texture = new THREE.CanvasTexture(canvas);

            const material = new THREE.PointsMaterial({
                size: 2,
                map: texture,
                color: 0xffffff,
                blending: THREE.AdditiveBlending,
                depthTest: false,
                transparent: true,
                opacity: 0.5
            });

            const snow = new THREE.Points(geometry, material);
            snow.name = "snow";
            scene.add(snow);
        }

        // --- Transition Logic ---

        function morphTo(shapeIndex) {
            if (shapeIndex === currentShape) return;
            
            const targetData = shapes[shapeIndex];
            const geometry = particleSystem.geometry;
            const currentPositions = geometry.attributes.position.array;
            const currentColors = geometry.attributes.color.array;
            
            // Update Text
            const titles = ["MERRY CHRISTMAS", "HAPPY HOLIDAYS", "SWEET DREAMS", "SANTA IS COMING"];
            const overlay = document.getElementById('overlay-text');
            overlay.style.opacity = 0;
            setTimeout(() => {
                overlay.innerText = titles[shapeIndex];
                overlay.style.opacity = 1;
            }, 500);

            // Tween Positions
            const tweenObj = { t: 0 };
            
            // Create a copy of start positions/colors to interpolate from
            const startPositions = Float32Array.from(currentPositions);
            const startColors = Float32Array.from(currentColors);

            new TWEEN.Tween(tweenObj)
                .to({ t: 1 }, 2000) // 2 seconds duration
                .easing(TWEEN.Easing.Exponential.InOut)
                .onUpdate(() => {
                    const t = tweenObj.t;
                    for (let i = 0; i < particleCount; i++) {
                        // Lerp Position
                        currentPositions[i*3] = startPositions[i*3] + (targetData.positions[i*3] - startPositions[i*3]) * t;
                        currentPositions[i*3+1] = startPositions[i*3+1] + (targetData.positions[i*3+1] - startPositions[i*3+1]) * t;
                        currentPositions[i*3+2] = startPositions[i*3+2] + (targetData.positions[i*3+2] - startPositions[i*3+2]) * t;

                        // Lerp Color
                        currentColors[i*3] = startColors[i*3] + (targetData.colors[i*3] - startColors[i*3]) * t;
                        currentColors[i*3+1] = startColors[i*3+1] + (targetData.colors[i*3+1] - startColors[i*3+1]) * t;
                        currentColors[i*3+2] = startColors[i*3+2] + (targetData.colors[i*3+2] - startColors[i*3+2]) * t;
                    }
                    geometry.attributes.position.needsUpdate = true;
                    geometry.attributes.color.needsUpdate = true;
                })
                .start();

            currentShape = shapeIndex;
            updateButtons();
        }

        function updateButtons() {
            const btns = document.querySelectorAll('.btn');
            btns.forEach((btn, idx) => {
                if(idx === currentShape) btn.classList.add('active');
                else btn.classList.remove('active');
            });
        }

        function manualSwitch(index) {
            clearInterval(autoSwitchTimer); // Stop auto switch if user interacts
            morphTo(index);
            // Restart auto switch after delay
            autoSwitchTimer = setInterval(nextShape, 8000);
        }

        function nextShape() {
            let next = (currentShape + 1) % shapes.length;
            morphTo(next);
        }

        function startAutoSwitch() {
            updateButtons();
            autoSwitchTimer = setInterval(nextShape, 6000); // Switch every 6 seconds
        }

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        function animate() {
            requestAnimationFrame(animate);
            time += 0.005;

            TWEEN.update();

            if (controls) controls.update();
            else scene.rotation.y += 0.005;

            // Snow animation
            const snow = scene.getObjectByName("snow");
            if (snow) {
                const positions = snow.geometry.attributes.position.array;
                for (let i = 1; i < positions.length; i += 3) {
                    positions[i] -= 0.7; 
                    if (positions[i] < -500) positions[i] = 500;
                }
                snow.geometry.attributes.position.needsUpdate = true;
                snow.rotation.y = Math.sin(time * 0.5) * 0.05;
            }

            renderer.render(scene, camera);
        }

        init();
    </script>
</body>
</html>

5. 总结

这个项目展示了前端图形学的魅力:

  1. 数学构建几何形态。
  2. 物理(简单的)模拟粒子运动。
  3. 设计提升视觉体验。

通过将这三者结合,我们在浏览器中创造了一个无需下载、即开即用的 3D 节日贺卡。


结尾

希望对初学者有帮助;致力于办公自动化的小小程序员一枚
希望能得到大家的【❤️一个免费关注❤️】感谢!
求个 🤞 关注 🤞 +❤️ 喜欢 ❤️ +👍 收藏 👍
此外还有办公自动化专栏,欢迎大家订阅:Python办公自动化专栏
此外还有爬虫专栏,欢迎大家订阅:Python爬虫基础专栏
此外还有Python基础专栏,欢迎大家订阅:Python基础学习专栏
相关推荐
dy17176 小时前
vue左右栏布局可拖拽
前端·css·html
咬人喵喵7 小时前
告别无脑 <div>:HTML 语义化标签入门
前端·css·编辑器·html·svg
旧梦吟7 小时前
脚本工具 批量md转html
前端·python·html5
暴风鱼划水8 小时前
三维重建【5】3D Gaussian Splatting:3R-GS论文解读
3d·3dgs·高斯泼溅·sfm
OranTech9 小时前
练习02-HTML语法
html
世界唯一最大变量11 小时前
一种全新的,自创的(2d无人开车)的算法
html
0思必得011 小时前
[Web自动化] CSS布局与定位
前端·css·自动化·html·web自动化
天外天-亮21 小时前
v-if、v-show、display: none、visibility: hidden区别
前端·javascript·html
be or not to be21 小时前
HTML入门系列:从图片到表单,再到音视频的完整实践
前端·html·音视频