粒子效果

111 阅读3分钟

全景图(这里特指球面全景图)是指一种图片宽高比为2比1的包含了360°x180°空间的图片,例如8000*4000的jpg图片或tiff图片等。 注:宽高比2:1为水平方向(360):垂直方向(180)

全景漫游(英文:panorama)技术可以让体验者在全景图像构建的全景空间里切换视角的浏览。它是通过拍摄全景图像,再采用计算机图形图像技术构建出全景空间,让使用者能用控制浏览的方向,或左或右、或上或下观看物体或场景,仿佛身临其境一般。与传统的3D建模相比,全景漫游技术制作简单,数据量小,系统消耗低,且更有真实感。

波浪粒子效果封装three.js

<template>
  <div id="indexLizi" />
</template>

<script>
import * as THREE from 'three'

export default {
  name: 'Pointwave',
  props: {
    amountX: {
      type: Number,
      default: 100
    },
    amountY: {
      type: Number,
      default: 100
    },
    color: {
      type: Number,
      default: 0xffffff
    },
    top: {
      type: Number,
      default: 350
    }
  },
  data() {
    return {
      count: 0,
      // 用来跟踪鼠标水平位置
      mouseX: 0,
      windowHalfX: null,
      // 相机
      camera: null,
      // 场景
      scene: null,
      // 批量管理粒子
      particles: null,
      // 渲染器
      renderer: null
    }
  },
  mounted() {
    this.init()
    this.animate()
  },
  methods: {
    init: function() {
      const SEPARATION = 100
      const SCREEN_WIDTH = window.innerWidth
      const SCREEN_HEIGHT = window.innerHeight
      const container = document.createElement('div')
      this.windowHalfX = window.innerWidth / 2
      container.style.position = 'relative'
      container.style.top = `${this.top}px`
      container.style.height = `${(SCREEN_HEIGHT - this.top)}px`
      document.getElementById('indexLizi').appendChild(container)

      this.camera = new THREE.PerspectiveCamera(75, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 10000)
      this.camera.position.z = 1000

      this.scene = new THREE.Scene()

      const numParticles = this.amountX * this.amountY
      const positions = new Float32Array(numParticles * 3)
      const scales = new Float32Array(numParticles)
      // 初始化粒子位置和大小
      let i = 0
      let j = 0
      for (let ix = 0; ix < this.amountX; ix++) {
        for (let iy = 0; iy < this.amountY; iy++) {
          positions[i] = ix * SEPARATION - ((this.amountX * SEPARATION) / 2)
          positions[i + 1] = 0
          positions[i + 2] = iy * SEPARATION - ((this.amountY * SEPARATION) / 2)
          scales[j] = 1
          i += 3
          j++
        }
      }

      const geometry = new THREE.BufferGeometry()
      geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3))
      geometry.addAttribute('scale', new THREE.BufferAttribute(scales, 1))
      // 初始化粒子材质
      const material = new THREE.ShaderMaterial({
        uniforms: {
          color: { value: new THREE.Color(this.color) }
        },
        vertexShader: `
          attribute float scale;
          void main() {
            vec4 mvPosition = modelViewMatrix * vec4( position, 2.0 );
            gl_PointSize = scale * ( 300.0 / - mvPosition.z );
            gl_Position = projectionMatrix * mvPosition;
          }
        `,
        fragmentShader: `
          uniform vec3 color;
          void main() {
            if ( length( gl_PointCoord - vec2( 0.5, 0.5 ) ) > 0.475 ) discard;
            gl_FragColor = vec4( color, 1.0 );
          }
        `
      })

      this.particles = new THREE.Points(geometry, material)
      this.scene.add(this.particles)

      this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
      this.renderer.setSize(container.clientWidth, container.clientHeight)
      this.renderer.setPixelRatio(window.devicePixelRatio)
      this.renderer.setClearAlpha(0)
      container.appendChild(this.renderer.domElement)

      window.addEventListener('resize', this.onWindowResize, { passive: false })
      document.addEventListener('mousemove', this.onDocumentMouseMove, { passive: false })
      document.addEventListener('touchstart', this.onDocumentTouchStart, { passive: false })
      document.addEventListener('touchmove', this.onDocumentTouchMove, { passive: false })
    },
    render: function() {
      this.camera.position.x += (this.mouseX - this.camera.position.x) * 0.05
      this.camera.position.y = 400
      this.camera.lookAt(this.scene.position)
      const positions = this.particles.geometry.attributes.position.array
      const scales = this.particles.geometry.attributes.scale.array
      // 计算粒子位置及大小
      let i = 0
      let j = 0
      for (let ix = 0; ix < this.amountX; ix++) {
        for (let iy = 0; iy < this.amountY; iy++) {
          positions[i + 1] = (Math.sin((ix + this.count) * 0.3) * 100) + (Math.sin((iy + this.count) * 0.5) * 100)
          scales[j] = (Math.sin((ix + this.count) * 0.3) + 1) * 8 + (Math.sin((iy + this.count) * 0.5) + 1) * 8
          i += 3
          j++
        }
      }
      // 重新渲染粒子
      this.particles.geometry.attributes.position.needsUpdate = true
      this.particles.geometry.attributes.scale.needsUpdate = true
      this.renderer.render(this.scene, this.camera)
      this.count += 0.1
    },
    animate: function() {
      requestAnimationFrame(this.animate)
      this.render()
    },
    onDocumentMouseMove: function(event) {
      this.mouseX = event.clientX - this.windowHalfX
    },
    onDocumentTouchStart: function(event) {
      if (event.touches.length === 1) {
        this.mouseX = event.touches[0].pageX - this.windowHalfX
      }
    },
    onDocumentTouchMove: function(event) {
      if (event.touches.length === 1) {
        event.preventDefault()
        this.mouseX = event.touches[0].pageX - this.windowHalfX
      }
    },
    onWindowResize: function() {
      this.windowHalfX = window.innerWidth / 2
      this.camera.aspect = window.innerWidth / window.innerHeight
      this.camera.updateProjectionMatrix()
      this.renderer.setSize(window.innerWidth, window.innerHeight)
    }
  }
}
</script>


