{"id":455,"date":"2025-12-07T19:36:17","date_gmt":"2025-12-07T11:36:17","guid":{"rendered":"https:\/\/jianbo.site\/?p=455"},"modified":"2025-12-26T14:09:14","modified_gmt":"2025-12-26T06:09:14","slug":"455","status":"publish","type":"post","link":"https:\/\/jianbo.site\/index.php\/2025\/12\/07\/455\/","title":{"rendered":"3D \u7c92\u5b50\u624b\u52bf\u4ea4\u4e92\u7cfb\u7edf"},"content":{"rendered":"\n<p><a href=\"https:\/\/jianbo.site\/ai_studio_code.html\">\u70b9\u51fb\u6d4b\u8bd5<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/jianbo.site\/christmas_tree.html\" data-type=\"link\" data-id=\"https:\/\/jianbo.site\/christmas_tree.html\">\u5723\u8bde\u6811<\/a><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><div class='fancybox-wrapper lazyload-container-unload' data-fancybox='post-images' href='https:\/\/jianbo.site\/wp-content\/uploads\/2025\/12\/\u5fae\u4fe1\u56fe\u7247_20251207235432_102_140-1024x475.png'><img class=\"lazyload lazyload-style-1\" src=\"data:image\/svg+xml;base64,PCEtLUFyZ29uTG9hZGluZy0tPgo8c3ZnIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjZmZmZmZmMDAiPjxnPjwvZz4KPC9zdmc+\"  loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"475\" data-original=\"https:\/\/jianbo.site\/wp-content\/uploads\/2025\/12\/\u5fae\u4fe1\u56fe\u7247_20251207235432_102_140-1024x475.png\" src=\"data:image\/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB\/AAffA0nNPuCLAAAAAElFTkSuQmCC\" alt=\"\" class=\"wp-image-458\"  sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/div><\/figure>\n\n\n\n<p>\u8fd9\u4e2a3D\u7c92\u5b50\u7cfb\u7edf\u662f\u901a\u8fc7gemini3\u5b9e\u73b0\u7684\uff0c\u53ea\u9700\u8981\u8f93\u5165\u4e0b\u9762\u7684\u63d0\u793a\u8bcd\u5373\u53ef\u751f\u6210\u3002\u5177\u4f53\u7ec6\u8282\u53ef\u53c2\u8003\u4e0a\u56fe\u8fdb\u884c\u5fae\u8c03\u3002\u6700\u540e\u6211\u628a\u751f\u6210\u597d\u7684\u4ee3\u7801\u4e5f\u653e\u5728\u4e86\u4e0b\u9762\u3002<\/p>\n\n\n\n<p><br>\u63d0\u793a\u8bcd<br>\u7528Three.js\u521b\u5efa\u4e00\u4e2a\u5b9e\u65f6\u4ea4\u4e92\u76843D\u7c92\u5b50\u7cfb\u7edf\u3002\u8981\u6c42:<\/p>\n\n\n\n<p>1.\u901a\u8fc7\u6444\u50cf\u5934\u68c0\u6d4b\u53cc\u624b\u5f20\u5408\u63a7\u5236\u7c92\u5b50\u7fa4\u7684\u7f29\u653e\u4e0e\u6269\u6563<\/p>\n\n\n\n<p>2.\u63d0\u4f9bU\u9762\u677f\u53ef\u9009\u62e9\u7231\u5fc3\/\u82b1\u6735\u571f\u661f\/\u4f5b\u50cf\/\u70df\u82b1\u7b49\u6a21\u578b<\/p>\n\n\n\n<p>3.\u652f\u6301\u989c\u8272\u9009\u62e9\u5668\u8c03\u6574\u7c92\u5b50\u989c\u8272<\/p>\n\n\n\n<p>4.\u7c92\u5b50\u9700\u5b9e\u65f6\u54cd\u5e94\u624b\u52bf\u53d8\u5316<\/p>\n\n\n\n<p>5.\u754c\u9762\u7b80\u6d01\u73b0\u4ee3\uff0c\u5305\u542b\u5168\u5c4f\u63a7\u5236\u6309\u94ae<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!DOCTYPE html&gt;\n&lt;html lang=\"zh-CN\"&gt;\n\n&lt;head&gt;\n    &lt;meta charset=\"UTF-8\"&gt;\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"&gt;\n    &lt;title&gt;3D \u7c92\u5b50\u624b\u52bf\u4ea4\u4e92\u7cfb\u7edf v2.1 - \u53cc\u6307\u65cb\u94ae\u64cd\u63a7\u7248&lt;\/title&gt;\n    &lt;script src=\"https:\/\/cdn.tailwindcss.com\"&gt;&lt;\/script&gt;\n    &lt;script async src=\"https:\/\/unpkg.com\/es-module-shims@1.6.3\/dist\/es-module-shims.js\"&gt;&lt;\/script&gt;\n    &lt;script type=\"importmap\"&gt;\n        {\n            \"imports\": {\n                \"three\": \"https:\/\/cdn.jsdelivr.net\/npm\/three@0.154.0\/build\/three.module.js\",\n                \"three\/addons\/\": \"https:\/\/cdn.jsdelivr.net\/npm\/three@0.154.0\/examples\/jsm\/\"\n            }\n        }\n    &lt;\/script&gt;\n    &lt;!-- MediaPipe Hands --&gt;\n    &lt;script src=\"https:\/\/cdn.jsdelivr.net\/npm\/@mediapipe\/camera_utils\/camera_utils.js\" crossorigin=\"anonymous\"&gt;&lt;\/script&gt;\n    &lt;script src=\"https:\/\/cdn.jsdelivr.net\/npm\/@mediapipe\/control_utils\/control_utils.js\"\n        crossorigin=\"anonymous\"&gt;&lt;\/script&gt;\n    &lt;script src=\"https:\/\/cdn.jsdelivr.net\/npm\/@mediapipe\/drawing_utils\/drawing_utils.js\"\n        crossorigin=\"anonymous\"&gt;&lt;\/script&gt;\n    &lt;script src=\"https:\/\/cdn.jsdelivr.net\/npm\/@mediapipe\/hands\/hands.js\" crossorigin=\"anonymous\"&gt;&lt;\/script&gt;\n\n    &lt;style&gt;\n        body {\n            margin: 0;\n            overflow: hidden;\n            background-color: #020205;\n            font-family: 'Segoe UI', sans-serif;\n        }\n\n        #canvas-container {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            z-index: 1;\n        }\n\n        #video-element {\n            display: none;\n        }\n\n        .glass-panel {\n            background: rgba(10, 10, 20, 0.7);\n            backdrop-filter: blur(16px);\n            -webkit-backdrop-filter: blur(16px);\n            border: 1px solid rgba(255, 255, 255, 0.08);\n            border-radius: 20px;\n            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);\n        }\n\n        .control-btn {\n            transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n            position: relative;\n            overflow: hidden;\n        }\n\n        .control-btn:hover {\n            background: rgba(255, 255, 255, 0.15);\n            transform: translateY(-2px);\n            box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);\n        }\n\n        .control-btn.active {\n            background: linear-gradient(135deg, rgba(100, 200, 255, 0.4), rgba(100, 200, 255, 0.1));\n            border: 1px solid rgba(100, 200, 255, 0.5);\n            box-shadow: 0 0 25px rgba(79, 209, 197, 0.3);\n            text-shadow: 0 0 8px rgba(255, 255, 255, 0.8);\n        }\n\n        .loader {\n            border: 3px solid rgba(255, 255, 255, 0.1);\n            border-left-color: #00ffff;\n            border-radius: 50%;\n            width: 50px;\n            height: 50px;\n            animation: spin 0.8s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite;\n        }\n\n        @keyframes spin {\n            0% {\n                transform: rotate(0deg);\n            }\n\n            100% {\n                transform: rotate(360deg);\n            }\n        }\n\n        .custom-scroll::-webkit-scrollbar {\n            height: 4px;\n        }\n\n        .custom-scroll::-webkit-scrollbar-track {\n            background: rgba(0, 0, 0, 0.2);\n            border-radius: 4px;\n        }\n\n        .custom-scroll::-webkit-scrollbar-thumb {\n            background: rgba(255, 255, 255, 0.15);\n            border-radius: 4px;\n        }\n\n        .custom-scroll::-webkit-scrollbar-thumb:hover {\n            background: rgba(255, 255, 255, 0.3);\n        }\n\n        .mode-item {\n            transition: all 0.2s ease;\n            opacity: 0.4;\n            transform: scale(0.95);\n            font-weight: normal;\n        }\n\n        .mode-item.active {\n            opacity: 1;\n            transform: scale(1.0);\n            color: #22d3ee;\n            font-weight: bold;\n            text-shadow: 0 0 10px rgba(34, 211, 238, 0.4);\n        }\n    &lt;\/style&gt;\n&lt;\/head&gt;\n\n&lt;body&gt;\n\n    &lt;div id=\"canvas-container\"&gt;&lt;\/div&gt;\n    &lt;video id=\"video-element\" playsinline&gt;&lt;\/video&gt;\n\n    &lt;!-- Loading --&gt;\n    &lt;div id=\"loader-overlay\"\n        class=\"fixed inset-0 z-50 flex flex-col items-center justify-center bg-black bg-opacity-95 transition-opacity duration-700\"&gt;\n        &lt;div class=\"loader mb-6 shadow-&#91;0_0_30px_rgba(0,255,255,0.3)]\"&gt;&lt;\/div&gt;\n        &lt;div\n            class=\"text-transparent bg-clip-text bg-gradient-to-r from-cyan-300 to-blue-500 text-2xl font-bold tracking-widest uppercase\"&gt;\n            Visual Core&lt;\/div&gt;\n        &lt;div class=\"text-gray-500 text-xs mt-3 tracking-widest\"&gt;INITIALIZING PARTICLE ENGINE v2.1&lt;\/div&gt;\n    &lt;\/div&gt;\n\n    &lt;!-- UI --&gt;\n    &lt;div class=\"absolute top-0 left-0 w-full h-full pointer-events-none z-10 p-4 md:p-8 flex flex-col justify-between\"&gt;\n        &lt;div class=\"flex justify-between items-start pointer-events-auto\"&gt;\n            &lt;div class=\"glass-panel p-5 animate-fade-in-down transform transition-all hover:scale-105 duration-500\"&gt;\n                &lt;h1\n                    class=\"text-white text-2xl font-bold tracking-tight mb-2 drop-shadow-&#91;0_0_10px_rgba(255,255,255,0.3)]\"&gt;\n                    \u7c92\u5b50 \u00b7 \u5e7b\u5883 &lt;span class=\"text-xs text-cyan-400 align-top\"&gt;v2.1&lt;\/span&gt;&lt;\/h1&gt;\n                &lt;div class=\"flex items-center gap-3 bg-black\/30 rounded-full px-3 py-1 w-fit border border-white\/5\"&gt;\n                    &lt;div id=\"status-dot\"\n                        class=\"w-2 h-2 rounded-full bg-red-500 transition-all duration-500 shadow-&#91;0_0_10px_red]\"&gt;&lt;\/div&gt;\n                    &lt;span id=\"status-text\" class=\"text-&#91;10px] text-gray-300 font-mono uppercase tracking-wider\"&gt;System\n                        Offline&lt;\/span&gt;\n                &lt;\/div&gt;\n                &lt;div class=\"text-&#91;10px] text-gray-400 mt-3 font-mono border-t border-white\/10 pt-2 flex flex-col gap-1\"&gt;\n                    &lt;div id=\"mode-scale\" class=\"mode-item\"&gt;\ud83d\udc4b \u4e94\u6307\u5f20\u5408: \u80fd\u91cf\u7206\u53d1 (\u7f29\u653e)&lt;\/div&gt;\n                    &lt;div id=\"mode-rotate\" class=\"mode-item\"&gt;\u261d\ufe0f \u98df\u6307\u6ed1\u52a8: \u89c6\u89d2\u65cb\u8f6c (\u9501\u5b9a\u7f29\u653e)&lt;\/div&gt;\n                    &lt;div id=\"mode-roll\" class=\"mode-item\"&gt;\u270c\ufe0f \u53cc\u6307\u65cb\u8f6c: \u5e73\u9762\u7ffb\u8f6c (\u65cb\u94ae\u64cd\u63a7)&lt;\/div&gt;\n                &lt;\/div&gt;\n            &lt;\/div&gt;\n\n            &lt;button onclick=\"toggleFullScreen()\"\n                class=\"glass-panel p-4 text-white hover:text-cyan-300 control-btn rounded-full group\"&gt;\n                &lt;svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-6 w-6 group-hover:scale-110 transition-transform\"\n                    fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"&gt;\n                    &lt;path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                        d=\"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4\" \/&gt;\n                &lt;\/svg&gt;\n            &lt;\/button&gt;\n        &lt;\/div&gt;\n\n        &lt;div\n            class=\"pointer-events-auto flex flex-col md:flex-row gap-6 items-end md:items-center justify-between w-full\"&gt;\n            &lt;div class=\"glass-panel p-2 flex gap-2 overflow-x-auto max-w-full custom-scroll\"&gt;\n                &lt;button onclick=\"changeShape('sphere')\"\n                    class=\"control-btn active px-6 py-3 rounded-xl text-sm text-white font-semibold whitespace-nowrap tracking-wide\"\n                    data-shape=\"sphere\"&gt;\u661f\u4e91\u7403&lt;\/button&gt;\n                &lt;button onclick=\"changeShape('heart')\"\n                    class=\"control-btn px-6 py-3 rounded-xl text-sm text-white font-semibold whitespace-nowrap tracking-wide\"\n                    data-shape=\"heart\"&gt;\u673a\u68b0\u5fc3&lt;\/button&gt;\n                &lt;button onclick=\"changeShape('saturn')\"\n                    class=\"control-btn px-6 py-3 rounded-xl text-sm text-white font-semibold whitespace-nowrap tracking-wide\"\n                    data-shape=\"saturn\"&gt;\u571f\u661f\u73af&lt;\/button&gt;\n                &lt;button onclick=\"changeShape('lotus')\"\n                    class=\"control-btn px-6 py-3 rounded-xl text-sm text-white font-semibold whitespace-nowrap tracking-wide\"\n                    data-shape=\"lotus\"&gt;\u80fd\u91cf\u83b2&lt;\/button&gt;\n                &lt;button onclick=\"changeShape('galaxy')\"\n                    class=\"control-btn px-6 py-3 rounded-xl text-sm text-white font-semibold whitespace-nowrap tracking-wide\"\n                    data-shape=\"galaxy\"&gt;\u9ed1\u6d1e&lt;\/button&gt;\n            &lt;\/div&gt;\n\n            &lt;div class=\"glass-panel p-4 flex items-center gap-4 hover:bg-white\/10 transition-colors\"&gt;\n                &lt;span class=\"text-white text-xs font-mono tracking-wider uppercase opacity-70\"&gt;Emission Color&lt;\/span&gt;\n                &lt;div\n                    class=\"relative w-8 h-8 rounded-full overflow-hidden ring-2 ring-white\/20 hover:ring-cyan-400 transition-all\"&gt;\n                    &lt;input type=\"color\" id=\"color-picker\" value=\"#00ffff\"\n                        class=\"absolute -top-1\/2 -left-1\/2 w-&#91;200%] h-&#91;200%] cursor-pointer p-0 m-0 border-0\"\n                        onchange=\"updateColor(this.value)\"&gt;\n                &lt;\/div&gt;\n            &lt;\/div&gt;\n        &lt;\/div&gt;\n    &lt;\/div&gt;\n\n    &lt;script type=\"module\"&gt;\n        import * as THREE from 'three';\n        import { OrbitControls } from 'three\/addons\/controls\/OrbitControls.js';\n        import { EffectComposer } from 'three\/addons\/postprocessing\/EffectComposer.js';\n        import { RenderPass } from 'three\/addons\/postprocessing\/RenderPass.js';\n        import { UnrealBloomPass } from 'three\/addons\/postprocessing\/UnrealBloomPass.js';\n\n        \/\/ --- Configuration ---\n        const PARTICLE_COUNT = 45000;\n        const PARTICLE_SIZE = 0.18;\n        const SATURN_BODY_RATIO = 0.3;\n\n        \/\/ State\n        let scene, camera, renderer, composer, particles, controls;\n        let targetPositions = &#91;];\n        let targetColors = &#91;];\n        let currentShape = 'sphere';\n        let handInfluence = 0;\n        let isHandDetected = false;\n        let clock = new THREE.Clock();\n\n        \/\/ Color State\n        let userBaseColor = new THREE.Color(0x00ffff);\n\n        \/\/ \u65cb\u8f6c\u4ea4\u4e92\u53d8\u91cf\n        let previousFingerPos = { x: 0, y: 0 };\n        let smoothedFingerPos = { x: 0, y: 0 };\n        let rotationVelocity = { x: 0, y: 0, z: 0 };\n        let isTrackingRotation = false;\n\n        \/\/ \u3010\u65b0\u589e\u3011\u53cc\u6307\u65cb\u94ae\u4ea4\u4e92\u53d8\u91cf\n        let isTrackingRoll = false;\n        let smoothedIndexTip = { x: 0, y: 0 };\n        let smoothedMiddleTip = { x: 0, y: 0 };\n        let previousRollAngle = 0;\n\n        \/\/ \u7f29\u653e\u4ea4\u4e92\u53d8\u91cf\n        let currentGestureMode = 'none';\n\n        \/\/ \u6a21\u5f0f\u9501\u5b9a\u4e0e\u4fe1\u53f7\u9632\u6296\n        let currentStableMode = 'scale';\n        let modeFrameCounter = 0;\n        let lastTimeHandDetected = 0;\n\n        async function init() {\n            \/\/ 1. Scene &amp; Camera\n            scene = new THREE.Scene();\n            scene.fog = new THREE.FogExp2(0x020205, 0.02);\n\n            camera = new THREE.PerspectiveCamera(60, window.innerWidth \/ window.innerHeight, 0.1, 1000);\n            camera.position.z = 30;\n            camera.position.y = 0;\n\n            \/\/ 2. Renderer\n            renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true, powerPreference: \"high-performance\" });\n            renderer.setSize(window.innerWidth, window.innerHeight);\n            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));\n            renderer.toneMapping = THREE.ReinhardToneMapping;\n            document.getElementById('canvas-container').appendChild(renderer.domElement);\n\n            \/\/ 3. Post Processing (BLOOM)\n            const renderScene = new RenderPass(scene, camera);\n            const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);\n            bloomPass.threshold = 0.05;\n            bloomPass.strength = 1.4;\n            bloomPass.radius = 0.6;\n\n            composer = new EffectComposer(renderer);\n            composer.addPass(renderScene);\n            composer.addPass(bloomPass);\n\n            \/\/ 4. Controls\n            controls = new OrbitControls(camera, renderer.domElement);\n            controls.enableDamping = true;\n            controls.dampingFactor = 0.05;\n            controls.autoRotate = true;\n            controls.autoRotateSpeed = 0.5;\n            controls.enableZoom = true;\n\n            \/\/ 5. Build World\n            createParticles();\n            await setupMediaPipe();\n\n            window.addEventListener('resize', onWindowResize);\n            animate();\n        }\n\n        function createParticles() {\n            const geometry = new THREE.BufferGeometry();\n            const positions = new Float32Array(PARTICLE_COUNT * 3);\n            const colors = new Float32Array(PARTICLE_COUNT * 3);\n\n            const sphere = getShapePositions('sphere');\n            const initialColors = getShapeColors('sphere');\n\n            for (let i = 0; i &lt; PARTICLE_COUNT; i++) {\n                positions&#91;i * 3] = sphere&#91;i * 3];\n                positions&#91;i * 3 + 1] = sphere&#91;i * 3 + 1];\n                positions&#91;i * 3 + 2] = sphere&#91;i * 3 + 2];\n\n                colors&#91;i * 3] = initialColors&#91;i * 3];\n                colors&#91;i * 3 + 1] = initialColors&#91;i * 3 + 1];\n                colors&#91;i * 3 + 2] = initialColors&#91;i * 3 + 2];\n            }\n\n            geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n            geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n\n            const sprite = createParticleTexture();\n\n            const material = new THREE.PointsMaterial({\n                size: PARTICLE_SIZE,\n                map: sprite,\n                color: userBaseColor,\n                transparent: true,\n                opacity: 0.8,\n                blending: THREE.AdditiveBlending,\n                depthWrite: false,\n                sizeAttenuation: true,\n                vertexColors: true\n            });\n\n            particles = new THREE.Points(geometry, material);\n            scene.add(particles);\n\n            targetPositions = Float32Array.from(sphere);\n            targetColors = Float32Array.from(initialColors);\n        }\n\n        function createParticleTexture() {\n            const canvas = document.createElement('canvas');\n            canvas.width = 32; canvas.height = 32;\n            const context = canvas.getContext('2d');\n            const gradient = context.createRadialGradient(16, 16, 0, 16, 16, 16);\n            gradient.addColorStop(0, 'rgba(255,255,255,1)');\n            gradient.addColorStop(0.4, 'rgba(255,255,255,0.5)');\n            gradient.addColorStop(1, 'rgba(0,0,0,0)');\n            context.fillStyle = gradient;\n            context.fillRect(0, 0, 32, 32);\n            return new THREE.CanvasTexture(canvas);\n        }\n\n        function getShapeColors(type) {\n            const cols = new Float32Array(PARTICLE_COUNT * 3);\n\n            for (let i = 0; i &lt; PARTICLE_COUNT; i++) {\n                let brightness = 0.2 + Math.random() * 0.8;\n                let r, g, b;\n\n                if (type === 'saturn') {\n                    if (i &lt; PARTICLE_COUNT * SATURN_BODY_RATIO) {\n                        r = 1.0; g = 0.7; b = 0.3;\n                    } else {\n                        r = 0.6; g = 0.8; b = 1.0;\n                    }\n                    r *= brightness; g *= brightness; b *= brightness;\n                } else {\n                    r = brightness; g = brightness; b = brightness;\n                }\n\n                cols&#91;i * 3] = r;\n                cols&#91;i * 3 + 1] = g;\n                cols&#91;i * 3 + 2] = b;\n            }\n            return cols;\n        }\n\n        function getShapePositions(type) {\n            const pos = new Float32Array(PARTICLE_COUNT * 3);\n\n            for (let i = 0; i &lt; PARTICLE_COUNT; i++) {\n                let x, y, z;\n\n                if (type === 'sphere') {\n                    const r = 10 + Math.random() * 2;\n                    const theta = Math.random() * Math.PI * 2;\n                    const phi = Math.acos(2 * Math.random() - 1);\n                    x = r * Math.sin(phi) * Math.cos(theta);\n                    y = r * Math.sin(phi) * Math.sin(theta);\n                    z = r * Math.cos(phi);\n                    if (i &lt; PARTICLE_COUNT * 0.2) { x *= 0.3; y *= 0.3; z *= 0.3; }\n                }\n                else if (type === 'heart') {\n                    const t = Math.PI - 2 * Math.PI * Math.random();\n                    const u = 2 * Math.PI * Math.random();\n                    x = 16 * Math.pow(Math.sin(t), 3);\n                    y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t);\n                    z = 6 * Math.cos(t) * Math.sin(u) * Math.sin(t);\n                    const scale = 0.6;\n                    x *= scale; y *= scale; z *= scale;\n                    if (Math.random() &gt; 0.8) { x *= 1.1; y *= 1.1; z *= 1.1; }\n                }\n                else if (type === 'saturn') {\n                    if (i &lt; PARTICLE_COUNT * SATURN_BODY_RATIO) {\n                        const r = 5.5;\n                        const theta = Math.random() * Math.PI * 2;\n                        const phi = Math.acos(2 * Math.random() - 1);\n                        x = r * Math.sin(phi) * Math.cos(theta);\n                        y = r * 0.9 * Math.sin(phi) * Math.sin(theta);\n                        z = r * Math.cos(phi);\n                    } else {\n                        const angle = Math.random() * Math.PI * 2;\n                        const ringSelector = Math.random();\n                        let r, thickness;\n                        if (ringSelector &lt; 0.45) {\n                            r = 7 + Math.random() * 3.5; thickness = 0.2;\n                        } else if (ringSelector &lt; 0.5) {\n                            r = 10.5 + Math.random() * 1.5; thickness = 0.1;\n                            if (Math.random() &gt; 0.2) { x = 0; y = 0; z = 0; pos&#91;i * 3] = x; pos&#91;i * 3 + 1] = y; pos&#91;i * 3 + 2] = z; continue; }\n                        } else {\n                            r = 12 + Math.random() * 5; thickness = 0.4;\n                        }\n                        r += (Math.random() - 0.5) * 0.3;\n                        x = r * Math.cos(angle);\n                        y = (Math.random() - 0.5) * thickness;\n                        z = r * Math.sin(angle);\n                        const tilt = 0.4;\n                        const y_new = y * Math.cos(tilt) - x * Math.sin(tilt);\n                        const x_new = y * Math.sin(tilt) + x * Math.cos(tilt);\n                        x = x_new; y = y_new;\n                    }\n                }\n                else if (type === 'lotus') {\n                    const u = Math.random() * Math.PI * 2;\n                    const v = Math.random();\n                    const petals = 7;\n                    const rBase = 8 * (0.5 + 0.5 * Math.pow(Math.sin(petals * u * 0.5), 2)) * v;\n                    x = rBase * Math.cos(u);\n                    z = rBase * Math.sin(u);\n                    y = 4 * Math.pow(v, 2) - 2;\n                    if (i &lt; PARTICLE_COUNT * 0.15) { x = (Math.random() - 0.5); z = (Math.random() - 0.5); y = (Math.random() - 0.5) * 10; }\n                }\n                else if (type === 'galaxy') {\n                    const arms = 3;\n                    const spin = i % arms;\n                    const angleOffset = (spin \/ arms) * Math.PI * 2;\n                    const dist = Math.pow(Math.random(), 0.5);\n                    const r = dist * 20;\n                    const angle = dist * 10 + angleOffset;\n                    x = r * Math.cos(angle);\n                    z = r * Math.sin(angle);\n                    y = (Math.random() - 0.5) * (15 - r) * 0.2;\n                    if (r &lt; 2) y *= 0.2;\n                }\n\n                pos&#91;i * 3] = x;\n                pos&#91;i * 3 + 1] = y;\n                pos&#91;i * 3 + 2] = z;\n            }\n            return pos;\n        }\n\n        function animate() {\n            requestAnimationFrame(animate);\n\n            const time = clock.getElapsedTime();\n\n            const positions = particles.geometry.attributes.position.array;\n            const colors = particles.geometry.attributes.color.array;\n\n            let targetScaleBase = 1.0;\n            let targetParticleSize = PARTICLE_SIZE;\n            let turbulence = 0.05;\n\n            \/\/ \u3010\u6a21\u5f0f\u5e94\u7528\u3011\n            if (isHandDetected) {\n                targetScaleBase = 0.2 + (handInfluence * 2.3);\n                turbulence = 0.02 + (handInfluence * 0.3);\n                targetParticleSize = PARTICLE_SIZE * (0.8 + handInfluence * 2.5);\n            } else {\n                targetScaleBase = 1.0 + Math.sin(time * 1.5) * 0.05;\n                targetParticleSize = PARTICLE_SIZE * (1.0 + Math.sin(time * 1.5) * 0.15);\n            }\n\n            if (particles &amp;&amp; particles.material) {\n                particles.material.size = THREE.MathUtils.lerp(particles.material.size, targetParticleSize, 0.1);\n\n                const camRight = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion).normalize();\n                const camUp = new THREE.Vector3(0, 1, 0).applyQuaternion(camera.quaternion).normalize();\n                const camForward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion).normalize();\n\n                particles.rotateOnWorldAxis(camUp, rotationVelocity.y);\n                particles.rotateOnWorldAxis(camRight, rotationVelocity.x);\n                particles.rotateOnWorldAxis(camForward, rotationVelocity.z);\n\n                const MAX_SPEED = 0.06;\n                const MAX_ROLL_SPEED = 0.035;\n\n                rotationVelocity.x = THREE.MathUtils.clamp(rotationVelocity.x, -MAX_SPEED, MAX_SPEED);\n                rotationVelocity.y = THREE.MathUtils.clamp(rotationVelocity.y, -MAX_SPEED, MAX_SPEED);\n                rotationVelocity.z = THREE.MathUtils.clamp(rotationVelocity.z, -MAX_ROLL_SPEED, MAX_ROLL_SPEED);\n\n                rotationVelocity.x *= 0.92;\n                rotationVelocity.y *= 0.92;\n                rotationVelocity.z *= 0.96;\n            }\n\n            const lerpSpeed = 0.06;\n            const colorLerpSpeed = 0.03;\n\n            for (let i = 0; i &lt; PARTICLE_COUNT; i++) {\n                const idx = i * 3;\n\n                let tx = targetPositions&#91;idx];\n                let ty = targetPositions&#91;idx + 1];\n                let tz = targetPositions&#91;idx + 2];\n\n                const dist = Math.sqrt(tx * tx + ty * ty + tz * tz);\n                const normalizedDist = Math.min(dist \/ 20.0, 1.0);\n\n                let distanceExpansionBoost = 1.0;\n                if (isHandDetected) {\n                    distanceExpansionBoost = 1.0 + (handInfluence * Math.pow(normalizedDist, 1.5) * 2.0);\n                }\n\n                const finalScale = targetScaleBase * distanceExpansionBoost;\n                tx *= finalScale; ty *= finalScale; tz *= finalScale;\n\n                if (currentShape === 'galaxy') {\n                    const angle = time * (0.1 + (1.0 - (Math.sqrt(tx * tx + tz * tz) \/ 20)) * 0.5);\n                    const cos = Math.cos(angle); const sin = Math.sin(angle);\n                    const rx = tx * cos - tz * sin;\n                    const rz = tx * sin + tz * cos;\n                    tx = rx; tz = rz;\n                } else if (currentShape === 'lotus') {\n                    ty += Math.sin(time + tx) * 0.5;\n                } else {\n                    tx += Math.sin(time * 2 + i) * turbulence;\n                    ty += Math.cos(time * 3 + i) * turbulence;\n                    tz += Math.sin(time * 4 + i) * turbulence;\n                }\n\n                positions&#91;idx] += (tx - positions&#91;idx]) * lerpSpeed;\n                positions&#91;idx + 1] += (ty - positions&#91;idx + 1]) * lerpSpeed;\n                positions&#91;idx + 2] += (tz - positions&#91;idx + 2]) * lerpSpeed;\n\n                if (targetColors.length &gt; 0) {\n                    colors&#91;idx] += (targetColors&#91;idx] - colors&#91;idx]) * colorLerpSpeed;\n                    colors&#91;idx + 1] += (targetColors&#91;idx + 1] - colors&#91;idx + 1]) * colorLerpSpeed;\n                    colors&#91;idx + 2] += (targetColors&#91;idx + 2] - colors&#91;idx + 2]) * colorLerpSpeed;\n                }\n\n                if (Math.random() &gt; 0.9995) {\n                    colors&#91;idx] = 2.0; colors&#91;idx + 1] = 2.0; colors&#91;idx + 2] = 2.0;\n                }\n                if (colors&#91;idx] &gt; 1.5) {\n                    colors&#91;idx] *= 0.9; colors&#91;idx + 1] *= 0.9; colors&#91;idx + 2] *= 0.9;\n                }\n            }\n\n            particles.geometry.attributes.position.needsUpdate = true;\n            particles.geometry.attributes.color.needsUpdate = true;\n\n            if (!isHandDetected) {\n                controls.autoRotate = true;\n                controls.autoRotateSpeed = 1.0;\n            } else {\n                controls.autoRotate = false;\n            }\n\n            controls.update();\n            composer.render();\n        }\n\n        async function setupMediaPipe() {\n            const videoElement = document.getElementById('video-element');\n            const statusDot = document.getElementById('status-dot');\n            const statusText = document.getElementById('status-text');\n            const modeScaleText = document.getElementById('mode-scale');\n            const modeRotateText = document.getElementById('mode-rotate');\n            const modeRollText = document.getElementById('mode-roll');\n\n            const hands = new Hands({\n                locateFile: (file) =&gt; {\n                    return `https:\/\/cdn.jsdelivr.net\/npm\/@mediapipe\/hands\/${file}`;\n                }\n            });\n\n            hands.setOptions({\n                maxNumHands: 1,\n                modelComplexity: 1,\n                minDetectionConfidence: 0.5,\n                minTrackingConfidence: 0.5\n            });\n\n            hands.onResults((results) =&gt; {\n                const now = Date.now();\n\n                \/\/ \u3010\u4fe1\u53f7\u9632\u6296\u6838\u5fc3\u903b\u8f91\u3011\n                if (results.multiHandLandmarks &amp;&amp; results.multiHandLandmarks.length &gt; 0) {\n                    \/\/ --- \u68c0\u6d4b\u5230\u624b ---\n                    isHandDetected = true;\n                    lastTimeHandDetected = now;\n\n                    statusDot.className = \"w-2 h-2 rounded-full bg-cyan-400 shadow-&#91;0_0_15px_cyan]\";\n                    statusText.innerText = \"LINK ESTABLISHED\";\n                    statusText.className = \"text-&#91;10px] text-cyan-300 font-mono uppercase tracking-wider font-bold\";\n\n                    const landmarks = results.multiHandLandmarks&#91;0];\n\n                    \/\/ ... &#91;\u624b\u52bf\u8bc6\u522b\u903b\u8f91] ...\n                    const getDist = (i, j) =&gt; {\n                        return Math.sqrt(Math.pow(landmarks&#91;i].x - landmarks&#91;j].x, 2) + Math.pow(landmarks&#91;i].y - landmarks&#91;j].y, 2));\n                    };\n\n                    \/\/ \u3010\u6838\u5fc3\u4f18\u5316\uff1a\u624b\u52bf\u5224\u5b9a\u5bbd\u677e\u5316\u3011\n                    \/\/ \u53ea\u8981\u6307\u5c16\u79bb\u624b\u8155\u8db3\u591f\u8fdc\uff0c\u5c31\u8ba4\u4e3a\u662f\u4f38\u76f4\u7684\u3002\n                    \/\/ \u7cfb\u6570 1.1 \u6bd4\u8f83\u5bbd\u677e\uff0c\u786e\u4fdd\u7a0d\u5fae\u5f2f\u4e00\u70b9\u4e5f\u7b97\u4f38\u76f4\n                    const isIndexOpen = getDist(8, 0) &gt; getDist(6, 0) * 1.1;\n                    const isMiddleOpen = getDist(12, 0) &gt; getDist(10, 0) * 1.1;\n\n                    \/\/ \u65e0\u540d\u6307\u548c\u5c0f\u6307\uff1a\u7528\u4e8e\u5224\u5b9a\u662f\u5426\u4e3a Scale \u6a21\u5f0f\n                    \/\/ \u53ea\u8981\u5b83\u4eec\u6709\u4e00\u4e2a\u662f\u660e\u663e\u4f38\u76f4\u7684\uff0c\u5c31\u8ba4\u4e3a\u4e0d\u662f\u53cc\u6307\u6a21\u5f0f\n                    const isRingOpen = getDist(16, 0) &gt; getDist(14, 0) * 1.1;\n                    const isPinkyOpen = getDist(20, 0) &gt; getDist(18, 0) * 1.1;\n\n                    let detectedMode = 'scale';\n\n                    \/\/ \u3010\u4f18\u5148\u7ea7\u903b\u8f91\u4f18\u5316\u3011\n                    \/\/ 1. \u5982\u679c\u65e0\u540d\u6307\u6216\u5c0f\u6307\u662f\u4f38\u76f4\u7684 -&gt; Scale (\u6700\u4f18\u5148\uff0c\u9632\u6b62\u4e94\u6307\u5f20\u5f00\u65f6\u8bef\u5224)\n                    \/\/ 2. \u5982\u679c\u53ea\u6709\u98df\u6307\u548c\u4e2d\u6307\u4f38\u76f4 -&gt; Roll (\u53cc\u6307\u753b\u5708)\n                    \/\/ 3. \u5982\u679c\u53ea\u6709\u98df\u6307\u4f38\u76f4 -&gt; Rotate (\u5355\u6307\u65cb\u8f6c)\n                    \/\/ 4. \u5176\u4ed6\u60c5\u51b5 (\u63e1\u62f3\u7b49) -&gt; Scale\n\n                    if (isRingOpen || isPinkyOpen) {\n                        detectedMode = 'scale';\n                    } else if (isIndexOpen &amp;&amp; isMiddleOpen) {\n                        detectedMode = 'roll';\n                    } else if (isIndexOpen &amp;&amp; !isMiddleOpen) {\n                        detectedMode = 'rotate';\n                    } else {\n                        detectedMode = 'scale'; \/\/ \u63e1\u62f3\u4e5f\u662f Scale (\u6536\u7f29)\n                    }\n\n                    if (detectedMode === currentStableMode) {\n                        modeFrameCounter = 0;\n                    } else {\n                        modeFrameCounter++;\n                        \/\/ \u51cf\u5c11\u5207\u6362\u9608\u503c\uff0c\u8ba9\u53cd\u5e94\u66f4\u5feb (4\u5e27\u7ea6 60ms)\n                        let switchThreshold = 4;\n\n                        if (modeFrameCounter &gt; switchThreshold) {\n                            currentStableMode = detectedMode;\n                            modeFrameCounter = 0;\n                            isTrackingRotation = false;\n                            isTrackingRoll = false;\n                            previousFingerPos = { x: 0, y: 0 };\n                        }\n                    }\n\n                    modeScaleText.classList.remove('active');\n                    modeRotateText.classList.remove('active');\n                    modeRollText.classList.remove('active');\n                    if (currentStableMode === 'roll') modeRollText.classList.add('active');\n                    else if (currentStableMode === 'rotate') modeRotateText.classList.add('active');\n                    else modeScaleText.classList.add('active');\n\n                    if (currentStableMode === 'roll') {\n                        \/\/ \u3010\u5e73\u9762\u65cb\u8f6c - \u7edd\u5bf9\u89d2\u5ea6\u8ddf\u624b\u7248\u3011\n                        const rawIndex = landmarks&#91;8];\n                        const rawMiddle = landmarks&#91;12];\n\n                        \/\/ 1. \u4f4e\u901a\u6ee4\u6ce2 (\u5e73\u6ed1\u5ea6 0.2, \u517c\u987e\u54cd\u5e94\u548c\u7a33\u5b9a)\n                        if (!isTrackingRoll) {\n                            smoothedIndexTip = { x: rawIndex.x, y: rawIndex.y };\n                            smoothedMiddleTip = { x: rawMiddle.x, y: rawMiddle.y };\n                            \/\/ \u521d\u59cb\u5316\u89d2\u5ea6\n                            const dx = rawIndex.x - rawMiddle.x;\n                            const dy = rawIndex.y - rawMiddle.y;\n                            previousRollAngle = Math.atan2(dy, dx);\n                            isTrackingRoll = true;\n                        } else {\n                            const lerpFactor = 0.2;\n                            smoothedIndexTip.x = THREE.MathUtils.lerp(smoothedIndexTip.x, rawIndex.x, lerpFactor);\n                            smoothedIndexTip.y = THREE.MathUtils.lerp(smoothedIndexTip.y, rawIndex.y, lerpFactor);\n                            smoothedMiddleTip.x = THREE.MathUtils.lerp(smoothedMiddleTip.x, rawMiddle.x, lerpFactor);\n                            smoothedMiddleTip.y = THREE.MathUtils.lerp(smoothedMiddleTip.y, rawMiddle.y, lerpFactor);\n                        }\n\n                        \/\/ 2. \u8ba1\u7b97\u5f53\u524d\u89d2\u5ea6 (Between fingers)\n                        const dx = smoothedIndexTip.x - smoothedMiddleTip.x;\n                        const dy = smoothedIndexTip.y - smoothedMiddleTip.y;\n                        const currentAngle = Math.atan2(dy, dx);\n\n                        let deltaAngle = currentAngle - previousRollAngle;\n\n                        \/\/ \u5904\u7406 -PI \u5230 PI \u7684\u8df3\u53d8\n                        if (deltaAngle &gt; Math.PI) deltaAngle -= 2 * Math.PI;\n                        if (deltaAngle &lt; -Math.PI) deltaAngle += 2 * Math.PI;\n\n                        \/\/ \u4fee\u6b63\u7cfb\u6570\uff1a-1.5 (\u8d1f\u53f7\u4fee\u6b63\u955c\u50cf\uff0c1.5\u653e\u5927\u624b\u611f)\n                        const inputVelocity = -deltaAngle * 1.5;\n\n                        \/\/ \u6df7\u5408\u8f93\u5165\u901f\u5ea6\u548c\u5f53\u524d\u60ef\u6027\uff0c\u8ba9\u5b83\u65e2\u8ddf\u624b\u53c8\u6709\u91cd\u91cf\n                        rotationVelocity.z = THREE.MathUtils.lerp(rotationVelocity.z, inputVelocity, 0.3);\n\n                        previousRollAngle = currentAngle;\n                        isTrackingRotation = false;\n\n                    } else if (currentStableMode === 'rotate') {\n                        const rawFingerX = landmarks&#91;8].x;\n                        const rawFingerY = landmarks&#91;8].y;\n\n                        if (!isTrackingRotation) {\n                            smoothedFingerPos = { x: rawFingerX, y: rawFingerY };\n                            previousFingerPos = { x: rawFingerX, y: rawFingerY };\n                            isTrackingRotation = true;\n                        }\n\n                        const smoothingFactor = 0.2;\n                        smoothedFingerPos.x = THREE.MathUtils.lerp(smoothedFingerPos.x, rawFingerX, smoothingFactor);\n                        smoothedFingerPos.y = THREE.MathUtils.lerp(smoothedFingerPos.y, rawFingerY, smoothingFactor);\n\n                        const deltaX = smoothedFingerPos.x - previousFingerPos.x;\n                        const deltaY = smoothedFingerPos.y - previousFingerPos.y;\n\n                        const MOVE_THRESHOLD = 0.005;\n\n                        if (Math.abs(deltaX) &gt; MOVE_THRESHOLD || Math.abs(deltaY) &gt; MOVE_THRESHOLD) {\n                            const sensitivity = 3.5;\n                            if (Math.abs(deltaX) &gt; Math.abs(deltaY)) {\n                                const effectiveX = deltaX &gt; 0 ? deltaX - MOVE_THRESHOLD : deltaX + MOVE_THRESHOLD;\n                                rotationVelocity.y -= effectiveX * sensitivity;\n                            } else {\n                                const effectiveY = deltaY &gt; 0 ? deltaY - MOVE_THRESHOLD : deltaY + MOVE_THRESHOLD;\n                                rotationVelocity.x += effectiveY * sensitivity;\n                            }\n                        }\n                        previousFingerPos = { x: smoothedFingerPos.x, y: smoothedFingerPos.y };\n                        isTrackingRoll = false;\n\n                    } else {\n                        \/\/ \u3010\u7f29\u653e\u6a21\u5f0f\u3011\n                        if (isTrackingRoll) { isTrackingRoll = false; }\n                        isTrackingRotation = false;\n\n                        const tips = &#91;4, 8, 12, 16, 20];\n                        let totalDist = 0;\n                        tips.forEach(idx =&gt; {\n                            totalDist += getDist(idx, 0);\n                        });\n\n                        const avgDist = totalDist \/ 5;\n                        let rawOpenness = (avgDist - 0.08) \/ (0.4 - 0.08);\n                        rawOpenness = Math.max(0, Math.min(1, rawOpenness));\n\n                        handInfluence = THREE.MathUtils.lerp(handInfluence, rawOpenness, 0.2);\n                    }\n\n                } else {\n                    \/\/ --- \u672a\u68c0\u6d4b\u5230\u624b ---\n                    if (now - lastTimeHandDetected &lt; 800) {\n                        return;\n                    }\n\n                    isHandDetected = false;\n                    isTrackingRotation = false;\n                    isTrackingRoll = false;\n                    modeFrameCounter = 0;\n                    currentStableMode = 'scale';\n\n                    statusDot.className = \"w-2 h-2 rounded-full bg-red-500 shadow-&#91;0_0_10px_red]\";\n                    statusText.innerText = \"SEARCHING SIGNAL...\";\n                    statusText.className = \"text-&#91;10px] text-red-400 font-mono uppercase tracking-wider animate-pulse\";\n\n                    handInfluence = THREE.MathUtils.lerp(handInfluence, 0.5, 0.02);\n\n                    modeScaleText.classList.remove('active');\n                    modeRotateText.classList.remove('active');\n                    modeRollText.classList.remove('active');\n                }\n            });\n\n            const cameraUtils = new Camera(videoElement, {\n                onFrame: async () =&gt; { await hands.send({ image: videoElement }); },\n                width: 640, height: 480\n            });\n\n            try {\n                await cameraUtils.start();\n                const loader = document.getElementById('loader-overlay');\n                loader.style.opacity = '0';\n                setTimeout(() =&gt; loader.remove(), 800);\n            } catch (err) {\n                console.error(err);\n                statusText.innerText = \"CAMERA ERROR\";\n            }\n        }\n\n        window.onWindowResize = () =&gt; {\n            camera.aspect = window.innerWidth \/ window.innerHeight;\n            camera.updateProjectionMatrix();\n            renderer.setSize(window.innerWidth, window.innerHeight);\n            composer.setSize(window.innerWidth, window.innerHeight);\n        };\n\n        window.changeShape = (shape) =&gt; {\n            currentShape = shape;\n            targetPositions = getShapePositions(shape);\n            targetColors = getShapeColors(shape);\n\n            if (shape === 'saturn') {\n                const white = new THREE.Color(0xffffff);\n                new TWEEN.Tween(particles.material.color)\n                    .to({ r: 1, g: 1, b: 1 }, 500)\n                    .start();\n            } else {\n                new TWEEN.Tween(particles.material.color)\n                    .to({ r: userBaseColor.r, g: userBaseColor.g, b: userBaseColor.b }, 500)\n                    .start();\n            }\n\n            document.querySelectorAll('&#91;data-shape]').forEach(btn =&gt; {\n                btn.classList.remove('active');\n                if (btn.dataset.shape === shape) btn.classList.add('active');\n            });\n        };\n\n        window.updateColor = (hex) =&gt; {\n            const c = new THREE.Color(hex);\n            userBaseColor = c;\n\n            if (particles) {\n                if (currentShape !== 'saturn') {\n                    const initial = particles.material.color.clone();\n                    let alpha = 0;\n                    const anim = () =&gt; {\n                        alpha += 0.05;\n                        particles.material.color.lerpColors(initial, c, alpha);\n                        if (alpha &lt; 1) requestAnimationFrame(anim);\n                    };\n                    anim();\n                }\n            }\n        };\n\n        window.toggleFullScreen = () =&gt; {\n            if (!document.fullscreenElement) document.documentElement.requestFullscreen();\n            else if (document.exitFullscreen) document.exitFullscreen();\n        };\n\n        const TWEEN = {\n            Tween: function (obj) {\n                this.obj = obj;\n                this.target = {};\n                this.duration = 1000;\n                this.startTime = 0;\n                this.to = function (target, duration) { this.target = target; this.duration = duration; return this; };\n                this.start = function () { this.startTime = performance.now(); this.initial = { r: this.obj.r, g: this.obj.g, b: this.obj.b }; requestAnimationFrame(this.update.bind(this)); return this; };\n                this.update = function (time) {\n                    const elapsed = time - this.startTime;\n                    const progress = Math.min(elapsed \/ this.duration, 1);\n                    const ease = 1 - Math.pow(1 - progress, 3);\n                    this.obj.r = this.initial.r + (this.target.r - this.initial.r) * ease;\n                    this.obj.g = this.initial.g + (this.target.g - this.initial.g) * ease;\n                    this.obj.b = this.initial.b + (this.target.b - this.initial.b) * ease;\n                    if (progress &lt; 1) { requestAnimationFrame(this.update.bind(this)); }\n                };\n            }\n        };\n\n        init();\n    &lt;\/script&gt;\n&lt;\/body&gt;\n\n&lt;\/html&gt;<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>\u70b9\u51fb\u6d4b\u8bd5 \u5723\u8bde\u6811 \u8fd9\u4e2a3D\u7c92\u5b50\u7cfb\u7edf\u662f\u901a\u8fc7gemini3\u5b9e\u73b0\u7684\uff0c\u53ea\u9700\u8981\u8f93\u5165\u4e0b\u9762\u7684\u63d0\u793a\u8bcd\u5373\u53ef\u751f\u6210\u3002\u5177\u4f53\u7ec6\u8282\u53ef\u53c2\u8003\u4e0a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":458,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,4],"tags":[],"class_list":["post-455","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","category-note"],"_links":{"self":[{"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/posts\/455","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/comments?post=455"}],"version-history":[{"count":5,"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/posts\/455\/revisions"}],"predecessor-version":[{"id":481,"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/posts\/455\/revisions\/481"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/media\/458"}],"wp:attachment":[{"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/media?parent=455"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/categories?post=455"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jianbo.site\/index.php\/wp-json\/wp\/v2\/tags?post=455"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}