Skip to content

粒子旋转拖尾

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="rotatingTail" class="stage-webgl"></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.clientWidth
  let height = c.clientHeight
  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)

  // 如果使用这行,就要设置成 let width = c.width  let height = c.height
  // 需要告诉 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-webgl"></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 = 3
    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-webgl"></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) {
    // 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>

线宽以及图片嵌入

点击运行
点击展示图片
<template>
  <div>
    <div class="flex space-between">
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
      <div @click="onShowImg = !onShowImg" class="pointer">点击展示图片</div>
    </div>
    <img v-if="onShowImg" src="/public/markdown/webgl/lineWidthAndMap.png" alt="lineWidthAndMap" />
    <canvas v-if="isRunning" id="lineWidthAndMap" class="stage-webgl"></canvas>
  </div>
</template>

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

// gl.POINTS:绘制单个点。
// gl.LINES:绘制线段,每两个顶点构成一条线段。
// gl.LINE_LOOP:绘制线环,所有顶点依次连接成一个闭合的环。
// gl.LINE_STRIP:绘制线带,所有顶点依次连接成一条连续的线。
// gl.TRIANGLES:绘制三角形,每三个顶点构成一个三角形。
// gl.TRIANGLE_STRIP:绘制三角形带,每三个连续的顶点构成一个三角形。
// gl.TRIANGLE_FAN:绘制三角形扇,第一个顶点作为中心点,后续每两个顶点与中心点构成一个三角形。

const lineVertex = `
	precision mediump float;
	
	attribute vec2 a_Position;
	
	varying vec3 vColor;
	
	void main() {
		
		vColor = vec3(0.0, 0.0, 1.0);
		
		gl_Position = vec4(a_Position, 0.0, 1.0);
		gl_PointSize = 1.0;
	}
`

const lineFragment = `
	precision mediump float;
	
	varying vec3 vColor;
	
	void main() {
		gl_FragColor = vec4(vColor, 1.0);
	}
`

const areaVertex = `
	precision mediump float;
	
	attribute vec2 a_Area;
	
	varying vec3 vColor;
	
	void main() {
		
		vColor = vec3(1.0, 0.0, 0.0);
		
		gl_Position = vec4(a_Area, 0.0, 1.0);
		gl_PointSize = 1.0;
	}
`

const areaFragment = `
	precision mediump float;
	
	varying vec3 vColor;
	
	void main() {
		gl_FragColor = vec4(vColor, 0.1);
	}
`

const imgVertex = `
	precision mediump float;
	
	attribute vec2 a_Position;
	
	attribute vec2 a_TexCoord;
	
	varying vec2 v_TexCoord;
	
	void main(){
		gl_Position = vec4(a_Position, 0.0, 1.0);; // 顶点坐标
		v_TexCoord = a_TexCoord; // 纹理坐标系下的坐标
	}
`

const imgFragment = `
	precision mediump float;
	
	uniform sampler2D u_Sampler0; // 纹理
	
	varying vec2 v_TexCoord; // 纹理坐标系下的坐标
	
	void main(){
		vec4 color0 = texture2D(u_Sampler0,v_TexCoord);
		gl_FragColor = color0; 
	}
`

