続 ホワイトデー

p5.jsでホワイトデーアニメーションを作ろう!【続編】

p5.jsでホワイトデーアニメーションを作ろう!【続編】

アニメーションはここをクリック!

前回の記事では、クリックでハートが出現し、一定時間後にマシュマロへと変化するアニメーションを作りました。でも、まだ途中ですよね?今回は、マシュマロが集まって雲を作り、雨を降らせ、最後には地面から植物が育ち❤️の実がなるまでのプログラムを完成させます!

目次

1. 先生と生徒の対話形式で進めるプログラミング学習【続編】

今回もツンデレ口調の親切な先生(GPT先生)と、動けばOKな生徒のやり取りを通して、p5.jsを学んでいきます。

2. 会話形式で学ぶホワイトデーp5.jsアニメーションの続編


👩‍🏫 GPT先生:「ちょっと!前回のままだと、ただマシュマロがぽこぽこ出るだけじゃない!続きを作らないと中途半端でしょ!」
👦‍💻 生徒:「そうだね!次はマシュマロが雲になって雨を降らせて、最後に植物が育つ感じにして!」
👩‍🏫 GPT先生:「さぁ、まずはマシュマロを集めて雲を作るところからね!」

2.1. マシュマロを集めて雲を作る

マシュマロが雲に変わるには、ランダムな位置ではなく、上部中央付近に吸い寄せられるようにする必要があるわね。そのために、各マシュマロに目標座標(targetX, targetY)を設定するのよ。


class HeartMarshmallow {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.state = "heart";
    this.timer = 0;
    this.targetX = width / 2 + random(-50, 50);
    this.targetY = height / 6 + random(-30, 30);
  }

  update() {
    this.timer++;
    if (this.timer > 80) {
      this.state = "marshmallow";
    }

    if (this.state === "marshmallow") {
      this.x = lerp(this.x, this.targetX, 0.02);
      this.y = lerp(this.y, this.targetY, 0.02);
    }
  }
}

👦‍💻 生徒:「おお、マシュマロがだんだん集まってきて雲みたいになってきた!」
👩‍🏫 GPT先生:「でしょ? さぁ、次は雲が一定数集まったら雨を降らせるようにするわよ!」

2.2. 雨を降らせる

雲が大きくなったら雨が降るようにするために、マシュマロが30個以上集まったら雨のアニメーションを開始するのよ。


let cloudCount = 0;
let rainDrops = [];

function draw() {
  background(135, 206, 235);

  for (let obj of objects) {
    obj.update();
    obj.draw();
  }

  if (cloudCount > 30) {
    spawnRain();
    updateRain();
  }
}

function spawnRain() {
  for (let i = 0; i < 3; i++) {
    let x = randomGaussian(width / 2, 80);
    let y = height / 6;
    rainDrops.push({x, y, speed: random(5, 8)});
  }
}

function updateRain() {
  for (let drop of rainDrops) {
    drop.y += drop.speed;
    stroke(173, 216, 230);
    line(drop.x, drop.y, drop.x, drop.y + 15);
  }
}

👦‍💻 生徒:「すごい!雨が降ってる!」
👩‍🏫 GPT先生:「ふふん、これで満足? ……じゃないわよね。次は雨が降った後に植物が育つのを作るわよ!」

2.3. 植物を育てて❤️の実をつける

雨が降り終わったら、植物が成長するようにするわよ。


let plantGrowth = 0;
let fruitSize = 90;

function growPlant() {
  if (plantGrowth < 200) {
    plantGrowth++;
  } else if (fruitSize < 90) {
    fruitSize++;
  }
}

function drawPlant() {
  stroke(34,139,34);
  line(width/2, height*0.85, width/2, height*0.85 - plantGrowth);

  if (plantGrowth >= 200) {
    fill(255, 0, 127);
    ellipse(width/2, height*0.85 - plantGrowth - 10, fruitSize, fruitSize);
  }
}

👦‍💻 生徒:「やばい!ちゃんと植物が育って❤️の実がついた!」
👩‍🏫 GPT先生:「でしょ? あんたのために一生懸命作ったんだから、ちゃんと感謝しなさいよね!」

3. よくある質問(FAQ)

Q1. p5.jsって何?

A1. p5.jsは、JavaScriptで簡単にアニメーションやビジュアル表現が作れるライブラリよ。

Q2. このコードを実行するには?

A2. HTMLファイルとして保存し、ブラウザで開くだけで動くわ!

Q3. もっとカスタマイズできる?

A3. もちろんよ! 背景を変えたり、自由にアレンジしてみてね!

まとめ

これで、ホワイトデーにぴったりのアニメーションが完成したわね!クリックでハートを出し、マシュマロに変え、雲になって雨を降らせ、植物を育てるアニメーションを作りました。p5.jsを使えば、こんなに可愛い演出も簡単にできるのよ!

