GPT先生と学ぶ!p5.jsで詩歌アニメーションを作ろう
この記事では、世界詩歌記念日をテーマに、p5.js を使ったインタラクティブな詩歌アニメーションを作成する過程を、AI先生と生徒の対話形式で紹介します!
💬 GPT先生との対話
🧑「GPT先生、詩歌のアニメーションを作りたいんだけど、コード全部書いて!」
🤖「ったく、しょうがないわね!アンタったら自分で学ぼうって気はないの?でもまあ、初心者のアンタに説明するのも楽しいから、特別に教えてあげるわ!」
「まずは p5.js を使って、画面に詩歌を表示するシンプルなコードを書いてみるわよ!」
// p5.js を使ったシンプルな背景
function setup() {
createCanvas(windowWidth, windowHeight);
background(20, 20, 40);
textAlign(CENTER, CENTER);
fill(255);
textSize(32);
text("世界詩歌記念日", width / 2, height / 2);
}
🧑「おお、背景ができた!でも、詩歌を選んでタップしたら表示されるようにしたい!」
🤖「あら、意外とやる気あるじゃない。じゃあ、タップで詩歌が表示されるようにするわよ!」
let poems = [
{ word: "詩歌", content: "古池や蛙飛び込む水の音 - 松尾芭蕉" },
{ word: "Poetry", content: "I wandered lonely as a cloud - Wordsworth" }
];
function mousePressed() {
let index = floor(random(poems.length));
background(20, 20, 40);
fill(255);
textSize(24);
text(poems[index].content, width / 2, height / 2);
}
🧑「やった!詩歌が変わるの楽しい!」
🤖「でしょ?次は、詩歌ごとにアニメーションを追加するわよ!」
function draw() {
background(20, 20, 40);
fill(255);
textSize(32);
text("タップして詩歌を見る", width / 2, height / 2);
// 簡単なアニメーション
fill(255, 215, 0);
ellipse(width / 2, height / 2 + 50, 50 + sin(frameCount * 0.1) * 20);
}
🧑「うおお!水仙が揺れてる!天才じゃん!」
🤖「ふふん、もっと褒めてもいいのよ?」
🧑「やった!完璧!GPT先生ありがとう!」
FAQ(よくある質問)
p5.jsって何?
p5.js は JavaScript ベースのクリエイティブコーディングライブラリで、アニメーションやインタラクティブなビジュアルを簡単に作成できます。
スマホで動く?
はい、p5.js はスマホ対応ですが、ブラウザの設定やOSによっては動作しない場合もあるので、最新のブラウザを使用してください。
このコードを自分でカスタマイズできる?
もちろん!詩歌データを増やしたり、アニメーションを変えたりして、自分だけの作品を作ってみてください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>世界詩歌記念日 - インタラクティブ詩歌表示+アニメーション(スクロール無し)</title>
<!-- p5.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/p5.js"></script>
<style>
/* オーバーレイ全体(詩歌表示画面) */
#overlay {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
color: #fff;
display: none;
padding: 20px;
box-sizing: border-box;
overflow-y: hidden; /* スクロール機能を無効 */
z-index: 9999;
cursor: pointer; /* タップで戻る */
}
/* preタグ内は自動改行 */
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
/* オーバーレイ内の各要素がタップで戻らないように */
.overlay-content * {
pointer-events: auto;
}
</style>
</head>
<body>
<div id="overlay">
<div id="overlay-content" class="overlay-content"></div>
</div>
<script>
/* ----------------------------
GLOBAL VARIABLES & DATA
---------------------------- */
let words = ["Poetry", "詩歌", "Poesía", "Poésie", "شعر", "Poesia", "Стихи", "Vers", "말", "Poema"];
let angleOffset = 0;
let centerX, centerY;
let sparkles = []; // タップ時エフェクト用
let selectedPoem = null; // 表示中の詩歌データ
let animationInstance = null; // オーバーレイ内アニメーション用 p5 インスタンス
// 各詩歌データ(出典URLは省略)
let poems = [
{
word: "Poetry",
language: "English",
year: "1804",
author: "William Wordsworth",
original: `I wandered lonely as a cloud
That floats on high o'er vales and hills,
When all at once I saw a crowd,
A host, of golden daffodils;
Beside the lake, beneath the trees,
Fluttering and dancing in the breeze.`,
translation: `私は雲のようにひとりさまよっていた
丘や谷の上に浮かびながら、
ふと目の前に群れが現れた、
金色に輝く水仙の大群が。
湖のほとり、木々の下で、
そよ風に揺れ、踊っていた。`
},
{
word: "詩歌",
language: "日本語",
year: "1686",
author: "松尾芭蕉",
original: "古池や蛙飛び込む水の音",
translation: "古池に蛙が飛び込み、水の音が響く"
},
{
word: "Poesía",
language: "Español",
year: "1860",
author: "Gustavo Adolfo Bécquer",
original: `¿Qué es poesía? dices mientras clavas
en mi pupila tu pupila azul.
¿Qué es poesía? ¿Y tú me lo preguntas?
Poesía... eres tú.`,
translation: `詩とは何か?と、あなたは尋ねながら、
あなたの青い瞳を私の瞳に突き刺す。
詩とは何か?と、あなたはまた問う。
詩とは……あなたそのものだ。`
},
{
word: "Poésie",
language: "Français",
year: "1870",
author: "Arthur Rimbaud",
original: `C'est un trou de verdure où chante une rivière,
Accrochant follement aux herbes des haillons
d'argent; où le soleil, de la montagne fière,
Luit: c'est un petit val qui mousse de rayons.`,
translation: `それは、緑に満ちた穴、川が歌い、
狂おしく草に銀のぼろ布をまとい、
山の誇り高い太陽が輝くところ、
それは光のしずくが泡立つ小さな谷である。`
},
{
word: "شعر",
language: "العربية",
year: "6世紀",
author: "Imru' al-Qais",
original: "قِفا نَبكِ مِن ذِكرى حبيبٍ ومَنزِلِ",
translation: `愛しき人と家の思い出に、
立ち止まり涙を流そう。`
},
{
word: "Poesia",
language: "Italiano",
year: "1819",
author: "Giacomo Leopardi",
original: `Sempre caro mi fu quest'ermo colle,
e questa siepe, che da tanta parte
dell'ultimo orizzonte il guardo esclude.
Ma sedendo e mirando, interminati
spazi di là da quella, e sovrumani
silenzi, e profondissima quiete
io nel pensier mi fingo; ove per poco
il cor non si spaura.`,
translation: `いつも私に愛おしかったこの孤高の丘、
そして遥か彼方を見せぬこの生垣。
しかし、座り込んで眺めると、
その向こうに果てしない空間、超越した静寂、
そして深淵なる静けさを、
心がひととき怯むかのように、
私は思い描く。`
},
{
word: "Стихи",
language: "Русский",
year: "1829",
author: "Александр Пушкин",
original: `Я вас любил: любовь ещё, быть может,
В душе моей угасла не совсем;
Но пусть она вас больше не тревожит;
Я не хочу печалить вас ничем.`,
translation: `私はあなたを愛しました。 もしかすると、
私の心の中の愛はまだ完全には消えていないかもしれません。
しかし、どうかもうあなたを悩ませず、
私はあなたをどんなことでも悲しませたくありません。`
},
{
word: "Vers",
language: "Latin",
year: "23 BC",
author: "Horace",
original: "Carpe diem, quam minimum credula postero.",
translation: "その日を摘め、明日をあまり信じるな。"
},
{
word: "말",
language: "한국어",
year: "1922",
author: "김소월",
original: `나 보기가 역겨워
가실 때에는
말없이 고이 보내드리우리다`,
translation: `あなたが私を見るのが憎らしいならば、
あなたが行かれる時は、
言葉もなく静かにお送りしましょう。`
},
{
word: "Poema",
language: "Português",
year: "1930",
author: "Fernando Pessoa",
original: `O poeta é um fingidor.
Finge tão completamente
Que chega a fingir que é dor
A dor que deveras sente.`,
translation: `詩人は偽り手である。
彼はあまりにも巧みに偽り、
実際に感じる痛みを、
痛みであるかのように偽る。`
}
];
/* ----------------------------
MAIN p5.js SKETCH (シンプルな背景)
---------------------------- */
function setup() {
createCanvas(windowWidth, windowHeight);
centerX = width / 2;
centerY = height / 2;
textAlign(CENTER, CENTER);
textSize(min(width, height) * 0.05);
textFont('Georgia');
}
function draw() {
// シンプルな背景色
background(20, 20, 40);
angleOffset += 0.01;
let swirlRadius = min(width, height) * 0.3;
fill(255);
let wordPositions = [];
for (let i = 0; i < words.length; i++) {
let angle = map(i, 0, words.length, 0, TWO_PI) + angleOffset;
let x = centerX + swirlRadius * cos(angle + angleOffset * 0.5);
let y = centerY + swirlRadius * sin(angle + angleOffset * 0.5);
push();
translate(x, y);
rotate(angle + PI / 2);
text(words[i], 0, 0);
pop();
wordPositions.push({ x: x, y: y, index: i });
}
for (let i = sparkles.length - 1; i >= 0; i--) {
sparkles[i].update();
sparkles[i].show();
if (sparkles[i].alpha <= 0) {
sparkles.splice(i, 1);
}
}
push();
textSize(min(width, height) * 0.03);
fill(200);
text("各言語の文字をタップすると詩歌が表示されます", centerX, height - 40);
pop();
window.currentWordPositions = wordPositions;
}
/* ----------------------------
SPARK CLASS (タップ時エフェクト)
---------------------------- */
class Spark {
constructor(x, y) {
this.x = x;
this.y = y;
this.r = random(10, 30);
this.growth = random(0.5, 1.5);
this.alpha = 255;
this.fade = random(3, 5);
}
update() {
this.r += this.growth;
this.alpha -= this.fade;
if (this.alpha < 0) this.alpha = 0;
}
show() {
noStroke();
fill(255, 200, 100, this.alpha);
ellipse(this.x, this.y, this.r);
}
}
/* ----------------------------
タップ/クリック判定
---------------------------- */
function mousePressed() {
handleTap(mouseX, mouseY);
}
function touchStarted(e) {
if (e) e.preventDefault();
if (touches.length > 0) {
handleTap(touches[0].x, touches[0].y);
}
return false;
}
function handleTap(x, y) {
// オーバーレイ表示中ならタップで戻る
if (document.getElementById("overlay").style.display === "block") {
hideOverlay();
return;
}
for (let pos of window.currentWordPositions) {
let d = dist(x, y, pos.x, pos.y);
if (d < 50) {
selectedPoem = poems[pos.index];
showOverlay(selectedPoem);
for (let i = 0; i < 5; i++) {
sparkles.push(new Spark(x, y));
}
break;
}
}
}
/* ----------------------------
オーバーレイ表示処理+アニメーション生成
---------------------------- */
function showOverlay(poemData) {
let overlay = document.getElementById("overlay");
let content = document.getElementById("overlay-content");
// 表示順序:上部にアニメーション、次に見出し(詩歌名/言語、年代・作者)、原文、和訳
content.innerHTML = `
<div id="animation-container" style="margin-bottom:20px;"></div>
<h2>${poemData.word} - ${poemData.language}</h2>
<p><strong>${poemData.year} - ${poemData.author}</strong></p>
<h3>原文</h3>
<pre>${poemData.original}</pre>
<h3>和訳</h3>
<pre>${poemData.translation}</pre>
<p style="font-size:0.9em; margin-top:20px;">タップして戻ります</p>
`;
overlay.style.display = "block";
// オーバーレイ全体がタップされたら戻る
overlay.onclick = function(e) {
// クリック位置がoverlay-content外なら戻る
if(e.target.id !== "overlay-content" && e.target.parentNode.id !== "overlay-content") {
hideOverlay();
}
};
// 自動スクロール処理は削除し、手動スクロールが行われるように(CSS overflow-y: hidden でスクロールバー非表示)
// 既存のアニメーションがあれば削除
if (animationInstance) {
animationInstance.remove();
animationInstance = null;
}
// 各言語に合わせたアニメーションを選択
let sketchFunc;
switch(poemData.word) {
case "Poetry": sketchFunc = englishAnimation; break;
case "詩歌": sketchFunc = japaneseAnimation; break;
case "Poesía": sketchFunc = spanishAnimation; break;
case "Poésie": sketchFunc = frenchAnimation; break;
case "شعر": sketchFunc = arabicAnimation; break;
case "Poesia": sketchFunc = italianAnimation; break;
case "Стихи": sketchFunc = russianAnimation; break;
case "Vers": sketchFunc = latinAnimation; break;
case "말": sketchFunc = koreanAnimation; break;
case "Poema": sketchFunc = portugueseAnimation; break;
default: sketchFunc = defaultAnimation; break;
}
animationInstance = new p5(sketchFunc, "animation-container");
}
function hideOverlay() {
let overlay = document.getElementById("overlay");
overlay.style.display = "none";
selectedPoem = null;
if (animationInstance) {
animationInstance.remove();
animationInstance = null;
}
overlay.onclick = null;
}
/* ----------------------------
ウィンドウリサイズ対応
---------------------------- */
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
centerX = width / 2;
centerY = height / 2;
textSize(min(width, height) * 0.05);
}
/* ============================
各言語ごとのアニメーション
※キャンバス幅は画面サイズに合わせ、最大800pxに制限し中央配置
============================ */
// English: 青空の雲と水仙
function englishAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.cloudX = 0;
let daffodils = [];
for (let i = 0; i < 10; i++) {
daffodils.push({
x: p.random(p.width),
y: p.random(p.height - 50, p.height),
phase: p.random(p.TWO_PI)
});
}
p.daffodils = daffodils;
};
p.draw = function() {
p.background(135, 206, 235);
p.noStroke();
p.fill(255);
p.ellipse(p.cloudX, p.height/3, 80, 50);
p.cloudX += 0.5;
if (p.cloudX > p.width + 40) p.cloudX = -40;
p.fill(255, 215, 0);
for (let d of p.daffodils) {
let offset = p.sin(p.frameCount * 0.05 + d.phase) * 5;
p.ellipse(d.x, d.y + offset, 20, 20);
}
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
// Japanese: 古池と波紋(タッチで波紋生成)
function japaneseAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.ripples = [];
};
p.draw = function() {
p.background(30, 144, 255);
p.fill(0, 191, 255);
p.noStroke();
p.ellipse(p.width/2, p.height, p.width, p.height * 1.5);
for (let i = p.ripples.length - 1; i >= 0; i--) {
let r = p.ripples[i].r;
p.noFill();
p.stroke(255, 255, 255, 200 - r*2);
p.ellipse(p.ripples[i].x, p.ripples[i].y, r, r);
p.ripples[i].r += 2;
if (p.ripples[i].r > 100) {
p.ripples.splice(i, 1);
}
}
};
p.mousePressed = function() {
p.ripples.push({x: p.mouseX, y: p.mouseY, r: 0});
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
// Spanish: パステルピンク背景に脈動するハート
function spanishAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.angleMode(p.RADIANS);
};
p.draw = function() {
p.background(255, 182, 193);
p.translate(p.width/2, p.height/2);
let scaleFactor = 1 + 0.3 * p.sin(p.frameCount * 0.05);
p.scale(scaleFactor);
p.fill(255, 105, 180);
p.noStroke();
p.beginShape();
for (let angle = 0; angle < p.TWO_PI; angle += 0.01) {
let x = 16 * Math.pow(Math.sin(angle), 3);
let y = 13 * Math.cos(angle) - 5 * Math.cos(2 * angle) - 2 * Math.cos(3 * angle) - Math.cos(4 * angle);
p.vertex(x, -y);
}
p.endShape(p.CLOSE);
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
// French: 緑の波がゆったり流れる
function frenchAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.yOffset = 0;
};
p.draw = function() {
p.background(144, 238, 144);
p.noFill();
p.stroke(34,139,34);
p.strokeWeight(2);
p.beginShape();
for (let x = 0; x <= p.width; x += 10) {
let y = p.height/2 + p.noise(x * 0.01, p.yOffset) * 50;
p.vertex(x, y);
}
p.endShape();
p.yOffset += 0.01;
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
// Arabic: 砂漠の夕焼けと動く砂丘
function arabicAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.xOffset = 0;
};
p.draw = function() {
for (let y = 0; y < p.height; y++) {
let inter = p.map(y, 0, p.height, 0, 1);
let c = p.lerpColor(p.color(255,140,0), p.color(75,0,130), inter);
p.stroke(c);
p.line(0, y, p.width, y);
}
p.noStroke();
p.fill(0, 0, 0, 150);
p.beginShape();
for (let x = 0; x <= p.width; x += 10) {
let y = p.height - p.noise(x * 0.005, p.xOffset) * 50;
p.vertex(x, y);
}
p.vertex(p.width, p.height);
p.vertex(0, p.height);
p.endShape(p.CLOSE);
p.xOffset += 0.005;
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
// Italian: 夜空に輝く星々が瞬く
function italianAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.stars = [];
for (let i = 0; i < 50; i++) {
p.stars.push({
x: p.random(p.width),
y: p.random(p.height),
size: p.random(1, 3),
phase: p.random(p.TWO_PI)
});
}
};
p.draw = function() {
p.background(10, 10, 40);
for (let star of p.stars) {
let brightness = 200 + 55 * p.sin(p.frameCount * 0.05 + star.phase);
p.noStroke();
p.fill(brightness);
p.ellipse(star.x, star.y, star.size, star.size);
}
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
// Russian: 落ちる雪
function russianAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.flakes = [];
for (let i = 0; i < 100; i++) {
p.flakes.push({
x: p.random(p.width),
y: p.random(-50, p.height),
size: p.random(2, 5),
speed: p.random(0.5, 2)
});
}
};
p.draw = function() {
p.background(50);
p.fill(255);
p.noStroke();
for (let flake of p.flakes) {
p.ellipse(flake.x, flake.y, flake.size);
flake.y += flake.speed;
if (flake.y > p.height) {
flake.y = -10;
flake.x = p.random(p.width);
}
}
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
// Latin: 昇る太陽(朝焼け)
function latinAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.sunY = p.height;
};
p.draw = function() {
for (let y = 0; y < p.height; y++) {
let inter = p.map(y, 0, p.height, 0, 1);
let c = p.lerpColor(p.color(0,0,102), p.color(255,165,0), inter);
p.stroke(c);
p.line(0, y, p.width, y);
}
p.noStroke();
p.fill(255, 204, 0);
p.ellipse(p.width/2, p.sunY, 80, 80);
p.sunY -= 0.3;
if (p.sunY < p.height/2) {
p.sunY = p.height;
}
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
// Korean: 秋の葉が舞う
function koreanAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.leaves = [];
for (let i = 0; i < 20; i++) {
p.leaves.push({
x: p.random(p.width),
y: p.random(-50, 0),
size: p.random(15, 30),
speed: p.random(1, 2),
angle: p.random(p.TWO_PI)
});
}
};
p.draw = function() {
p.background(245, 222, 179);
p.noStroke();
p.fill(210, 105, 30, 200);
for (let leaf of p.leaves) {
p.push();
p.translate(leaf.x, leaf.y);
p.rotate(leaf.angle);
p.ellipse(0, 0, leaf.size, leaf.size/2);
p.pop();
leaf.y += leaf.speed;
leaf.angle += 0.01;
if (leaf.y > p.height) {
leaf.y = -20;
leaf.x = p.random(p.width);
}
}
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
// Portuguese: 抽象的なシェイプが現れる
function portugueseAnimation(p) {
p.setup = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.canvas = p.createCanvas(canvasWidth, 200);
p.canvas.style.display = "block";
p.canvas.style.margin = "0 auto";
p.noStroke();
};
p.draw = function() {
p.background(30);
for (let i = 0; i < 10; i++) {
let x = p.random(p.width);
let y = p.random(p.height);
let size = p.random(20, 50);
let r = p.random(50, 255);
let g = p.random(50, 255);
let b = p.random(50, 255);
p.fill(r, g, b, 150);
p.ellipse(x, y, size, size);
}
};
p.windowResized = function() {
let canvasWidth = p.min(p.windowWidth, 800);
p.resizeCanvas(canvasWidth, 200);
};
}
</script>
</body>
</html>
コメント