Open18

p5js with AI 100日チャレンジ

Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

これは何?

https://www.amazon.co.jp/dp/4296071068

こちらの書籍を知って読んでみて触発されたので,自分もプロンプトのみでコーディングをしてどこまでいけるのか?を試してみたくなった次第.

もちろんやりたいのは p5.js を用いた Generative Art.

ルール

  • どの AI モデルを使っても良い
  • 極力 AI が吐き出したコードに手を加えない(あくまでプロンプトのみ)
  • うまくいかなかったとしても,時間が来たらその時点での作品とコードをここに晒す
  • 少しでも変化をつけ,全く同じなものは作らない
Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day1: Door like shapes

まずは格子状にシェイプ(△,□,◯)を並べ,時間経過とともに小さくなったり,本のサイズに戻るようなアニメーションをする幾何学アート.吐かれたコードを自分で手直しをすれば頭の中の完成品になるんだが,あえてプロンプトのみでコーディングをしてみたかったので,完成には至らず.だいぶ気持ちの悪い挙動になってしまった…

ちなみに今日は Cursor の Agent で Auto-select で書いてみた.

コード
// Grid parameters
const gridSize = 10; // Number of cells in each row/column
let cellSize;
let shapes = [];
let colors = [];
let currentPalette = 'Hokusai'; // You can change this to any palette name from colorPalette.js
let globalTime = 0;

function setup() {
  createCanvas((W = min(windowWidth, windowHeight) - 50), W);
  rectMode(CENTER);

  cellSize = W / gridSize;

  // Select 3 colors from the chosen palette
  const palette = colorPalette.find((p) => p.name === currentPalette);
  if (palette) {
    // Take the first 3 colors from the palette
    colors = palette.colors.slice(0, 3);
  } else {
    // Fallback colors if palette not found
    colors = ['#074A59', '#F2C166', '#F28241'];
  }

  // Initialize shapes
  for (let i = 0; i < gridSize; i++) {
    for (let j = 0; j < gridSize; j++) {
      // Randomly choose shape type (0: square, 1: triangle, 2: circle)
      const shapeType = floor(random(3));
      // Randomly choose color index
      const colorIndex = floor(random(3));

      // For squares and triangles, ensure one side is parallel to x-axis
      // by setting rotation to multiples of 90 degrees
      const baseRotation =
        shapeType === 2 ? random(TWO_PI) : (floor(random(4)) * PI) / 2;

      // Animation parameters
      const animationDelay = random(0, 10); // Random delay before animation starts
      const animationDuration = random(3, 8); // Random duration for each animation cycle
      const isActive = random() < 0.3; // Only 30% of shapes are active at a time
      const phase = random(TWO_PI); // Random phase for sine wave

      // Door-like animation parameters
      const doorDirection = floor(random(2)); // 0: horizontal, 1: vertical
      const doorSide = floor(random(2)); // 0: left/top, 1: right/bottom

      shapes.push({
        x: i * cellSize + cellSize / 2,
        y: j * cellSize + cellSize / 2,
        type: shapeType,
        color: colors[colorIndex],
        rotation: baseRotation,
        baseRotation: baseRotation,
        animationDelay: animationDelay,
        animationDuration: animationDuration,
        isActive: isActive,
        phase: phase,
        lastAnimationTime: 0,
        isAnimating: false,
        doorDirection: doorDirection,
        doorSide: doorSide,
        isClosing: false,
        isOpening: false,
      });
    }
  }
}

function draw() {
  background(255);
  globalTime += 0.03;

  // Draw each shape
  for (let shape of shapes) {
    push();
    translate(shape.x, shape.y);
    rotate(shape.baseRotation);

    // Set fill color
    fill(shape.color);
    noStroke();

    // Check if it's time to start a new animation cycle
    if (
      shape.isActive &&
      globalTime - shape.lastAnimationTime > shape.animationDelay
    ) {
      if (!shape.isAnimating && !shape.isClosing && !shape.isOpening) {
        shape.isAnimating = true;
        shape.lastAnimationTime = globalTime;

        // Start with closing phase
        shape.isClosing = true;
        shape.isOpening = false;
      }

      // Handle closing phase
      if (shape.isClosing) {
        let closeProgress =
          (globalTime - shape.lastAnimationTime) / shape.animationDuration;
        if (closeProgress > 1) {
          closeProgress = 1;
          shape.isClosing = false;
          shape.isOpening = true;
          shape.lastAnimationTime = globalTime;
        }

        // Draw shape with door-like closing effect
        if (shape.type === 0) {
          // Square
          if (shape.doorDirection === 0) {
            // Horizontal door
            const width = cellSize * 0.8 * (1 - closeProgress);
            if (shape.doorSide === 0) {
              // Left side
              rect(-cellSize * 0.4 + width / 2, 0, width, cellSize * 0.8);
            } else {
              // Right side
              rect(cellSize * 0.4 - width / 2, 0, width, cellSize * 0.8);
            }
          } else {
            // Vertical door
            const height = cellSize * 0.8 * (1 - closeProgress);
            if (shape.doorSide === 0) {
              // Top side
              rect(0, -cellSize * 0.4 + height / 2, cellSize * 0.8, height);
            } else {
              // Bottom side
              rect(0, cellSize * 0.4 - height / 2, cellSize * 0.8, height);
            }
          }
        } else if (shape.type === 1) {
          // Triangle
          const size = cellSize * 0.8;
          if (shape.doorDirection === 0) {
            // Horizontal door
            const width = size * (1 - closeProgress);
            if (shape.doorSide === 0) {
              // Left side
              const x = -size / 2 + width / 2;
              triangle(x, -size / 2, x + width, size / 2, x, size / 2);
            } else {
              // Right side
              const x = size / 2 - width / 2;
              triangle(x, -size / 2, x + width, size / 2, x, size / 2);
            }
          } else {
            // Vertical door
            const height = size * (1 - closeProgress);
            if (shape.doorSide === 0) {
              // Top side
              const y = -size / 2 + height / 2;
              triangle(0, y, size / 2, y + height, -size / 2, y + height);
            } else {
              // Bottom side
              const y = size / 2 - height / 2;
              triangle(0, y, size / 2, y + height, -size / 2, y + height);
            }
          }
        } else {
          // Circle - use a different approach for circles
          const diameter = cellSize * 0.8 * (1 - closeProgress);
          ellipse(0, 0, diameter, diameter);
        }
      }

      // Handle opening phase
      if (shape.isOpening) {
        let openProgress =
          (globalTime - shape.lastAnimationTime) / shape.animationDuration;
        if (openProgress > 1) {
          openProgress = 1;
          shape.isOpening = false;
          shape.isAnimating = false;
          shape.lastAnimationTime = globalTime;
          // shape.animationDelay = random(2, 8); // New random delay before next animation
        }

        // Draw shape with door-like opening effect
        if (shape.type === 0) {
          // Square
          if (shape.doorDirection === 0) {
            // Horizontal door
            const width = cellSize * 0.8 * openProgress;
            if (shape.doorSide === 0) {
              // Left side
              rect(-cellSize * 0.4 + width / 2, 0, width, cellSize * 0.8);
            } else {
              // Right side
              rect(cellSize * 0.4 - width / 2, 0, width, cellSize * 0.8);
            }
          } else {
            // Vertical door
            const height = cellSize * 0.8 * openProgress;
            if (shape.doorSide === 0) {
              // Top side
              rect(0, -cellSize * 0.4 + height / 2, cellSize * 0.8, height);
            } else {
              // Bottom side
              rect(0, cellSize * 0.4 - height / 2, cellSize * 0.8, height);
            }
          }
        } else if (shape.type === 1) {
          // Triangle
          const size = cellSize * 0.8;
          if (shape.doorDirection === 0) {
            // Horizontal door
            const width = size * openProgress;
            if (shape.doorSide === 0) {
              // Left side
              const x = -size / 2 + width / 2;
              triangle(x, -size / 2, x + width, size / 2, x, size / 2);
            } else {
              // Right side
              const x = size / 2 - width / 2;
              triangle(x, -size / 2, x + width, size / 2, x, size / 2);
            }
          } else {
            // Vertical door
            const height = size * openProgress;
            if (shape.doorSide === 0) {
              // Top side
              const y = -size / 2 + height / 2;
              triangle(0, y, size / 2, y + height, -size / 2, y + height);
            } else {
              // Bottom side
              const y = size / 2 - height / 2;
              triangle(0, y, size / 2, y + height, -size / 2, y + height);
            }
          }
        } else {
          // Circle - use a different approach for circles
          const diameter = cellSize * 0.8 * openProgress;
          ellipse(0, 0, diameter, diameter);
        }
      }
    } else {
      // Draw shape normally when not animating
      if (shape.type === 0) {
        // Square
        rectMode(CENTER);
        rect(0, 0, cellSize * 0.8, cellSize * 0.8);
      } else if (shape.type === 1) {
        // Triangle
        const size = cellSize * 0.8;
        triangle(0, -size / 2, size / 2, size / 2, -size / 2, size / 2);
      } else {
        // Circle
        ellipse(0, 0, cellSize * 0.8, cellSize * 0.8);
      }
    }

    pop();
  }
}

function keyPressed() {
  if (key === 's') {
    saveGif(`mySketch-${round(new Date().getTime() / 100000)}`, 5);
  }

  if (key === 'c') {
    saveCanvas(`mySketch-${round(new Date().getTime() / 100000)}`, 'jpeg');
  }

  // Change palette with number keys 1-9
  if (key >= '1' && key <= '9') {
    const index = parseInt(key) - 1;
    if (index < colorPalette.length) {
      currentPalette = colorPalette[index].name;
      // Reinitialize shapes with new colors
      setup();
    }
  }

  // Reset animation with 'r' key
  if (key === 'r') {
    setup();
  }
}

Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Dat2: Geometric Art - 2

三角関数を多重にかけることで,変わった挙動をするように変更してみた.
本当は point() 関数を使って全然違う絵を描きたかったが,昨日の絵の文脈のまま書いてしまったのと,思ったより見たら面白かったので,今日はこれを作品とする.

ちなみに今日も Cursor に全振りしてる.

コード
// Grid parameters
const gridSize = 10; // Number of cells in each row/column
let cellSize;
let shapes = [];
let colors = [];
let currentPalette = 'Hokusai'; // You can change this to any palette name from colorPalette.js
let globalTime = 0;

function setup() {
  createCanvas((W = min(windowWidth, windowHeight) - 50), W);
  cellSize = W / gridSize;

  // Select 3 colors from the chosen palette
  const palette = colorPalette.find((p) => p.name === currentPalette);
  if (palette) {
    // Take the first 3 colors from the palette
    colors = palette.colors.slice(0, 3);
  } else {
    // Fallback colors if palette not found
    colors = ['#074A59', '#F2C166', '#F28241'];
  }

  // Initialize shapes
  for (let i = 0; i < gridSize; i++) {
    for (let j = 0; j < gridSize; j++) {
      // Randomly choose shape type (0: square, 1: triangle, 2: circle)
      const shapeType = floor(random(3));
      // Randomly choose color index
      const colorIndex = floor(random(3));

      // For squares and triangles, ensure one side is parallel to x-axis
      // by setting rotation to multiples of 90 degrees
      const baseRotation =
        shapeType === 2 ? random(TWO_PI) : (floor(random(4)) * PI) / 2;

      // Animation parameters
      const animationDelay = random(0, 10); // Random delay before animation starts
      const animationDuration = random(3, 8); // Random duration for each animation cycle
      const isActive = random() < 0.3; // Only 30% of shapes are active at a time
      const phase = random(TWO_PI); // Random phase for sine wave

      // Fish fin animation parameters
      const finType = floor(random(3)); // 0: dorsal fin, 1: pectoral fin, 2: caudal fin
      const finSize = random(0.5, 1.5); // Random size multiplier
      const finSpeed = random(0.5, 2); // Random speed multiplier
      const finAmplitude = random(0.2, 0.5); // Random amplitude for fin movement
      const finFrequency = random(1, 3); // Random frequency for fin movement

      // Create the shape object
      const shape = {
        x: i * cellSize + cellSize / 2,
        y: j * cellSize + cellSize / 2,
        type: shapeType,
        color: colors[colorIndex],
        rotation: baseRotation,
        baseRotation: baseRotation,
        animationDelay: animationDelay,
        animationDuration: animationDuration,
        isActive: isActive,
        phase: phase,
        lastAnimationTime: 0,
        isAnimating: false,
        finType: finType,
        finSize: finSize,
        finSpeed: finSpeed,
        finAmplitude: finAmplitude,
        finFrequency: finFrequency,
        points: [],
      };

      // Generate points for the shape
      if (shapeType === 0) {
        // Square points
        const size = cellSize * 0.8;
        shape.points = [
          { x: -size / 2, y: -size / 2 },
          { x: size / 2, y: -size / 2 },
          { x: size / 2, y: size / 2 },
          { x: -size / 2, y: size / 2 },
        ];
      } else if (shapeType === 1) {
        // Triangle points
        const size = cellSize * 0.8;
        shape.points = [
          { x: 0, y: -size / 2 },
          { x: size / 2, y: size / 2 },
          { x: -size / 2, y: size / 2 },
        ];
      } else {
        // Circle points (approximated with many points)
        const radius = cellSize * 0.4;
        const numPoints = 20;
        for (let p = 0; p < numPoints; p++) {
          const angle = (p / numPoints) * TWO_PI;
          shape.points.push({
            x: radius * cos(angle),
            y: radius * sin(angle),
          });
        }
      }

      // Add the shape to the shapes array
      shapes.push(shape);
    }
  }
}

