Chanomic Sketch

Silent Light

const vs = `
attribute vec3 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;

void main() {
  vec4 positionVec4 = vec4(aPosition, 1.0);
  positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
  gl_Position = positionVec4;
  vTexCoord = aTexCoord;
}
`
const fs = `
precision mediump float;

uniform sampler2D uTexture;
uniform float uTime;
uniform vec2 uResolution;

varying vec2 vTexCoord;

float rand(vec2 co){
  return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453);
}

void main() {
  vec2 uv = vTexCoord;
  uv.y = 1.0 - uv.y;

  vec4 base = texture2D(uTexture, uv);

  vec2 px = 1.0 / uResolution;
  float blurRadius = 8.0; // ← ボケ量(大きいほどピンボケ)
  vec4 blur = vec4(0.0);
  float total = 0.0;

  for (float a = 0.0; a < 6.28318; a += 1.2566) {
    vec2 offset =
      vec2(cos(a), sin(a)) * blurRadius * px;

    float w = 1.0;
    blur += texture2D(uTexture, uv + offset) * w;
    total += w;
  }
  blur /= total;

  float brightness =
    dot(base.rgb, vec3(0.299, 0.587, 0.114));
  float glowStrength = smoothstep(0.2, 0.8, brightness);
  vec3 color =
    mix(base.rgb, blur.rgb, glowStrength * 0.9);

  float grain =
    rand(uv * uResolution + uTime) - 0.5;
  color += grain * 0.04;

  gl_FragColor = vec4(color, 1.0);
}
`

let bgColor = '#000000';
let palette = [
  '#450693',
  '#8C00FF',
  '#FF3F7F',
  '#FFC400'
];
let bubbles = [];
const MAX_FLAKES = 250;
let pg;
let distortShader;

function preload() {
  distortShader = createShader(vs, fs);
}

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  pg = createGraphics(windowWidth, windowHeight);
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  pg = createGraphics(windowWidth, windowHeight);
}

function draw() {
  pg.background(bgColor);

  if (bubbles.length < MAX_FLAKES && random() < 0.6) {
    bubbles.push(new Bubble());
  }

  for (let i = bubbles.length - 1; i >= 0; i--) {
    bubbles[i].update();
    bubbles[i].draw(pg);

    if (bubbles[i].isDead()) {
      bubbles.splice(i, 1);
    }
  }

  shader(distortShader);

  distortShader.setUniform("uTexture", pg);
  distortShader.setUniform("uTime", millis() * 0.001);
  distortShader.setUniform("uResolution", [width, height]);

  rect(-width / 2, -height / 2, width, height);
}

class Bubble {
  constructor() {
    this.pos = createVector(
      random(width),
      height + random(50, 150)
    );

    this.vel = createVector(
      random(-0.15, 0.15),
      random(-0.8, -1.4)
    );
    this.col = color(random(palette));
    this.col.setAlpha(150);

    this.acc = createVector(0, 0);

    this.r = random(12, 40);

    this.buoyancy = random(0.016, 0.024);
    this.drag = random(0.98, 0.992);

    this.noiseSeed = random(1000);
  }

  applyForce(f) {
    this.acc.add(f);
  }

  update() {
    // 浮力(強め)
    this.applyForce(createVector(0, -this.buoyancy));

    // 水流ノイズ
    const n =
      noise(this.noiseSeed, frameCount * 0.01) - 0.5;
    this.applyForce(createVector(n * 0.025, 0));

    // 物理更新
    this.vel.add(this.acc);
    this.vel.mult(this.drag);
    this.pos.add(this.vel);

    this.acc.mult(0);
  }

  draw(g) {
    g.push();
    g.translate(this.pos.x, this.pos.y);

    // === 外側の光(加算的) ===
    g.blendMode(ADD);

    for (let i = 4; i >= 1; i--) {
      let alpha = map(i, 1, 4, 8, 2);
      g.fill(
        red(this.col),
        green(this.col),
        blue(this.col),
        alpha
      );
      g.ellipse(0, 0, this.r * 2 + i * 10);
    }

    // === 中心 ===
    g.fill(
      red(this.col),
      green(this.col),
      blue(this.col),
      120
    );
    g.ellipse(0, 0, this.r * 2);

    g.blendMode(BLEND);
    g.pop();
  }

  isDead() {
    return this.pos.y < -this.r;
  }
}