ムーンサルトアニメーションを作る!ライブラリ活用で簡単実装

アプリはこちらクリック!

はじめに

👩‍🏫【先生】
「ちょっと、あんた! まさか全部のアニメーションをゼロから書こうとしてるんじゃないでしょうね? プログラム初心者でもムーンサルトの動きを再現できるように、今回はライブラリを使って簡単に実装する方法を教えてあげるわ。感謝しなさいよね!」

👦【生徒】
「おお、先生! 俺はプロレス技が画面で動いてくれればそれで満足なんだけど、できれば楽に作りたいから、ライブラリっていうのを使うのはアリだね!」

👩‍🏫【先生】
「まったく、楽することばっかり考えて…でも、実はそれが賢い方法なのよ。今回は GSAP(GreenSock Animation Platform) っていう強力なアニメーションライブラリを使うわよ。ムーンサルトの動きをスムーズに作れるから、しっかり学びなさい!」

GSAPを使ってムーンサルトを実装する

👩‍🏫【先生】
「ムーンサルトの基本動作は、ジャンプ → 回転 → 着地 でしょ? GSAPを使えば、こういう動きを簡単に記述できるの。試しに、レスラーがリングのロープから飛び出して回転し、着地するアニメーションを作るわよ!」

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ムーンサルトアニメーション</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
body { background: #222; text-align: center; color: white; }
.ring { width: 300px; height: 200px; background: gray; margin: 50px auto; position: relative; }
.wrestler { width: 50px; height: 50px; background: red; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); border-radius: 50%; }
</style>
</head>
<body>
<h1>ムーンサルトアニメーション</h1>
<div class="ring">
<div class="wrestler"></div>
</div>
<button onclick="moonsault()">ムーンサルト発動!</button>

<script>
function moonsault() {
gsap.to(".wrestler", {
y: -150, // ジャンプ
rotation: 360, // 1回転
duration: 1,
ease: "power2.out",
onComplete: () => {
gsap.to(".wrestler", {
y: 0, // 着地
duration: 0.5,
ease: "bounce.out"
});
}
});
}
</script>
</body>
</html>

コード解説

👩‍🏫【先生】
「ほら、GSAPを使えば、たった数行のコードでムーンサルトができちゃうのよ! ポイントはこの部分ね。」

gsap.to(".wrestler", {
y: -150, // ジャンプ
rotation: 360, // 1回転
duration: 1,
ease: "power2.out",
onComplete: () => {
gsap.to(".wrestler", {
y: 0, // 着地
duration: 0.5,
ease: "bounce.out"
});
}
});

👩‍🏫【先生】
「最初に .wrestler を y=-150 まで持ち上げて、rotation: 360 で1回転。duration: 1 で1秒かけて実行するのよ。ease: "power2.out" っていうのは、ジャンプの勢いを自然にするためのものね。」

👦【生徒】
「へぇ、ライブラリを使うとこんなに短く書けるんだ! しかも、onComplete を使って、ジャンプのあとに自動で着地するようになってるんだね!」

👩‍🏫【先生】
「そうよ、さすがにそこは理解できたわね! 着地のアニメーションでは ease: "bounce.out" を指定してるから、プロレスの試合みたいに自然なバウンドで着地するの。すごいでしょ?」

応援メッセージ&ヤジの追加

👩‍🏫【先生】
「ムーンサルトが決まったときに、観客の声援が出てくるようにするわよ! ヤジはミスのときに出るようにして、シュッと斜め下から現れる演出をつけるの。」

function showMessage(text, isHit) {
let msg = document.createElement("div");
msg.innerText = text;
msg.style.position = "absolute";
msg.style.color = "white";
msg.style.fontSize = "24px";
msg.style.left = Math.random() * window.innerWidth + "px";
msg.style.top = window.innerHeight + "px"; // 画面下から出る
document.body.appendChild(msg);

gsap.to(msg, {
y: -200, // シュッと上がる
duration: 1,
ease: "power1.out",
onComplete: () => setTimeout(() => msg.remove(), 1000) // 消える
});
}

👩‍🏫【先生】
「試合を盛り上げるために、ムーンサルトが成功したら 応援メッセージ、ミスしたら ヤジ を表示するようにしてるのよ。こうやってゲームっぽく仕上げていくの!」