const lineWidthVertex = `
	precision mediump float;
	
	attribute vec2 a_center;
	attribute vec2 a_prev_point;
	attribute vec2 a_next_point;
	attribute float a_side;
	
	uniform float u_line_width;
	
	varying vec3 vColor;
	
	vec2 calculateNormal(vec2 dir) {
		// 只能两行两列乘以两行一列,第一个的列数=第二个的行数
		// 旋转矩阵 |cos, -sin||x| ---> 【x * cosθ - y * sinθ】 ---> x' 
		// 旋转矩阵 |sin, +cos||y| ---> 【x * sinθ + y * cosθ】 ---> y' 
		return normalize(vec2(-dir.y, dir.x));
	}
	
	void main() {
    vColor = vec3(1.0, 0.0, 1.0);
    
    float len = u_line_width * 0.5;
    vec2 extrudedPosition;
    
    // 特殊情况处理:起点和终点
    if (distance(a_prev_point, a_center) < 0.000001) {
      
      // 起点 - 使用下一段的法线

      // 方法一:
      // vec2 nextDir = normalize(a_center - a_next_point); // 注意:这两个※※※※这里是 a_center - a_next_point

      // 方法二:
      vec2 nextDir = normalize(a_next_point - a_center); // 注意:这两个※※※※这里是 a_next_point - a_center

      vec2 normal = calculateNormal(nextDir);
      extrudedPosition = a_center + normal * len * a_side;
      
    } else if (distance(a_next_point, a_center) < 0.000001) {
      
      // 终点 - 使用前一段的法线

      // 方法一:
      // vec2 prevDir = normalize(a_prev_point - a_center); // 注意:这两个※※※※这里是 a_prev_point - a_center

      // 方法二:
      vec2 prevDir = normalize(a_center - a_prev_point); // 注意:这两个※※※※这里是 a_center - a_prev_point
      
      vec2 normal = calculateNormal(prevDir);
      extrudedPosition = a_center + normal * len * a_side;
      
    } else {
      
      // 方法一:
      // vec2 v1Norm = normalize(a_center - a_next_point);
      // vec2 v2Norm = normalize(a_center - a_prev_point);
      // vec2 v3Norm = calculateNormal(v1Norm);
      // vec2 vNorm = normalize(v1Norm + v2Norm);
      // float scale = len / dot(v3Norm, vNorm);
      // extrudedPosition = a_center + vNorm * scale * a_side;
      
      // 方法二:
      vec2 v1Norm = normalize(a_next_point - a_center);
      vec2 v2Norm = normalize(a_center - a_prev_point);
      vec2 v3Norm = calculateNormal(v1Norm);
      vec2 v4Norm = calculateNormal(v2Norm);
      vec2 vNorm = normalize(v3Norm + v4Norm);
      float scale = len / dot(v4Norm, vNorm);
      extrudedPosition = a_center + vNorm * scale * a_side;
    }
    
    gl_Position = vec4(extrudedPosition, 0.0, 1.0);
    gl_PointSize = 1.0;
  }
`

const lineWidthFragment = `
	precision mediump float;
	
	varying vec3 vColor;
	
	void main() {
		gl_FragColor = vec4(vColor, 1.0);
	}
`

class Webgl {
  gl: any = null
  ele: any = null
  width = 0
  height = 0
  constructor(ele, width, height) {
    this.ele = ele
    this.width = width
    this.height = height
    this.gl = null
  }

  init() {
    const canvas = document.querySelector(this.ele)
    canvas.width = this.width // 设置绘制缓冲区宽度
    canvas.height = this.height // 设置绘制缓冲区高度
    this.gl = canvas.getContext('webgl2', {
      antialias: true,
      depth: true
    })
  }

  initShader(vsSource, fsSource) {
    // 创建程序对象
    const program = this.gl.createProgram()

    // 创建着色对象
    const vertexShader = this._loadShader(this.gl.VERTEX_SHADER, vsSource)
    const fragmentShader = this._loadShader(this.gl.FRAGMENT_SHADER, fsSource)

    // 把顶点着色对象/片元着色对象,装进程序对象中
    this.gl.attachShader(program, vertexShader)
    this.gl.attachShader(program, fragmentShader)

    // 连接webgl上下文对象和程序对象
    this.gl.linkProgram(program)

    // 启动程序对象
    this.gl.useProgram(program)

    // 将程序对象挂到上下文对象上
    this.gl.program = program
  }