function draw() {
  background(255);
  globalTime += 0.03;

  // Draw each shape
  for (let shape of shapes) {
    push();
    translate(shape.x, shape.y);
    rotate(shape.baseRotation);

    // Set fill color
    fill(shape.color);
    noStroke();

    // Check if it's time to start a new animation cycle
    if (
      shape.isActive &&
      globalTime - shape.lastAnimationTime > shape.animationDelay
    ) {
      if (!shape.isAnimating) {
        shape.isAnimating = true;
        shape.lastAnimationTime = globalTime;
      }

      // Draw shape with fish fin-like animation
      if (shape.type === 0) {
        // Square
        drawFinShape(shape);
      } else if (shape.type === 1) {
        // Triangle
        drawFinShape(shape);
      } else {
        // Circle
        drawFinShape(shape);
      }
    } else {
      // Draw shape normally when not animating
      if (shape.type === 0) {
        // Square
        beginShape();
        for (let point of shape.points) {
          vertex(point.x, point.y);
        }
        endShape(CLOSE);
      } else if (shape.type === 1) {
        // Triangle
        beginShape();
        for (let point of shape.points) {
          vertex(point.x, point.y);
        }
        endShape(CLOSE);
      } else {
        // Circle
        beginShape();
        for (let point of shape.points) {
          vertex(point.x, point.y);
        }
        endShape(CLOSE);
      }
    }

    pop();
  }
}

function drawFinShape(shape) {
  // Calculate time-based animation
  const t = globalTime * shape.finSpeed;

  // Draw the shape with fin-like movement
  beginShape();

  if (shape.type === 0) {
    // Square with fin movement
    for (let i = 0; i < shape.points.length; i++) {
      const point = shape.points[i];
      let dx = 0;
      let dy = 0;

      // Apply different fin movements based on fin type
      if (shape.finType === 0) {
        // Dorsal fin (top edge)
        if (i === 0 || i === 1) {
          const wave =
            sin(t * shape.finFrequency + shape.phase) *
            cos(t * shape.finFrequency * 0.5) *
            shape.finAmplitude *
            cellSize;
          dy = wave;
        }
      } else if (shape.finType === 1) {
        // Pectoral fin (side edges)
        if (i === 0 || i === 3) {
          const wave =
            sin(t * shape.finFrequency + shape.phase) *
            cos(t * shape.finFrequency * 0.7) *
            shape.finAmplitude *
            cellSize;
          dx = wave;
        }
      } else {
        // Caudal fin (all points with varying intensity)
        const intensity = map(i, 0, shape.points.length - 1, 0.2, 1);
        const wave =
          sin(t * shape.finFrequency + shape.phase + i * 0.5) *
          cos(t * shape.finFrequency * 0.3) *
          shape.finAmplitude *
          cellSize *
          intensity;
        dx = wave * cos((i * PI) / 2);
        dy = wave * sin((i * PI) / 2);
      }

      vertex(point.x + dx, point.y + dy);
    }
  } else if (shape.type === 1) {
    // Triangle with fin movement
    for (let i = 0; i < shape.points.length; i++) {
      const point = shape.points[i];
      let dx = 0;
      let dy = 0;

      // Apply different fin movements based on fin type
      if (shape.finType === 0) {
        // Dorsal fin (top point)
        if (i === 0) {
          const wave =
            sin(t * shape.finFrequency + shape.phase) *
            cos(t * shape.finFrequency * 0.5) *
            shape.finAmplitude *
            cellSize;
          dy = wave;
        }
      } else if (shape.finType === 1) {
        // Pectoral fin (bottom points)
        if (i === 1 || i === 2) {
          const wave =
            sin(t * shape.finFrequency + shape.phase) *
            cos(t * shape.finFrequency * 0.7) *
            shape.finAmplitude *
            cellSize;
          dx = wave * (i === 1 ? 1 : -1);
        }
      } else {
        // Caudal fin (all points with varying intensity)
        const intensity = map(i, 0, shape.points.length - 1, 0.2, 1);
        const wave =
          sin(t * shape.finFrequency + shape.phase + i * 0.5) *
          cos(t * shape.finFrequency * 0.3) *
          shape.finAmplitude *
          cellSize *
          intensity;
        dx = wave * cos((i * PI) / 2);
        dy = wave * sin((i * PI) / 2);
      }

      vertex(point.x + dx, point.y + dy);
    }
  } else {
    // Circle with fin movement
    for (let i = 0; i < shape.points.length; i++) {
      const point = shape.points[i];
      let dx = 0;
      let dy = 0;

      // Apply different fin movements based on fin type
      if (shape.finType === 0) {
        // Dorsal fin (top half)
        if (point.y < 0) {
          const wave =
            sin(t * shape.finFrequency + shape.phase) *
            cos(t * shape.finFrequency * 0.5) *
            shape.finAmplitude *
            cellSize;
          dy = wave;
        }
      } else if (shape.finType === 1) {
        // Pectoral fin (left/right sides)
        if (abs(point.x) > abs(point.y)) {
          const wave =
            sin(t * shape.finFrequency + shape.phase) *
            cos(t * shape.finFrequency * 0.7) *
            shape.finAmplitude *
            cellSize;
          dx = wave * (point.x > 0 ? 1 : -1);
        }
      } else {
        // Caudal fin (all points with varying intensity)
        const angle = atan2(point.y, point.x);
        const intensity = map(sin(angle), -1, 1, 0.2, 1);
        const wave =
          sin(t * shape.finFrequency + shape.phase + i * 0.5) *
          cos(t * shape.finFrequency * 0.3) *
          shape.finAmplitude *
          cellSize *
          intensity;
        dx = wave * cos(angle);
        dy = wave * sin(angle);
      }

      vertex(point.x + dx, point.y + dy);
    }
  }

  endShape(CLOSE);
}

function keyPressed() {
  if (key === 's') {
    saveGif(`mySketch-${round(new Date().getTime() / 100000)}`, 5);
  }

  if (key === 'c') {
    saveCanvas(`mySketch-${round(new Date().getTime() / 100000)}`, 'jpeg');
  }

  // Change palette with number keys 1-9
  if (key >= '1' && key <= '9') {
    const index = parseInt(key) - 1;
    if (index < colorPalette.length) {
      currentPalette = colorPalette[index].name;
      // Reinitialize shapes with new colors
      setup();
    }
  }

  // Reset animation with 'r' key
  if (key === 'r') {
    setup();
  }
}

Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day3: Bubble

正直ちょっと不満というか,納得はいっていない.というのも,開始10minくらいでできたものをアップしているから.ただ,完全に不満ではなく,思ったよりは良いものが一瞬で出てきたなと.

これをブラッシュアップしてより良い作品に仕上げられそうに感じている.

コード
// Splatoon-inspired fluid paint simulation
let splatters = [];
let colors = [];
let maxSplatters = 50;

function setup() {
  createCanvas(800, 600);
  background(240);

  // Splatoon-inspired color palette
  colors = [
    color(240, 28, 132), // Pink
    color(0, 232, 254), // Cyan
    color(255, 137, 1), // Orange
    color(0, 249, 138), // Green
    color(190, 92, 255), // Purple
    color(255, 221, 0), // Yellow
  ];

  textAlign(CENTER);
  textSize(16);
}

function draw() {
  // Update and display all splatters
  for (let i = 0; i < splatters.length; i++) {
    splatters[i].update();
    splatters[i].display();
  }

  // Display instructions
  fill(50);
  noStroke();
  text('Click or drag to create fluid paint splatters!', width / 2, 30);
}

function mouseDragged() {
  createSplatter(mouseX, mouseY);
  return false; // Prevent default behavior
}

function mousePressed() {
  createSplatter(mouseX, mouseY);
}

function createSplatter(x, y) {
  // Limit the number of splatters for performance
  if (splatters.length >= maxSplatters) {
    // Remove the oldest splatter
    splatters.shift();
  }

  let selectedColor = random(colors);
  splatters.push(new FluidSplatter(x, y, selectedColor));
}

class FluidSplatter {
  constructor(x, y, color) {
    this.x = x;
    this.y = y;
    this.color = color;
    this.size = random(40, 120);
    this.blobs = [];
    this.blobCount = floor(random(5, 15));

    // Create main blob
    this.mainBlob = {
      offsetX: 0,
      offsetY: 0,
      size: this.size,
      noiseScale: random(0.3, 0.8),
      noiseOffset: random(100),
    };

    // Create connected blobs
    let mainAngle = random(TWO_PI);
    for (let i = 0; i < this.blobCount; i++) {
      // Create blobs in a more connected pattern
      let angle = mainAngle + random(-PI / 2, PI / 2);
      let distance = random(this.size * 0.3, this.size * 0.8);

      this.blobs.push({
        offsetX: cos(angle) * distance,
        offsetY: sin(angle) * distance,
        size: random(this.size * 0.3, this.size * 0.7),
        noiseScale: random(0.3, 0.8),
        noiseOffset: random(100),
        connectionWidth: random(this.size * 0.1, this.size * 0.3),
      });

      // Slightly adjust the main angle for next blob to create a flow
      mainAngle += random(-PI / 4, PI / 4);
    }

    this.splashProgress = 0;
    this.splashSpeed = random(0.03, 0.08);
    this.splashComplete = false;

    // Add some flow properties
    this.flowDirection = random(TWO_PI);
    this.flowSpeed = random(0.2, 1);
    this.flowAmount = 0;
    this.maxFlow = random(10, 30);
  }

  update() {
    if (!this.splashComplete) {
      // Animate the splash effect
      this.splashProgress += this.splashSpeed;

      if (this.splashProgress >= 1) {
        this.splashComplete = true;
      }
    } else {
      // After splash is complete, add some subtle flow
      this.flowAmount += this.flowSpeed;
      if (this.flowAmount > this.maxFlow) {
        this.flowSpeed = 0;
      }
    }
  }

  display() {
    push();

    // Apply the color with some transparency
    let c = this.color;
    fill(red(c), green(c), blue(c), 220);
    noStroke();

    // Draw connections first (underneath)
    if (this.splashProgress > 0.3) {
      let connectionProgress = map(this.splashProgress, 0.3, 1, 0, 1);

      for (let blob of this.blobs) {
        // Draw connection between main blob and this blob
        this.drawConnection(
          this.x,
          this.y,
          this.x + blob.offsetX * this.splashProgress,
          this.y + blob.offsetY * this.splashProgress,
          blob.connectionWidth * connectionProgress,
        );
      }
    }

    // Draw main blob
    let flowOffsetX = cos(this.flowDirection) * this.flowAmount;
    let flowOffsetY = sin(this.flowDirection) * this.flowAmount;

    this.drawFluidBlob(
      this.x + flowOffsetX,
      this.y + flowOffsetY,
      this.mainBlob.size * this.splashProgress,
      this.mainBlob.noiseScale,
      this.mainBlob.noiseOffset,
    );

    // Draw connected blobs
    if (this.splashProgress > 0.2) {
      let blobProgress = map(this.splashProgress, 0.2, 1, 0, 1);

      for (let blob of this.blobs) {
        this.drawFluidBlob(
          this.x + blob.offsetX * this.splashProgress + flowOffsetX * 0.7,
          this.y + blob.offsetY * this.splashProgress + flowOffsetY * 0.7,
          blob.size * blobProgress,
          blob.noiseScale,
          blob.noiseOffset,
        );
      }
    }

    pop();
  }