👦【生徒】
「いいね! これでプロレス観戦の雰囲気がバッチリ出るよ。あとは…試合に勝ったら『WIN』のエフェクトが出れば完璧!」

👩‍🏫【先生】
「ま、そこまで言うなら、最後に『WIN』エフェクトを追加してあげるわよ。」

function showWinEffect() {
let winText = document.createElement("div");
winText.innerText = "WIN!";
winText.style.position = "absolute";
winText.style.color = "gold";
winText.style.fontSize = "60px";
winText.style.left = "50%";
winText.style.top = "-100px"; // 画面上から降ってくる
winText.style.transform = "translateX(-50%)";
document.body.appendChild(winText);

gsap.to(winText, {
y: 200, // 落ちてくる
duration: 1,
ease: "bounce.out"
});
}

FAQコーナー

Q1: GSAPは無料で使えますか?
A1: はい、GSAPの基本機能は無料で利用できます。有料版もありますが、今回のアニメーションには無料版で十分です。

Q2: 他のアニメーションライブラリでも実装できますか?
A2: Anime.js や CSS アニメーションでも可能ですが、GSAPは特に動きが滑らかで、複数の動きを連携させるのが簡単です。

Q3: 実装が難しく感じるのですが…
A3: GSAPの基本構文さえ理解すれば、かなり直感的にアニメーションを作れるようになります。まずは試してみましょう!

まとめ

👩‍🏫【先生】
「今回のコードで、ムーンサルトのジャンプ・回転・着地が簡単にできるようになったでしょ? さらに、声援やヤジの演出、勝利時の『WIN』エフェクトまで完備よ! これで試合の雰囲気がバッチリ出るわね!」

👦【生徒】
「うん、これでプロレスの試合の興奮を再現できる! 先生、ありがとう!」

アプリはこちらクリック!

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <!-- スマートフォン向け -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
  <title>プロレス技 アニメーション(声援・ヤジアニメーション付き)</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      background: #222;
    }
    canvas {
      display: block;
      transform-origin: top left;
      touch-action: manipulation;
    }
  </style>
