Skip to content

粒子旋转拖尾

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="rotatingTail" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { ref, nextTick, onUnmounted } from 'vue'

let animationFrame
let sceneResources

const isRunning = ref(false)

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const initScene = () => {
  const c = document.getElementById('rotatingTail') as any
  const gl: any = c.getContext('webgl', {
      preserveDrawingBuffer: true
    })
  let width = c.width
  let height = c.height
  const webgl: any = {}

  webgl.vertexShaderSource = `
    attribute vec2 a_position;
    attribute vec2 a_color;
    uniform float u_tick;
    uniform vec2 u_resolution;
    varying vec2 v_color;

    void main() {

      gl_Position = vec4(vec2(1, -1) * (a_position / u_resolution) * 2.0, 0, 1);

      v_color = a_color;
      if (a_color.x > 0.0)
        v_color = vec2(a_color.x + u_tick / 100.0, a_color.y);
      }
    `
  webgl.fragmentShaderSource = `
    precision mediump float;
    varying vec2 v_color;

    vec3 h2rgb(float h) {
      return clamp(abs(mod(h * 6.0 + vec3(0, 4, 2), 6.0) - 3.0) -1.0, 0.0, 1.0);
    }
    void main() {
      vec4 color = vec4(0, 0, 0, v_color.y);
      if (v_color.x > 0.0) {
        color.rgb = h2rgb(v_color.x / 5.0);
      }
      gl_FragColor = color;
    }
  `

  webgl.vertexShader = gl.createShader(gl.VERTEX_SHADER)
  gl.shaderSource(webgl.vertexShader, webgl.vertexShaderSource)
  gl.compileShader(webgl.vertexShader)

  webgl.fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
  gl.shaderSource(webgl.fragmentShader, webgl.fragmentShaderSource)
  gl.compileShader(webgl.fragmentShader)

  webgl.shaderProgram = gl.createProgram()
  gl.attachShader(webgl.shaderProgram, webgl.vertexShader)
  gl.attachShader(webgl.shaderProgram, webgl.fragmentShader)
  gl.linkProgram(webgl.shaderProgram)
  gl.useProgram(webgl.shaderProgram)
  webgl.attribLocs = {
    // 从之前创建的 GLSL 着色程序中找到这个属性值所在的位置a_position
    position: gl.getAttribLocation(webgl.shaderProgram, 'a_position'),
    color: gl.getAttribLocation(webgl.shaderProgram, 'a_color')
  }
  webgl.buffers = {
    position: gl.createBuffer(),
    color: gl.createBuffer()
  }
  webgl.uniformLocs = {
    tick: gl.getUniformLocation(webgl.shaderProgram, 'u_tick'),
    resolution: gl.getUniformLocation(webgl.shaderProgram, 'u_resolution')
  }

  // 需要告诉 WebGL 怎么从之前准备的缓冲中获取数据给着色器中的属性。
  // 1、首先需要启用对应属性
  gl.enableVertexAttribArray(webgl.attribLocs.position)
  gl.enableVertexAttribArray(webgl.attribLocs.color)
  // 2、指定从缓冲中读取数据的方式
  // gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
  gl.bindBuffer(gl.ARRAY_BUFFER, webgl.buffers.position)
  // 一个隐藏信息是 gl.vertexAttribPointer 是将属性绑定到当前的 ARRAY_BUFFER。
  // 换句话说就是属性绑定到了 positionBuffer 上。
  // 意味着现在利用绑定点将 ARRAY_BUFFER 绑定到其它数据上后,该属性依然从 positionBuffer 上读取数据。
  // gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset)

  // var positionAttributeLocation = webgl.attribLocs.position
  // var size = 2 每次迭代运行提取两个单位数据
  // var type = gl.FLOAT 每个单位的数据类型是32位浮点型
  // var normalize = false 不需要归一化数据
  // var stride = 0 0 = 移动单位数量 * 每个单位占用内存(sizeof(type)),每次迭代运行运动多少内存到下一个数据开始点
  // var offset = 0 从缓冲起始位置开始读取
  gl.vertexAttribPointer(
    webgl.attribLocs.position,
    2,
    gl.FLOAT,
    false,
    0,
    0
  )

  gl.bindBuffer(gl.ARRAY_BUFFER, webgl.buffers.color)
  gl.vertexAttribPointer(webgl.attribLocs.color, 2, gl.FLOAT, false, 0, 0)
  gl.enable(gl.BLEND)
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

  // 需要告诉 WebGL 怎样把提供的 gl_Position 裁剪空间坐标对应到画布像素坐标。
  // 通常也把画布像素坐标叫做屏幕空间。
  // 为了实现这个目的,只需要调用 gl.viewport 方法并传递画布的当前尺寸。
  gl.viewport(0, 0, width, height)
  // 设置全局变量 分辨率
  gl.uniform2f(webgl.uniformLocs.resolution, width, height)

  webgl.data = {
    position: [],
    color: []
  }

  webgl.draw = function (glType, glAmount) {
    // 绑定位置信息缓冲
    gl.bindBuffer(gl.ARRAY_BUFFER, webgl.buffers.position)
    // 通过绑定点向缓冲中存放数据
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array(webgl.data.position),
      gl.STATIC_DRAW
    )

    gl.bindBuffer(gl.ARRAY_BUFFER, webgl.buffers.color)
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array(webgl.data.color),
      gl.STATIC_DRAW
    )

    // 绘制
    // var primitiveType = gl.TRIANGLES或者其他的gl形状;
    // var offset = 0;

    // var count = 3;
    // 因为 count = 3,所以顶点着色器将运行三次。
    // 第一次运行将会从位置缓冲中读取前两个值赋给属性值 a_position.x 和 a_position.y。
    // 第二次运行 a_position.xy 将会被赋予后两个值,最后一次运行将被赋予最后两个值。

    // var count = 6;
    // 显然需要告诉 WebGL 要运行六次顶点着色器来画两个三角形,所以将 count 改成 6。
    // 此时的各个顶点为var positions = [10, 20, 80, 20, 10, 30, 10, 30, 80, 20, 80, 30]

    // gl.drawArrays(primitiveType, offset, count)

    gl.drawArrays(glType, 0, glAmount)
  }

  webgl.clear = function (z) {
    const a = width / 2,
      A = -a,
      b = height / 2,
      B = -b

    // 将位置信息转换为像素坐标,通过绘制两个三角形来绘制一个矩形,每个三角形有三个点
    webgl.data.position = [A, B, a, B, A, b, A, b, a, b, a, B]
    webgl.data.color = [-1, z, -1, z, -1, z, -1, z, -1, z, -1, z]

    webgl.draw(gl.TRIANGLES, 6)
    webgl.data.color.length = webgl.data.position.length = 0
  }

  const particles: any = []
  let tick = 0
  const opts = {
      baseW: 0.01,
      addedW: 0.03
    }

  function Particle(radius, radian) {
    this.radius = radius
    this.radian = radian + 6.2831853071
    this.x = this.radius * Math.cos(this.radian)
    this.y = this.radius * Math.sin(this.radian)
    this.w = opts.baseW + opts.addedW * Math.random()
    this.wcos = Math.cos(this.w)
    this.wsin = Math.sin(this.w)
  }
  Particle.prototype.step = function () {
    if (Math.random() < 0.1) {
      this.w += (Math.random() - 0.5) / 100000
      this.wcos = Math.cos(this.w)
      this.wsin = Math.sin(this.w)
    }

    const px = this.x,
      py = this.y,
      pr = this.radian

    this.x = this.x * this.wcos - this.y * this.wsin
    this.y = px * this.wsin + this.y * this.wcos

    this.radian += this.w

    webgl.data.position.push(px, py, this.x, this.y, 0, 0, this.x, this.y)
    webgl.data.color.push(
      pr,
      1,
      this.radian,
      1,
      this.radian,
      0.01,
      this.radian,
      0.04
    )
  }

  function anim() {
    animationFrame = requestAnimationFrame(anim)

    if (!animationFrame) return

    webgl.clear(0.1)

    if (particles.length < 1000) {
      for (let i = 0; i < 3; ++i) {
        particles.push(
          new Particle(
            Math.random() * Math.min(width, height),
            Math.random() * 6.283185307179586476925286766559
          )
        )
      }
    }

    particles.map(function (particle) {
      particle.step()
    })

    ++tick
    gl.uniform1f(webgl.uniformLocs.tick, tick)

    webgl.draw(gl.LINES, webgl.data.position.length / 2)
    webgl.data.position.length = 0
    webgl.data.color.length = 0
  }
  anim()

  return {
    gl
  }

}

