render.vert

#version 300 es

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

out float vTheta;

uniform float uTime;

// 円周率
const float PI = 3.1415926;

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

render.frag

#version 300 es

precision highp float;

in float vTheta;

out vec4 fragColor;

uniform float uTime;

// @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)
  );
  
  fragColor = vec4(color, 1.0);
}

render.ts

import { SketchGl, type SketchConfig, SketchFn } from "sketchgl"
import type { RawVector3 } from "sketchgl/math"
import { InstancedCircle2D } from "sketchgl/geometry"
import { Program, Uniforms } from "sketchgl/program"
import { Timer } from "sketchgl/interactive"

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

const sketch: SketchFn = ({ gl, canvas }) => {
  let clearColor: RawVector3 = [0.345, 0.529, 0.776]

  const uniforms = new Uniforms(gl, ["uTime"])

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

  uniforms.init(program.glProgram)

  const circle = new InstancedCircle2D(gl, {
    radius: 0.05,
    segments: 32,
    instanceCount: 100,
    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
      }
    }
  })
  circle.setLocations({ vertices: 0, offset: 1 })

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

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

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

      circle.bind()
      uniforms.float("uTime", timer.elapsed * 0.001)
      circle.draw({ primitive: "LINE_STRIP" })
    },

    control(ui) {
      ui.rgb("Background", clearColor, (color) => {
        clearColor = color
      })
    }
  }
}

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