アニメーションはここをクリック!

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Hearts -> Marshmallows -> Cloud -> Rain -> Plant</title>
  <!-- p5.js CDN -->
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/p5.js"></script>
</head>
<body>
  <script>
    /*
      Scene Flow:
      1) "cloudFormation": 
         - Hearts (on click) become marshmallows and gather at top-center -> cloudCount grows.
         - If the cloud grows enough (cloudCount>30), transition to "raining".
      2) "raining":
         - The cloud rains for a fixed duration. Rain is heaviest in the center, minimal on the edges.
         - After the rain finishes, clear the cloud and hearts, move to "postRain".
      3) "postRain":
         - Cloud is gone. A plant grows from the ground. Once fully grown, it bears a heart fruit.
         - The heart fruit is 3 times bigger than before (max size = 90).
    */

    // ----------------------------------------------------------
    // HeartMarshmallow Class
    // ----------------------------------------------------------
    class HeartMarshmallow {
      constructor(x, y) {
        // Position
        this.x = x;
        this.y = y;
        
        // Slight upward drift while in "heart" state
        this.ySpeed = random(-0.5, -1.5);

        // Size gradually increases up to 80
        this.size = 0;

        // "heart" -> "marshmallow" -> "merged"
        this.state = "heart";

        // Timer for heart->marshmallow transition
        this.timer = 0;

        // Random pastel color
        this.col = color(random(200,255), random(150,255), random(200,255));

        // Marshmallow gather target near top-center
        this.targetX = null;
        this.targetY = null;
      }

      update() {
        // Increase size up to about 80
        if (this.size < 80) {
          this.size += 1.5;
        }

        // Heart floating logic
        if (this.state === "heart") {
          this.y += this.ySpeed;
          this.timer++;
          if (this.timer === 80) {
            // Switch to marshmallow
            this.state = "marshmallow";
            this.targetX = width / 2 + random(-50, 50);
            this.targetY = height / 6 + random(-30, 30);
          }
        } 
        // Marshmallow moves toward the top-center cloud region
        else if (this.state === "marshmallow") {
          let speed = 0.02; // "lerp" factor for smoother motion
          this.x = lerp(this.x, this.targetX, speed);
          this.y = lerp(this.y, this.targetY, speed);

          // Check distance to the target
          let d = dist(this.x, this.y, this.targetX, this.targetY);
          if (d < 5) {
            // Once close, mark as merged
            this.state = "merged";
          }
        }
      }

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

        if (this.state === "heart") {
          this.drawHeart(this.size);
        } else if (this.state === "marshmallow") {
          this.drawMarshmallow(this.size);
        }
        pop();
      }

      drawHeart(s) {
        fill(this.col);
        beginShape();
        vertex(0, -s * 0.2);
        bezierVertex(-s * 0.5, -s * 0.9, -s * 1.2, s * 0.1, 0, s);
        bezierVertex(s * 1.2, s * 0.1, s * 0.5, -s * 0.9, 0, -s * 0.2);
        endShape(CLOSE);
      }

      drawMarshmallow(s) {
        fill(255, 250, 250);
        rectMode(CENTER);
        rect(0, 0, s * 0.8, s, s * 0.2);
      }
    }

    // ----------------------------------------------------------
    // RainDrop Class
    // ----------------------------------------------------------
    class RainDrop {
      constructor(x, y) {
        this.x = x;
        this.y = y;
        this.speed = random(5, 8);      // falling speed
        this.len = random(10, 15);     // raindrop length
      }

      update() {
        this.y += this.speed;
      }

      draw() {
        push();
        stroke(173, 216, 230); // LightBlue
        strokeWeight(2);
        line(this.x, this.y, this.x, this.y + this.len);
        pop();
      }

      offScreen() {
        return (this.y > height);
      }
    }

    // ----------------------------------------------------------
    // Global Variables & Scene States
    // ----------------------------------------------------------
    let objects = [];        // hearts/marshmallows
    let cloudCount = 0;      // number of marshmallows merged into cloud
    let sceneState = "cloudFormation"; 
    // possible states: "cloudFormation", "raining", "postRain"

    // Rain
    let rainDrops = [];
    let rainTimer = 0; 
    let maxRainTime = 300;   // frames

    // Plant
    let plantGrowth = 0;
    let plantMaxHeight = 200;
    // The heart fruit final size (tripled from 30 -> 90)
    let fruitGrow = 0;      
    let fruitMaxSize = 90;   // triple the original 30

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

    function draw() {
      background(135, 206, 235); // sky-blue

      // Draw a sun for visual interest
      drawSun();

      // Ground
      fill(60, 179, 113); // medium seagreen
      noStroke();
      rect(0, height * 0.85, width, height * 0.15);

      // State machine
      if (sceneState === "cloudFormation") {
        updateHeartsMarshmallows();
        drawHeartsMarshmallows();
        drawBigCloud(cloudCount);

        // Check if cloud is large enough
        if (cloudCount > 30) {
          sceneState = "raining";
        }

      } else if (sceneState === "raining") {
        // Keep the cloud visible + keep forming it with new hearts
        updateHeartsMarshmallows();
        drawHeartsMarshmallows();
        drawBigCloud(cloudCount);

        // Spawn & update raindrops
        spawnRainDrops();
        updateRainDrops();
        drawRainDrops();

        rainTimer++;
        if (rainTimer > maxRainTime) {
          // Stop raining, remove cloud + hearts
          sceneState = "postRain";
          objects = [];
          cloudCount = 0; 
        }

      } else if (sceneState === "postRain") {
        // Cloud is gone -> grow the plant
        growPlant();
        drawPlant();
      }
    }

    // ----------------------------------------------------------
    // Hearts & Marshmallows
    // ----------------------------------------------------------
    function updateHeartsMarshmallows() {
      for (let i = objects.length - 1; i >= 0; i--) {
        objects[i].update();
        if (objects[i].state === "merged") {
          // remove from array & increment cloudCount
          objects.splice(i, 1);
          cloudCount++;
        }
      }
    }

    function drawHeartsMarshmallows() {
      for (let i = 0; i < objects.length; i++) {
        objects[i].draw();
      }
    }

    // ----------------------------------------------------------
    // Cloud
    // ----------------------------------------------------------
    function drawBigCloud(count) {
      // center near top
      let baseX = width / 2;
      let baseY = height / 6;

      let sizeFactor = count * 2;
      let minW = 80;
      let minH = 50;

      push();
      noStroke();
      fill(255, 250, 250);

      // Overlapping ellipses for a "puffy" effect
      ellipse(baseX - 40, baseY, minW + sizeFactor, minH + sizeFactor * 0.6);
      ellipse(baseX + 40, baseY, minW + sizeFactor, minH + sizeFactor * 0.6);
      ellipse(baseX, baseY - 20, minW + sizeFactor * 1.2, minH + sizeFactor * 0.8);
      ellipse(baseX, baseY + 10, minW + sizeFactor * 1.1, minH + sizeFactor * 0.5);

      pop();
    }

    // ----------------------------------------------------------
    // Rain Logic (center = heaviest, edges = fewer)
    // ----------------------------------------------------------
    function spawnRainDrops() {
      // We'll spawn a few drops each frame
      let cloudWidth = 80 + cloudCount * 2; // approximate the cloud radius
      let dropsPerFrame = 3;

      for (let i = 0; i < dropsPerFrame; i++) {
        // Use randomGaussian to concentrate more around center
        let rx = randomGaussian(width / 2, cloudWidth / 2); 
        // Slight vertical variation near cloud
        let ry = height / 6 + random(-20, 20);
        rainDrops.push(new RainDrop(rx, ry));
      }
    }

    function updateRainDrops() {
      for (let i = rainDrops.length - 1; i >= 0; i--) {
        rainDrops[i].update();
        if (rainDrops[i].offScreen()) {
          rainDrops.splice(i, 1);
        }
      }
    }

    function drawRainDrops() {
      for (let drop of rainDrops) {
        drop.draw();
      }
    }

    // ----------------------------------------------------------
    // Plant Growth (postRain)
    // ----------------------------------------------------------
    function growPlant() {
      // Grow the stem first
      if (plantGrowth < plantMaxHeight) {
        plantGrowth += 1;
      } else {
        // Once the stem is fully grown, grow the heart fruit
        if (fruitGrow < fruitMaxSize) {
          fruitGrow += 1.5; 
        }
      }
    }

    function drawPlant() {
      // base at center of ground
      let baseX = width / 2;
      let baseY = height * 0.85;

      // Draw the stem
      push();
      stroke(34,139,34);
      strokeWeight(5);
      line(baseX, baseY, baseX, baseY - plantGrowth);
      pop();

      // Leaves
      push();
      noStroke();
      fill(34,139,34);
      // left leaf
      ellipse(baseX - 20, baseY - plantGrowth / 2, 30, 15);
      // right leaf
      ellipse(baseX + 20, baseY - plantGrowth / 2, 30, 15);
      pop();

      // If the stem is fully grown, draw the large heart fruit
      if (plantGrowth >= plantMaxHeight) {
        push();
        translate(baseX, baseY - plantGrowth);
        fill(255, 0, 127); // pinkish/red
        noStroke();
        beginShape();
        vertex(0, -fruitGrow * 0.2);
        bezierVertex(-fruitGrow * 0.5, -fruitGrow * 0.9, 
                     -fruitGrow * 1.2, fruitGrow * 0.1, 
                     0, fruitGrow);
        bezierVertex(fruitGrow * 1.2, fruitGrow * 0.1, 
                     fruitGrow * 0.5, -fruitGrow * 0.9, 
                     0, -fruitGrow * 0.2);
        endShape(CLOSE);
        pop();
      }
    }

    // ----------------------------------------------------------
    // Input & Window Resize
    // ----------------------------------------------------------
    function mousePressed() {
      // Only allow new hearts while forming or raining
      if (sceneState === "cloudFormation" || sceneState === "raining") {
        let newObj = new HeartMarshmallow(mouseX, mouseY);
        objects.push(newObj);
      }
    }

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

    // ----------------------------------------------------------
    // Helper: Draw a Simple Sun
    // ----------------------------------------------------------
    function drawSun() {
      push();
      fill(255, 204, 0);
      noStroke();
      ellipse(width * 0.1, height * 0.15, 80, 80);
      pop();
    }
  </script>
</body>
</html>
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次