render.vert

#version 300 es

layout (location = 0) in vec2 aVertexPosition;
layout (location = 1) in vec2 aTexCoord;
layout (location = 2) in vec2 aInstanceOffset;

out vec2 vTexCoord;
out float vTheta;

uniform float uTime;

// 円周率
const float PI = 3.1415926;

void main() {
  vTexCoord = aTexCoord;
  
  float theta = float(gl_InstanceID) * PI * 2.0;
  float base = 1.0;
  float instanceRadius = base * sin(theta * 0.9 + uTime) + base;
  
  vTheta = theta;
  gl_Position = vec4(aVertexPosition * instanceRadius + aInstanceOffset * 0.7, 0.0, 1.0);
}

render.frag

#version 300 es

precision highp float;

in vec2 vTexCoord;
in float vTheta;

out vec4 fragColor;

uniform sampler2D uSprite;

// @see https://iquilezles.org/articles/palettes/
vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
  return a + b * cos(6.28318 * (c * t + d));
}

void main(){
  
  vec3 color = palette(
    mod(vTheta,7.0),
    vec3(0.5, 0.5, 0.5),
    vec3(0.5, 0.5, 0.5),
    vec3(1.0, 1.0, 1.0),
    vec3(0.0, 0.33, 0.67)
  );
  
  vec4 texColor = texture(uSprite, vTexCoord);
  
  if (texColor.a == 0.0) {
    discard;
  }
  
  fragColor = vec4(color, 1.0);
}

render.ts

import { SketchGl, type SketchConfig, type SketchFn } from "sketchgl"
import { InstancedSquare2D } from "sketchgl/geometry"
import { Program, Uniforms } from "sketchgl/program"
import { Timer } from "sketchgl/interactive"
import { ImageTexture } from "sketchgl/texture"

import vert from "./render.vert?raw"
import frag from "./render.frag?raw"

import sprite from "@/assets/for-particle/lace23.png"

const sketch: SketchFn = ({ gl, canvas }) => {
  const uniforms = new Uniforms(gl, ["uTime"])

  const program = new Program(gl)
  program.attach(vert, frag)
  program.activate()

  uniforms.init(program.glProgram)

  const square = new InstancedSquare2D(gl, {
    size: 0.1,
    instanceCount: 70,
    calcOffset: (instanceCount) => {
      const data = []

      for (let i = 0; i < instanceCount; i++) {
        const theta = (Math.PI * 2 * i) / instanceCount
        data.push(Math.cos(theta), Math.sin(theta))
      }

      return {
        components: 2,
        buffer: new Float32Array(data),
        divisor: 1
      }
    }
  })
  square.setLocations({ vertices: 0, uv: 1, offset: 2 })

  const spriteTexture = new ImageTexture(gl, sprite)
  spriteTexture.MAG_FILTER = "LINEAR"
  spriteTexture.MIN_FILTER = "LINEAR"

  const timer = new Timer()
  timer.start()

  gl.clearColor(0.118, 0.235, 0.447, 1.0)

  gl.enable(gl.BLEND)
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE)

  return {
    preload: [spriteTexture.load()],

    drawOnFrame() {
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
      gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)

      square.bind()

      spriteTexture.activate(program.glProgram!, "uSprite")
      uniforms.float("uTime", timer.elapsed * 0.001)

      square.draw({ primitive: "TRIANGLES" })
    }
  }
}

export const onload = () => {
  const config: SketchConfig = {
    canvas: {
      el: "gl-canvas",
      fit: "square",
      autoResize: true
    }
  }
  SketchGl.init(config, sketch)
}