  /**
   * 开启混合模式
   * 这里的混合模式是指:当两个图形重叠时,如何处理它们的颜色
   */
  runBlend() {
    // 即重合部分的,则会叠加颜色
    this.gl.enable(this.gl.BLEND)
    this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE)
  }

  _loadShader(type, source) {
    // 根据着色类型,创建着色器对象
    const shader = this.gl.createShader(type)

    // 将着色器源文件传入着色器对象中
    this.gl.shaderSource(shader, source)

    // 编译着色器对象
    this.gl.compileShader(shader)

    // 检查编译是否成功
    if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
      console.error('Shader compile error: ', this.gl.getShaderInfoLog(shader))
      this.gl.deleteShader(shader)
      return null
    }

    // 返回着色器对象
    return shader
  }

  setAttribute(info) {
    // 缓冲对象
    const vertexBuffer = this.gl.createBuffer()

    // 绑定缓冲对象
    this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer)

    // 写入数据
    this.gl.bufferData(this.gl.ARRAY_BUFFER, info.data, this.gl.STATIC_DRAW)

    // 获取 attribute 变量
    const attribute = this.gl.getAttribLocation(this.gl.program, info.attrName)

    // 修改 attribute 变量
    this.gl.vertexAttribPointer(attribute, info.size, this.gl.FLOAT, false, 0, 0)

    // 赋能-批处理
    this.gl.enableVertexAttribArray(attribute)
  }

  setIndexBuffer(indices) {
    const indexBuffer = this.gl.createBuffer()
    this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
    this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), this.gl.STATIC_DRAW)
  }

  // gl.uniform1f 用于传递 float 类型
  // gl.uniform2f 用于传递 vec2 类型
  // gl.uniform3f / gl.uniform4f:分别传递 vec3 和 vec4 类型的数据
  // gl.uniform1fv(传递浮点数组)、gl.uniformMatrix4fv(传递 4x4 矩阵)
  setUniform1f(name, value) {
    const location = this.gl.getUniformLocation(this.gl.program, name)
    if (location) {
      this.gl.uniform1f(location, value)
    }
  }

  setUniform2f(name, value1, value2) {
    const location = this.gl.getUniformLocation(this.gl.program, name)
    if (location) {
      this.gl.uniform2f(location, value1, value2)
    }
  }

  setUniform1i(name, value) {
    const location = this.gl.getUniformLocation(this.gl.program, name)
    if (location) {
      this.gl.uniform1i(location, value)
    }
  }

  setUniform2fv(name, value) {
    const location = this.gl.getUniformLocation(this.gl.program, name)
    if (location) {
      if (Array.isArray(value[0])) { // 如果是数组的数组(如 vec2 数组)
        const flat = value.flat() // 需要浏览器支持 .flat()
        this.gl.uniform2fv(location, new Float32Array(flat))
      } else {
        this.gl.uniform2fv(location, value) // 假设是已扁平化的数组
      }
    }
  }

  setTexture(imgObj, uSamplerName, texturePassNum, textureIndex) {
    const uSampler = this.gl.getUniformLocation(this.gl.program, uSamplerName)

    const texture = this.gl.createTexture()

    this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, 1)

    //开始 texturePassNum 号纹理通道
    this.gl.activeTexture(this.gl[texturePassNum])

    //想目标绑定纹理对象
    this.gl.bindTexture(this.gl.TEXTURE_2D, texture)

    //配置纹理的参数
    this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR)
    this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
    this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.MIRRORED_REPEAT)

    //设置着色器参数
    this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGB, this.gl.RGB, this.gl.UNSIGNED_BYTE, imgObj)

    //设置纹理数据
    this.gl.uniform1i(uSampler, textureIndex)
  }

  clear() {
    this.gl.clearColor(0, 0, 0, 1)
  }

  // gl.drawArrays 是一种直接使用顶点数组进行绘制的方法。它会按照顶点数组的顺序依次绘制顶点。
  // gl.drawElements 是一种使用索引数组进行绘制的方法。它通过索引数组来指定顶点的顺序,从而可以重复使用顶点数据,减少顶点数据的冗余。
  draw(type, count, useIndices = false) {
    if (useIndices) {
      this.gl.drawElements(this.gl[type], count, this.gl.UNSIGNED_SHORT, 0)
    } else {
      this.gl.drawArrays(this.gl[type], 0, count)
    }
  }
}



let animationFrame 
let sceneResources

const isRunning = ref(false)
const onShowImg = ref(false)

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