  drawFluidBlob(x, y, size, noiseScale, noiseOffset) {
    push();
    translate(x, y);

    beginShape();
    for (let a = 0; a < TWO_PI; a += 0.1) {
      // Use noise to create organic, fluid-like edges
      let xoff = map(cos(a), -1, 1, 0, noiseScale);
      let yoff = map(sin(a), -1, 1, 0, noiseScale);
      let r = map(
        noise(xoff + noiseOffset, yoff + noiseOffset, frameCount * 0.01),
        0,
        1,
        size * 0.7,
        size,
      );

      let px = cos(a) * r;
      let py = sin(a) * r;
      vertex(px, py);
    }
    endShape(CLOSE);

    // Add some smaller circles inside for a more fluid look
    for (let i = 0; i < 3; i++) {
      let angle = random(TWO_PI);
      let distance = random(size * 0.1, size * 0.4);
      let smallSize = random(size * 0.1, size * 0.3);
      ellipse(
        cos(angle) * distance,
        sin(angle) * distance,
        smallSize,
        smallSize,
      );
    }

    pop();
  }

  drawConnection(x1, y1, x2, y2, width) {
    // Draw a smooth connection between two points
    push();

    // Find the midpoint with some random offset for natural flow
    let midX = (x1 + x2) / 2 + random(-width / 2, width / 2);
    let midY = (y1 + y2) / 2 + random(-width / 2, width / 2);

    // Draw a curved shape to connect the blobs
    beginShape();

    // Start at first point
    vertex(x1, y1);

    // Add control points for a natural curve
    let controlX1 = x1 + (midX - x1) * 0.5;
    let controlY1 = y1 + (midY - y1) * 0.5;
    let controlX2 = midX + (x2 - midX) * 0.5;
    let controlY2 = midY + (y2 - midY) * 0.5;

    // Add points along the curve with some width
    for (let t = 0; t <= 1; t += 0.1) {
      // Bezier curve calculation
      let bx = bezierPoint(x1, controlX1, controlX2, x2, t);
      let by = bezierPoint(y1, controlY1, controlY2, y2, t);

      // Add perpendicular width
      let angle =
        atan2(
          bezierTangent(y1, controlY1, controlY2, y2, t),
          bezierTangent(x1, controlX1, controlX2, x2, t),
        ) + HALF_PI;

      // Width varies along the connection for a more natural look
      let currentWidth = width * (1 - abs(t - 0.5) * 1.5);
      if (currentWidth > 0) {
        vertex(bx + cos(angle) * currentWidth, by + sin(angle) * currentWidth);
      }
    }

    // Go back along the other side
    for (let t = 1; t >= 0; t -= 0.1) {
      let bx = bezierPoint(x1, controlX1, controlX2, x2, t);
      let by = bezierPoint(y1, controlY1, controlY2, y2, t);

      let angle =
        atan2(
          bezierTangent(y1, controlY1, controlY2, y2, t),
          bezierTangent(x1, controlX1, controlX2, x2, t),
        ) + HALF_PI;

      let currentWidth = width * (1 - abs(t - 0.5) * 1.5);
      if (currentWidth > 0) {
        vertex(bx - cos(angle) * currentWidth, by - sin(angle) * currentWidth);
      }
    }

    endShape(CLOSE);

    pop();
  }
}

function keyPressed() {
  if (key === 's') {
    saveGif(`mySketch-${round(new Date().getTime() / 100000)}`, 5);
  }

  if (key === 'c') {
    saveCanvas(`mySketch-${round(new Date().getTime() / 100000)}`, 'jpeg');
  }
}

Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day4: Circle Packing

今日からどんなプロンプトを投げたかも乗せておく
本当は,任意の絵文字を画面の中央に拡大して描画し,それを画像をして認識し,その画像内を円充填したかったが,何度やっても loadPixles() 後の pixels 配列を観ても,絵文字の部分がすべて 255 のままだったため,無理だった.途中からガチで自分自身でデバッグして無理だったので,自分の能力不足のため,どっかでリベンジする.

ということで,違う円充填を描いた.
キャンバス内に quad を用いてランダムな配置とサイズで図形を描き,その内部を4色の円で充填させたもの.

- 一回絵文字からやるのは忘れて,シンプルに円充填をしてみて欲しい
- 円充填がどういうものかそもそも知っていますか?
- デフォルメした牛を横から見た姿の外枠だけを線で描き,その内部を円充填できる?
- 流石に牛の外枠は難しかったと思うので,ランダムな四角形(rect ではなく,quadを使う)をいくつか画面上にランダム配置させ,その内部を円充填して
- isInsideQuad() メソッドの頂点判定処理を書いて
- 色はデザインや色彩のノウハウに基づいた3or4色にして
コード
let W; // キャンバスのサイズ
let circles = []; // 円の配列
let gridSize = 10; // グリッドのサイズ
let maxAttempts = 1000; // 円を配置する試行回数
let quads = [];
let maxQuads = 5; // 四角形の数
let colors = [];

function setup() {
  W = min(windowWidth, windowHeight) - 100;
  createCanvas(W, W);
  background(255);
  noStroke();
  drawCowOutline();
  noLoop();
  colors = [
    color(41, 128, 185), // ベースカラー(青)
    color(39, 174, 96), // アナログスキーム(青緑)
    color(142, 68, 173), // アナログスキーム(紫)
    color(230, 126, 34), // アクセントカラー(オレンジ)
  ];

  // ランダムな四角形を生成
  for (let i = 0; i < maxQuads; i++) {
    let x1 = random(width);
    let y1 = random(height);
    let x2 = random(width);
    let y2 = random(height);
    let x3 = random(width);
    let y3 = random(height);
    let x4 = random(width);
    let y4 = random(height);
    quads.push({ x1, y1, x2, y2, x3, y3, x4, y4 });
  }

  // 円を配置
  for (let i = 0; i < maxAttempts; i++) {
    let x = random(width);
    let y = random(height);
    let r = random(5, 20);
    let valid = true;

    // 既存の円と重ならないかチェック
    for (let circle of circles) {
      let d = dist(x, y, circle.x, circle.y);
      if (d < r + circle.r) {
        valid = false;
        break;
      }
    }

    // 四角形の内部にあるかチェック
    if (valid && isInsideQuad(x, y)) {
      circles.push({ x, y, r, color: colors[floor(random(colors.length))] });
    }
  }
}

function draw() {
  background(255);
  drawCowOutline();
  // 四角形を描画
  console.log(quads);
  for (let item of quads) {
    quad(
      item.x1,
      item.y1,
      item.x2,
      item.y2,
      item.x3,
      item.y3,
      item.x4,
      item.y4,
    );
  }
  push();
  for (let circle of circles) {
    fill(circle.color);
    ellipse(circle.x, circle.y, circle.r * 2);
  }
  pop();
}

// 円のサイズを決定する関数
function getCircleSize(x, y) {
  let minSize = 2;
  let maxSize = gridSize;
  // ここで円の大きさを決定するロジックを追加
  return random(minSize, maxSize);
}

// 点が四角形の内部にあるかチェックする関数
function isInsideQuad(x, y) {
  let inside = false;
  for (let quad of quads) {
    // 四角形の頂点を配列に格納
    let vertices = [
      { x: quad.x1, y: quad.y1 },
      { x: quad.x2, y: quad.y2 },
      { x: quad.x3, y: quad.y3 },
      { x: quad.x4, y: quad.y4 },
    ];

    // Ray Casting Algorithm
    let j = vertices.length - 1;
    for (let i = 0; i < vertices.length; i++) {
      let xi = vertices[i].x;
      let yi = vertices[i].y;
      let xj = vertices[j].x;
      let yj = vertices[j].y;

      // 点が辺の上にある場合は内部と判定
      if (xi === xj && yi === yj) continue;
      if (xi === x && yi === y) return true;

      // 辺と水平線の交差判定
      let intersect =
        yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
      if (intersect) inside = !inside;
      j = i;
    }
    if (inside) return true;
  }
  return false;
}

function drawCowOutline() {
  beginShape();
  // 牛の頭部
  vertex(100, 100);
  vertex(150, 50);
  vertex(200, 100);
  // 牛の胴体
  vertex(250, 150);
  vertex(300, 200);
  vertex(250, 250);
  // 牛の後ろ足
  vertex(200, 300);
  vertex(150, 250);
  // 牛の前足
  vertex(100, 200);
  vertex(50, 150);
  endShape(CLOSE);
}

function keyPressed() {
  if (key === 'c') {
    saveCanvas('myCanvas', 'png');
  }
}

Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day5: simple animation with trigonometric functions

アイディアそのものが全く浮かばなかったので,ちょっと置きに行った作品.
困ったら誰かの作品を模倣するところから言っても良いかもなぁ.

- ドットを使ってアニメーションを描いてください.三角関数を複数組み合わせて,美しい動きをして欲しい
- あまりにも単調な動きのため,もっと三角関数を複数回組み合わせて動きも複雑化して
- もっと複雑な動きをして欲しい.ダイナミックさも付け加えて
- elilpseを絵文字にしたのですが,進行方向に向かって角度を自動で変更して欲しい

コード
let time = 0;
const numDots = 80;
let baseRadius;

// 動的なパラメータ
let pulse = 0;
let rotationSpeed = 0.3;
let expansionFactor = 1;

// 前フレームの位置を保存する配列
let prevPositions = [];

function setup() {
  createCanvas((W = min(windowWidth, windowHeight) - 300), W);
  baseRadius = W / 4.5;
  textSize(W / 32);
  textAlign(CENTER, CENTER);
}

function draw() {
  background(255);

  // 動的なパラメータの更新
  pulse = sin(time * 0.5) * 0.2 + 1; // 0.8から1.2の間で脈動
  rotationSpeed = 0.3 + sin(time * 0.2) * 0.1; // 回転速度の変化
  expansionFactor = 1 + sin(time * 0.3) * 0.3; // 全体の拡大縮小

  push();
  translate(width / 2, height / 2);

  // 現在の位置を保存する配列
  let currentPositions = [];

  for (let i = 0; i < numDots; i++) {
    const angle = (i / numDots) * TWO_PI;

    // より複雑な半径の変化
    const radiusVariation =
      sin(time + angle * 3) * 30 * pulse +
      cos(time * 0.7 + angle * 2) * 20 +
      sin(time * 1.3 + angle * 4) * 15 +
      cos(time * 0.4 + angle * 5) * 10 +
      sin(time * 2.1 + angle * 6) * 8;

    const radius = (baseRadius + radiusVariation) * expansionFactor;

    // より複雑な位置計算
    const xOffset =
      sin(time * 0.5 + angle * 2) * 40 * pulse +
      cos(time * 0.3 + angle * 3) * 30 +
      sin(time * 1.2 + angle * 4) * 20 +
      cos(time * 0.8 + angle * 5) * 15;

    const yOffset =
      cos(time * 0.7 + angle * 2) * 40 * pulse +
      sin(time * 0.4 + angle * 3) * 30 +
      cos(time * 1.5 + angle * 4) * 20 +
      sin(time * 0.9 + angle * 5) * 15;

    const x = cos(angle + time * rotationSpeed) * radius + xOffset;
    const y = sin(angle + time * rotationSpeed) * radius + yOffset;

    // 現在の位置を保存
    currentPositions[i] = { x, y };

    // より複雑な色の変化
    const hue =
      (i * 2 +
        time * 30 +
        sin(time + i * 0.1) * 30 +
        cos(time * 0.5 + i * 0.2) * 20) %
      360;
    const saturation =
      70 + sin(time * 2 + i * 0.05) * 30 + cos(time * 1.2 + i * 0.1) * 15;
    const brightness =
      80 + cos(time * 1.5 + i * 0.03) * 20 + sin(time * 0.8 + i * 0.07) * 15;

    fill(hue, saturation, brightness);
    noStroke();

    // 回転角度の計算
    let rotation = 0;
    if (prevPositions[i]) {
      const dx = x - prevPositions[i].x;
      const dy = y - prevPositions[i].y;
      rotation = atan2(dy, dx);
    }

    // はさみの絵文字を回転させて描画
    push();
    translate(x, y);
    rotate(rotation);
    text('✄', 0, 0);
    pop();
  }

  // 前フレームの位置を更新
  prevPositions = currentPositions;

  pop();

  time += 0.03;
}

function keyPressed() {
  if (key === 's') {
    saveGif(`mySketch-${round(new Date().getTime() / 100000)}`, 4);
  }

  if (key === 'c') {
    saveCanvas(`mySketch-${round(new Date().getTime() / 100000)}`, 'jpeg');
  }
}

Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day6: Dotts Image

ドットによる点描のアニメーションや画像は地味に好きなのでなにかないかなーということで書いてみた.
改良の余地はあるが,眠すぎるのでこれが今の実力ということで,明日以降の俺に託す.