</head>
<body>
  <!-- 効果音 -->
  <audio id="impactSound" src="impact.mp3"></audio>
  <audio id="crowdSound" src="crowd.mp3"></audio>
  
  <script>
    // キャンバスの設定
    const canvas = document.createElement("canvas");
    document.body.appendChild(canvas);
    const ctx = canvas.getContext("2d");
  
    let width = canvas.width = window.innerWidth;
    let height = canvas.height = window.innerHeight;
    // 着地位置は常に画面下から100px
    let landingY = height - 100;
    window.addEventListener("resize", () => {
      width = canvas.width = window.innerWidth;
      height = canvas.height = window.innerHeight;
      landingY = height - 100;
    });
  
    // カメラシェイク用オフセット
    let shakeOffsetX = 0, shakeOffsetY = 0;
  
    // 表示メッセージ管理
    // 各メッセージは target (最終表示位置) と start (画面斜め下から入る開始位置) を持つ
    let displayMessages = [];
    function getRandomMessagePosition() {
      const padding = 50;
      return {
        x: Math.random() * (width - 2 * padding) + padding,
        y: Math.random() * (height - 2 * padding) + padding
      };
    }
    function addDisplayMessage(text) {
      const target = getRandomMessagePosition();
      const offset = 150;  // 斜め下から入るためのオフセット
      displayMessages.push({ 
        text: text,
        targetX: target.x,
        targetY: target.y,
        startX: target.x + offset,
        startY: target.y + offset,
        timer: 120,             // 表示時間(フレーム数)
        transitionDuration: 30  // 最初の30フレームで滑り込み
      });
    }
  
    let gameOver = false, gameResetTimer = 0;
  
    // WINアニメーション用
    let winActive = false, winResetTimer = 0, winY = -200, winVY = 0;
  
    // コンフェッティクラス
    class Confetti {
      constructor(x, y) {
        this.x = x;
        this.y = y;
        this.vx = (Math.random() - 0.5) * 8;
        this.vy = (Math.random() - 0.5) * 8;
        this.size = Math.random() * 5 + 3;
        this.color = `hsl(${Math.floor(Math.random() * 360)}, 100%, 50%)`;
        this.life = 100;
      }
      update() {
        this.x += this.vx;
        this.y += this.vy;
        this.vy += 0.2;
        this.life--;
      }
      draw() {
        ctx.fillStyle = this.color;
        ctx.fillRect(this.x, this.y, this.size, this.size);
      }
    }
    const confettiParticles = [];
    function spawnConfetti(x, y, count = 50) {
      for (let i = 0; i < count; i++) {
        confettiParticles.push(new Confetti(x, y));
      }
    }
  
    // カメラシェイク処理
    function cameraShake() {
      const intensity = 10;
      const duration = 500;
      const startTime = performance.now();
      function shake() {
        const elapsed = performance.now() - startTime;
        if (elapsed < duration) {
          shakeOffsetX = (Math.random() - 0.5) * intensity;
          shakeOffsetY = (Math.random() - 0.5) * intensity;
          requestAnimationFrame(shake);
        } else {
          shakeOffsetX = 0;
          shakeOffsetY = 0;
        }
      }
      shake();
    }
  
    // 簡易衝突判定(矩形同士の重なり)
    function checkCollision(wrestler, opponent) {
      const aLeft = wrestler.x - 20;
      const aRight = wrestler.x + 20;
      const aTop = wrestler.y - 40;
      const aBottom = wrestler.y + 40;
      const bLeft = opponent.x - opponent.width / 2;
      const bRight = opponent.x + opponent.width / 2;
      const bTop = opponent.y - opponent.height;
      const bBottom = opponent.y;
      return !(aLeft > bRight || aRight < bLeft || aTop > bBottom || aBottom < bTop);
    }
  
    // プロレスラー(技をかける側)のクラス
    class Wrestler {
      constructor(x, y, color) {
        this.startX = x;
        this.startY = y;
        this.x = x;
        this.y = y;
        this.vx = 0;
        this.vy = 0;
        this.gravity = 0.6;
        this.onRope = true;
        this.flipping = false;
        this.angle = 0;
        this.color = color;
      }
      draw() {
        ctx.save();
        ctx.translate(this.x, this.y);
        ctx.rotate(this.angle);
        ctx.fillStyle = this.color;
        ctx.fillRect(-20, -40, 40, 80);
        ctx.restore();
      }
      update() {
        if (this.flipping) {
          this.vy += this.gravity;
          this.y += this.vy;
          this.x += this.vx;
          this.angle += 0.2;
          // 着地判定:常に landingY(画面下から100px)に固定
          if (this.y > landingY) {
            this.y = landingY;
            this.vy = 0;
            this.flipping = false;
            this.angle = 0;
            document.getElementById("impactSound").play();
            cameraShake();
            if (checkCollision(this, opponent)) {
              const cheersArray = ["オーッ!", "すげぇ!", "最高だ!", "やったー!", "レッツゴー!", "勝利だ!"];
              const selectedCheer = cheersArray[Math.floor(Math.random() * cheersArray.length)];
              addDisplayMessage("Hit! Success!");
              addDisplayMessage(selectedCheer);
              opponent.fall();
              spawnConfetti(opponent.x, opponent.y - opponent.height / 2, 80);
              winActive = true;
              winResetTimer = 180;
              winY = -200;
              winVY = 0;
            } else {
              const jeersArray = ["なんだと!", "ブッ殺すぞ!", "あかん!", "まだまだだ!", "ぐはは!"];
              const selectedJeer = jeersArray[Math.floor(Math.random() * jeersArray.length)];
              addDisplayMessage("Game Over");
              addDisplayMessage(selectedJeer);
              gameOver = true;
              gameResetTimer = 180;
            }
          }
        }
      }
      // ムーンサルト発動:ジャンプ力は固定(-15)
      moonsault() {
        if (!this.onRope) return;
        this.vx = 5;
        this.vy = -15;
        this.flipping = true;
        this.onRope = false;
        document.getElementById("crowdSound").play();
      }
      reset() {
        this.x = this.startX;
        this.y = this.startY;
        this.vx = 0;
        this.vy = 0;
        this.flipping = false;
        this.onRope = true;
        this.angle = 0;
      }
    }
  
    // 相手キャラのクラス
    class Opponent {
      constructor(x, y, color) {
        this.startX = x;
        this.startY = y;
        this.x = x;
        this.y = y;
        this.width = 40;
        this.height = 80;
        this.color = color;
        this.vx = 2;
        this.falling = false;
        this.angle = 0;
        this.vy = 0;
        this.rotationSpeed = 0;
      }
      fall() {
        if (this.falling) return;
        this.falling = true;
        this.vy = -8;
        this.vx = 4 * (Math.random() < 0.5 ? -1 : 1);
        this.rotationSpeed = 0.3;
      }
      update() {
        if (this.falling) {
          this.vy += 0.6;
          this.y += this.vy;
          this.x += this.vx;
          this.angle += this.rotationSpeed;
        } else {
          this.x += this.vx;
          if (this.x > width - this.width / 2 || this.x < this.width / 2) {
            this.vx = -this.vx;
          }
        }
      }
      draw() {
        ctx.save();
        ctx.translate(this.x, this.y);
        if (this.falling) ctx.rotate(this.angle);
        ctx.fillStyle = this.color;
        ctx.fillRect(-this.width / 2, -this.height, this.width, this.height);
        ctx.restore();
      }
      reset() {
        this.x = this.startX;
        this.y = this.startY;
        this.vx = 2;
        this.falling = false;
        this.angle = 0;
        this.vy = 0;
      }
    }
  
    let wrestler = new Wrestler(width * 0.2, height - 200, "red");
    let opponent = new Opponent(width * 0.8, height - 100, "blue");
  
    function resetGame(isWin = false) {
      wrestler.reset();
      opponent.reset();
      confettiParticles.length = 0;
      displayMessages = [];
      gameOver = false;
      gameResetTimer = 0;
      winActive = false;
      winResetTimer = 0;
    }
  
    function animate() {
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.clearRect(0, 0, width, height);
  
      wrestler.update();
      wrestler.draw();
      opponent.update();
      opponent.draw();
  
      for (let i = confettiParticles.length - 1; i >= 0; i--) {
        const p = confettiParticles[i];
        p.update();
        p.draw();
        if (p.life <= 0) {
          confettiParticles.splice(i, 1);
        }
      }
  
      // 各メッセージのアニメーション表示
      for (let i = displayMessages.length - 1; i >= 0; i--) {
        let msgObj = displayMessages[i];
        // transitionDuration フレーム分は開始位置からターゲット位置へ線形補間
        let progress = 1;
        const total = msgObj.transitionDuration;
        const elapsed = 120 - msgObj.timer;
        if (elapsed < total) {
          progress = elapsed / total;
        }
        const currentX = msgObj.startX * (1 - progress) + msgObj.targetX * progress;
        const currentY = msgObj.startY * (1 - progress) + msgObj.targetY * progress;
  
        ctx.font = "24px sans-serif";  // フォントサイズ半分(24px)
        ctx.fillStyle = "#fff";
        const textWidth = ctx.measureText(msgObj.text).width;
        ctx.fillText(msgObj.text, currentX - textWidth / 2, currentY);
  
        msgObj.timer--;
        if (msgObj.timer <= 0) {
          displayMessages.splice(i, 1);
        }
      }
  
      if (winActive) {
        winVY += 0.5;
        winY += winVY;
        if (winY > height / 2) {
          winY = height / 2;
          winVY *= -0.6;
          if (Math.abs(winVY) < 2) winVY = 0;
        }
        ctx.save();
        ctx.font = "bold 100px sans-serif";
        ctx.fillStyle = "#FFD700";
        ctx.textAlign = "center";
        ctx.fillText("WIN", width / 2, winY);
        ctx.restore();
        winResetTimer--;
        if (winResetTimer <= 0) {
          resetGame(true);
        }
      }
      
      if (gameOver) {
        if (gameResetTimer > 0) {
          gameResetTimer--;
        } else {
          resetGame();
        }
      }
  
      // ズームアウトは無効(カメラシェイクのみ適用)
      canvas.style.transform = "translate(" + shakeOffsetX + "px, " + shakeOffsetY + "px)";
      
      requestAnimationFrame(animate);
    }
  
    canvas.addEventListener("click", () => {
      if (!wrestler.flipping && !gameOver && !winActive) {
        wrestler.reset();
        wrestler.moonsault();
      }
    });
  
    animate();
  </script>
</body>
</html>
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次