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>
コメント