前端嘛 Logo
前端嘛
duang~~~

duang~~~

2025-11-12
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>梦幻般的光晕与粒子</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        width: 100%;
        height: 100%;
        overflow: hidden;
      }
      #container {
        position: fixed;
        width: 100%;
        height: 100%;
        background: radial-gradient(
          circle at 50% 50%,
          #1a0632 0%,
          #140426 25%,
          #0c021a 50%,
          #06020e 75%,
          #020108 100%
        );
      }
      canvas {
        display: block;
        width: 100%;
        height: 100%;
      }
      .glow-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        pointer-events: none;
        background: radial-gradient(
          circle at 50% 50%,
          rgba(120, 50, 255, 0.05) 0%,
          rgba(80, 40, 200, 0.03) 40%,
          transparent 70%
        );
        mix-blend-mode: screen;
      }
    </style>
  </head>
  <body>
    <div id="container"></div>
    <div class="glow-overlay"></div>
    <script type="importmap">
      {
        "imports": {
          "three": "https://cdn.jsdelivr.net/npm/three@0.166.1/build/three.module.js",
          "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.166.1/examples/jsm/"
        }
      }
    </script>
    <script type="module">
      import * as THREE from 'three';
      import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
      import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
      import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';

      let scene, camera, renderer, composer;
      let particleLayers = [];
      let time = 0;
      const mouse = new THREE.Vector3(0, 0, 0);
      const targetMouse = new THREE.Vector3(0, 0, 0);
      const mouseRadius = 40;
      let ripples = [];

      const layersConfig = [
        {
          count: 20000,
          size: 0.3,
          colorRange: { hue: [0.75, 0.9], sat: [0.7, 1], light: [0.5, 0.7] },
          rotationSpeed: 0.001,
        },
        {
          count: 25000,
          size: 0.2,
          colorRange: { hue: [0.45, 0.6], sat: [0.6, 0.8], light: [0.4, 0.6] },
          rotationSpeed: 0.0005,
        },
      ];

      function createParticleSystem(config) {
        const geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(config.count * 3);
        const colors = new Float32Array(config.count * 3);
        const basePositions = new Float32Array(config.count * 3);
        const baseColors = new Float32Array(config.count * 3);

        for (let i = 0; i < config.count; i++) {
          const i3 = i * 3;

          const radius = 25 + Math.random() * 30;
          const theta = Math.random() * Math.PI * 2;
          const phi = Math.acos(2 * Math.random() - 1);

          const x = radius * Math.sin(phi) * Math.cos(theta);
          const y = radius * Math.sin(phi) * Math.sin(theta);
          const z = radius * Math.cos(phi);

          positions[i3] = x;
          positions[i3 + 1] = y;
          positions[i3 + 2] = z;

          basePositions[i3] = x;
          basePositions[i3 + 1] = y;
          basePositions[i3 + 2] = z;

          const dist = Math.sqrt(x * x + y * y + z * z) / 55;
          const hue = THREE.MathUtils.lerp(
            config.colorRange.hue[0],
            config.colorRange.hue[1],
            dist
          );
          const sat = THREE.MathUtils.lerp(
            config.colorRange.sat[0],
            config.colorRange.sat[1],
            dist
          );
          const light = THREE.MathUtils.lerp(
            config.colorRange.light[0],
            config.colorRange.light[1],
            dist
          );

          const color = new THREE.Color().setHSL(hue, sat, light);
          colors[i3] = color.r;
          colors[i3 + 1] = color.g;
          colors[i3 + 2] = color.b;

          baseColors[i3] = color.r;
          baseColors[i3 + 1] = color.g;
          baseColors[i3 + 2] = color.b;
        }

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

        const textureLoader = new THREE.TextureLoader();
        const particleTexture = textureLoader.load(
          'https://placehold.co/32x32/ffffff/ffffff.png?text=+'
        );

        const material = new THREE.PointsMaterial({
          size: config.size,
          vertexColors: true,
          transparent: true,
          opacity: 0.8,
          blending: THREE.AdditiveBlending,
          depthWrite: false,
          sizeAttenuation: true,
          map: particleTexture,
        });

        const points = new THREE.Points(geometry, material);
        points.userData = {
          velocities: new Float32Array(config.count * 3),
          basePositions,
          baseColors,
          colorVelocities: new Float32Array(config.count * 3),
          rotationSpeed: config.rotationSpeed,
        };

        return points;
      }

      function createRipple(x, y) {
        ripples.push({
          x,
          y,
          radius: 0,
          strength: 2.5,
          maxRadius: mouseRadius * 4,
          speed: 4,
          color: new THREE.Color(0xffffff),
        });
      }

      function init() {
        const container = document.getElementById('container');

        scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x020108, 0.008);

        camera = new THREE.PerspectiveCamera(
          75,
          window.innerWidth / window.innerHeight,
          0.1,
          1000
        );
        camera.position.z = 100;

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

        const renderScene = new RenderPass(scene, camera);
        const bloomPass = new UnrealBloomPass(
          new THREE.Vector2(window.innerWidth, window.innerHeight),
          1.5,
          0.4,
          0.85
        );
        bloomPass.threshold = 0;
        bloomPass.strength = 1.2;
        bloomPass.radius = 0.5;

        composer = new EffectComposer(renderer);
        composer.addPass(renderScene);
        composer.addPass(bloomPass);

        layersConfig.forEach((config) => {
          const particles = createParticleSystem(config);
          particleLayers.push(particles);
          scene.add(particles);
        });

        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('click', onClick);
        window.addEventListener('resize', onWindowResize);
      }

      function updateParticles() {
        mouse.lerp(targetMouse, 0.05);

        ripples = ripples.filter((ripple) => {
          ripple.radius += ripple.speed;
          ripple.strength *= 0.96;
          return ripple.radius < ripple.maxRadius;
        });

        particleLayers.forEach((layer) => {
          const positions = layer.geometry.attributes.position.array;
          const colors = layer.geometry.attributes.color.array;
          const { velocities, basePositions, baseColors, colorVelocities } =
            layer.userData;
          const totalParticles = positions.length / 3;

          for (let i = 0; i < totalParticles; i++) {
            const i3 = i * 3;
            const px = positions[i3];
            const py = positions[i3 + 1];
            const pz = positions[i3 + 2];

            let totalForce = new THREE.Vector3();
            let colorShift = new THREE.Vector3();

            const mouseDist = mouse.distanceTo(new THREE.Vector3(px, py, pz));
            if (mouseDist < mouseRadius) {
              const forceStrength = (1 - mouseDist / mouseRadius) * 0.1;
              const forceDirection = new THREE.Vector3(px, py, pz)
                .sub(mouse)
                .normalize();
              totalForce.add(forceDirection.multiplyScalar(forceStrength));

              const colorIntensity = (1 - mouseDist / mouseRadius) * 0.8;
              colorShift.set(colorIntensity, colorIntensity, colorIntensity);
            }

            ripples.forEach((ripple) => {
              const rippleDist = Math.sqrt(
                Math.pow(ripple.x - px, 2) + Math.pow(ripple.y - py, 2)
              );
              const rippleWidth = 15;
              if (Math.abs(rippleDist - ripple.radius) < rippleWidth) {
                const falloff =
                  1 - Math.abs(rippleDist - ripple.radius) / rippleWidth;
                const rippleForce = ripple.strength * falloff * 0.1;
                const forceDirection = new THREE.Vector3(px, py, pz)
                  .sub(new THREE.Vector3(ripple.x, ripple.y, pz))
                  .normalize();
                totalForce.add(forceDirection.multiplyScalar(rippleForce));

                const rippleColor = new THREE.Vector3(
                  ripple.color.r,
                  ripple.color.g,
                  ripple.color.b
                );
                colorShift.add(
                  rippleColor.multiplyScalar(falloff * ripple.strength)
                );
              }
            });

            velocities[i3] += totalForce.x;
            velocities[i3 + 1] += totalForce.y;
            velocities[i3 + 2] += totalForce.z;

            const returnForce = 0.02;
            velocities[i3] += (basePositions[i3] - px) * returnForce;
            velocities[i3 + 1] += (basePositions[i3 + 1] - py) * returnForce;
            velocities[i3 + 2] += (basePositions[i3 + 2] - pz) * returnForce;

            const damping = 0.94;
            velocities[i3] *= damping;
            velocities[i3 + 1] *= damping;
            velocities[i3 + 2] *= damping;

            positions[i3] += velocities[i3];
            positions[i3 + 1] += velocities[i3 + 1];
            positions[i3 + 2] += velocities[i3 + 2];

            colorVelocities[i3] += colorShift.x;
            colorVelocities[i3 + 1] += colorShift.y;
            colorVelocities[i3 + 2] += colorShift.z;

            const colorReturnForce = 0.05;
            colorVelocities[i3] +=
              (baseColors[i3] - colors[i3]) * colorReturnForce;
            colorVelocities[i3 + 1] +=
              (baseColors[i3 + 1] - colors[i3 + 1]) * colorReturnForce;
            colorVelocities[i3 + 2] +=
              (baseColors[i3 + 2] - colors[i3 + 2]) * colorReturnForce;

            const colorDamping = 0.9;
            colorVelocities[i3] *= colorDamping;
            colorVelocities[i3 + 1] *= colorDamping;
            colorVelocities[i3 + 2] *= colorDamping;

            colors[i3] += colorVelocities[i3];
            colors[i3 + 1] += colorVelocities[i3 + 1];
            colors[i3 + 2] += colorVelocities[i3 + 2];
          }

          layer.geometry.attributes.position.needsUpdate = true;
          layer.geometry.attributes.color.needsUpdate = true;
        });
      }

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

        updateParticles();

        particleLayers.forEach((layer) => {
          layer.rotation.y += layer.userData.rotationSpeed;
          layer.rotation.x = Math.sin(time * 0.1) * 0.05;
        });

        camera.position.x = Math.sin(time * 0.2) * 2;
        camera.position.y = Math.cos(time * 0.3) * 2;
        camera.lookAt(scene.position);

        composer.render();
      }

      function onMouseMove(event) {
        targetMouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        targetMouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

        const vector = new THREE.Vector3(targetMouse.x, targetMouse.y, 0.5);
        vector.unproject(camera);
        const dir = vector.sub(camera.position).normalize();
        const distance = -camera.position.z / dir.z;
        const pos = camera.position.clone().add(dir.multiplyScalar(distance));
        targetMouse.copy(pos);
      }

      function onClick(event) {
        const clickMouse = new THREE.Vector2();
        clickMouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        clickMouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

        const vector = new THREE.Vector3(clickMouse.x, clickMouse.y, 0.5);
        vector.unproject(camera);
        const dir = vector.sub(camera.position).normalize();
        const distance = -camera.position.z / dir.z;
        const pos = camera.position.clone().add(dir.multiplyScalar(distance));

        createRipple(pos.x, pos.y);
      }

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

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