const initScene = () => {
  const points = [
    { x: 0, y: 0 },
    { x: 0.5, y: 0.5 },
    { x: 0.5, y: 0 },
    { x: 0.25, y: -0.25 },
    { x: 0.5, y: -0.5 },
    { x: 0, y: -0.5 },
    { x: -0.5, y: -0.25 },
    { x: -0.5, y: 0.25 },
    { x: 0, y: 0 }, // 这两个补充线段的闭合
    { x: 0.5, y: 0.5 }, // 这两个补充线段的闭合
  ]

  const lw = 0.04
  const centers: any = []
  const prevPoints: any = []
  const nextPoints: any = []
  const sides: any = []
  const indices: any = []

  for (let i = 0; i < points.length; i++) {
    const now = points[i]

    let prev, next

    // 开始的点
    if (i === 0) {
      prev = now
    } else {
      prev = points[i - 1]
    }

    // 结束的点
    if (i === points.length - 1) {
      next = now
    } else {
      next = points[i + 1]
    }

    // 旋转90度 1
    centers.push(now.x, now.y)
    prevPoints.push(prev.x, prev.y)
    nextPoints.push(next.x, next.y)
    sides.push(1)

    // 旋转90度 -1
    centers.push(now.x, now.y)
    prevPoints.push(prev.x, prev.y)
    nextPoints.push(next.x, next.y)
    sides.push(-1)

  }

  // 创建三角形索引
  for (let i = 0; i < points.length - 1; i++) {
    const base = i * 2
    // 两个三角形组成一个四边形段
    // 0   2   4
    // ---------
    // |  /|  /|
    // | / | / |
    // |/  |/  |
    // ---------
    // 1   3   5
    indices.push(base, base + 1, base + 2)
    indices.push(base + 1, base + 2, base + 3)
  }

  const tempLines: any = []
  points.forEach((v: any) => {
    tempLines.push(v.x)
    tempLines.push(v.y)
  })
  const lines = new Float32Array(tempLines)

  const ele: any = document.querySelector('#lineWidthAndMap')
  const w = ele.clientWidth
  const h = ele.clientHeight

  const myGl = new Webgl('#lineWidthAndMap', w, h)
  myGl.init()
  myGl.clear()

  const img = new Image()
  img.src = '/images/star.jpg'
  img.crossOrigin = ''
  img.onload = () => {
    // 区域填充色
    const areaIndices = earcut(tempLines, [], 2) // 2表示每个顶点有2个坐标(x, y)。返回的是组成三角形的index。
    const triangles = new Float32Array(areaIndices.length * 2)
    for (let i = 0; i < areaIndices.length; i++) {
    triangles[i * 2] = lines[areaIndices[i] * 2]
    triangles[i * 2 + 1] = lines[areaIndices[i] * 2 + 1]
    }
    myGl.initShader(areaVertex, areaFragment)
    myGl.setAttribute({ data: triangles, size: 2, attrName: 'a_Area' })
    myGl.draw('TRIANGLES', triangles.length / 2)

    // 创建纹理坐标
    // 1、indices 和 lines 的含义
    // 	---- indices 是一个数组,表示三角形的顶点索引。这些索引是从 earcut 函数返回的,用于将多边形的顶点分割成三角形。
    // 	---- lines 是一个 Float32Array,包含多边形的所有顶点坐标。每个顶点由两个浮点数表示(x 和 y 坐标)。
    // 2、texCoords 的作用
    // 	---- texCoords 是一个 Float32Array,用于存储每个顶点的纹理坐标。纹理坐标是二维的,范围通常是 [0, 1],表示纹理图像上的位置。
    // 	---- 纹理坐标用于告诉 GPU 如何将纹理图像映射到几何图形上。每个顶点都有一个对应的纹理坐标,GPU 会根据这些坐标将纹理图像映射到三角形上。
    // 3、映射顶点坐标到纹理坐标
    // 	---- 顶点坐标范围是 [-1, 1](这是常见的归一化坐标范围),需要将这些坐标映射到 [0, 1] 范围,以便作为纹理坐标。
    // 	---- texCoords[i * 2] = (lines[indices[i] * 2] + 1) / 2:将 x 坐标从 [-1, 1] 映射到 [0, 1]。
    // 	---- texCoords[i * 2 + 1] = (lines[indices[i] * 2 + 1] + 1) / 2:将 y 坐标从 [-1, 1] 映射到 [0, 1]。
    // 	---- 原始范围:[−1,1]
    // 	---- 目标范围:[0,1]
    // 	---- 新坐标 = (旧坐标+1) / 2
    const texCoords = new Float32Array(areaIndices.length * 2)
    for (let i = 0; i < areaIndices.length; i++) {
      texCoords[i * 2] = (lines[areaIndices[i] * 2] + 1) / 2 // 将顶点坐标映射到 [0, 1] 范围
      texCoords[i * 2 + 1] = (lines[areaIndices[i] * 2 + 1] + 1) / 2
    }
    myGl.initShader(imgVertex, imgFragment)
    myGl.setAttribute({ data: triangles, size: 2, attrName: 'a_Position' })
    myGl.setAttribute({ data: texCoords, size: 2, attrName: 'a_TexCoord' })
    myGl.setTexture(img, 'u_Sampler0', 'TEXTURE0', 0)
    myGl.draw('TRIANGLES', triangles.length / 2)

    // 画有宽度的线段
    myGl.initShader(lineWidthVertex, lineWidthFragment)
    myGl.setAttribute({ data: new Float32Array(centers), size: 2, attrName: 'a_center' })
    myGl.setAttribute({ data: new Float32Array(prevPoints), size: 2, attrName: 'a_prev_point' })
    myGl.setAttribute({ data: new Float32Array(nextPoints), size: 2, attrName: 'a_next_point' })
    myGl.setAttribute({ data: new Float32Array(sides), size: 1, attrName: 'a_side' })
    myGl.setIndexBuffer(indices)
    myGl.setUniform1f('u_line_width', lw)
    myGl.draw('TRIANGLES', indices.length, true)

    // 画线段
    myGl.initShader(lineVertex, lineFragment)
    myGl.setAttribute({ data: lines, size: 2, attrName: 'a_Position' })
    myGl.draw('LINE_STRIP', lines.length / 2)
  }
}

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

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

