目次
会話形式記事: 冥王の目覚めアニメーションコード作成の裏側
アプリはこちらをクリック!https://invisiblelagoon.com/lp/plu/
👩 先生: はぁ?またコード作成の依頼したいの?全く、あなたはいつも面倒なことばっかり頼むのね。でも、今日も特別に、作成した後に私が丁寧に教えてあげるわよ!
👦 生徒: うん、先生。俺、コードの中身はよくわかんないけど、とにかく動けばOKだし、美しいアニメーションが見たいんだ。お願いします!
冥王星発見をテーマに歌詞作成して、その歌詞をテーマに画像生成して、歌詞と画像をテーマにインタラクティブ性のあるjs htmlアニメーションコード作成。iphone用
👩 先生: ふん、分かってるわよ。まずは、最初のコードは歌詞に合わせたアニメーションを作るために、背景や星、冥王星の動きをモードごとに変化させたの。たとえば、ここを見なさい:
// 冥王星の描画(モード2:脈動エフェクト)
const plutoX = cw * 0.8;
const plutoY = ch * 0.7;
const basePlutoRadius = Math.min(cw, ch) * 0.05;
let plutoRadius = basePlutoRadius;
if (mode === 2) {
plutoRadius = basePlutoRadius * (1 + 0.1 * Math.sin(time * 0.005));
}
👦 生徒: なるほど、ただの冥王星が、モード2では脈動するんだね。俺、見た目だけ大事だから、そんな感じで十分だよ!
👩 先生: そう、いいわ。次に、モードごとにインタラクティブ性を追加するために、クリックイベントで違う効果を発生させたの。たとえば、モード1ではクリック位置から近い星が反発するようにしてるのよ。こんな感じ:
// Mode 1: クリック位置から近い星を反発させる
interactions.push({ type: 'repel', x: x, y: y, time: performance.now(), duration: 500 });
👦 生徒: クリックすると星がはじかれるの、かっこいいね!俺はただ、アニメーションが派手ならOKだから、これで大丈夫!
👩 先生: まぁ、そういう考え方もあるわね。さらに、他のモードも用意してあって、たとえばモード3では、クリック位置付近に追加の宇宙塵を発生させる効果を入れてるの。以下のコードを見なさい:
// Mode 3: クリック位置に追加の宇宙塵を発生
for (let i = 0; i < 10; i++) {
dustParticles.push({
x: x + (Math.random()-0.5)*20,
y: y + (Math.random()-0.5)*20,
radius: Math.random()*3 + 1,
speedX: (Math.random() - 0.5) * 1,
speedY: (Math.random() - 0.5) * 1,
alpha: 1
});
}
👦 生徒: すごい、クリックするたびにキラキラ舞い上がる感じ?最高だね!
👩 先生: でしょ?そして、モード4では背景が一瞬ランダムな色にフラッシュする効果、モード5ではクリック位置で星のバースト、そしてモード6ではクリック位置にスワールエフェクトが現れるように工夫したの。全体的に、各モードごとにインタラクティブな動きが変わるようにしてるから、君みたいに美しいアニメーションを愛する人にはたまらないはずよ!
👦 生徒: うん、ほんとにすげーよ。俺、コードの細かいことはよくわかんないけど、こういうのが動けば十分だね!
👩 先生: まったく、あなたはいつもそう。だけど、ちゃんとコードの中身も見ておきなさい。例えば、この部分は全体のアニメーションループを管理しているのよ:
function animate(time) {
// deltaTimeの計算
const deltaTime = time - lastTime;
lastTime = time;
// 画面のクリアと背景描画
ctx.clearRect(0, 0, cw, ch);
// ここで各エフェクトや歌詞のフェード処理などを実施
...
requestAnimationFrame(animate);
}
👦 生徒: ふーん、なるほど。俺はただ美しいアニメーションが動けばいいから、細かいことは気にしないよ。
👩 先生: もう、本当に頭が悪いわね。でも、あなたのような人でも分かるように、できるだけシンプルに解説してあげる。実は、今回のコードは利用者の入力と私の返答を元に段階的に機能を追加して作られたの。最初は基本の星空と冥王星のアニメーションから始めたのよ。そこに、歌詞のタイミングに合わせたフェード処理を加えて、さらに各モードごとにクリック時のインタラクティブ性を追加して、最終的にこの美しい仕上がりになったの。
👦 生徒: ふーん、なんか面倒そうだけど、結果はバッチリだね!俺、コードの中身はよく見ないけど、動くのが見えると嬉しいし。
👩 先生: そりゃそうだけど、あなたもいつかはコードの仕組みを理解しなさいよ。ま、今回はこれで許してあげる。最後に、このコード全体を確認して、iPhoneでも快適に動くようにviewportの設定も入れてあるの。これで君も、美しいアニメーションを存分に楽しめるはずよ。
👦 生徒: ありがとう、先生!やっぱり、あなたのコードはいつも最高だね。今すぐ実行してみるよ!
👩 先生: ふん、感謝なんかしなくていいわ。でも、ちゃんと結果を見せなさいよね。これで私の授業は終わりよ。次はもっと勉強してから来なさい!
FAQ
Q1: このコードはどのような機能がありますか?
A1: このコードは、歌詞の進行に合わせて背景、星、冥王星の動きが変化し、各モードごとにクリック時のインタラクティブな効果(星の反発、冥王星の脈動、宇宙塵の追加、背景フラッシュ、星のバースト、スワールエフェクト)を実現しています。
Q2: コードはどの環境で動作しますか?
A2: HTML5とJavaScriptを使用しているため、最新のブラウザ(特にiPhoneのSafariなどのモバイルブラウザ)で快適に動作します。viewport設定により、モバイルデバイスでも最適な表示がされるようになっています。
Q3: インタラクティブ性はどのように実現されていますか?
A3: クリックまたはタッチイベントにより、各モードで異なるインタラクション効果(例えば、星の反発や背景の色フラッシュなど)が発生する仕組みになっています。インタラクションは配列で管理され、時間経過に合わせて効果がフェードアウトします。
Q4: コードのカスタマイズは可能ですか?
A4: はい、各モードやエフェクトのパラメーター(例えば、星の速度、冥王星の脈動の強さ、エフェクトの持続時間など)は自由に変更可能です。コード内のコメントを参考に、目的に合わせて調整してください。
Q5: 初心者でもこのコードを理解できますか?
A5: 先生の解説を参考にすれば、全くの初心者でもコードの基本的な構造を理解することができます。また、動作する美しいアニメーションを実際に見ることで、学習意欲も高まるでしょう。
アプリはこちらをクリック!https://invisiblelagoon.com/lp/plu/
<!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>冥王の目覚め - Pluto Discovery</title>
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
background: #000;
color: #fff;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
canvas {
display: block;
}
#startButton {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 15px 30px;
font-size: 18px;
background: rgba(255,255,255,0.8);
border: none;
border-radius: 8px;
cursor: pointer;
z-index: 10;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<button id="startButton">Start Journey</button>
<script>
// キャンバス設定
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let cw = canvas.width = window.innerWidth;
let ch = canvas.height = window.innerHeight;
window.addEventListener('resize', () => {
cw = canvas.width = window.innerWidth;
ch = canvas.height = window.innerHeight;
});
// 星の生成
const starCount = 150;
const stars = [];
for (let i = 0; i < starCount; i++) {
stars.push({
x: Math.random() * cw,
y: Math.random() * ch,
radius: Math.random() * 1.5 + 0.5,
speed: Math.random() * 0.3 + 0.1,
phase: Math.random() * Math.PI * 2
});
}
// 宇宙塵(モード3用)の生成
const dustCount = 50;
const dustParticles = [];
for (let i = 0; i < dustCount; i++) {
dustParticles.push({
x: Math.random() * cw,
y: Math.random() * ch,
radius: Math.random() * 3 + 1,
speedX: (Math.random() - 0.5) * 0.5,
speedY: (Math.random() - 0.5) * 0.5,
alpha: Math.random()
});
}
// 歌詞配列(各行)
const lyrics = [
"闇夜に漂う 星々の海",
"誰も知らぬ物語 秘めたる謎",
"遠い彗星の軌跡 追い求めた夢",
"新たな時代の 幕が今、上がる",
"冥王よ、冥王よ その輝きを見よ",
"遥かなる宇宙に 未来が映る",
"冥王よ、冥王よ 謎めく扉を開け",
"無限の冒険が 君を包む",
"時の彼方に眠る 伝説の記憶",
"科学と運命が 交わる瞬間",
"冷たい星の息吹 感じる鼓動",
"未知なる発見 心を震わせる",
"小さな点が描く 大いなる奇跡",
"新たな歴史を 刻む一筆",
"永遠の煌めき 瞬間の証",
"未来へ紡ぐ 希望のメロディ",
"冥王よ、冥王よ その輝きを見よ",
"遥かなる宇宙に 夢が舞い降りる",
"冥王よ、冥王よ 謎めく扉を開け",
"無限の可能性が 君を呼んでる",
"遠い星の彼方 鼓動が響く",
"今、新たな時代 始まる"
];
let currentLyricIndex = 0;
let lyricTimer = 0;
const fadeInDuration = 2000; // 2秒
const fullDuration = 3000; // 3秒
const fadeOutDuration= 2000; // 2秒
const totalDuration = fadeInDuration + fullDuration + fadeOutDuration;
let lastTime = performance.now();
let animationStarted = false;
// インタラクション用グローバル変数
let interactions = [];
// 現在のモード判定(歌詞の進行に応じて)
function getMode() {
if (currentLyricIndex < 4) return 1;
else if (currentLyricIndex < 8) return 2;
else if (currentLyricIndex < 12) return 3;
else if (currentLyricIndex < 16) return 4;
else if (currentLyricIndex < 20) return 5;
else return 6;
}
// ランダムな色を生成する関数(背景フラッシュ用)
function getRandomColor() {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return `rgb(${r},${g},${b})`;
}
// クリック/タッチイベントリスナー(モード別のインタラクション)
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const mode = getMode();
switch (mode) {
case 1:
// Mode 1: クリック位置から近い星を反発させる
interactions.push({ type: 'repel', x: x, y: y, time: performance.now(), duration: 500 });
break;
case 2:
// Mode 2: 冥王星の脈動をブースト
interactions.push({ type: 'plutoBoost', time: performance.now(), duration: 1000 });
break;
case 3:
// Mode 3: クリック位置に追加の宇宙塵を発生
for (let i = 0; i < 10; i++) {
dustParticles.push({
x: x + (Math.random()-0.5)*20,
y: y + (Math.random()-0.5)*20,
radius: Math.random()*3 + 1,
speedX: (Math.random() - 0.5) * 1,
speedY: (Math.random() - 0.5) * 1,
alpha: 1
});
}
break;
case 4:
// Mode 4: 背景を一時的にフラッシュ
interactions.push({ type: 'bgFlash', time: performance.now(), duration: 500, color1: getRandomColor(), color2: getRandomColor() });
break;
case 5:
// Mode 5: クリック位置で星のバーストエフェクト
interactions.push({ type: 'starBurst', x: x, y: y, time: performance.now(), duration: 800 });
break;
case 6:
// Mode 6: クリック位置にスワール(渦巻き)エフェクト
interactions.push({ type: 'swirl', x: x, y: y, time: performance.now(), duration: 1000 });
break;
}
});
function animate(time) {
const deltaTime = time - lastTime;
lastTime = time;
ctx.clearRect(0, 0, cw, ch);
// 現在のモード
const mode = getMode();
// 背景グラデーション設定
let bgGradient = ctx.createLinearGradient(0, 0, 0, ch);
if (mode === 4) {
bgGradient.addColorStop(0, "#330033");
bgGradient.addColorStop(1, "#000022");
} else {
bgGradient.addColorStop(0, "#000010");
bgGradient.addColorStop(1, "#000");
}
// 背景フラッシュ(モード4のインタラクション)
const bgFlash = interactions.find(inter => inter.type === 'bgFlash');
if (mode === 4 && bgFlash) {
const elapsedFlash = time - bgFlash.time;
const flashAlpha = 1 - (elapsedFlash / bgFlash.duration);
const flashGradient = ctx.createLinearGradient(0, 0, 0, ch);
flashGradient.addColorStop(0, bgFlash.color1);
flashGradient.addColorStop(1, bgFlash.color2);
ctx.fillStyle = flashGradient;
ctx.globalAlpha = flashAlpha;
ctx.fillRect(0, 0, cw, ch);
ctx.globalAlpha = 1;
} else {
ctx.fillStyle = bgGradient;
ctx.fillRect(0, 0, cw, ch);
}
// 星の描画
// ※モード1はゆっくり、モード5は速めに更新
let starSpeedFactor = 1;
if (mode === 1) starSpeedFactor = 0.5;
if (mode === 5) starSpeedFactor = 2;
stars.forEach(star => {
// モード1:repelインタラクションによる反発効果
interactions.filter(inter => inter.type === 'repel').forEach(rep => {
const dx = star.x - rep.x;
const dy = star.y - rep.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 100) {
const force = (100 - dist) / 100;
star.x += (dx/dist) * force * (deltaTime * 0.05);
star.y += (dy/dist) * force * (deltaTime * 0.05);
}
});
// 星の輝き(瞬き)
const twinkle = 0.5 + 0.5 * Math.sin((time * 0.002 * star.speed) + star.phase);
ctx.beginPath();
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${twinkle})`;
ctx.fill();
// 星の位置更新
star.y += star.speed * starSpeedFactor;
if (star.y > ch) {
star.y = 0;
star.x = Math.random() * cw;
}
});
// モード3:宇宙塵の描画・更新
if (mode === 3) {
dustParticles.forEach(particle => {
particle.x += particle.speedX;
particle.y += particle.speedY;
if (particle.x < 0) particle.x = cw;
if (particle.x > cw) particle.x = 0;
if (particle.y < 0) particle.y = ch;
if (particle.y > ch) particle.y = 0;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI*2);
ctx.fillStyle = `rgba(255,255,255,${particle.alpha * 0.5})`;
ctx.fill();
});
}
// 冥王星の描画(モード2:クリックで脈動ブースト)
const plutoX = cw * 0.8;
const plutoY = ch * 0.7;
const basePlutoRadius = Math.min(cw, ch) * 0.05;
let boostFactor = 0;
interactions.filter(inter => inter.type === 'plutoBoost').forEach(boost => {
const elapsedBoost = time - boost.time;
if (elapsedBoost < boost.duration) {
boostFactor += (1 - elapsedBoost / boost.duration) * 0.3;
}
});
let plutoRadius = basePlutoRadius;
if (mode === 2) {
plutoRadius = basePlutoRadius * (1 + 0.1 * Math.sin(time * 0.005) + boostFactor);
}
const plutoGradient = ctx.createRadialGradient(plutoX, plutoY, plutoRadius * 0.3, plutoX, plutoY, plutoRadius);
plutoGradient.addColorStop(0, "#a0d8ff");
plutoGradient.addColorStop(1, "#002244");
ctx.beginPath();
ctx.arc(plutoX, plutoY, plutoRadius, 0, Math.PI * 2);
ctx.fillStyle = plutoGradient;
ctx.fill();
// モード6:中央に渦巻くエフェクト(基本エフェクト)
if (mode === 6) {
const vortexRadius = Math.min(cw, ch) * 0.15;
const numSpirals = 5;
for (let i = 0; i < numSpirals; i++) {
const angle = time * 0.001 + (i * 2 * Math.PI / numSpirals);
ctx.beginPath();
ctx.arc(cw/2, ch/2, vortexRadius * Math.abs(Math.sin(angle * 3)), 0, Math.PI * 2);
ctx.strokeStyle = `rgba(200,200,255,0.3)`;
ctx.lineWidth = 2;
ctx.stroke();
}
}
// インタラクション効果:モード5 星バースト
interactions.filter(inter => inter.type === 'starBurst').forEach(burst => {
const elapsedBurst = time - burst.time;
const progress = elapsedBurst / burst.duration;
if (progress < 1) {
const burstAlpha = 1 - progress;
const burstRadius = 50 * progress;
ctx.beginPath();
ctx.arc(burst.x, burst.y, burstRadius, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255,255,100,${burstAlpha})`;
ctx.lineWidth = 3;
ctx.stroke();
}
});
// インタラクション効果:モード6 スワールエフェクト(クリック位置周辺)
interactions.filter(inter => inter.type === 'swirl').forEach(swirl => {
const elapsedSwirl = time - swirl.time;
const progress = elapsedSwirl / swirl.duration;
if (progress < 1) {
const swirlAlpha = 1 - progress;
const swirlRadius = 80 * progress;
ctx.beginPath();
// 複数の円弧を描画して渦巻き感を演出
for (let j = 0; j < 3; j++) {
const startAngle = (time * 0.002) + j;
ctx.arc(swirl.x, swirl.y, swirlRadius + j*10, startAngle, startAngle + Math.PI/2);
}
ctx.strokeStyle = `rgba(150,150,255,${swirlAlpha})`;
ctx.lineWidth = 2;
ctx.stroke();
}
});
// 歌詞のフェードイン・表示・フェードアウト
lyricTimer += deltaTime;
if (lyricTimer >= totalDuration) {
lyricTimer -= totalDuration;
currentLyricIndex = (currentLyricIndex + 1) % lyrics.length;
}
let lyricAlpha = 1;
if (lyricTimer < fadeInDuration) {
lyricAlpha = lyricTimer / fadeInDuration;
} else if (lyricTimer < fadeInDuration + fullDuration) {
lyricAlpha = 1;
} else {
lyricAlpha = (totalDuration - lyricTimer) / fadeOutDuration;
}
ctx.globalAlpha = lyricAlpha;
ctx.font = `${Math.floor(Math.min(cw, ch) * 0.04)}px sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#ffffff";
ctx.fillText(lyrics[currentLyricIndex], cw / 2, ch * 0.2);
ctx.globalAlpha = 1;
// 期限切れのインタラクションは削除
interactions = interactions.filter(inter => (time - inter.time) < inter.duration);
if (animationStarted) {
requestAnimationFrame(animate);
}
}
document.getElementById('startButton').addEventListener('click', () => {
animationStarted = true;
lastTime = performance.now();
document.getElementById('startButton').style.display = 'none';
requestAnimationFrame(animate);
});
</script>
</body>
</html>
コメント