- p5jsを用いて点描でなにか作品を作ってみたい.デフォルメした犬のシュナウザーを横から見た姿を描ける?
- 添付画像のように割といい感じのコードでした!これをもっとシンプルに,シルエットのようにして欲しい
- おしいが,手足が単なる長方形になってしまっているので,もう少し犬の足に近づけて.顔も,斜め左下方向への長方形で,角丸に
- コードが途中で途切れたので続けて

途中経過のものもすべて添付



コード
function setup() {
  createCanvas(600, 400);
  background(240);
  noLoop();
}

function draw() {
  let silhouetteColor = color(50); // シルエットの色 (濃いグレー)

  // 点描の密度や点の大きさを調整するパラメータ
  let densityFactor = 1.3; // 点の密度を少し上げる
  let dotMinSize = 2;
  let dotMaxSize = 5; // 少し点の最大サイズを上げてみる

  // 元の各パーツの密度目安
  let baseDensity = 1000;

  // --- シュナウザーのシルエット描画 ---

  // 体 (少し調整)
  drawDottedEllipse(
    width / 2,
    height / 2 + 35,
    200,
    130,
    silhouetteColor,
    baseDensity * 1.5 * densityFactor,
    dotMinSize,
    dotMaxSize,
  );

  // 顔 (斜め左下方向への角丸長方形)
  push();
  translate(width / 2 - 105, height / 2 - 30); // 顔の中心位置を調整
  rotate(radians(-25)); // 回転角度を調整
  // drawDottedRoundedRect のx,yは回転後のローカル座標なので0,0でOK
  drawDottedRoundedRect(
    0,
    0,
    90,
    60,
    15,
    silhouetteColor,
    baseDensity * 0.9 * densityFactor,
    dotMinSize,
    dotMaxSize,
  );
  pop();

  // マズル (顔と一体化するように位置調整)
  // 顔の回転と位置に合わせて調整が必要
  // push/popとtranslate/rotateで顔と一緒に描画する方が連動しやすいが、ここでは個別に配置
  push();
  translate(width / 2 - 130, height / 2 + 5); // マズルの基準点
  rotate(radians(-10)); // マズルも少し傾ける
  drawDottedRectangle(
    0,
    0,
    70,
    40,
    silhouetteColor,
    baseDensity * 0.7 * densityFactor,
    dotMinSize,
    dotMaxSize,
  );
  pop();

  // 耳 (垂れ耳風シルエット)
  push();
  translate(width / 2 - 90, height / 2 - 65); // 位置調整
  rotate(radians(-30)); // 角度調整
  drawDottedEllipse(
    0,
    0,
    45,
    75,
    silhouetteColor,
    baseDensity * 0.5 * densityFactor,
    dotMinSize,
    dotMaxSize,
  );
  pop();

  // 前足1
  drawDottedLeg(
    width / 2 - 55,
    height / 2 + 70,
    35,
    55,
    28,
    48,
    10,
    silhouetteColor,
    baseDensity * 0.45 * densityFactor,
    dotMinSize,
    dotMaxSize,
  );
  // 前足2
  drawDottedLeg(
    width / 2 - 20,
    height / 2 + 70,
    35,
    55,
    28,
    48,
    -5,
    silhouetteColor,
    baseDensity * 0.45 * densityFactor,
    dotMinSize,
    dotMaxSize,
  );

  // 後ろ足1 (太もも部分がより角度がつくように)
  drawDottedLeg(
    width / 2 + 55,
    height / 2 + 60,
    45,
    65,
    35,
    55,
    -35,
    silhouetteColor,
    baseDensity * 0.5 * densityFactor,
    dotMinSize,
    dotMaxSize,
  );
  // 後ろ足2 (少し奥にあるように見せるため、サイズや位置を微調整)
  drawDottedLeg(
    width / 2 + 85,
    height / 2 + 65,
    42,
    62,
    33,
    53,
    -45,
    silhouetteColor,
    baseDensity * 0.45 * densityFactor,
    dotMinSize,
    dotMaxSize,
  );

  // 尻尾
  push();
  translate(width / 2 + 105, height / 2 + 15); // 位置調整
  rotate(radians(30)); // 角度調整
  drawDottedEllipse(
    0,
    0,
    25,
    45,
    silhouetteColor,
    baseDensity * 0.3 * densityFactor,
    dotMinSize,
    dotMaxSize,
  );
  pop();
}

// 指定された範囲に点を描画するヘルパー関数 (楕円)
function drawDottedEllipse(x, y, w, h, col, numPoints, minSize, maxSize) {
  // 楕円の中心をx,yとする
  for (let i = 0; i < numPoints; i++) {
    let angle = random(TWO_PI);
    // 楕円内部に均等に分布させるための工夫
    let rScale = sqrt(random()); // sqrt(random()) にすると中心付近の密度が下がり、より均一に近くなる
    let rX = (w / 2) * rScale;
    let rY = (h / 2) * rScale;
    let dotX = x + cos(angle) * rX;
    let dotY = y + sin(angle) * rY;
    let dSize = random(minSize, maxSize);
    fill(col);
    noStroke();
    ellipse(dotX, dotY, dSize, dSize);
  }
}

// 指定された範囲に点を描画するヘルパー関数 (四角形)
function drawDottedRectangle(x, y, w, h, col, numPoints, minSize, maxSize) {
  // 四角形の左上をx,yとする
  for (let i = 0; i < numPoints; i++) {
    let dotX = random(x, x + w);
    let dotY = random(y, y + h);
    let dSize = random(minSize, maxSize);
    fill(col);
    noStroke();
    ellipse(dotX, dotY, dSize, dSize);
  }
}

// 指定された範囲に点を描画するヘルパー関数 (角丸四角形)
// rectX, rectY は現在の描画コンテキストでの角丸四角形の左上の座標(通常は0,0 nếu translate後)
function drawDottedRoundedRect(
  rectX,
  rectY,
  w,
  h,
  r,
  col,
  numPoints,
  minSize,
  maxSize,
) {
  r = min(r, w / 2, h / 2); // 半径が幅や高さの半分を超えないように調整
  let drawnPoints = 0;
  let maxAttempts = numPoints * 10; // 無限ループを避けるための最大試行回数

  for (let i = 0; i < maxAttempts && drawnPoints < numPoints; i++) {
    // 角丸四角形のバウンディングボックス内でランダムな点を生成 (ローカル座標)
    let px = random(w); // rectX から rectX + w ではなく、ローカルの 0 から w
    let py = random(h); // rectY から rectY + h ではなく、ローカルの 0 から h

    let inside = false;
    // 点が角丸四角形の内部にあるか判定
    if (px > r && px < w - r) {
      // 中央の垂直帯内部(yは全長OK)
      inside = true;
    } else if (py > r && py < h - r) {
      // 中央の水平帯内部(xは全長OK)
      inside = true;
    } else {
      // 角の領域にある可能性
      if (px <= r && py <= r) {
        // 左上角
        if (dist(px, py, r, r) <= r) inside = true;
      } else if (px >= w - r && py <= r) {
        // 右上角
        if (dist(px, py, w - r, r) <= r) inside = true;
      } else if (px <= r && py >= h - r) {
        // 左下角
        if (dist(px, py, r, h - r) <= r) inside = true;
      } else if (px >= w - r && py >= h - r) {
        // 右下角
        if (dist(px, py, w - r, h - r) <= r) inside = true;
      }
    }

    if (inside) {
      let dSize = random(minSize, maxSize);
      fill(col);
      noStroke();
      // 描画はローカル座標 (px, py) で行う。`translate()` で既に位置決めされている前提。
      ellipse(rectX + px, rectY + py, dSize, dSize); // 描画は rectX, rectY を基準にオフセット
      drawnPoints++;
    }
  }
}

// 改良された足の描画関数 (2つの楕円セグメントで構成)
// baseX, baseY: 足の付け根の回転軸となる位置
// upperW, upperH: 上部セグメントの幅と高さ
// lowerW, lowerH: 下部セグメントの幅と高さ
// legAngle: 足全体の基準線からの傾き (度数法)
function drawDottedLeg(
  baseX,
  baseY,
  upperW,
  upperH,
  lowerW,
  lowerH,
  legAngle,
  col,
  numPoints,
  minSize,
  maxSize,
) {
  push();
  translate(baseX, baseY); // 足の付け根に原点を移動
  rotate(radians(legAngle)); // 足全体を指定された角度だけ回転

  // 上部セグメント (例: 太もも)
  // 回転軸(0,0)から下に伸びるように、中心を(0, upperH/2)に配置
  drawDottedEllipse(
    0,
    upperH / 2,
    upperW,
    upperH,
    col,
    floor(numPoints * 0.6),
    minSize,
    maxSize,
  );

  // 下部セグメント (例: すね + 足先)
  // 上部セグメントの終端あたりから始まるように配置
  // Y座標のオフセット: 上部セグメントの高さ + 下部セグメントの高さの半分
  // 少し重なるように upperH * 0.85 などで調整可能
  let lowerSegmentCenterY = upperH * 0.9 + (lowerH / 2) * 0.8; // Y位置を微調整して接続を自然に
  let lowerSegmentOffsetX = upperW * 0.05; // X位置を微調整して「く」の字に(お好みで)
  drawDottedEllipse(
    lowerSegmentOffsetX,
    lowerSegmentCenterY,
    lowerW,
    lowerH,
    col,
    floor(numPoints * 0.4),
    minSize,
    maxSize,
  );
  pop();
}

// キーボード操作
function keyPressed() {
  const timestamp = round(new Date().getTime() / 100000);
  saveCanvas(`screenshot-${timestamp}`, 'jpeg');
}
Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day7: easing animation

プロンプトは以前のものがなくなってしまったので今回はなしで.
Okazz さんの様なアニメーションをしてみたかったので,ちょっとコードはパクった.
といってもまだ不完全さは残る.

コード
const ANIMATION_SPAN = 200; // アニメーションの長さ
const GRID_SIZE = 10; // グリッドのサイズ
const SCALE_FACTOR = 0; // 最小サイズを0に設定
const DELAY_FACTOR = 5; // 隣接する正方形間の遅延

// キャンバス設定
let canvasSize;
let cellSize;
const rectangles = [];

function setup() {
  canvasSize = min(windowWidth, windowHeight) - 200;
  createCanvas(canvasSize, canvasSize);
  noStroke();
  rectMode(CENTER);

  // セルのサイズを計算
  cellSize = canvasSize / GRID_SIZE;

  // グリッド状に正方形を配置
  for (let i = 0; i < GRID_SIZE; i++) {
    for (let j = 0; j < GRID_SIZE; j++) {
      // 格子状に配置
      const x = i * cellSize + cellSize / 2;
      const y = j * cellSize + cellSize / 2;
      // 位置による遅延を設定 (左上から右下へ)
      const delay = (i + j) * DELAY_FACTOR;

      rectangles.push(new AnimatedRectangle(x, y, cellSize, cellSize, delay));
    }
  }
}

function draw() {
  background(255);

  // すべての長方形を更新して描画
  for (const rect of rectangles) {
    rect.update();
    rect.display();
  }
}

class AnimatedRectangle {
  constructor(x, y, width, height, delay) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.delay = delay;
    this.timer = 0;
    this.direction = 1; // 1: 増加, -1: 減少
    this.animationProgress = 0;
  }

  update() {
    // 遅延後にアニメーションを開始
    if (frameCount < this.delay) {
      this.animationProgress = 1; // 最初は最大サイズ
      return;
    }

    // タイマーを更新
    this.timer += this.direction;

    // アニメーション方向を切り替え
    if (this.timer >= ANIMATION_SPAN) {
      this.direction = -1;
    } else if (this.timer <= 0) {
      this.direction = 1;
    }

    // イージング関数を適用してアニメーション進捗を計算
    const normalizedTime =
      this.direction === -1
        ? norm(this.timer, 0, ANIMATION_SPAN)
        : norm(this.timer, ANIMATION_SPAN, 0);

    this.animationProgress =
      this.direction === 1
        ? easeInOutQuint(normalizedTime)
        : easeInOutQuint(1 - normalizedTime);
  }

  display() {
    // サイズをアニメーション進捗に基づいて補間
    const currentWidth =
      lerp(SCALE_FACTOR, 1, this.animationProgress) * this.width;
    const currentHeight =
      lerp(SCALE_FACTOR, 1, this.animationProgress) * this.height;

    fill(0);
    rect(this.x, this.y, currentWidth, currentHeight);
  }
}

// イージング関数(5次元の入出力補間)
function easeInOutQuint(x) {
  return x < 0.5 ? 16 * x * x * x * x * x : 1 - pow(-2 * x + 2, 5) / 2;
}