const destroy = () => {
  cancelAnimationFrame(animationFrame)
  animationFrame = null
}

onUnmounted(() => {
  if (sceneResources) {
    sceneResources.gl && sceneResources.gl.getExtension("WEBGL_lose_context").loseContext()
  }
  destroy()
})
</script>

<style scoped>
canvas {
  background-color: #111;
}
</style>

粒子波浪

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="pointsWave" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { ref, nextTick, onUnmounted } from 'vue'

let animationFrame 
let sceneResources

const isRunning = ref(false)

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const initScene = () => {
  let canvas = document.getElementById('pointsWave') as any
  let gl: any = canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
  let w = canvas.width
  let h = canvas.height
  let num = 100
  initWebGl()
  window.addEventListener('resize', function () {
    w = canvas.width = window.innerWidth
    h = canvas.height = window.innerHeight
  })
  // attribute声明vec4类型变量a_Position
  // precision mediump float; 表示片元着色器中所有浮点型精度为中精度
  // uniform vec4类型的color变量,并设置为片元着色器的输出
  function initWebGl() {
    const vs_source = `
      attribute vec4 a_Position;
      void main() {
        gl_Position = a_Position;
        gl_PointSize = 3.0;
      }
    `
    // 绘制圆
    // 距离大于0.5都舍去
    const fs_source = `
      #ifdef GL_ES
      precision mediump float;
      #endif
      uniform vec4 color;
      void main() {
        float d = distance(gl_PointCoord, vec2(0.5, 0.5));
        if (d < 0.5) {
          gl_FragColor = vec4(0.2, 0.3, 0.5, 1.0);
        } else {
          discard;
        }
      }
    `
    // 创建顶点着色器
    let sShader = gl.createShader(gl.VERTEX_SHADER)
    // 创建片段着色器
    let fShader = gl.createShader(gl.FRAGMENT_SHADER)
    // 创建一个着色器程序
    let glprogam = gl.createProgram()

    // 绑定资源
    gl.shaderSource(sShader, vs_source)
    gl.shaderSource(fShader, fs_source)

    // 编译着色器
    gl.compileShader(sShader)
    gl.compileShader(fShader)
    gl.attachShader(glprogam, sShader) // 把顶点着色器添加到着色器程序
    gl.attachShader(glprogam, fShader) // 把片元着色器添加到着色器程序

    // 链接程序,在链接操作执行以后,可以任意修改 shader 的源代码,对 shader 重新编译
    // 不会影响整个程序,除非重新链接程序
    gl.linkProgram(glprogam)

    // 加载并使用链接好的程序
    gl.useProgram(glprogam)
    gl.program = glprogam

    render()
  }

  function render() {
    animationFrame = requestAnimationFrame(render)

    if (!animationFrame) return


    num = num - 2
    let pointdata = createPointData()
    setPointType(pointdata.data, pointdata.nums)
  }

  function createPointData() {
    let max = 10
    let number = 100
    let tier = 10
    let arr = []
    let degs = function (deg) {
      return (Math.PI * deg) / 180
    }
    for (let i = 0; i < number; i++) {
      for (let j = 0; j < tier; j++) {
        let gap = i * 7 - j * 20 + num
        let x = webX(-(w / 2) - 280 + i * ((w + 300) / number) + j * 20)
        let y = webY(
          -(h / 2) + Math.sin(degs(gap)) * (max + j * 10) + j * 20
        )
        let z = -1
        let arrtwo: any = [x, y, z]
        arr = arr.concat(arrtwo)
      }
    }
    return {
      data: new Float32Array(arr),
      nums: number * tier
    }
  }

  function setPointType(data, num) {
    // 1.创建缓冲区对象
    let buffer = gl.createBuffer()
    if (!buffer) {
      console.log('缓冲区创建失败')
      return -1
    }

    // 2.绑定缓冲区对象
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer)

    // 3.向缓冲区对象中写入数据
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)

    // 4.获取存储位置变量
    let aPosition = gl.getAttribLocation(gl.program, 'a_Position')

    // 5.把缓冲区对象分配给a_Position变量
    gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0)

    // 连接缓冲区对象和a_Position 变量
    gl.enableVertexAttribArray(aPosition)

    //用設置的顔色清空畫布
    gl.clearColor(0.1, 0.7, 0.8, 1.0)
    gl.clear(gl.COLOR_BUFFER_BIT)

    //繪製畫布内容
    gl.drawArrays(gl.POINTS, 0, num)
  }

  //X軸
  function webX(n) {
    return n / (w / 2)
  }

  //Y軸
  function webY(n) {
    return n / (h / 2)
  }

  return {
    gl
  }
}