粒子图片切换渲染

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>新年关键词</title>
    <style>
        .ui {
            position: absolute;
            left: 50%;
            bottom: 5%;
            width: 300px;
            margin-left: -150px;
        }

        .ui-input {
            width: 100%;
            height: 50px;
            background: none;
            font-size: 20px;
            font-weight: bold;
            color: #fff;
            text-align: center;
            border: none;
            border-bottom: 2px solid white;
        }

        .ui-input:focus {
            outline: none;
            border: none;
            border-bottom: 2px solid white;
        }

        .ui--wide {
            width: 76%;
            margin-left: 12%;
            left: 0;
        }

        body {
            background-color: #010101;
            width: 100vw;
            height: 100vh;
            overflow: hidden;
            margin: 0;
        }

        #canvas {
            position: absolute;
            left: 50%;
            transform: translateX(-50%);
            top: 10%;
        }
    </style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
    function rand(min, max) {
        return Math.floor(Math.random() * (max - min)) + min;
    }

    function randomColor() {
        return `rgb(${Math.round(rand(0, 255))}, ${Math.round(rand(0, 255))}, ${Math.round(rand(0, 255))})`;
    }

    function splitKeyList(text) {
        console.log(text);
        let keyList = text.split(",");
        if (keyList.length === 1) {
            keyList = text.split(",");
        }
        if (keyList.length === 1) {
            keyList = text.split(" ");
        }
        return keyList;
    }

    const defaultKeyList = [
        "./111.png",
        "./222.png",
        "./333.png",
    ];

    let timer = null;
    let currentIndex = 0;

    class Particle {
        constructor(particle) {
            this.x = particle.x;
            this.y = particle.y;
            this.tx = particle.tx;
            this.ty = particle.ty;
            this.radius = particle.radius || 2;
            this.color = particle.color || "#F00000";
        }

        draw(ctx) {
            ctx.save();
            ctx.translate(this.x, this.y);
            ctx.fillStyle = this.color;
            ctx.beginPath();
            ctx.arc(0, 0, this.radius, 0, Math.PI * 2, true);
            ctx.closePath();
            ctx.fill();
            ctx.restore();
            return this;
        }
    }

    function drawFrame(particles, finished) {
        timer = window.requestAnimationFrame(() => {
            drawFrame(particles, finished);
        });

        ctx.clearRect(0, 0, canvas.width, canvas.height);
        const easing = 0.3; // Increased easing value to make particles move faster
        const finishedParticles = particles.filter((particle) => {
            const dx = particle.tx - particle.x;
            const dy = particle.ty - particle.y;
            let vx = dx * easing;
            let vy = dy * easing;

            if (Math.abs(dx) < 0.1 && Math.abs(dy) < 0.1) {
                particle.finished = true;
                particle.x = particle.tx;
                particle.y = particle.ty;
            } else {
                particle.x += vx;
                particle.y += vy;
            }
            particle.draw(ctx);
            return particle.finished;
        });

        if (finishedParticles.length === particles.length) {
            window.cancelAnimationFrame(timer);
            finished && finished();
        }

        return particles;
    }

    function getWordPxInfo(target, interval = 3) {
        const canvas = document.createElement("canvas");
        const ctx = canvas.getContext("2d");
        const viewWidth = window.innerWidth * 0.5;
        const viewHeight = window.innerHeight * 0.5;
        if (window.innerWidth < 1080) {
            interval = 2;
        }
        canvas.width = viewWidth;
        canvas.height = viewHeight;

        return new Promise((resolve) => {
            if (typeof target === "string" && (target.startsWith('http') || target.startsWith('./') || target.startsWith('/'))) {
                const img = new Image();
                img.crossOrigin = "Anonymous";  // 设置跨域
                img.src = target;
                img.onload = () => {
                    ctx.drawImage(
                        img,
                        (viewWidth - img.width) / 2,
                        (viewHeight - img.height) / 2,
                        img.width,
                        img.height
                    );
                    resolve(extractPixels(ctx, viewWidth, viewHeight, interval));
                };
                img.onerror = () => resolve([]);
            } else {
                resolve([]);
            }
        });
    }

    function extractPixels(ctx, width, height, interval) {
        const { data } = ctx.getImageData(0, 0, width, height);
        const pixels = [];
        for (let x = 0; x < width; x += interval) {
            for (let y = 0; y < height; y += interval) {
                const pos = (y * width + x) * 4;
                if (data[pos + 3] > 128) {
                    pixels.push({
                        x,
                        y,
                        rgba: [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]],
                    });
                }
            }
        }
        return pixels;
    }

    function createParticles({ text, radius, interval }) {
        return getWordPxInfo(text, interval).then((pixels) => {
            return pixels.map(({ x, y, rgba: color }) => {
                return new Particle({
                    x: Math.random() * (50 + window.innerWidth * 0.5) - 50,
                    y: Math.random() * (50 + window.innerHeight * 0.5) - 50,
                    tx: x,
                    ty: y,
                    radius,
                    color: `rgba(${color.join(",")})`,
                });
            });
        });
    }

    function displayNextImage() {
        // 取消之前的动画帧(如果有的话)
        window.cancelAnimationFrame(timer);
        
        // 选择当前索引映像
        createParticles({ text: defaultKeyList[currentIndex], radius: 1, interval: 2 }).then((particles) => {
            drawFrame(particles);
        });
        
        // 增加下一张图像的索引(通过defaultKeyList循环)
        currentIndex = (currentIndex + 1) % defaultKeyList.length;
    }

    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    canvas.width = window.innerWidth * 0.5;
    canvas.height = window.innerHeight * 0.5;

    canvas.addEventListener("click", displayNextImage);
    displayNextImage(); // 初次加载时显示第一张图片
</script>
</body>
</html>