空痕博客 - 编程学习分享

粒子特效圣诞祝福HTML页面模板及AI提示词

KongHen02
2天前发布 /正在检测是否收录...

编写背景

最近刷抖音时,频繁刷到用 AI 编写炫酷的 3D+AI 手势交互粒子动画代码的视频。这类动画视觉层次细腻、动态效果灵动,已成内容创作新风口。作品里代码与艺术交融,让静态粒子汇聚成鲜活形态,既彰显前端技术魅力,又极具情感传递力,也让我萌生将这一技术潮流与即将到来的圣诞节结合的想法。

圣诞节满是温暖与祝福,传统贺卡、海报早已满足不了当下年轻人对 “仪式感 + 科技感” 的双重需求。大家更渴望可互动、可分享、能直观感知心意的祝福形式 —— 粒子特效恰好是绝佳载体:无数微小题粒子从混沌归为有序,最终汇聚成圣诞树形态,这一过程恰似 “思念与祝福凝聚成真” 的隐喻;而飘落的雪花、滚动的弹幕,更能为这份祝福增添沉浸式节日氛围。

恰逢 AI 开发工具日趋成熟,技术实现效率大幅提升。我选用Trae+GPT-5,基于 Vue3 与 Three.js 开发了这款单文件 HTML 页面,无需安装、点开即玩,让每个打开链接的人,都能在粒子汇聚的瞬间,感受圣诞的惊喜与温暖。

站点演示

点击观看演示

演示图片