// キーボード操作
function keyPressed() {
  if (key === 's') {
    saveGif(`animation-${round(new Date().getTime() / 100000)}`, 8);
  } else if (key === 'c') {
    saveCanvas(`screenshot-${round(new Date().getTime() / 100000)}`, 'jpeg');
  }
}

Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day8: Colorful interactive and animation

最近は Claude Code で書かせることが増えてきた.𝕏 からパクったプロンプトをそのまま Claude Code で実行した形.

プロンプト
Create an amazing animation multicolor and interactive using p5js

  • マウス操作に応じて,背景のドットの動きが変化する(左右のクリックで若干違いもある)
  • あとは,s,c, r, p キーごとに違う挙動が設定されていたりと

結果としては,自分の普段書くクセなども盛り込んだコードになっており,なかなかやるなと感心している.
コードは長いのでこちらをご参照

Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day9: Pop Image

まさに小さな子どもの服やおもちゃなどの柄に使われそうな作品が出来上がった.今回も Claude Code に書かせてみた.ちなみにプロンプトは2つで微調整をした形.

プロンプト
- Create an pop and multicolor image using p5js
- Textbooks are not required. Place the objects in a grid. The shape and size of the  objects are random

コード
let W;
let popColors;
let grid;
let shapes = [];

function setup() {
  W = min(windowWidth, windowHeight) - 50;
  createCanvas(W, W);

  // Pop art inspired color palette
  popColors = [
    '#FF006E', // Hot Pink
    '#FFBE0B', // Electric Yellow
    '#FB5607', // Bright Orange
    '#8338EC', // Electric Purple
    '#3A86FF', // Bright Blue
    '#06FFA5', // Electric Green
    '#FF4081', // Pink
    '#00E676', // Green
    '#FF5722', // Deep Orange
    '#E91E63', // Pink
    '#9C27B0', // Purple
    '#2196F3', // Blue
    '#4CAF50', // Green
    '#FF9800', // Orange
    '#F44336', // Red
    '#FFEB3B', // Yellow
    '#00BCD4', // Cyan
    '#795548', // Brown
  ];

  noLoop(); // Static image

  createPopArtComposition();
}

function draw() {
  // Create clean background
  background(255);

  // Draw geometric pop art elements in grid
  drawPopArtShapes();
}

function createPopArtComposition() {
  // Create grid of pop art elements with random shapes and sizes
  let cols = 8;
  let rows = 8;
  let cellW = W / cols;
  let cellH = W / rows;

  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      let x = i * cellW;
      let y = j * cellH;

      // Random size variations
      let sizeVariation = random(0.3, 0.9);
      let w = cellW * sizeVariation;
      let h = cellH * sizeVariation;

      // Sometimes make shapes very different aspect ratios
      if (random() < 0.3) {
        w = cellW * random(0.2, 0.9);
        h = cellH * random(0.2, 0.9);
      }

      shapes.push({
        x: x + cellW / 2,
        y: y + cellH / 2,
        w: w,
        h: h,
        type: random([
          'circle',
          'rect',
          'triangle',
          'star',
          'hexagon',
          'diamond',
          'pentagon',
        ]),
        color: random(popColors),
        rotation: random(TWO_PI),
      });
    }
  }
}

function drawPopArtShapes() {
  for (let shape of shapes) {
    push();
    translate(shape.x, shape.y);
    rotate(shape.rotation);

    // Drop shadow effect
    fill(0, 100);
    noStroke();
    drawShape(shape.type, 5, 5, shape.w, shape.h);

    // Main shape
    fill(shape.color);
    stroke(0);
    strokeWeight(4);
    drawShape(shape.type, 0, 0, shape.w, shape.h);

    // Highlight effect
    fill(255, 150);
    noStroke();
    drawShape(shape.type, -2, -2, shape.w * 0.3, shape.h * 0.3);

    pop();
  }
}

function drawShape(type, x, y, w, h) {
  push();
  translate(x, y);

  switch (type) {
    case 'circle':
      ellipse(0, 0, w, h);
      break;
    case 'rect':
      rectMode(CENTER);
      rect(0, 0, w, h, 10);
      break;
    case 'triangle':
      triangle(-w / 2, h / 2, w / 2, h / 2, 0, -h / 2);
      break;
    case 'star':
      drawStar(0, 0, w / 4, w / 2, 5);
      break;
    case 'hexagon':
      drawHexagon(0, 0, w / 2);
      break;
    case 'diamond':
      drawDiamond(0, 0, w / 2, h / 2);
      break;
    case 'pentagon':
      drawPentagon(0, 0, w / 2);
      break;
  }
  pop();
}

