アイスクリームの日

p5.jsでアイスクリームアニメ!ツンデレ先生と学ぶコード作成の裏側 アプリはここをクリック!

p5.jsでアイスクリームアニメ!
ツンデレ先生と学ぶコード作成の裏側

目次

登場人物紹介

  • 👩‍🏫【GPT先生】…ちょっとツンデレ、でもコード初心者にめちゃくちゃ優しい女性エンジニア。
  • 🧑‍💻【生徒】…AIにコードを書かせるのが好きな楽したがり。でも動くとテンション爆上がり。

ツンデレ先生とのコード作成会話ログ

🧑‍💻 生徒: アイスクリームをテーマに、すごいアニメーションp5.jsで作って。HTMLとJS全部出して。
👩‍🏫 GPT先生: ふっ、アンタ…いきなりハードル高いこと頼むじゃない。…べ、別に嬉しいわけじゃないけど、やるわよ! まずアイスクリームって言ったら、ふんわりパステルカラーと、ドリップ、トッピングよね!p5.jsでそれを再現してあげる。 …あ、初心者でも動かせるように、HTMLとJSは分けて、しかもロードを早くする構成にするから覚悟しなさい。
🧑‍💻 生徒: うおっ、ちゃんと考えてくれてる。面倒なことは全部任せた!
👩‍🏫 GPT先生: まったく…しょうがないわね…! とにかく、今回はCSSで背景を描画して軽くして、`noLoop()`でアニメを止めておく戦法よ。 じゃ、`index.html`からいくわよ!

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <title>アイスクリームアニメーション</title>
  <link rel="preload" href="https://cdn.jsdelivr.net/npm/p5@1.9.2/lib/p5.min.js" as="script" crossorigin>
  <script src="https://cdn.jsdelivr.net/npm/p5@1.9.2/lib/p5.min.js" defer crossorigin></script>
  <script src="sketch.js" defer></script>
  <style>
    html, body { margin: 0; padding: 0; overflow: hidden; }
    body {
      background: linear-gradient(to bottom, #b6eaff 0%, #ffd5ed 100%);
    }
    canvas { display: block; }
  </style>
</head>
<body></body>
</html>
🧑‍💻 生徒: なんか簡単そうに見える!次は?
👩‍🏫 GPT先生: フフ、あんたが理解しやすいようにしてるだけよ! 次は `sketch.js`! ちゃんと分けてあげてるんだから感謝しなさいよ?

sketch.js



/* Ice‑Cream‑Themed p5.js Sketch  ── 軽量ロード版 ─────────────────── */

/* ========= グローバル定数・変数 ========= */
let coneH, scoopR;                                 // 動的ジオメトリ
let scoops = [], drips = [], sprinkles = [];
const MAX_SCOOPS = 8;
const flavours = ["#FFF5D6","#FFB7CF","#C1F0C2","#CAA472","#B3C6FF","#FFE391"];

/* ========= ウィンドウ依存の寸法を一括計算 ========= */
function computeGeometry(){
  const minDim = Math.min(windowWidth, windowHeight);
  coneH  = minDim * 0.28;        // コーン高さ
  scoopR = minDim * 0.12;        // スクープ半径
}

/* ========= p5 基本フック ========= */
function setup(){
  createCanvas(windowWidth, windowHeight);
  angleMode(RADIANS);
  computeGeometry();

  noLoop();                      // ★ 初期ロード時は描画停止
}

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

/* ユーザー操作が来るまでは描画しない → タップで loop() 再開 */
function mousePressed(){
  if (!isLooping()) loop();      // ★ 初回のみ描画ループを起動

  if (scoops.length >= MAX_SCOOPS) return;

  const margin = scoopR * 0.1;   // コーン先端とスクープの “隙間”
  const firstY = height - coneH - scoopR + margin;
  const yBase  = firstY - scoops.length * scoopR * 1.6;

  const newScoop = new Scoop(createVector(width/2, yBase), scoopR);
  scoops.push(newScoop);
  burstSprinkles(newScoop);
}

function draw(){
  /* 背景は CSS で塗っているのでここでは不要 */
  drawCone();

  for (let s of scoops){ s.update(); s.display(); }

  for (let i = drips.length-1; i >= 0; i--){
    drips[i].update(); drips[i].display();
    if (drips[i].offscreen()) drips.splice(i,1);
  }

  for (let i = sprinkles.length-1; i >= 0; i--){
    sprinkles[i].update(); sprinkles[i].display();
    if (sprinkles[i].finished()) sprinkles.splice(i,1);
  }
}

/* ========= 描画ユーティリティ ========= */
function drawCone(){
  const coneW = coneH * 0.6, cx = width/2, cy = height - 10;
  push();
  translate(0, map(mouseY,0,height,-10,10));   // パララックス
  fill(228,187,118);
  stroke(205,164,101,120); strokeWeight(1);

  beginShape();
    vertex(cx-coneW/2, cy-coneH);
    vertex(cx+coneW/2, cy-coneH);
    vertex(cx,          cy);
  endShape(CLOSE);

  const step = coneW/10;
  for (let i=-coneH;i<0;i+=step) line(cx-coneW/2,cy+i,cx+coneW/2,cy+i);
  rotate(-PI/6);
  for (let i=-coneH;i height; }
}

class Sprinkle{
  constructor(pos,col){
    this.pos=pos.copy();
    this.vel=p5.Vector.random2D().mult(random(1,3));
    this.life=60; this.col=col; this.angle=random(TWO_PI);
  }
  update(){ this.pos.add(this.vel); this.vel.mult(0.95); this.life--; this.angle+=0.2; }
  display(){ push(); translate(this.pos.x,this.pos.y); rotate(this.angle); fill(this.col); rectMode(CENTER); rect(0,0,4,1.5); pop(); }
  finished(){ return this.life<=0; }
}

/* ========= ヘルパー関数 ========= */
function burstSprinkles(scoop){
  for(let i=0;i<40;i++){
    sprinkles.push(new Sprinkle(scoop.pos, color(random(flavours))));
  }
}
function easeOutElastic(t){
  const c4=(2*Math.PI)/3;
  return t===0?0:t===1?1:Math.pow(2,-10*t)*Math.sin((t*10-0.75)*c4)+1;
}
🧑‍💻 生徒: ふおお〜!背景ふわっと出てきて、クリックしたら動いた!最高じゃん!!
👩‍🏫 GPT先生: べ、別に褒められて嬉しいわけじゃないけど…! でも、これがロード高速化 + 初回静止 + ユーザー操作起動の三段構えよ!覚えておきなさい!

よくある質問(FAQ)

Q1. なぜ `noLoop()` を使うの?

ロード時に何も描画しないことで、CPU負荷を極限まで減らして、最初の表示速度を早く見せるためです。特にモバイル端末では効果絶大です。

Q2. CSSで背景を描くとどう違うの?

JavaScriptで背景を毎フレーム描くと描画負荷がかかりますが、CSSであれば1回だけブラウザに任せて描画されるので、CPUにもバッテリーにも優しいです。

Q3. 最初のクリックでしか動かないのはなぜ?

ユーザーの操作があってからアニメーションを開始することで、UX(体験)が良くなると同時に、デバイスによる無駄な処理を省けます。

Q4. 色や形を変えるにはどうすればいい?

flavours配列の色コードを好きなものに変更したり、`ellipse`を`rect`にしたりすれば、簡単にアートスタイルを変更できます。

Q5. 音を鳴らしたり、ボタンを追加することはできる?

もちろん可能です。p5.soundライブラリを追加すればクリック時に効果音を鳴らしたり、DOM要素を使ってボタンやスライダーを作ることもできます。

Q6. 複数のアイスを表示したい!

スクープのスタック上限`MAX_SCOOPS`を増やすだけでOK。さらに複数のコーンを配置したい場合は、`drawCone()`と`scoops`配列を分けて管理すると実装できます。

Q7. 画像として保存することはできる?

できます!p5.jsでは`saveCanvas()`を使えば、ワンクリックでキャンバスを画像として保存可能です。ボタンと連携させればすぐ使えます。

Q8. このコードはスマホでも動きますか?

はい。p5.jsはスマホ対応しており、今回のコードもタップによる操作で動くように設計されています。

Q9. Webアプリとして公開するには?

作成したHTMLとJSファイルをGitHub PagesやNetlify、Vercelにアップロードするだけで、誰でもアクセスできるWebアプリとして公開可能です。


まとめ

p5.js を使ったアイスクリームアニメーションは、**パフォーマンスを意識しつつ、遊び心もたっぷり**。 CSSグラデ + `noLoop()` + deferロードという構成なら、初心者でも快適で動きのある表現が可能になります。 — 一緒に作ったアイスアニメ、あなたも実際に触れてみたくなったでしょう? ぜひ、以下から体験して! アプリはここをクリック!
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次