const destroy = () => {
  cancelAnimationFrame(animationFrame)
  animationFrame = null
}

onUnmounted(() => {
  if (sceneResources) {
    sceneResources.gl && sceneResources.gl.getExtension("WEBGL_lose_context").loseContext()
  }
  destroy()
})
</script>

图片合并

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="mergeImage" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { ref, nextTick, onUnmounted } from 'vue'

let animationFrame
let sceneResources

const vertexSource = `
  precision mediump float;
  attribute vec4 a_Position;
  attribute vec2 a_TexCoord;
  varying vec2 v_TexCoord;
  void main() {
    gl_Position = a_Position; // 顶点坐标
    v_TexCoord = a_TexCoord; // 纹理坐标系下的坐标
  }
`
const fragmentSource = `
  precision mediump float;
  uniform sampler2D u_Sampler0; // 纹理
  uniform sampler2D u_Sampler1; // 纹理
  varying vec2 v_TexCoord; // 纹理坐标系下的坐标
  void main() {
    vec4 color0 = texture2D(u_Sampler0, v_TexCoord);
    vec4 color1 = texture2D(u_Sampler1, v_TexCoord);
    gl_FragColor = color0 * color1; 
  }
`

const isRunning = ref(false)

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const initScene = () => {
  const canvas: any = document.getElementById('mergeImage')
  const gl = canvas.getContext('webgl')

  // 创建顶点着色器
  const vertexShader = gl.createShader(gl.VERTEX_SHADER)
  // 给顶点着色器赋值
  gl.shaderSource(vertexShader, vertexSource)
  // 编译顶点着色器
  gl.compileShader(vertexShader)

  // 创建片元着色器
  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
  // 给片元着色器赋值
  gl.shaderSource(fragmentShader, fragmentSource)
  // 编译片元着色器
  gl.compileShader(fragmentShader)
  // 检测着色器创建是否正确
  if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
    alert(gl.getShaderInfoLog(fragmentShader))
  }
  // 创建程序
  const program = gl.createProgram()
  // 给程序赋值
  gl.attachShader(program, vertexShader)
  gl.attachShader(program, fragmentShader)
  // 连接程序
  gl.linkProgram(program)
  // 使用此着色器
  gl.useProgram(program)


  // 变量的处理
  // 获取顶点着色器中的变量a_Position
  const a_Position = gl.getAttribLocation(program, 'a_Position')
  // 获取顶点着色器中的变量a_TexCoord
  const a_TexCoord = gl.getAttribLocation(program, 'a_TexCoord')
  // 获取片元着色器中的变量u_Sampler
  const u_Sampler0 = gl.getUniformLocation(program, 'u_Sampler0')
  // 获取片元着色器中的变量u_Sampler
  const u_Sampler1 = gl.getUniformLocation(program, 'u_Sampler1')
  // 顶点坐标与纹理坐标
  // -0.5, 0.5, 0.0, 1.0:
  // U坐标 -0.5 和 V坐标 0.5 表示这个顶点位于平面的左上角。纹理图像的左边缘(U=0)和上边缘(V=1)被映射到这里。
  // 最后两个数字 0.0 和 1.0 可能是用来表示颜色或者其他属性的,但在标准的UV坐标中,它们不是UV坐标的一部分。
  // -0.5, -0.5, 0.0, 0.0:
  // U坐标 -0.5 和 V坐标 -0.5 表示这个顶点位于平面的左下角。纹理图像的左边缘(U=0)和下边缘(V=0)被映射到这里。
  // 最后两个数字 0.0 和 0.0 同样表示额外的数据。
  // 0.5, 0.5, 1.0, 1.0:
  // U坐标 0.5 和 V坐标 0.5 表示这个顶点位于平面的右上角。纹理图像的右边缘(U=1)和上边缘(V=1)被映射到这里。
  // 最后两个数字 1.0 和 1.0 表示额外的数据。
  // 0.5, -0.5, 1.0, 0.0:
  // U坐标 0.5 和 V坐标 -0.5 表示这个顶点位于平面的右下角。纹理图像的右边缘(U=1)和下边缘(V=0)被映射到这里。
  // 最后两个数字 1.0 和 0.0 表示额外的数据。
  const vertexTexCoords = new Float32Array([
    -0.5, 0.5, 0.0, 1.0,
    -0.5, -0.5, 0.0, 0.0,
    0.5, 0.5, 1.0, 1.0,
    0.5, -0.5, 1.0, 0.0,
  ])
  const f32Seize = vertexTexCoords.BYTES_PER_ELEMENT
  // 给定点设置坐标 几何图形与纹理的坐标
  const vertexBuffer = gl.createBuffer()
  // 绑定buffer
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  // 绑定数据
  gl.bufferData(gl.ARRAY_BUFFER, vertexTexCoords, gl.STATIC_DRAW)
  // 给a_Position赋值
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, f32Seize * 4, 0)
  // 使用此变量
  gl.enableVertexAttribArray(a_Position)
  // 纹理坐标
  const texCoordBuffer = gl.createBuffer()
  // 绑定buffer
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer)
  // 绑定数据
  gl.bufferData(gl.ARRAY_BUFFER, vertexTexCoords, gl.STATIC_DRAW)
  // 给a_TexCoord赋值
  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, f32Seize * 4, f32Seize * 2)
  // 使用此变量
  gl.enableVertexAttribArray(a_TexCoord)

  gl.clearColor(0, 0, 0, 1.0)
  // 获取图片的素材  
  getImage('/images/mergeImage1.jpg', u_Sampler0, gl.TEXTURE0, 0)
  getImage('/images/mergeImage2.jpg', u_Sampler1, gl.TEXTURE1, 1)

  function getImage(imgYrl, u_Sampler, TEXTURE, num) {
    // 文件里的文本会在这里被打印出来 
    const img = new Image()
    img.src = imgYrl
    img.crossOrigin = ''
    img.onload = () => {
      const texture = gl.createTexture()
      showImage(texture, img, u_Sampler, TEXTURE, num)
    }
  }

  function showImage(texture, img, u_Sampler, TEXTURE, texUnit) {
    console.log(texture, img, u_Sampler, TEXTURE, texUnit)
    // document.body.appendChild(img)
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
    // 开始0号纹理通道
    gl.activeTexture(TEXTURE)
    // 想目标绑定纹理对象
    gl.bindTexture(gl.TEXTURE_2D, texture)
    // 配置纹理的参数
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT)
    // 设置着色器参数
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img)
    // 设置纹理数据
    gl.uniform1i(u_Sampler, texUnit)
    gl.clear(gl.COLOR_BUFFER_BIT)
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
  }
}

const destroy = () => {
  cancelAnimationFrame(animationFrame)
  animationFrame = null
}

onUnmounted(() => {
  if (sceneResources) {
    sceneResources.gl && sceneResources.gl.getExtension("WEBGL_lose_context").loseContext()
  }
  destroy()
})
</script>