线宽以及线条阴影

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

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

// gl.POINTS:绘制单个点。
// gl.LINES:绘制线段,每两个顶点构成一条线段。
// gl.LINE_LOOP:绘制线环,所有顶点依次连接成一个闭合的环。
// gl.LINE_STRIP:绘制线带,所有顶点依次连接成一条连续的线。
// gl.TRIANGLES:绘制三角形,每三个顶点构成一个三角形。
// gl.TRIANGLE_STRIP:绘制三角形带,每三个连续的顶点构成一个三角形。
// gl.TRIANGLE_FAN:绘制三角形扇,第一个顶点作为中心点,后续每两个顶点与中心点构成一个三角形。

// uv的垂直距离到当前线段的距离
const lineWidthVertex = `
  precision highp float;
  
  attribute vec2 a_center;
  attribute vec2 a_prev_point;
  attribute vec2 a_next_point;
  attribute float a_side;
  
  uniform float u_line_width;
  
  varying vec3 vColor;
  
  vec2 calculateNormal(vec2 dir) {
    // 只能两行两列乘以两行一列,第一个的列数=第二个的行数
    // 旋转矩阵 |cos, -sin||x| ---> 【x * cosθ - y * sinθ】 ---> x' 
    // 旋转矩阵 |sin, +cos||y| ---> 【x * sinθ + y * cosθ】 ---> y' 
    return normalize(vec2(-dir.y, dir.x));
  }
  
  void main() {
    vColor = vec3(0.9, 0.0, 1.0);
    
    float len = u_line_width * 0.5;
    vec2 extrudedPosition;
    
    // 特殊情况处理:起点和终点
    if (distance(a_prev_point, a_center) < 0.00001) {
      
      // 起点 - 使用下一段的法线
      vec2 nextDir = normalize(a_center - a_next_point); // 注意:这两个※※※※这里是 a_center - a_next_point
      vec2 normal = calculateNormal(nextDir);
      extrudedPosition = a_center + normal * len * a_side;
      
    } else if (distance(a_center, a_next_point) < 0.00001) {
      
      // 终点 - 使用前一段的法线
      vec2 prevDir = normalize(a_prev_point - a_center); // 注意:这两个※※※※这里是 a_prev_point - a_center
      vec2 normal = calculateNormal(prevDir);
      extrudedPosition = a_center + normal * len * a_side;
      
    } else {
      
      vec2 v1Norm = normalize(a_center - a_next_point);
      vec2 v2Norm = normalize(a_center - a_prev_point);
      vec2 v3Norm = calculateNormal(v1Norm);
      vec2 vNorm = normalize(v1Norm + v2Norm);
      float scale = len / dot(v3Norm, vNorm);
      extrudedPosition = a_center + vNorm * scale * a_side;
      
    }
    
    gl_Position = vec4(extrudedPosition, 0.0, 1.0);
    gl_PointSize = 1.0;
  }
`