function drawStar(x, y, radius1, radius2, npoints) {
  let angle = TWO_PI / npoints;
  let halfAngle = angle / 2.0;
  beginShape();
  for (let a = 0; a < TWO_PI; a += angle) {
    let sx = x + cos(a) * radius2;
    let sy = y + sin(a) * radius2;
    vertex(sx, sy);
    sx = x + cos(a + halfAngle) * radius1;
    sy = y + sin(a + halfAngle) * radius1;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}

function drawHexagon(x, y, radius) {
  beginShape();
  for (let i = 0; i < 6; i++) {
    let angle = (i * TWO_PI) / 6;
    let sx = x + cos(angle) * radius;
    let sy = y + sin(angle) * radius;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}

function drawDiamond(x, y, w, h) {
  beginShape();
  vertex(x, y - h);
  vertex(x + w, y);
  vertex(x, y + h);
  vertex(x - w, y);
  endShape(CLOSE);
}

function drawPentagon(x, y, radius) {
  beginShape();
  for (let i = 0; i < 5; i++) {
    let angle = (i * TWO_PI) / 5 - PI / 2;
    let sx = x + cos(angle) * radius;
    let sy = y + sin(angle) * radius;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}

function mousePressed() {
  // Regenerate the composition
  shapes = [];
  createPopArtComposition();
  redraw();
}

function keyPressed() {
  if (key === 'c') {
    saveCanvas(`pop-art-${round(new Date().getTime() / 100000)}`, 'jpeg');
  }

  if (key === 'r') {
    // Regenerate composition
    shapes = [];
    createPopArtComposition();
    redraw();
  }
}
Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day10: Symbiosis of digital(images) and reality

タイトル通りだが,やってみたかったというか,個人的に追っているものの一つに.デジタルアートと現実を上手く同居したような作品を考えていて,その一環の一つ.

指定した画像をピクセルで管理し,デジタルでまばらなピクセルと1ピクセルできっちり指定した画像を表示することで現実っぽい描写.

今回の作品は OpenProcessing にも投稿済み
https://openprocessing.org/sketch/2705195

コード
let img;
let pixelData = [];
let canvasSize;
let isVertical = false;

function setup() {
  canvasSize = min(windowWidth, windowHeight) - 200;
  let canvas = createCanvas(canvasSize, canvasSize);
  canvas.parent('canvas-container');
  noLoop();

  background(255);
  textAlign(CENTER, CENTER);
  textSize(16);
  fill(100);
  text('Upload an image to begin', width / 2, height / 2);

  let imageInput = select('#imageInput');
  imageInput.changed(handleImageUpload);
}

function draw() {
  if (!img || !img.width) {
    return;
  }

  background(255);
  drawPixelatedImage();
}

function handleImageUpload() {
  let input = select('#imageInput').elt;
  if (input.files && input.files[0]) {
    let file = input.files[0];
    let reader = new FileReader();

    reader.onload = function (e) {
      img = loadImage(e.target.result, imageLoaded);
    };

    reader.readAsDataURL(file);
  }
}

function imageLoaded() {
  if (!img) return;

  isVertical = img.height > img.width;
  processImagePixels();
  redraw();
}

function processImagePixels() {
  pixelData = [];
  img.loadPixels();

  let scaleFactor = min(canvasSize / img.width, canvasSize / img.height);
  let displayWidth = img.width * scaleFactor;
  let displayHeight = img.height * scaleFactor;
  let offsetX = (canvasSize - displayWidth) / 2;
  let offsetY = (canvasSize - displayHeight) / 2;

  let minRes = 1;
  let maxRes = 30;

  if (isVertical) {
    for (let y = 0; y < displayHeight; ) {
      let progress = 1 - y / displayHeight;
      let pixelSize =
        progress > 0.7 ? minRes : lerp(maxRes, minRes, progress / 0.7);

      for (let x = 0; x < displayWidth; x += pixelSize) {
        let sourceX = int(x / scaleFactor);
        let sourceY = int(y / scaleFactor);

        if (
          sourceX >= 0 &&
          sourceX < img.width &&
          sourceY >= 0 &&
          sourceY < img.height
        ) {
          let index = (sourceY * img.width + sourceX) * 4;
          let r = img.pixels[index];
          let g = img.pixels[index + 1];
          let b = img.pixels[index + 2];

          pixelData.push({
            x: x + offsetX,
            y: y + offsetY,
            color: color(r, g, b),
            size: pixelSize,
            progress: progress,
          });
        }
      }
      y += pixelSize;
    }
  } else {
    for (let x = 0; x < displayWidth; ) {
      let progress = 1 - x / displayWidth;
      let pixelSize =
        progress > 0.7 ? minRes : lerp(maxRes, minRes, progress / 0.7);

      for (let y = 0; y < displayHeight; y += pixelSize) {
        let sourceX = int(x / scaleFactor);
        let sourceY = int(y / scaleFactor);

        if (
          sourceX >= 0 &&
          sourceX < img.width &&
          sourceY >= 0 &&
          sourceY < img.height
        ) {
          let index = (sourceY * img.width + sourceX) * 4;
          let r = img.pixels[index];
          let g = img.pixels[index + 1];
          let b = img.pixels[index + 2];

          pixelData.push({
            x: x + offsetX,
            y: y + offsetY,
            color: color(r, g, b),
            size: pixelSize,
            progress: progress,
          });
        }
      }
      x += pixelSize;
    }
  }
}

function drawPixelatedImage() {
  if (pixelData.length === 0) return;

  for (let pixel of pixelData) {
    let progress = pixel.progress;
    let pixelColor = pixel.color;

    if (
      brightness(pixelColor) > 90 &&
      red(pixelColor) > 240 &&
      green(pixelColor) > 240 &&
      blue(pixelColor) > 240
    ) {
      pixelColor = color(255, 255, 255);
    }

    if (progress > 0.7) {
      fill(pixelColor);
      noStroke();
      rect(pixel.x, pixel.y, pixel.size, pixel.size);
    } else {
      let normalizedProgress = progress / 0.7;

      let fillAlpha = map(normalizedProgress, 0, 1, 0, 255);
      fillAlpha = constrain(fillAlpha, 0, 255);

      let strokeOpacity = map(normalizedProgress, 0, 1, 255, 100);
      let strokeColor = lerpColor(color(0), pixelColor, normalizedProgress);
      strokeColor = color(
        red(strokeColor),
        green(strokeColor),
        blue(strokeColor),
        strokeOpacity,
      );

      stroke(strokeColor);
      strokeWeight(map(normalizedProgress, 0, 1, 1, 0.3));

      fill(red(pixelColor), green(pixelColor), blue(pixelColor), fillAlpha);

      drawGradualRect(
        pixel.x,
        pixel.y,
        pixel.size,
        pixel.size,
        normalizedProgress,
      );
    }
  }
}

function drawGradualRect(x, y, w, h, progress) {
  let maxJitter = 3;
  let jitterAmount = map(progress, 0, 1, maxJitter, 0);

  if (progress < 0.9) {
    let corners = [
      {
        x: x + random(-jitterAmount, jitterAmount),
        y: y + random(-jitterAmount, jitterAmount),
      },
      {
        x: x + w + random(-jitterAmount, jitterAmount),
        y: y + random(-jitterAmount, jitterAmount),
      },
      {
        x: x + w + random(-jitterAmount, jitterAmount),
        y: y + h + random(-jitterAmount, jitterAmount),
      },
      {
        x: x + random(-jitterAmount, jitterAmount),
        y: y + h + random(-jitterAmount, jitterAmount),
      },
    ];

    beginShape();
    for (let corner of corners) {
      vertex(corner.x, corner.y);
    }
    endShape(CLOSE);
  } else {
    rect(x, y, w, h);
  }
}

function keyPressed() {
  if (key === 's') {
    saveGif(`photo-pixel-gradient-${round(new Date().getTime() / 100000)}`, 1);
  }

  if (key === 'c') {
    saveCanvas(
      `photo-pixel-gradient-${round(new Date().getTime() / 100000)}`,
      'jpeg',
    );
  }
}
Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day11: BATTLE

本当はミルククラウンを描きたかったが,プロンプトが下手すぎてうまく描けず.引き続き言語化に挑戦する.

https://openprocessing.org/sketch/2722461

コード
let points = [];
let cp = [];
let seed;

const numPoints = 500;
const volumeSize = 500;
const noiseScale = 0.8;
const sphereRadius = 2.5;

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);

  cp = random(colorPalette).colors;
  seed = random(1000);

  let xOffset = random(1000);
  let yOffset = random(1000);
  let zOffset = random(1000);

  for (let i = 0; i < numPoints; i++) {
    let x = map(noise(xOffset), 0, 1, -volumeSize, volumeSize);
    let y = map(noise(yOffset), 0, 1, -volumeSize, volumeSize);
    let z = map(noise(zOffset), 0, 1, -volumeSize, volumeSize);

    points.push(createVector(x, y, z));

    xOffset += noiseScale;
    yOffset += noiseScale;
    zOffset += noiseScale;
  }

  const blendAmount = numPoints / 4;
  for (let i = 0; i < blendAmount; i++) {
    const ratio = i / blendAmount;
    const index = numPoints - blendAmount + i;
    points[index] = p5.Vector.lerp(points[index], points[0], ratio);
  }
}

function draw() {
  background(255);
  randomSeed(seed);
  orbitControl();
  noFill();

  beginShape();
  for (const p of points) {
    stroke(random(cp));
    vertex(p.x, p.y, p.z);
  }
  vertex(points[0].x, points[0].y, points[0].z);
  endShape();

  noStroke();
  fill(50);

  for (const p of points) {
    push();
    translate(p.x, p.y, p.z);
    stroke(cp[3]);
    sphere(sphereRadius);
    pop();
  }
}

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

function keyPressed() {
  if (key === 'c') {
    saveCanvas(
      `dense-geometric-pattern-${round(new Date().getTime() / 100000)}`,
      'jpeg',
    );
  }
}
Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day12: with Torus Knot

タイトル通り,Torus knot をベースに,たくさんの点が流れるような動きでドーナツを魅せる感じにしたかったのだが,一本のつながった点描による線がドーナツを表示する形になってしまい,なんかうまいプロンプトが思いつかなかったので置きにいってしまった⋯

https://x.com/i/status/1987667030896759255

https://openprocessing.org/sketch/2785721

コード
// Torus Knot Configuration
class TorusKnotConfig {
  constructor(R, r, p, q) {
    this.R = R; // Major radius
    this.r = r; // Minor radius
    this.p = p; // Knot parameter p
    this.q = q; // Knot parameter q
    this.noiseScale = random(0.001, 0.005); // Noise scale for irregularity
    this.noiseOffset = random(1000); // Unique noise offset for each knot
  }
}

// Particle class for better organization
class Particle {
  constructor(color, offset, knotIndex) {
    this.color = color;
    this.offset = offset;
    this.knotIndex = knotIndex;
    this.phase = random(TWO_PI);
    this.rotationSpeedX = random(0.05, 0.15);
    this.rotationSpeedY = random(0.05, 0.15);
    this.rotationSpeedZ = random(0.05, 0.15);
    this.rotationX = random(TWO_PI);
    this.rotationY = random(TWO_PI);
    this.rotationZ = random(TWO_PI);
    this.size = random(12, 20);
    this.pulseSpeed = random(0.02, 0.05);
    this.pulseOffset = random(TWO_PI);
  }

  update(t, knot) {
    this.rotationX += this.rotationSpeedX;
    this.rotationY += this.rotationSpeedY;
    this.rotationZ += this.rotationSpeedZ;
  }

  getPosition(t, knot) {
    // Base torus knot position
    let pos = torusKnot(t, knot);

    // Add Perlin noise for irregularity
    let noiseX = noise(t * knot.noiseScale + knot.noiseOffset, 0) * 50 - 25;
    let noiseY = noise(t * knot.noiseScale + knot.noiseOffset, 100) * 50 - 25;
    let noiseZ = noise(t * knot.noiseScale + knot.noiseOffset, 200) * 50 - 25;

    pos.x += noiseX;
    pos.y += noiseY;
    pos.z += noiseZ;

    return pos;
  }

  display(t, knot) {
    let pos = this.getPosition(t, knot);

    // Pulsing size effect
    let pulseSize =
      this.size + sin(frameCount * this.pulseSpeed + this.pulseOffset) * 3;

    // Distance-based alpha (fade distant particles)
    let distance = dist(0, 0, 0, pos.x, pos.y, pos.z);
    let alpha = map(distance, 0, 500, 255, 100);

    let displayColor = color(
      red(this.color),
      green(this.color),
      blue(this.color),
      // alpha,
    );

    stroke(displayColor);
    strokeWeight(0.5);
    // fill(displayColor);

    push();
    translate(pos.x, pos.y, pos.z);
    rotateX(this.rotationX);
    rotateY(this.rotationY);
    rotateZ(this.rotationZ);
    box(pulseSize);
    pop();
  }
}

// Global variables
let knotConfigs = [];
let particles = [];
let speed = 0.002;
let numParticlesPerKnot = 80;
let trails = [];

// Color palettes
let colorPalettes = [];
let currentPalette = 0;

// Camera control
let camX = 0;
let camY = 0;

function setup() {
  createCanvas(
    min(windowWidth, windowHeight) - 50,
    min(windowWidth, windowHeight) - 50,
    WEBGL,
  );

  colorPalettes = [
	 [color(255, 150, 0), color(0, 200, 255), color(255, 0, 150)],
    [color(255, 100, 100), color(100, 150, 255), color(150, 255, 150)],
    [color(255, 200, 100), color(200, 100, 255), color(100, 255, 200)],
    [color(255, 255, 255), color(200, 200, 255), color(255, 200, 200)],
  ];

  // Initialize multiple torus knot configurations
  initializeKnots();

  // Initialize particles for each knot
  initializeParticles();
}

function initializeKnots() {
  knotConfigs = [];

  // Create 3-5 different torus knots with varying parameters
  let numKnots = floor(random(3, 6));

  for (let i = 0; i < numKnots; i++) {
    let R = random(200, 350);
    let r = random(50, 100);
    let p = floor(random(2, 5));
    let q = floor(random(5, 10));

    knotConfigs.push(new TorusKnotConfig(R, r, p, q));
  }
}

function initializeParticles() {
  particles = [];

  for (let k = 0; k < knotConfigs.length; k++) {
    let palette = colorPalettes[currentPalette];

    for (let i = 0; i < numParticlesPerKnot; i++) {
      let particleColor = palette[floor(random(palette.length))];
      let offset = (i / numParticlesPerKnot) * TWO_PI;

      particles.push(new Particle(particleColor, offset, k));
    }
  }
}

function draw() {
  // Semi-transparent background for trail effect
  background(25);

  rotateX(PI / 6);

  // Lighting
  ambientLight(80);
  pointLight(255, 255, 255, 400, -400, 400);
  pointLight(150, 150, 255, -400, 400, -400);

  // Camera control
  let camDist = 800 + sin(frameCount * 0.01) * 100; // Slight zoom animation
  camera(
    camX + sin(frameCount * 0.005) * 50,
    camY + cos(frameCount * 0.003) * 50,
    camDist,
    0,
    0,
    0,
    0,
    1,
    0,
  );

  // Draw all particles
  for (let particle of particles) {
    let knot = knotConfigs[particle.knotIndex];
    let t = particle.offset + frameCount * speed;

    particle.update(t, knot);
    particle.display(t, knot);
  }

  // Draw subtle wireframe reference
  if (frameCount % 60 < 30) {
    drawKnotWireframes();
  }
}

function drawKnotWireframes() {
  noFill();
  strokeWeight(0.3);

  for (let k = 0; k < knotConfigs.length; k++) {
    stroke(255, 255, 255, 20);
    beginShape();

    let steps = 200;
    for (let i = 0; i <= steps; i++) {
      let t = (i / steps) * TWO_PI;
      let pos = torusKnot(t, knotConfigs[k]);
      vertex(pos.x, pos.y, pos.z);
    }

    endShape(CLOSE);
  }
}

// Torus Knot parametric equations
function torusKnot(t, knot) {
  let x = (knot.R + knot.r * cos(knot.q * t)) * cos(knot.p * t);
  let y = (knot.R + knot.r * cos(knot.q * t)) * sin(knot.p * t);
  let z = knot.r * sin(knot.q * t);

  return { x: x, y: y, z: z };
}

function keyPressed() {
  // Save functions
  if (key === 's') {
    saveGif(`torusKnot-${round(new Date().getTime() / 100000)}`, 5);
  }

  if (key === 'c') {
    saveCanvas(`torusKnot-${round(new Date().getTime() / 100000)}`, 'jpeg');
  }

  // Regenerate with new parameters
  if (key === 'r') {
    initializeKnots();
    initializeParticles();
  }

  // Change color palette
  if (key === 'p' || key === 'P') {
    currentPalette = (currentPalette + 1) % colorPalettes.length;
    initializeParticles();
  }

  // Toggle wireframe
  if (key === 'w' || key === 'W') {
    // Already implemented as animation
  }
}

function windowResized() {
  let size = min(windowWidth, windowHeight) - 50;
  resizeCanvas(size, size);
}

Hidden comment
Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day13: Random MRT Network Map

https://x.com/ArtmanKKeeth/status/1988156661052342353?s=20

code
// ===========================
// Config
// ===========================
let W,
  routes = [],
  gridSize,
  hubPoints = [];

const GRID = { edgeMin: 2, edgeMax: 12, hubMin: 4, hubMax: 10, cells: 14 };
const ROUTE = {
  min: 7,
  max: 10,
  waypoints: [6, 9],
  stations: [8, 12],
  hubs: [3, 6],
};
const THRESH = {
  selfIntersect: 2.5,
  minWaypoint: 1.5,
  hubReach: 1.5,
  cornerRadius: 0.8,
  transfer: 0.6,
};
const PROB = { hubPass: 0.7, maxAttempts: 20 };
const STYLE = {
  line: 5,
  lineOutline: 8,
  stationNormal: 9,
  stationTransfer: 11,
  strokeNormal: 3.5,
  strokeTransfer: 4.5,
};

const colors = [
  '#E63946',
  '#06FFA5',
  '#4361EE',
  '#F77F00',
  '#9B5DE5',
  '#FFD60A',
  '#FF006E',
  '#00B4D8',
  '#90E0EF',
];
const lineNames = [
  'Red',
  'Green',
  'Blue',
  'Orange',
  'Purple',
  'Yellow',
  'Magenta',
  'Cyan',
  'Light Blue',
].map((n) => n + ' Line');
const stationNames = [
  'Dawn Square',
  'Memory Cross',
  'Light Terminal',
  'Time Gap',
  'Silent Station',
  'Hope Bridge',
  'Shadow Town',
  'Star Plaza',
  'Wind Passage',
  'Mirror Port',
  'Forest Gate',
  'Rainbow',
  'Moon Stop',
  'Cloud Nine',
  'Sunrise Hill',
  'Daybreak',
  'Poet Corner',
  'Music Hall',
  'Gallery',
  'Dance Plaza',
  'Soul Junction',
  'Heart Station',
  'Future Gate',
  'Past Window',
  'Galaxy Central',
  'Sky Port',
  'Seaside',
  'Summit',
  'Dream Station',
  'Echo Park',
  'Crystal Bay',
  'Zenith',
  'Ember Square',
  'Twilight Ave',
  'Lotus Garden',
  'Harmony',
  'Cascade',
  'Velvet Station',
  'Iris Gate',
  'Nova Plaza',
  'Horizon',
  'Serenity',
  'Phoenix Park',
  'Meteor Stop',
];

// ===========================
// Helpers
// ===========================
const dist2d = (p1, p2) => Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
const pathDist = (path) =>
  path.reduce((sum, p, i) => (i > 0 ? sum + dist2d(path[i - 1], p) : sum), 0);
const getEdgePos = () => {
  const side = floor(random(4));
  return [
    { x: GRID.edgeMin, y: floor(random(3, 11)) },
    { x: GRID.edgeMax, y: floor(random(3, 11)) },
    { x: floor(random(3, 11)), y: GRID.edgeMin },
    { x: floor(random(3, 11)), y: GRID.edgeMax },
  ][side];
};

const isTooClose = (x, y, visited, thresh) =>
  visited.slice(0, -1).some((v) => dist(x, y, v.x, v.y) < thresh);

// ===========================
// Setup
// ===========================
function setup() {
  createCanvas((W = min(windowWidth, windowHeight) - 50), W);
  gridSize = W / GRID.cells;
  generate();
}

function generate() {
  routes = [];

  // Create hubs
  hubPoints = Array.from({ length: floor(random(...ROUTE.hubs)) }, () => ({
    x: floor(random(GRID.hubMin, GRID.hubMax)),
    y: floor(random(GRID.hubMin, GRID.hubMax)),
  }));

  // Generate routes
  const numRoutes = floor(random(ROUTE.min, ROUTE.max));
  routes.push(createLoopLine());

  for (let i = 0; i < numRoutes - 1; i++) {
    routes.push(createRoute(colors[i], lineNames[i]));
  }
}

// ===========================
// Route Creation
// ===========================
function createRoute(color, name) {
  const waypoints = [];
  const visited = [];
  const start = getEdgePos();

  waypoints.push(start);
  visited.push({ ...start });

  // Generate waypoints
  let { x, y } = start;
  let lastDir = null;
  let dirCounter = 0;
  const numWaypoints = floor(random(...ROUTE.waypoints));
  const targetHub =
    random() > 1 - PROB.hubPass && hubPoints.length > 0
      ? random(hubPoints)
      : null;
  let hubReached = false;

  for (let i = 1; i < numWaypoints; i++) {
    let valid = false;
    let attempts = 0;
    let newX, newY, dir;

    while (!valid && attempts++ < PROB.maxAttempts) {
      // Move towards hub or random direction
      if (targetHub && !hubReached && i > 2 && random() > 0.5) {
        const dx = targetHub.x - x;
        const dy = targetHub.y - y;
        if (abs(dx) > abs(dy)) {
          newX = constrain(
            x + (dx > 0 ? floor(random(1, 3)) : -floor(random(1, 3))),
            GRID.edgeMin,
            GRID.edgeMax,
          );
          newY = y;
        } else {
          newX = x;
          newY = constrain(
            y + (dy > 0 ? floor(random(1, 3)) : -floor(random(1, 3))),
            GRID.edgeMin,
            GRID.edgeMax,
          );
        }
        if (dist(newX, newY, targetHub.x, targetHub.y) < THRESH.hubReach) {
          [newX, newY] = [targetHub.x, targetHub.y];
          hubReached = true;
        }
      } else {
        dirCounter++;
        const forceChange = dirCounter >= floor(random(2, 4));

        // Select direction
        let validDir = false;
        while (!validDir && attempts < PROB.maxAttempts) {
          const r = random();
          dir = r < 0.35 ? 'h' : r < 0.7 ? 'v' : 'd';
          if (dir !== lastDir || forceChange) validDir = true;
        }

        // Apply movement
        const d = floor(random(2, dir === 'd' ? 4 : 5));
        if (dir === 'h') {
          newX = constrain(
            x + (random() > 0.5 ? d : -d),
            GRID.edgeMin,
            GRID.edgeMax,
          );
          newY = y;
        } else if (dir === 'v') {
          newX = x;
          newY = constrain(
            y + (random() > 0.5 ? d : -d),
            GRID.edgeMin,
            GRID.edgeMax,
          );
        } else {
          newX = constrain(
            x + (random() > 0.5 ? d : -d),
            GRID.edgeMin,
            GRID.edgeMax,
          );
          newY = constrain(
            y + (random() > 0.5 ? d : -d),
            GRID.edgeMin,
            GRID.edgeMax,
          );
        }
      }

      // Validate
      const notClose = dist(newX, newY, x, y) > THRESH.minWaypoint;
      const notVisited = !isTooClose(newX, newY, visited, THRESH.selfIntersect);
      valid = notClose && notVisited;
    }

    if (valid) {
      waypoints.push({ x: newX, y: newY });
      visited.push({ x: newX, y: newY });
      [x, y] = [newX, newY];
      if (dir !== lastDir) {
        lastDir = dir;
        dirCounter = 0;
      }
    }
  }

  return createRouteFromWaypoints(color, waypoints, name);
}

function createLoopLine() {
  const cx = random(5, 9),
    cy = random(5, 9),
    r = random(2.3, 3.5);
  const numStations = floor(random(...ROUTE.stations));
  const stations = [];
  const path = [];

  for (let i = 0; i < numStations; i++) {
    const angle = (TWO_PI / numStations) * i - HALF_PI;
    stations.push({
      x: (cx + cos(angle) * r) * gridSize,
      y: (cy + sin(angle) * r) * gridSize,
      name: random(stationNames),
    });
  }

  for (let i = 0; i <= 100; i++) {
    const angle = (TWO_PI / 100) * i - HALF_PI;
    path.push({
      x: (cx + cos(angle) * r) * gridSize,
      y: (cy + sin(angle) * r) * gridSize,
    });
  }

  return { color: random(colors), name: 'Loop Line', stations, path };
}

function createRouteFromWaypoints(color, waypoints, name) {
  const path = generatePath(waypoints);
  const route = { color, name, stations: [], path };

  if (path.length === 0) return route;

  // Place stations
  const totalDist = pathDist(path);
  const numStations = floor(random(...ROUTE.stations));
  const spacing = totalDist / (numStations - 1);

  route.stations.push({
    x: path[0].x,
    y: path[0].y,
    name: random(stationNames),
  });

  for (let s = 1; s < numStations - 1; s++) {
    const pos = findPointAtDist(path, s * spacing);
    if (pos) route.stations.push({ ...pos, name: random(stationNames) });
  }

  const last = path[path.length - 1];
  route.stations.push({ x: last.x, y: last.y, name: random(stationNames) });

  return route;
}

function findPointAtDist(path, targetDist) {
  let acc = 0;
  for (let i = 0; i < path.length - 1; i++) {
    const segDist = dist2d(path[i], path[i + 1]);
    if (acc + segDist >= targetDist) {
      const t = (targetDist - acc) / segDist;
      return {
        x: lerp(path[i].x, path[i + 1].x, t),
        y: lerp(path[i].y, path[i + 1].y, t),
      };
    }
    acc += segDist;
  }
  return null;
}

function generatePath(waypoints) {
  const path = [];
  const cornerR = gridSize * THRESH.cornerRadius;

  for (let i = 0; i < waypoints.length; i++) {
    const cur = waypoints[i],
      next = waypoints[i + 1],
      prev = waypoints[i - 1];

    if (i === 0) path.push({ x: cur.x * gridSize, y: cur.y * gridSize });

    if (next) {
      const isCorner =
        prev &&
        (cur.x - prev.x !== next.x - cur.x ||
          cur.y - prev.y !== next.y - cur.y);

      if (isCorner) {
        // Rounded corner
        let [pdx, pdy] = [cur.x - prev.x, cur.y - prev.y];
        let [ndx, ndy] = [next.x - cur.x, next.y - cur.y];
        const pLen = Math.sqrt(pdx ** 2 + pdy ** 2);
        const nLen = Math.sqrt(ndx ** 2 + ndy ** 2);
        [pdx, pdy] = [pdx / pLen, pdy / pLen];
        [ndx, ndy] = [ndx / nLen, ndy / nLen];

        const approach = Math.min(cornerR / gridSize, 0.8);
        const [ax, ay] = [
          (cur.x - pdx * approach) * gridSize,
          (cur.y - pdy * approach) * gridSize,
        ];
        const [ex, ey] = [
          (cur.x + ndx * approach) * gridSize,
          (cur.y + ndy * approach) * gridSize,
        ];

        // Line to approach
        const last = path[path.length - 1];
        const steps = Math.floor(dist2d(last, { x: ax, y: ay }) / 10);
        for (let t = 1; t <= steps; t++) {
          path.push({
            x: lerp(last.x, ax, t / steps),
            y: lerp(last.y, ay, t / steps),
          });
        }

        // Bezier curve
        const [cx, cy] = [cur.x * gridSize, cur.y * gridSize];
        for (let t = 0; t <= 15; t++) {
          const p = t / 15;
          path.push({
            x: (1 - p) ** 2 * ax + 2 * (1 - p) * p * cx + p ** 2 * ex,
            y: (1 - p) ** 2 * ay + 2 * (1 - p) * p * cy + p ** 2 * ey,
          });
        }
      } else {
        // Straight line
        const [dx, dy] = [next.x - cur.x, next.y - cur.y];
        const steps = Math.floor(Math.sqrt(dx ** 2 + dy ** 2) * 8);
        for (let t = 1; t <= steps; t++) {
          path.push({
            x: cur.x * gridSize + dx * gridSize * (t / steps),
            y: cur.y * gridSize + dy * gridSize * (t / steps),
          });
        }
      }
    }
  }

  return path;
}

// ===========================
// Drawing
// ===========================
function draw() {
  background(245, 243, 238);

  // Grid
  push();
  stroke(230, 230, 230);
  strokeWeight(0.5);
  for (let i = 0; i <= GRID.cells; i++) {
    const p = i * gridSize;
    line(p, 0, p, W);
    line(0, p, W, p);
  }
  pop();

  // Routes
  for (const r of routes) {
    push();
    stroke(255);
    strokeWeight(STYLE.lineOutline);
    strokeCap(ROUND);
    strokeJoin(ROUND);
    noFill();
    beginShape();
    r.path.forEach((p) => vertex(p.x, p.y));
    endShape();

    stroke(r.color);
    strokeWeight(STYLE.line);
    beginShape();
    r.path.forEach((p) => vertex(p.x, p.y));
    endShape();
    pop();
  }

  // Stations
  for (const r of routes) {
    push();
    for (const s of r.stations) {
      const isTransfer = routes.some(
        (route) =>
          route.stations.filter(
            (st) => dist(s.x, s.y, st.x, st.y) < gridSize * THRESH.transfer,
          ).length > 1,
      );
      const size = isTransfer ? STYLE.stationTransfer : STYLE.stationNormal;
      const sw = isTransfer ? STYLE.strokeTransfer : STYLE.strokeNormal;

      fill(255);
      stroke(r.color);
      strokeWeight(sw);
      ellipse(s.x, s.y, size * 2);

      if (isTransfer) {
        noStroke();
        fill(255);
        ellipse(s.x, s.y, size * 0.6);
      }
    }
    pop();
  }

  // Station names
  push();
  textFont('sans-serif');
  textSize(10);
  for (const r of routes) {
    for (let i = 0; i < r.stations.length; i++) {
      const s = r.stations[i];
      let angle;
      if (r.name === 'Loop Line') {
        angle = atan2(s.y - W / 2, s.x - W / 2);
      } else if (i > 0 && i < r.stations.length - 1) {
        angle =
          atan2(
            r.stations[i + 1].y - r.stations[i - 1].y,
            r.stations[i + 1].x - r.stations[i - 1].x,
          ) + HALF_PI;
      } else if (i === 0) {
        angle = atan2(r.stations[1].y - s.y, r.stations[1].x - s.x) + HALF_PI;
      } else {
        angle =
          atan2(s.y - r.stations[i - 1].y, s.x - r.stations[i - 1].x) + HALF_PI;
      }

      const [tx, ty] = [s.x + cos(angle) * 22, s.y + sin(angle) * 22];
      const tw = textWidth(s.name);

      fill(245, 243, 238, 240);
      noStroke();
      rect(tx - tw / 2 - 3, ty - 7, tw + 6, 14, 2);

      fill(r.color);
      textAlign(CENTER, CENTER);
      text(s.name, tx, ty);
    }
  }
  pop();

  // Legend
  push();
  const [lx, ly] = [W * 0.03, W * 0.88];
  textFont('sans-serif');
  textSize(10);
  routes.forEach((r, i) => {
    const y = ly - (routes.length - 1 - i) * 16;
    stroke(r.color);
    strokeWeight(3.5);
    line(lx, y, lx + 22, y);
    noStroke();
    fill(80);
    textAlign(LEFT, CENTER);
    text(r.name, lx + 28, y);
  });
  pop();

  // Title
  push();
  fill(0, 0, 0, 100);
  noStroke();
  textAlign(LEFT, TOP);
  textSize(W * 0.022);
  textFont('sans-serif');
  textStyle(BOLD);
  text('MRT NETWORK MAP', W * 0.03, W * 0.02);
  textStyle(NORMAL);
  textSize(W * 0.016);
  fill(0, 0, 0, 80);
  text(
    `${routes.length} Lines • ${routes.reduce(
      (t, r) => t + r.stations.length,
      0,
    )} Stations`,
    W * 0.03,
    W * 0.05,
  );
  pop();
}

function keyPressed() {
  if (key === 'c') saveCanvas(`mrt-${round(Date.now() / 100000)}`, 'jpeg');
  if (key === 'r') generate();
}
Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day14: 3-D Particles

ぶっちゃけこの 𝕏 の投稿のパクリです

https://x.com/dn0t_/status/1983476402650521955?s=20

https://openprocessing.org/sketch/2790962

code
let fallingParticles = [];
let roadParticlesTop = [];
let roadParticlesBottom = [];
let numFallingParticles = 300;
let numRoadParticles = 200;
let cylinderRadius;
let innerCircleRadius; // 中央の円の半径

function setup() {
  createCanvas(windowWidth, windowHeight);
  cylinderRadius = min(width, height) * 0.35;
  innerCircleRadius = cylinderRadius * 0.5; // 中央島の半径

  // 落下するパーティクルの初期化
  for (let i = 0; i < numFallingParticles; i++) {
    fallingParticles.push(createFallingParticle());
  }

  // 上の環状交差点のパーティクル
  for (let i = 0; i < numRoadParticles; i++) {
    roadParticlesTop.push(
      createRoadParticle(height * 0.15, 1, i / numRoadParticles),
    );
  }

  // 下の環状交差点のパーティクル
  for (let i = 0; i < numRoadParticles; i++) {
    roadParticlesBottom.push(
      createRoadParticle(height * 0.85, 0.8, i / numRoadParticles),
    );
  }
}

function createFallingParticle() {
  // 上の中央円からランダムに出発
  const angle = random(TWO_PI);
  const radius = random(innerCircleRadius * 0.8); // 中央円の範囲内
  const topY = height * 0.15;
  const scaleY = 0.5; // 上の円の遠近スケール

  return {
    x: width / 2 + cos(angle) * radius,
    z: sin(angle) * radius,
    y: topY + height * 0.85 * random() + sin(angle) * radius * scaleY, // 楕円の高さに合わせる
    targetY: height * 0.85, // 下の円の位置
    speed: random(1.5, 4),
    baseSize: random(1.5, 3),
    trail: [],
    noiseOffsetX: random(1000),
    noiseOffsetZ: random(1000),
    flowSpeed: random(0.01, 0.03),
    flowStrength: random(8, 20),
    type: 'falling',
    startAngle: angle, // 開始角度を記憶
    startRadius: radius, // 開始半径を記憶
    currentAngle: angle, // 現在の角度
    currentRadius: radius, // 現在の半径
  };
}

function createRoadParticle(centerY, depthPosition, angleOffset) {
  const angle = angleOffset * TWO_PI;
  const laneRadius = cylinderRadius * random(0.4, 0.95);
  const scaleY = map(depthPosition, 0, 1, 0.3, 0.5);

  return {
    centerY: centerY,
    angle: angle,
    laneRadius: laneRadius,
    scaleY: scaleY,
    speed: random(0.005, 0.01),
    baseSize: random(1, 2.5),
    trail: [],
    type: 'road',
    direction: random() > 0.5 ? 1 : -1,
  };
}

function resetFallingParticle(particle) {
  const angle = random(TWO_PI);
  const radius = random(innerCircleRadius * 0.8);
  const topY = height * 0.15;
  const scaleY = 0.5;

  particle.currentAngle = angle;
  particle.currentRadius = radius;
  particle.y = topY + sin(angle) * radius * scaleY;
  particle.speed = random(1.5, 4);
  particle.baseSize = random(1.5, 3);
  particle.trail = [];
  particle.noiseOffsetX = random(1000);
  particle.noiseOffsetZ = random(1000);
  particle.flowSpeed = random(0.01, 0.03);
  particle.flowStrength = random(8, 20);
  particle.startAngle = angle;
  particle.startRadius = radius;
}

function draw() {
  // 半透明の背景で残像効果
  fill(20, 20, 30, 30);
  noStroke();
  rect(0, 0, width, height);

  // すべてのパーティクルを統合
  let allParticles = [];

  // 上の環状交差点のパーティクルを更新
  for (let particle of roadParticlesTop) {
    updateRoadParticle(particle);
    allParticles.push(particle);
  }

  // 落下するパーティクルを更新
  for (let particle of fallingParticles) {
    updateFallingParticle(particle);
    allParticles.push(particle);
  }

  // 下の環状交差点のパーティクルを更新
  for (let particle of roadParticlesBottom) {
    updateRoadParticle(particle);
    allParticles.push(particle);
  }

  // パーティクルを描画
  for (let particle of allParticles) {
    if (particle.type === 'falling') {
      drawFallingParticle(particle);
    } else {
      drawRoadParticle(particle);
    }
  }

  // 中央の円を描画(視覚的な参考)
  drawCentralCircles();

  // UI表示
  displayInfo();
}

function updateFallingParticle(particle) {
  // 軌跡を保存
  particle.trail.push({ x: particle.x, y: particle.y, z: particle.z });
  if (particle.trail.length > 25) {
    particle.trail.shift();
  }

  // 流線形の動き(角度方向に)
  const noiseAngle = noise(
    particle.noiseOffsetX + frameCount * particle.flowSpeed,
  );
  const noiseRadius = noise(
    particle.noiseOffsetZ + frameCount * particle.flowSpeed,
  );

  particle.currentAngle += (noiseAngle - 0.5) * 0.02;
  particle.currentRadius += (noiseRadius - 0.5) * particle.flowStrength * 0.02;

  // 落下
  particle.y += particle.speed;

  // 進行度を計算
  const progress = map(particle.y, height * 0.15, height * 0.85, 0, 1);

  // 中央円の範囲内に緩やかに収束
  const targetRadius = innerCircleRadius * 0.8;
  if (particle.currentRadius > targetRadius && progress > 0.3) {
    const pullStrength = map(progress, 0.3, 1, 0, 0.1);
    particle.currentRadius = lerp(
      particle.currentRadius,
      targetRadius,
      pullStrength,
    );
  }

  // 円柱の円形を保ちながらXとZを更新
  particle.x = width / 2 + cos(particle.currentAngle) * particle.currentRadius;
  particle.z = sin(particle.currentAngle) * particle.currentRadius;

  // 下の円に到達したらリセット
  if (particle.y > height * 0.85 - particle.z) {
    resetFallingParticle(particle);
  }
}

function updateRoadParticle(particle) {
  // 軌跡を保存
  const x = width / 2 + cos(particle.angle) * particle.laneRadius;
  const y =
    particle.centerY +
    sin(particle.angle) * particle.laneRadius * particle.scaleY;

  particle.trail.push({ x: x, y: y });
  if (particle.trail.length > 20) {
    particle.trail.shift();
  }

  // 角度を更新(回転)
  particle.angle += particle.speed * particle.direction;
}

function drawFallingParticle(particle) {
  // 奥行きに応じたスケールと透明度
  const depthScale = map(particle.z, -cylinderRadius, cylinderRadius, 0.3, 1.2);
  const alpha = map(particle.z, -cylinderRadius, cylinderRadius, 100, 255);

  // 2D描画位置を計算(Xはそのまま、Yに奥行き(Z)の影響を加える)
  const drawX = particle.x;
  const drawY = particle.y; // Zの影響は透明度とサイズのみに反映

  // 軌跡を描画
  if (particle.trail.length >= 2) {
    noFill();
    for (let i = 0; i < particle.trail.length - 1; i++) {
      const t1 = particle.trail[i];
      const t2 = particle.trail[i + 1];

      // 各軌跡点の2D位置
      const t1x = t1.x;
      const t1y = t1.y;
      const t2x = t2.x;
      const t2y = t2.y;

      const t1DepthScale = map(t1.z, -cylinderRadius, cylinderRadius, 0.3, 1.2);
      const trailAlpha = map(i, 0, particle.trail.length, 0, alpha * 0.7);
      const weight = map(i, 0, particle.trail.length, 0.3, 1) * t1DepthScale;

      stroke(255, 255, 255, trailAlpha);
      strokeWeight(weight);
      line(t1x, t1y, t2x, t2y);
    }
  }

  // パーティクル本体
  const size = particle.baseSize * depthScale;
  noStroke();
  fill(255, 255, 255, alpha);
  ellipse(drawX, drawY, size * 2, size * 2);
}

function drawRoadParticle(particle) {
  const x = width / 2 + cos(particle.angle) * particle.laneRadius;
  const y =
    particle.centerY +
    sin(particle.angle) * particle.laneRadius * particle.scaleY;

  // 軌跡を描画
  if (particle.trail.length >= 2) {
    noFill();
    for (let i = 0; i < particle.trail.length - 1; i++) {
      const t1 = particle.trail[i];
      const t2 = particle.trail[i + 1];
      const trailAlpha = map(i, 0, particle.trail.length, 0, 180);
      const weight = map(i, 0, particle.trail.length, 0.3, 0.8);

      stroke(150, 200, 255, trailAlpha);
      strokeWeight(weight);
      line(t1.x, t1.y, t2.x, t2.y);
    }
  }

  // パーティクル本体
  noStroke();
  ellipse(x, y, particle.baseSize * 2, particle.baseSize * 2);
}

function drawCentralCircles() {
  // 上の中央円
  push();
  noFill();
  stroke(80, 100, 120, 100);
  strokeWeight(1.5);
  const topScaleY = 0.5;
  ellipse(
    width / 2,
    height * 0.15,
    innerCircleRadius * 2,
    innerCircleRadius * 2 * topScaleY,
  );
  pop();

  // 下の中央円
  push();
  noFill();
  stroke(80, 100, 120, 100);
  strokeWeight(1.5);
  const bottomScaleY = 0.4;
  ellipse(
    width / 2,
    height * 0.85,
    innerCircleRadius * 2,
    innerCircleRadius * 2 * bottomScaleY,
  );
  pop();
}

function displayInfo() {
  fill(255);
  noStroke();
  textAlign(LEFT, TOP);
  textSize(14);
  text(`Falling: ${numFallingParticles}`, 10, 10);
  text(`Road: ${numRoadParticles * 2}`, 10, 30);
  text('Keys:', 10, 50);
  text('1/2: Falling particles ±50', 10, 70);
  text('3/4: Road particles ±25', 10, 90);
  text('5/6: Flow strength ±', 10, 110);
  text('R: Reset', 10, 130);
  text('S: Save screenshot', 10, 150);
}

function adjustFallingParticles() {
  const diff = numFallingParticles - fallingParticles.length;
  if (diff > 0) {
    for (let i = 0; i < diff; i++) {
      fallingParticles.push(createFallingParticle());
    }
  } else if (diff < 0) {
    fallingParticles = fallingParticles.slice(0, numFallingParticles);
  }
}

function adjustRoadParticles() {
  roadParticlesTop = [];
  roadParticlesBottom = [];

  for (let i = 0; i < numRoadParticles; i++) {
    roadParticlesTop.push(
      createRoadParticle(height * 0.15, 1, i / numRoadParticles),
    );
    roadParticlesBottom.push(
      createRoadParticle(height * 0.85, 0.8, i / numRoadParticles),
    );
  }
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  cylinderRadius = min(width, height) * 0.35;
  innerCircleRadius = cylinderRadius * 0.5;
}

function keyPressed() {
  if (key === '1') {
    numFallingParticles = max(50, numFallingParticles - 50);
    adjustFallingParticles();
  }
  if (key === '2') {
    numFallingParticles = min(1000, numFallingParticles + 50);
    adjustFallingParticles();
  }
  if (key === '3') {
    numRoadParticles = max(50, numRoadParticles - 25);
    adjustRoadParticles();
  }
  if (key === '4') {
    numRoadParticles = min(400, numRoadParticles + 25);
    adjustRoadParticles();
  }
  if (key === '5') {
    fallingParticles.forEach(
      (p) => (p.flowStrength = max(3, p.flowStrength - 3)),
    );
  }
  if (key === '6') {
    fallingParticles.forEach(
      (p) => (p.flowStrength = min(50, p.flowStrength + 3)),
    );
  }
  if (key === 'r' || key === 'R') {
    particles = [];
    for (let i = 0; i < numFallingParticles; i++) {
      particles.push(createFallingParticle());
    }
  }
  if (key === 's' || key === 'S') {
    saveGif(`mySketch-${round(new Date().getTime() / 100000)}`, 5);
  }

  if (key === 'c' || key === 'C') {
    saveCanvas(`mySketch-${round(new Date().getTime() / 100000)}`, 'jpeg');
  }
}
Kiyohito KEETH KuwaharaKiyohito KEETH Kuwahara

Day16: A rotating particle system

https://x.com/ArtmanKKeeth/status/1995694663672758421?s=20

元々のプロンプトは以下.これをちょいちょい微調整していった感じ.

中心から放射状に広がる複数の「腕」を持ち、各腕に沿って粒子が流れる。腕は対称的に配置され、回転しながら呼吸するようにサイズが変化。

コードもに関してもこちらをご参照ください💁
https://openprocessing.org/sketch/2812883