index.vert

#version 300 es

in vec2 aVertexTextureCoords;
in vec3 aVertexPosition;

uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;

out vec2 vTextureCoords;

void main() {
  vTextureCoords = aVertexTextureCoords;
  gl_Position = vec4(aVertexPosition, 1.0);
}

index.frag

#version 300 es

precision highp float;

uniform sampler2D uTexture0;
uniform sampler2D uTexture1;
uniform int uBlendMode;

in vec2 vTextureCoords;

out vec4 fragColor;

void main() {
  vec3 finalColor = vec3(0.0);
  
  vec2 texCoord = vec2(vTextureCoords.x, 1.0 - vTextureCoords.y);
  
  vec4 texture0 = texture(uTexture0, texCoord);
  vec4 texture1 = texture(uTexture1, texCoord);
  
  vec3 background = texture0.rgb;
  vec3 foreground = texture1.rgb;
  
  float bgAlpha = texture0.a;
  float fgAlpha = texture1.a;
  
  if (uBlendMode == 0) {
    // add
    finalColor = background + foreground;
  } else if (uBlendMode == 1) {
    // subtract
    finalColor = background - foreground;
  } else if (uBlendMode == 2) {
    // difference
    finalColor = abs(background - foreground);
  } else if (uBlendMode == 3) {
    // lighten
    finalColor = max(background, foreground);
  } else if (uBlendMode == 4) {
    // darken
    finalColor = min(background, foreground);
  } else if (uBlendMode == 5) {
    // multiply
    finalColor = background * foreground;
  } else if (uBlendMode == 6) {
    // screen
    finalColor = 1.0 - (1.0 - background) * (1.0 - foreground);
  } else if (uBlendMode == 7) {
    // overlay
    // 明度brightnessが0.5以下かどうかで分岐
    float brightness = max(background.r, max(background.g, background.b));
    finalColor = mix(
      2.0 * background * foreground,
      1.0 - 2.0 * (1.0 - background) * (1.0 - foreground),
      step(0.5, brightness)
    );
  } else if (uBlendMode == 8) {
    // color-dodge
    finalColor = background / (1.0 - foreground);
  } else if (uBlendMode == 9) {
    // color-burn
    finalColor = 1.0 - (1.0 - background) / foreground;
  }
  
  fragColor = vec4(mix(background, finalColor, fgAlpha), bgAlpha);
}

render.ts

import { Space } from "@/lib/canvas/index"
import { Program } from "@/lib/webgl/program"
import { Scene } from "@/lib/webgl/scene"
import { Clock } from "@/lib/event/clock"
import { ControlUi } from "@/lib/gui/control-ui"
import { UniformLoader } from "@/lib/webgl/uniform-loader"
import { Texture } from "@/lib/webgl/texture"

import mainVertSrc from "./index.vert?raw"
import mainFragSrc from "./index.frag?raw"

// foreground
import imageTomixy from "@/assets/542x542/darken-purple-tomixy_opacity.png"
import imagePatternRock from "@/assets/542x542/usg-pattern_opacity.png"
import imagePatternClassic from "@/assets/542x542/trianglify-lowres.png"
import imagePatternPop from "@/assets/542x542/layered-steps-haikei.png"
// background
import imageWater from "@/assets/542x542/water_00032.png"
import imageTwinklePink from "@/assets/542x542/twinkle_00029.png"
import imageLightDark from "@/assets/542x542/light_00034.png"
import imageTreeWoods from "@/assets/542x542/tree-woods_00123.png"
import imageFogBridge from "@/assets/542x542/fog-bridge.png"

type BlendMode =
  | "add"
  | "subtract"
  | "difference"
  | "lighten"
  | "darken"
  | "multiply"
  | "screen"
  | "overlay"
  | "color-dodge"
  | "color-burn"

const blendModes: BlendMode[] = [
  "add",
  "subtract",
  "difference",
  "lighten",
  "darken",
  "multiply",
  "screen",
  "overlay",
  "color-dodge",
  "color-burn"
]

export const onload = () => {
  const space = new Space("gl-canvas")
  const canvas = space.canvas
  const gl = space.gl
  if (!canvas || !gl) return

  let scene: Scene
  let program: Program
  let clock: Clock
  let texture0s: Texture[] = []
  let texture1s: Texture[] = []

  const uniforms = new UniformLoader(gl, ["uBlendMode"])

  const foregrounds = [
    { name: "ロゴ", image: imageTomixy },
    { name: "黒青系", image: imagePatternRock },
    { name: "鮮やか", image: imagePatternPop },
    { name: "グレー", image: imagePatternClassic }
  ]
  const foregroundNames = foregrounds.map((obj) => obj.name)

  const backgrounds = [
    { name: "水", image: imageWater },
    { name: "明るいキラキラ", image: imageTwinklePink },
    { name: "暗いモヤモヤ", image: imageLightDark },
    { name: "霧", image: imageFogBridge },
    { name: "木々", image: imageTreeWoods }
  ]
  const backgroundNames = backgrounds.map((obj) => obj.name)

  let activeForeground = 3
  let activeBackground = 0

  const initBlendMode = 7

  const initGuiControls = () => {
    const ui = new ControlUi()
    ui.select("Foreground", foregroundNames[activeForeground], foregroundNames, (name) => {
      const idx = foregroundNames.indexOf(name)
      if (idx < 0) return
      activeForeground = idx
      space.fitImage(texture1s[activeForeground].image)
    })
    ui.select("Background", backgroundNames[activeBackground], backgroundNames, (name) => {
      const idx = backgroundNames.indexOf(name)
      if (idx < 0) return
      activeBackground = idx
      space.fitImage(texture0s[activeBackground].image)
    })
    ui.select<BlendMode>("Blend Mode", blendModes[initBlendMode], blendModes, (name) => {
      const idx = blendModes.indexOf(name)
      if (idx < 0) return
      uniforms.int("uBlendMode", idx)
    })
  }

  const onResize = () => {
    space.fitImage(texture0s[activeBackground].image)
    render()
  }

  const configure = async () => {
    gl.clearColor(1.0, 0.0, 0.0, 1.0)
    gl.clearDepth(1.0)

    program = new Program(gl, mainVertSrc, mainFragSrc)

    scene = new Scene(gl, program)
    clock = new Clock()

    uniforms.init(program)
    uniforms.int("uBlendMode", initBlendMode)

    await Promise.all(
      backgrounds.map(async (obj) => {
        const texture = new Texture(gl, program, obj.image, 0)
        texture0s.push(texture)
        await texture.load()
      })
    )

    await Promise.all(
      foregrounds.map(async (obj) => {
        const texture = new Texture(gl, program, obj.image, 1)
        texture1s.push(texture)
        await texture.load()
      })
    )

    space.fitImage(texture0s[activeBackground].image)
    space.onResize = onResize
  }

  const registerGeometry = () => {
    // 画面を覆う板ポリゴン
    const vertices = [-1.0, 1.0, 0.0, 1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0]
    const texCoords = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]
    const indices = [0, 2, 1, 2, 3, 1]
    scene.add({ vertices, indices, texCoords })
  }

  const render = () => {
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

    scene.traverseDraw((obj) => {
      obj.bind()

      texture0s[activeBackground].use()
      texture1s[activeForeground].use()

      gl.drawElements(gl.TRIANGLES, obj.indices.length, gl.UNSIGNED_SHORT, 0)

      obj.cleanup()
    })
  }

  const init = async () => {
    await configure()
    registerGeometry()
    clock.on("tick", render)

    initGuiControls()
  }

  init()
}