Chanomic Sketch

Striped Bubbles

let bgColor = '#f5f5f5';
const palette = [
  "#001F3D",
  "#ED985F",
  "#F7B980",
  "#E6E6E6",
  "#F3E2D4",
  "#C5B0CD",
  "#415E72",
  "#17313E"
];

let bubbles = [];
const MAX_BUBBLES = 120;

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(bgColor);
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  background(bgColor);
}

function draw() {
  background(bgColor);

  if (bubbles.length < MAX_BUBBLES && random() < 0.4) {
    bubbles.push(new Bubble());
  }

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

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

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.acc = createVector(0, 0);

    this.r = random(14, 42);
    this.buoyancy = random(0.016, 0.024);

    this.depth = random(0.3, 1.0);
    this.r *= this.depth;
    this.buoyancy *= this.depth;
    this.drag = lerp(0.995, 0.985, this.depth);

    this.noiseSeed = random(1000);

    // ストライプ用
    this.stripeAngle = random(TAU);
    this.stripeGap = random(5, 9);
    this.stripeCount = int(random(4, 8));
    this.stripePalette = shuffle([...palette]).slice(0, int(random(2, 4)));
    this.stripeColors = [];
    for (let i = 0; i < this.stripeCount; i++) {
      this.stripeColors.push(
        color(this.stripePalette[i % this.stripePalette.length])
      );
    }
  }

  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() {
    push();
    translate(this.pos.x, this.pos.y);
    rotate(this.stripeAngle);

    const path = this.getWobblyPath();

    // === ① ゆらいだ泡でクリップ ===
    drawingContext.save();
    drawingContext.beginPath();
    drawingContext.moveTo(path[0].x, path[0].y);
    for (let p of path) {
      drawingContext.lineTo(p.x, p.y);
    }
    drawingContext.closePath();
    drawingContext.clip();

    // === ② ストライプで中身を塗る ===
    noStroke();

    const stripeH = (this.r * 2) / this.stripeCount;

    for (let i = 0; i < this.stripeCount; i++) {
      const col = this.stripeColors[i % this.stripeColors.length];
      col.setAlpha(lerp(200, 255, this.depth));
      fill(col);

      rect(
        -this.r,
        -this.r + i * stripeH,
        this.r * 2,
        stripeH + 1
      );
    }

    drawingContext.restore();

    // === ③ ゆらいだ輪郭を描く ===
    noFill();
    stroke(0, 50);
    strokeWeight(lerp(0.6, 1.2, this.depth));

    beginShape();
    for (let p of path) {
      vertex(p.x, p.y);
    }
    endShape(CLOSE);

    pop();
  }

  getWobblyPath() {
    const pts = [];
    const steps = 60;

    for (let i = 0; i <= steps; i++) {
      const a = (i / steps) * TAU;

      const wobble =
        noise(
          cos(a) + 1 + this.noiseSeed,
          sin(a) + 1,
          frameCount * 0.02
        ) - 0.5;

      const rr = this.r + wobble * 10 * this.depth;

      pts.push({
        x: cos(a) * rr,
        y: sin(a) * rr
      });
    }

    return pts;
  }

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