const lineWidthFragment = `
  precision highp float;
  
  uniform vec2 u_viewport;
  uniform vec2 u_points[6];
  uniform int u_points_len;
  
  varying vec3 vColor;
  
  void main() {
    vec3 finalColor = vColor;
    
    vec2 fragCoord = gl_FragCoord.xy;
    vec2 uv = (fragCoord / u_viewport) * 2.0 - 1.0;
    
    
    float distanceCenterToLine = 1000000.0; // 初始化为一个很大的值
    // 定义线段的点
    vec2 points[7];
    points[0] = vec2(0.5, 0.5);
    points[1] = vec2(0.5, 0.0);
    points[2] = vec2(0.0, 0.0);
    points[3] = vec2(0.0, -0.5);
    points[4] = vec2(0.5, -0.5);
    points[5] = vec2(0.5, -0.25);
    points[6] = vec2(0.7, -0.35);
    // 遍历所有线段
    // 这个10不能设置得太高,在某些移动端 GPU 上无法正确执行
    for (int i = 0; i < 10; i++) {
      
      if (i >= u_points_len - 1) break;
      
      	vec2 p1 = points[i];
      	vec2 p2 = points[i + 1];
      // vec2 p1 = u_points[i];
      // vec2 p2 = u_points[i + 1];

      // 计算线段的方向向量 lineDir,即终点 p2 减去起点 p1。
      vec2 lineDir = p2 - p1;
      
      // 计算线段的长度 lineLength,即方向向量 lineDir 的长度。
      float lineLength = length(lineDir);
      // float lineLength = distance(p2, p1);
      
      // 将方向向量 lineDir 归一化,使其长度为 1。归一化后的方向向量可以用于后续的计算。
      lineDir /= lineLength;

      // 计算从线段起点 p1 到当前片段位置 uv 的向量 toFrag(碎片)。
      vec2 toFrag = uv - p1;
      
      // 计算向量 toFrag 在方向向量 lineDir 上的投影长度 projection。投影长度表示 uv 在线段方向上的位置。
      float projection = dot(toFrag, lineDir);
      
      // 计算当前片段 uv 在线段上的最近点 closestPoint。clamp(projection, 0.0, lineLength) 确保投影长度在 [0, 线段长度] 的范围内,避免超出线段范围。然后将投影长度乘以方向向量 lineDir,并加到起点 p1 上,得到最近点。
      vec2 closestPoint = p1 + lineDir * clamp(projection, 0.0, lineLength);
      
      // 计算当前片段 uv 到最近点 closestPoint 的距离 distance。
      float distance = length(uv - closestPoint);

      // 更新最小距离 distanceCenterToLine,取当前距离 distance 和之前计算的最小距离中的较小值。
      distanceCenterToLine = min(distanceCenterToLine, distance);
    }
    if (distanceCenterToLine < 0.04) {
      finalColor = vec3(0.0, 1.0, 0.0);
    }
    
    
    // 这个有瑕疵
    // float distanceCenterToLine = 0.0; // 初始化
    // float disA = distance(vPrev, vCenter);
    // float disB = distance(vCenter, vNext);
    // vec2 pointA = vec2(0.0);
    // vec2 pointB = vec2(0.0);
    // if (disA >= disB) {
    // 	pointA = vCenter;
    // 	pointB = vNext;
    // } else {
    // 	pointA = vPrev;
    // 	pointB = vCenter;
    // }
    // vec2 lineDir = normalize(pointA - pointB);
    // float distanceCenterToPoint = distance(pointA, uv);
    // float distanceProject = distanceCenterToPoint * dot(lineDir, normalize(pointA - uv));
    // distanceCenterToLine = pow(distanceCenterToPoint * distanceCenterToPoint - distanceProject * distanceProject, 0.5);
    // if (distanceCenterToLine > 0.02) {
    // 	finalColor = vec3(0.0, 1.0, 0.0);
    // }
    
  
    
    gl_FragColor = vec4(finalColor, 1.0);
  }
`

const lineVertex = `
	precision highp float;
	
	attribute vec2 a_Position;
	
	varying vec3 vColor;
	
	void main() {
		
		vColor = vec3(0.0, 0.0, 1.0);
		
		gl_Position = vec4(a_Position, 0.0, 1.0);
		gl_PointSize = 1.0;
	}
`

const lineFragment = `
	precision highp float;
	
	varying vec3 vColor;
	
	void main() {
		gl_FragColor = vec4(vColor, 1.0);
	}
`

