アプリはこちらクリック!
はじめに
👩🏫【先生】
「ちょっと、あんた! まさか全部のアニメーションをゼロから書こうとしてるんじゃないでしょうね? プログラム初心者でもムーンサルトの動きを再現できるように、今回はライブラリを使って簡単に実装する方法を教えてあげるわ。感謝しなさいよね!」
👦【生徒】
「おお、先生! 俺はプロレス技が画面で動いてくれればそれで満足なんだけど、できれば楽に作りたいから、ライブラリっていうのを使うのはアリだね!」
👩🏫【先生】
「まったく、楽することばっかり考えて…でも、実はそれが賢い方法なのよ。今回は 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>
コメント