页面代码

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
    <title>圣诞快乐</title>
    <style>
      * { box-sizing: border-box; }
      html, body { height: 100%; }
      body { margin: 0; overflow: hidden; background: #000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, PingFangSC, "Microsoft YaHei", sans-serif; }
      #app { position: relative; width: 100vw; height: 100vh; }
      #canvas-container { position: absolute; inset: 0; }
      
      .barrage { position: fixed; inset: 0; pointer-events: none; z-index: 9; }
      .barrage-item { position: fixed; left: 100vw; white-space: nowrap; font-weight: 700; text-shadow: 0 0 8px rgba(255,255,255,0.35), 0 0 16px rgba(255,255,255,0.2); filter: drop-shadow(0 2px 4px rgba(0,0,0,0.35)); animation-name: fly; animation-timing-function: linear; animation-fill-mode: forwards; }
      @keyframes fly { 0% { transform: translateX(0); } 100% { transform: translateX(-120vw); } }
      .fullscreen-btn { position: fixed; right: 14px; bottom: 14px; z-index: 11; padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.12); color: #fff; font-weight: 600; letter-spacing: 0.5px; backdrop-filter: blur(4px); cursor: pointer; }
      .fullscreen-btn:hover { background: rgba(255,255,255,0.18); }
      @media (max-width: 768px) { .fullscreen-btn { padding: 9px 12px; right: 10px; bottom: 10px; font-size: 13px; } }
      @media (max-width: 768px) {
        .barrage-item { font-size: clamp(14px, 3.5vw, 20px); }
      }
      @media (min-width: 769px) {
        .barrage-item { font-size: clamp(16px, 2.1vw, 28px); }
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script type="module">
      import { createApp, ref, onMounted, h } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
      import * as THREE from 'https://unpkg.com/three@0.164.1/build/three.module.js';

      const App = {
        setup() {
          const isFs = ref(false);
          const barrageItems = ref([]);
          const containerEl = ref(null);

          let renderer, scene, camera, group;
          let geoLeaves, matLeaves, pointsLeaves, posLeaves, colLeaves, velLeaves, tgtLeaves, startMorphLeaves;
          let geoOrnaments, matOrnaments, pointsOrnaments, posOrnaments, colOrnaments, velOrnaments, tgtOrnaments, startMorphOrnaments;
          let geoTrunk, matTrunk, pointsTrunk, posTrunk, colTrunk, velTrunk, tgtTrunk, startMorphTrunk;
          let geoStar, matStar, pointsStar, posStar, colStar, velStar, tgtStar, startMorphStar;
          let geoAccents, matAccents, pointsAccents, posAccents, colAccents, velAccents, tgtAccents, startMorphAccents;
          let snowGroup, snowSprites, snowVel, snowRot, snowScale, snowOpacityPhase;
          let startTime = 0;
          const disperseDuration = 3500;
          const morphDuration = 3000;
          const particleCountLeaves = 5400;
          const particleCountOrnaments = 400;
          const particleCountTrunk = 1200;
          const particleCountStar = 320;
          const particleCountAccents = 160;
          const snowCount = 200;
          const treeHeight = 14;
          const baseRadius = 6.2;
          let running = true;
          const topY = treeHeight + 1.2;
          const bottomY = -1.4;

          const messages = [
            '圣诞快乐', '平安喜乐', '万事胜意', '心想事成', '前程似锦', '阖家幸福',
            '福星高照', '岁岁常欢愉', '诸事顺遂', '新年好运常在', '健康平安', '幸福常伴'
          ];

          function randomHsl() {
            const h = Math.floor(Math.random() * 360);
            const s = 68 + Math.random() * 22;
            const l = 50 + Math.random() * 10;
            return `hsl(${h}deg, ${s}%, ${l}%)`;
          }

          function createSnowflakeTexture() {
            const c = document.createElement('canvas');
            c.width = 128; c.height = 128;
            const ctx = c.getContext('2d');
            ctx.clearRect(0, 0, 128, 128);
            ctx.translate(64, 64);
            ctx.strokeStyle = 'rgba(255,255,255,0.95)';
            ctx.lineWidth = 2;
            for (let i = 0; i < 6; i++) {
              const ang = (Math.PI * 2 / 6) * i;
              const x = Math.cos(ang) * 48;
              const y = Math.sin(ang) * 48;
              ctx.beginPath();
              ctx.moveTo(0, 0);
              ctx.lineTo(x, y);
              ctx.stroke();
              for (let b = 1; b <= 3; b++) {
                const t = b / 4;
                const bx = Math.cos(ang) * 48 * t;
                const by = Math.sin(ang) * 48 * t;
                const sideAng = ang + Math.PI / 2;
                const len = 10 * (1 - t);
                ctx.beginPath();
                ctx.moveTo(bx, by);
                ctx.lineTo(bx + Math.cos(sideAng) * len, by + Math.sin(sideAng) * len);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(bx, by);
                ctx.lineTo(bx - Math.cos(sideAng) * len, by - Math.sin(sideAng) * len);
                ctx.stroke();
              }
            }
            const tex = new THREE.CanvasTexture(c);
            tex.minFilter = THREE.LinearMipmapLinearFilter;
            tex.magFilter = THREE.LinearFilter;
            tex.needsUpdate = true;
            return tex;
          }

          function createSnowGlowTexture() {
            const c = document.createElement('canvas');
            c.width = 128; c.height = 128;
            const ctx = c.getContext('2d');
            const grd = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
            grd.addColorStop(0, 'rgba(255,255,255,0.57)');
            grd.addColorStop(0.4, 'rgba(255,255,255,0.23)');
            grd.addColorStop(1, 'rgba(255,255,255,0.0)');
            ctx.fillStyle = grd;
            ctx.fillRect(0, 0, 128, 128);
            const tex = new THREE.CanvasTexture(c);
            tex.minFilter = THREE.LinearMipmapLinearFilter;
            tex.magFilter = THREE.LinearFilter;
            tex.needsUpdate = true;
            return tex;
          }

          function spawnBarrage() {
            const count = 2 + Math.floor(Math.random() * 3);
            const now = Date.now();
            for (let i = 0; i < count; i++) {
              const text = messages[Math.floor(Math.random() * messages.length)];
              const top = Math.random() * 90 + 5;
              const duration = 10 + Math.random() * 8;
              const color = randomHsl();
              const id = `${now}-${i}-${Math.random().toString(36).slice(2,7)}`;
              barrageItems.value.push({ id, text, top: `${top}%`, duration: `${duration}s`, color, shadow: color });
              setTimeout(() => {
                const idx = barrageItems.value.findIndex(x => x.id === id);
                if (idx >= 0) barrageItems.value.splice(idx, 1);
              }, duration * 1000 + 400);
            }
          }

          function enterFs() {
            const el = document.getElementById('app') || containerEl.value || document.documentElement;
            const rfs = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen;
            if (rfs) rfs.call(el);
          }

          function exitFs() {
            const efs = document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen;
            if (efs) efs.call(document);
          }

          function toggleFs() { if (document.fullscreenElement) exitFs(); else enterFs(); }

          function computeLeafTargets(count) {
            const arr = new Float32Array(count * 3);
            const layers = 12;
            for (let i = 0; i < count; i++) {
              const layer = Math.floor(Math.random() * layers);
              const yBase = (layer / (layers - 1)) * treeHeight;
              const y = yBase + (Math.random() - 0.5) * (treeHeight * 0.035 + (1 - layer / layers) * 0.12);
              const t = y / treeHeight;
              const shelf = 0.45 * Math.sin(3.5 * (1 - t));
              const wobble = 0.25 * Math.sin(8 * t + Math.random() * 0.8);
              const r = Math.max(0.04, (baseRadius * (1 - t)) + shelf + wobble);
              const a = Math.random() * Math.PI * 2;
              const x = r * Math.cos(a);
              const z = r * Math.sin(a);
              arr[i * 3] = x;
              arr[i * 3 + 1] = y;
              arr[i * 3 + 2] = z;
            }
            return arr;
          }

          function computeTrunkTargets(count) {
            const arr = new Float32Array(count * 3);
            for (let i = 0; i < count; i++) {
              const ty = -1 + Math.random() * (treeHeight * 0.22);
              const tr = 0.35 + Math.random() * 0.22;
              const ta = Math.random() * Math.PI * 2;
              arr[i * 3] = tr * Math.cos(ta);
              arr[i * 3 + 1] = ty;
              arr[i * 3 + 2] = tr * Math.sin(ta);
            }
            return arr;
          }

          function computeOrnamentTargets(count) {
            const arr = new Float32Array(count * 3);
            for (let i = 0; i < count; i++) {
              const y = Math.random() * treeHeight * 0.95;
              const t = y / treeHeight;
              const r = Math.max(0.05, (baseRadius * (1 - t)) + 0.3 * Math.sin(7 * t + Math.random()));
              const a = Math.random() * Math.PI * 2;
              const x = r * Math.cos(a);
              const z = r * Math.sin(a);
              arr[i * 3] = x;
              arr[i * 3 + 1] = y;
              arr[i * 3 + 2] = z;
            }
            return arr;
          }

          function computeStarTargets(count) {
            const arr = new Float32Array(count * 3);
            for (let i = 0; i < count; i++) {
              const a = Math.random() * Math.PI * 2;
              const rBase = 0.75;
              const r = rBase + 0.25 * Math.cos(5 * a) + (Math.random() - 0.5) * 0.12;
              const x = r * Math.cos(a);
              const z = r * Math.sin(a);
              const y = treeHeight + 0.9 + (Math.random() - 0.5) * 0.35;
              arr[i * 3] = x;
              arr[i * 3 + 1] = y;
              arr[i * 3 + 2] = z;
            }
            return arr;
          }

          function computeAccentsTargets(count) {
            const arr = new Float32Array(count * 3);
            for (let i = 0; i < count; i++) {
              const y = Math.random() * treeHeight;
              const t = y / treeHeight;
              const r = Math.max(0.05, (baseRadius * (1 - t)) + 0.35 * Math.sin(6 * t + Math.random()));
              const a = Math.random() * Math.PI * 2;
              const x = r * Math.cos(a);
              const z = r * Math.sin(a);
              arr[i * 3] = x;
              arr[i * 3 + 1] = y;
              arr[i * 3 + 2] = z;
            }
            return arr;
          }

          function easeInOutCubic(t) {
            return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
          }

          function ornamentColors(index) {
            const palette = [
              new THREE.Color(0xff4d6d), new THREE.Color(0xffb703), new THREE.Color(0x32c3ff),
              new THREE.Color(0x8ce99a), new THREE.Color(0xb197fc), new THREE.Color(0xffaad4)
            ];
            return palette[index % palette.length];
          }


          function initSet(count, size, colorFn, computeTargets) {
            const geo = new THREE.BufferGeometry();
            const pos = new Float32Array(count * 3);
            const col = new Float32Array(count * 3);
            const vel = new Float32Array(count * 3);
            for (let i = 0; i < count; i++) {
              const ix = i * 3;
              pos[ix] = (Math.random() - 0.5) * 0.4;
              pos[ix + 1] = (Math.random() - 0.5) * 0.4;
              pos[ix + 2] = (Math.random() - 0.5) * 0.4;
              let vx = (Math.random() - 0.5);
              let vy = (Math.random() - 0.5) * 0.6;
              let vz = (Math.random() - 0.5);
              const s = 0.012 + Math.random() * 0.018;
              vel[ix] = vx * s;
              vel[ix + 1] = vy * s;
              vel[ix + 2] = vz * s;
              const c = colorFn(i);
              col[ix] = c.r;
              col[ix + 1] = c.g;
              col[ix + 2] = c.b;
            }
            geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
            geo.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
            const mat = new THREE.PointsMaterial({ size, sizeAttenuation: true, transparent: true, opacity: 0.95, vertexColors: true, blending: THREE.AdditiveBlending, depthWrite: false });
            const points = new THREE.Points(geo, mat);
            const targets = computeTargets(count);
            group.add(points);
            return { geo, pos, col, vel, mat, points, targets };
          }

          function initThree() {
            scene = new THREE.Scene();
            scene.fog = new THREE.FogExp2(0x06101a, 0.035);
            camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.set(0, 6.5, 20);
            renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.outputColorSpace = THREE.SRGBColorSpace;
            containerEl.value.appendChild(renderer.domElement);

            group = new THREE.Group();
            scene.add(group);

            const ambient = new THREE.AmbientLight(0xffffff, 0.35);
            scene.add(ambient);
            const point = new THREE.PointLight(0xfff3e0, 3.2, 100);
            point.position.set(4, treeHeight + 2, 6);
            scene.add(point);
            ({ geo: geoLeaves, pos: posLeaves, col: colLeaves, vel: velLeaves, mat: matLeaves, points: pointsLeaves, targets: tgtLeaves } = initSet(
              particleCountLeaves,
              0.12,
              () => {
                const g = 0.35 + Math.random() * 0.65;
                const r = 0.2 + 0.8 * Math.random();
                const green = 0.5 + 0.5 * Math.random();
                return new THREE.Color(r * g, green * g, 0.2 * g);
              },
              computeLeafTargets
            ));

            ({ geo: geoOrnaments, pos: posOrnaments, col: colOrnaments, vel: velOrnaments, mat: matOrnaments, points: pointsOrnaments, targets: tgtOrnaments } = initSet(
              particleCountOrnaments,
              0.14,
              (i) => ornamentColors(i),
              computeOrnamentTargets
            ));

            ({ geo: geoTrunk, pos: posTrunk, col: colTrunk, vel: velTrunk, mat: matTrunk, points: pointsTrunk, targets: tgtTrunk } = initSet(
              particleCountTrunk,
              0.12,
              () => new THREE.Color(0.36, 0.24, 0.18),
              computeTrunkTargets
            ));
            ({ geo: geoStar, pos: posStar, col: colStar, vel: velStar, mat: matStar, points: pointsStar, targets: tgtStar } = initSet(
              particleCountStar,
              0.12,
              () => new THREE.Color(1.0, 0.83, 0.3),
              computeStarTargets
            ));
            ({ geo: geoAccents, pos: posAccents, col: colAccents, vel: velAccents, mat: matAccents, points: pointsAccents, targets: tgtAccents } = initSet(
              particleCountAccents,
              0.22,
              (i) => ornamentColors(i + 3),
              computeAccentsTargets
            ));

            const snowTex = createSnowflakeTexture();
            const glowTex = createSnowGlowTexture();
            snowGroup = new THREE.Group();
            snowGroup.renderOrder = 0;
            scene.add(snowGroup);
            snowSprites = new Array(snowCount);
            const snowHalos = new Array(snowCount);
            snowVel = new Float32Array(snowCount * 2);
            snowRot = new Float32Array(snowCount);
            snowScale = new Float32Array(snowCount);
            snowOpacityPhase = new Float32Array(snowCount);
            for (let i = 0; i < snowCount; i++) {
              const mat = new THREE.SpriteMaterial({ map: snowTex, color: 0xffffff, transparent: true, opacity: 1.0, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, fog: false });
              const spr = new THREE.Sprite(mat);
              const s = (1.6 + Math.random() * 1.6) * (2/3);
              spr.scale.set(s, s, 1);
              const x = (Math.random() - 0.5) * 120;
              const y = 20 + Math.random() * 40;
              const z = -22 - Math.random() * 12;
              spr.position.set(x, y, z);
              snowScale[i] = s;
              snowVel[i * 2] = (Math.random() - 0.5) * 0.006;
              snowVel[i * 2 + 1] = -0.008 - Math.random() * 0.01;
              snowRot[i] = (-0.004 + Math.random() * 0.008);
              snowOpacityPhase[i] = Math.random() * Math.PI * 2;
              snowSprites[i] = spr;
              snowGroup.add(spr);
              const haloMat = new THREE.SpriteMaterial({ map: glowTex, color: 0xffffff, transparent: true, opacity: 0.6, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, fog: false });
              const halo = new THREE.Sprite(haloMat);
              halo.scale.set(s * 2.6, s * 2.6, 1);
              halo.position.copy(spr.position);
              snowHalos[i] = halo;
              snowGroup.add(halo);
            }
            pointsLeaves.renderOrder = 1;
            pointsOrnaments.renderOrder = 1;
            pointsTrunk.renderOrder = 1;
            pointsStar.renderOrder = 1;
            pointsAccents.renderOrder = 1;
            fitCamera();
            startTime = performance.now();
            animate();
            window.addEventListener('resize', onResize);
          }

          function animate() {
            if (!running) return;
            const now = performance.now();
            const elapsed = now - startTime;
            function updateSet(geo, posArr, velArr, startMorphArr, targetsArr, count) {
              const attr = geo.getAttribute('position');
              const arr = attr.array;
              if (elapsed < disperseDuration) {
                const dt = 16;
                for (let i = 0; i < count; i++) {
                  const ix = i * 3;
                  arr[ix] += velArr[ix] * dt;
                  arr[ix + 1] += velArr[ix + 1] * dt;
                  arr[ix + 2] += velArr[ix + 2] * dt;
                }
              } else {
              if (!startMorphArr) {
                  startMorphArr = Float32Array.from(arr);
                  if (geo === geoLeaves) startMorphLeaves = startMorphArr;
                  if (geo === geoOrnaments) startMorphOrnaments = startMorphArr;
                  if (geo === geoTrunk) startMorphTrunk = startMorphArr;
                  if (geo === geoStar) startMorphStar = startMorphArr;
                  if (geo === geoAccents) startMorphAccents = startMorphArr;
                }
                const t = Math.min(1, (elapsed - disperseDuration) / morphDuration);
                const e = easeInOutCubic(t);
                for (let i = 0; i < count; i++) {
                  const ix = i * 3;
                  arr[ix] = startMorphArr[ix] + (targetsArr[ix] - startMorphArr[ix]) * e;
                  arr[ix + 1] = startMorphArr[ix + 1] + (targetsArr[ix + 1] - startMorphArr[ix + 1]) * e;
                  arr[ix + 2] = startMorphArr[ix + 2] + (targetsArr[ix + 2] - startMorphArr[ix + 2]) * e;
                }
              }
              attr.needsUpdate = true;
            }

            const dt = 16;
            for (let i = 0; i < snowCount; i++) {
              const spr = snowSprites[i];
              spr.position.x += snowVel[i * 2] * dt;
              spr.position.y += snowVel[i * 2 + 1] * dt;
              spr.material.rotation += snowRot[i];
              const f = Math.min(1.0, 0.9 + 0.45 * Math.abs(Math.sin(now * 0.0015 + snowOpacityPhase[i])));
              spr.material.opacity = 0.67 * f;
              const halo = snowGroup.children[(i * 2) + 1];
              if (halo) {
                halo.position.copy(spr.position);
                halo.material.opacity = Math.min(1.0, 0.47 * f);
              }
              if (spr.position.y < -30) {
                spr.position.set((Math.random() - 0.5) * 120, 20 + Math.random() * 40, -22 - Math.random() * 12);
                spr.material.rotation = Math.random() * Math.PI * 2;
                if (halo) {
                  halo.position.copy(spr.position);
                  halo.material.rotation = spr.material.rotation;
                }
              }
            }

            updateSet(geoLeaves, posLeaves, velLeaves, startMorphLeaves, tgtLeaves, particleCountLeaves);
            updateSet(geoOrnaments, posOrnaments, velOrnaments, startMorphOrnaments, tgtOrnaments, particleCountOrnaments);
            updateSet(geoTrunk, posTrunk, velTrunk, startMorphTrunk, tgtTrunk, particleCountTrunk);
            updateSet(geoStar, posStar, velStar, startMorphStar, tgtStar, particleCountStar);
            updateSet(geoAccents, posAccents, velAccents, startMorphAccents, tgtAccents, particleCountAccents);
            group.rotation.y += 0.0032;
            renderer.render(scene, camera);
            requestAnimationFrame(animate);
          }

          function onResize() {
            const w = window.innerWidth;
            const h = window.innerHeight;
            camera.aspect = w / h;
            camera.updateProjectionMatrix();
            renderer.setSize(w, h);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            fitCamera();
          }

          function fitCamera() {
            const aspect = camera.aspect;
            const vfov = THREE.MathUtils.degToRad(camera.fov);
            const halfH = (topY - bottomY) * 0.5 * 1.06;
            const radNeeded = (baseRadius + 1.6) * 1.06;
            const zH = halfH / Math.tan(vfov / 2);
            const zW = radNeeded / (Math.tan(vfov / 2) * aspect);
            const z = Math.max(zH, zW);
            camera.position.z = Math.max(z, 20);
            const centerY = (topY + bottomY) * 0.5;
            camera.position.y = centerY;
            camera.lookAt(0, centerY, 0);
          }

          onMounted(() => {
            const el = document.createElement('div');
            el.id = 'canvas-container';
            document.getElementById('app').appendChild(el);
            containerEl.value = el;
            initThree();
            setInterval(spawnBarrage, 1200);
            document.addEventListener('fullscreenchange', () => { isFs.value = !!document.fullscreenElement; });
          });

          return () => h('div', { style: { width: '100%', height: '100%' } }, [
            h('div', { class: 'barrage' }, barrageItems.value.map(item => h('div', {
              key: item.id,
              class: 'barrage-item',
              style: {
                top: item.top,
                color: item.color,
                animationDuration: item.duration,
              }
            }, item.text))),
            h('button', { class: 'fullscreen-btn', onClick: toggleFs }, isFs.value ? '退出全屏' : '全屏'),
          ]);
        }
      };

      createApp(App).mount('#app');
    </script>
  </body>
</html>

AI提示词

- 你是一名资深前端与图形工程师,使用 Vue3 与 Three.js 的 CDN 版本构建单文件 HTML(内联 CSS 与 `<script type="module">`)。
- 不使用打包工具或额外依赖;仅加载官方 CDN;遵循安全与性能最佳实践。
- 三维场景包含 Scene、PerspectiveCamera、WebGLRenderer,启用 SRGB、雾效与基础光源;渲染器像素比限制在合理范围。
- 所有效果基于独立粒子或精灵实现:分阶段发散→缓动汇聚为分层圣诞树,整组缓慢自转;背景雪花为 Sprite(含光晕),不遮挡主体;弹幕为 CSS 动画层。
- 移动端与 PC 适配:初始化与窗口缩放时相机自适应,保证树体在不同纵横比下完整可见。

**请按以下要求帮我编写代码**
- 使用 Vue3 + Three.js(CDN ESM)输出一个完整单文件 HTML 页面,包含:
  - 粒子动画:从中心向外发散,再缓动汇聚为“分层”圣诞树;整体缓慢自转
  - 粒子分集:树冠、主干、挂饰、顶部装饰与点缀均用粒子或精灵实现,并参与统一发散/汇聚
  - 背景雪花:Sprite 雪花纹理与柔光光晕;轻微旋转、漂移与闪烁;层级最低且不遮挡树体
  - 弹幕:多条、色彩丰富,从右向左飘过,位于画布上层
  - 适配:相机自适应保证在移动端与 PC 均完整显示树体,`resize` 时更新
  - 功能:右上角有一个全屏按钮,点击按钮页面全屏显示
- 禁用外部依赖与打包,保持代码清晰结构化:初始化、目标分布计算、动画循环、相机适配与 UI 层分离。
© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
取消
易航博客