class Webgl {
  gl: any = null
  ele: any = null
  width = 0
  height = 0
  constructor(ele, width, height) {
    this.ele = ele
    this.width = width
    this.height = height
    this.gl = null
  }

  init() {
    const canvas = document.querySelector(this.ele)
    canvas.width = this.width // 设置绘制缓冲区宽度
    canvas.height = this.height // 设置绘制缓冲区高度
    this.gl = canvas.getContext('webgl2', {
      antialias: true,
      depth: true
    })
  }

  initShader(vsSource, fsSource) {
    // 创建程序对象
    const program = this.gl.createProgram()

    // 创建着色对象
    const vertexShader = this._loadShader(this.gl.VERTEX_SHADER, vsSource)
    const fragmentShader = this._loadShader(this.gl.FRAGMENT_SHADER, fsSource)

    // 把顶点着色对象/片元着色对象,装进程序对象中
    this.gl.attachShader(program, vertexShader)
    this.gl.attachShader(program, fragmentShader)

    // 连接webgl上下文对象和程序对象
    this.gl.linkProgram(program)

    // 启动程序对象
    this.gl.useProgram(program)

    // 将程序对象挂到上下文对象上
    this.gl.program = program
  }

  /**
   * 开启混合模式
   * 这里的混合模式是指:当两个图形重叠时,如何处理它们的颜色
   */
  runBlend() {
    // 即重合部分的,则会叠加颜色
    this.gl.enable(this.gl.BLEND)
    this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE)
  }

  _loadShader(type, source) {
    // 根据着色类型,创建着色器对象
    const shader = this.gl.createShader(type)

    // 将着色器源文件传入着色器对象中
    this.gl.shaderSource(shader, source)

    // 编译着色器对象
    this.gl.compileShader(shader)

    // 检查编译是否成功
    if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
      console.error('Shader compile error: ', this.gl.getShaderInfoLog(shader))
      this.gl.deleteShader(shader)
      return null
    }

    // 返回着色器对象
    return shader
  }

  setAttribute(info) {
    // 缓冲对象
    const vertexBuffer = this.gl.createBuffer()

    // 绑定缓冲对象
    this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer)

    // 写入数据
    this.gl.bufferData(this.gl.ARRAY_BUFFER, info.data, this.gl.STATIC_DRAW)

    // 获取 attribute 变量
    const attribute = this.gl.getAttribLocation(this.gl.program, info.attrName)

    // 修改 attribute 变量
    this.gl.vertexAttribPointer(attribute, info.size, this.gl.FLOAT, false, 0, 0)

    // 赋能-批处理
    this.gl.enableVertexAttribArray(attribute)
  }

  setIndexBuffer(indices) {
    const indexBuffer = this.gl.createBuffer()
    this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
    this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), this.gl.STATIC_DRAW)
  }

  // gl.uniform1f 用于传递 float 类型
  // gl.uniform2f 用于传递 vec2 类型
  // gl.uniform3f / gl.uniform4f:分别传递 vec3 和 vec4 类型的数据
  // gl.uniform1fv(传递浮点数组)、gl.uniformMatrix4fv(传递 4x4 矩阵)
  setUniform1f(name, value) {
    const location = this.gl.getUniformLocation(this.gl.program, name)
    if (location) {
      this.gl.uniform1f(location, value)
    }
  }

  setUniform2f(name, value1, value2) {
    const location = this.gl.getUniformLocation(this.gl.program, name)
    if (location) {
      this.gl.uniform2f(location, value1, value2)
    }
  }

  setUniform1i(name, value) {
    const location = this.gl.getUniformLocation(this.gl.program, name)
    if (location) {
      this.gl.uniform1i(location, value)
    }
  }

  setUniform2fv(name, value) {
    const location = this.gl.getUniformLocation(this.gl.program, name)
    if (location) {
      if (Array.isArray(value[0])) { // 如果是数组的数组(如 vec2 数组)
        const flat = value.flat() // 需要浏览器支持 .flat()
        this.gl.uniform2fv(location, new Float32Array(flat))
      } else {
        this.gl.uniform2fv(location, value) // 假设是已扁平化的数组
      }
    }
  }

  setTexture(imgObj, uSamplerName, texturePassNum, textureIndex) {
    const uSampler = this.gl.getUniformLocation(this.gl.program, uSamplerName)

    const texture = this.gl.createTexture()

    this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, 1)

    //开始 texturePassNum 号纹理通道
    this.gl.activeTexture(this.gl[texturePassNum])

    //想目标绑定纹理对象
    this.gl.bindTexture(this.gl.TEXTURE_2D, texture)

    //配置纹理的参数
    this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR)
    this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
    this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.MIRRORED_REPEAT)

    //设置着色器参数
    this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGB, this.gl.RGB, this.gl.UNSIGNED_BYTE, imgObj)

    //设置纹理数据
    this.gl.uniform1i(uSampler, textureIndex)
  }

  clear() {
    this.gl.clearColor(0, 0, 0, 1)
  }

  // gl.drawArrays 是一种直接使用顶点数组进行绘制的方法。它会按照顶点数组的顺序依次绘制顶点。
  // gl.drawElements 是一种使用索引数组进行绘制的方法。它通过索引数组来指定顶点的顺序,从而可以重复使用顶点数据,减少顶点数据的冗余。
  draw(type, count, useIndices = false) {
    if (useIndices) {
      this.gl.drawElements(this.gl[type], count, this.gl.UNSIGNED_SHORT, 0)
    } else {
      this.gl.drawArrays(this.gl[type], 0, count)
    }
  }
}


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 points = [
    { x: 0.5, y: 0.5 },
    { x: 0.5, y: 0 },
    { x: 0, y: 0 },
    { x: 0, y: -0.5 },
    { x: 0.5, y: -0.5 },
    { x: 0.5, y: -0.25 },
    { x: 0.7, y: -0.35 },
  ]

  let lw = 0.16
  let centers: any = []
  let prevPoints: any = []
  let nextPoints: any = []
  let sides: any = []
  let indices: any = []

  for (let i = 0; i < points.length; i++) {
    const now = points[i]

    let prev, next

    // 开始的点
    if (i === 0) {
      prev = now
    } else {
      prev = points[i - 1]
    }

    // 结束的点
    if (i === points.length - 1) {
      next = now
    } else {
      next = points[i + 1]
    }

    // 旋转90度 1
    centers.push(now.x, now.y)
    prevPoints.push(prev.x, prev.y)
    nextPoints.push(next.x, next.y)
    sides.push(1)

    // 旋转90度 -1
    centers.push(now.x, now.y)
    prevPoints.push(prev.x, prev.y)
    nextPoints.push(next.x, next.y)
    sides.push(-1)
  }

  // 创建三角形索引
  for (let i = 0; i < points.length - 1; i++) {
    const base = i * 2
    // 两个三角形组成一个四边形段
    // 0   2   4
    // ---------
    // |  /|  /|
    // | / | / |
    // |/  |/  |
    // ---------
    // 1   3   5
    indices.push(base, base + 1, base + 2)
    indices.push(base + 1, base + 2, base + 3)
  }

  const ele: any = document.querySelector('#lineWidthAndShadow')
  const w = ele.clientWidth
  const h = ele.clientHeight

  const myGl = new Webgl('#lineWidthAndShadow', w, h)
  myGl.init()
  myGl.clear()

  myGl.initShader(lineWidthVertex, lineWidthFragment)
  myGl.setAttribute({ data: new Float32Array(centers), size: 2, attrName: 'a_center' })
  myGl.setAttribute({ data: new Float32Array(prevPoints), size: 2, attrName: 'a_prev_point' })
  myGl.setAttribute({ data: new Float32Array(nextPoints), size: 2, attrName: 'a_next_point' })
  myGl.setAttribute({ data: new Float32Array(sides), size: 1, attrName: 'a_side' })

  myGl.setUniform1i('u_points_len', points.length)
  myGl.setUniform1f('u_line_width', lw)
  myGl.setUniform2fv('u_viewport', [w, h])
  myGl.setUniform2fv('u_points', points.map(point => [point.x, point.y]))

  myGl.setIndexBuffer(indices)
  myGl.draw('TRIANGLES', indices.length, true)


  const tempLines: any = []
  points.forEach((v: any) => {
    tempLines.push(v.x)
    tempLines.push(v.y)
  })

  const lines = new Float32Array(tempLines)
  myGl.initShader(lineVertex, lineFragment)
  myGl.setAttribute({ data: lines, size: 2, attrName: 'a_Position' })
  myGl.draw('LINE_STRIP', lines.length / 2)
}

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

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