実験#91:キーボードで一筆お絵描き体験(一筆書き小花ゲーム)

■91個目 「一筆書き小花ゲーム」

こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/

今回は、のんびり癒やされる「一筆書きの植物ゲーム」を作ってみました。

主役になるのは……なんと “あなたが打ち込むキーボードの文字” です!

画面をスーーッと進む線に合わせてキーをポンッと押すと、
そこから「くるんっ」と可愛い小花や葉っぱが一筆書きで咲いていきます。

今回こだわったのは、
どんなに線が波打っていても絶対に途切れない、
一筆書きの気持ちよさ。(=v=*)

「いろんな質感で描けたら、もっと楽しいかも?」と思って、
ペンの種類もたくさん用意しました。

ペンは全部で5種類。

  • ① スタンダードな 「普通のひも」
  • ② ツヤツヤで甘い 「パステルリボン」
  • ③ ちょっぴりハードな 「有刺鉄線」
  • ④ ゴツゴツ重たい 「鎖(チェーン)」
  • ⑤ 和紙みたいにフワフワなお花が咲く 「植物の蔓(つる)」

すりガラスのように柔らかくぼけた背景の上で、
ペンを切り替えながら植物を描いていきます。

そして描き終わったお花や葉っぱたちは……
画面の下に「押し花アルバム」としてちょこんとストックされていくんです。

自分だけの植物標本ができていくのを、
のんびり眺めて癒やされてくださいね。


■ AIとのやりとりで面白かった部分

● ゲーム仕様の相談からスタート

みー:(^v^) 今日もゲームを作ります。

htmlで動かせる線引きゲーム。

常に直線が引かれていて、
キーボードのボタンを押すと「くるん」と小花のような一筆書きが描かれ、
また直線に戻ります。

すべてのキーで違う小花を描けるようにしたいです。

AI: 了解。常に進む直線から、
キー入力に合わせて「くるん」と小花が咲くゲームを作成しました。

押したキー(文字コード)を計算式に当てはめて、
花びらの数・大きさ・線の色が変化する仕組みです。

みー:(*^_^)”いいね。画面が切り替わるたびに、
直線が波打ったり、ジェットコースターみたいに回転したり、
軌道を変化させてみましょう。

AI: おっ、遊び心があっていいね。

画面の右端に到達するたびに、
軌道が波線・宙返りなどに切り替わるように改良しました。

どんな軌道でも、小花は一筆書きでズレずに元の線へ戻るよう調整済みです。

みー:(^o^) 線が波打つと、小花が歪むね。
しっかり描いてから線が進むようにできますか。

AI: なるほど。 小花を描いている間も軌道が進んでいたため、
形が歪んでいました。 そこで、

  • 小花モード中は軌道の移動を完全停止
  • キーを押した瞬間の位置を「花の中心点」として固定
  • 描き終わったら軌道を再開

という処理に修正しました。

みー:(^~^*) テンキーのところは葉っぱの一筆書きにできるかな。

AI: お、粋なアイデアだね。

テンキー(0〜9)を押したときだけ「葉っぱモード」に切り替わり、
鮮やかなグリーンの葉っぱがシュッと描かれるようにしました。
数字によってサイズや色味も変わります。

みー:( ̄~ ̄*) せっかく描いたのがすぐ消えるのは寂しいね。
縮小版でいいので、描いた絵をストックできる?

AI: 任せな。

描いた花や葉っぱを「植物標本」みたいに記録して、
軌道が4パターンを1周するまで画面下にストックする
「押し花アルバム」 を追加したよ。

みー:(^v^) ひもの種類を変えたい。
リボンみたいな幅広もできますか?
リボン感はリアルに。

AI: いいねぇ。リボンの立体感やねじれを表現するのは腕が鳴るよ。

中世ヨーロッパではリボンは金銀糸の超高級品で、
身分によって幅まで決められていたんだ。
あんたのリボンも、それくらい存在感のある仕上がりにしておいたぜ。

みー:(=v=) 次は有刺鉄線で。
有刺鉄線は一色のみ。
背景は明るいすりガラス調で。

AI: 了解。
有刺鉄線の無骨さと、明るいすりガラス背景の組み合わせで、
モダンアートみたいな雰囲気に仕上げたよ。

みー:(*^^) いいね。植物の蔓も追加しよう。
蔓のときの花は、和紙みたいにフワフワでカラフルに。

AI: 最高の組み合わせだ。
蔓の軌道にはランダムで小さな葉っぱが芽吹くようにして、
花を咲かせるときは繊維が絡むようなランダム描画+色のグラデーションで、

ちぎり絵のような和紙の質感 を再現したよ。

■ 実際の動作

仕上がりのスタート画面です。
左から右へ向かって、すーっと線が伸びていきます。

(画像:スタート画面)



キーボードを押すと、
キーの種類に応じて小花が一筆書きで「くるん」と咲きます。

描いた小花は画面下の 標本アルバム に並んでいきます。
右端まで到達すると、次の画面へ進みます。

(画像:ゲーム画面)


画面ごとに線の進み方が変化します。
波のように揺れたり、ジェットコースターのように回転したり、
見ているだけでも楽しい動きです。

(画像:軌道変化)



線の種類は全部で5つ。それぞれに個性があります。

● リボン

パステルカラーの柔らかい質感で、ふわっと可愛い雰囲気になります。

(画像:リボン)

● 有刺鉄線

無骨でハードな素材ですが、描かれる小花は意外と繊細で美しいです。

(画像:有刺鉄線)

● 鎖(チェーン)

重たくゴツゴツした質感。 線が進むだけで、
ちょっとメカニカルな雰囲気になります。

(画像:鎖)

● 植物の蔓(つる)

細い蔓が伸びていき、ところどころに小さな葉っぱが芽吹きます。
咲く花は、和紙のようにふんわりした質感で、とても優しい印象です。

(画像:植物の蔓)


■ コードのポイント

今回の記事では、
AI と相談しながら作った「一筆書き小花ゲーム」 の HTML コードを公開します。

このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。

ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。

▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く

Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)

② 下のコードをコピーして貼り付けます。
③ 「kobana.html」など、好きな名前で保存します。

(※拡張子は .html にしてください)

④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。

■ 操作方法

操作はとっても簡単です!(^_^)
キーボードとマウスを使って、
あなただけの植物標本を作ってみてくださいね。

【キーボードで花を咲かせる】

・文字キー(A〜Z など)
色とりどりの「小花」が咲きます 🌸
押すキーによって、花びらの数・色・大きさが変わるんですよ。

・テンキー(1〜5)
5種類の「植物の葉っぱ」が描かれます 🌿
数字ごとに形が変わって、ちょっとした植物図鑑みたいになります。

【ペンを持ち替える】

・画面をクリック ペンの種類が順番に切り替わります ✨
(画面上部のボタンを押してもOK!)

選べるペンは全部で5種類。
「普通のひも」→「パステルリボン」→「有刺鉄線」→「鎖」→「植物の蔓」

植物の蔓(つる)を選んでいるときに小花を咲かせると、
和紙のちぎり絵みたいなフワフワのお花 になります。(=v=*)

【自動で進むレール】

・線は「直線」「波打ち」「ジェットコースター」など、
4つのコースを自動で進みます。

・4つのコースを描き終えると、
画面下の 押し花アルバム がリセットされて新しいページになります。

どんなコースに、どんなお花を咲かせるか。
組み合わせは無限大です!(´▽`*)
ぜひ、お気に入りの植物ラインを作って遊んでみてくださいね。


“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>一筆書き小花&葉っぱゲーム - 植物の蔓と和紙</title>
<style>
body {
margin: 0;
overflow: hidden;
background:
radial-gradient(circle at 15% 20%, #ffe3d8 0%, transparent 40%),
radial-gradient(circle at 85% 80%, #d4ebf2 0%, transparent 40%),
radial-gradient(circle at 50% 50%, #fefefe 0%, #e0e5ec 100%);
color: #333;
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
user-select: none;
}
#info {
position: absolute;
top: 10px;
text-align: center;
pointer-events: none;
text-shadow: 0 1px 2px rgba(255,255,255,0.8);
z-index: 10;
}
h2 { margin-bottom: 5px; color: #2c3e50; }
canvas {
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
z-index: 5;
cursor: pointer;
}
kbd {
background: #ddd;
padding: 2px 6px;
border-radius: 4px;
font-weight: bold;
box-shadow: 0 1px 1px rgba(0,0,0,0.2);
}
.badge {
display: inline-block;
margin-top: 5px;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.9em;
font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
color: white;
background: #2c3e50;
}
#penBadge { background: #3498db; transition: background 0.3s; }
/* ペン切り替えボタンエリア */
#controls {
margin-top: 10px;
pointer-events: auto;
display: flex;
justify-content: center;
flex-wrap: wrap; /* ボタンが増えたので折り返し対応 */
gap: 10px;
max-width: 800px;
}
.pen-btn {
background: rgba(255, 255, 255, 0.6);
border: 2px solid rgba(255, 255, 255, 0.8);
padding: 6px 14px;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
color: #555;
transition: all 0.2s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
font-size: 0.9em;
}
.pen-btn:hover {
background: rgba(255, 255, 255, 0.9);
transform: translateY(-1px);
}
/* 選択中のボタン色 */
#btn-normal.active { background: #3498db; color: white; border-color: #3498db; }
#btn-ribbon.active { background: #e84393; color: white; border-color: #e84393; }
#btn-wire.active { background: #7f8c8d; color: white; border-color: #7f8c8d; }
#btn-chain.active { background: #95a5a6; color: white; border-color: #95a5a6; }
#btn-vine.active { background: #27ae60; color: white; border-color: #27ae60; } /* 蔓の色 */
</style>
</head>
<body>
<div id="info">
<h2>一筆書き小花&葉っぱゲーム 🌸🌿⛓️🔗🌱</h2>
<p>文字: <kbd>小花</kbd> / テンキー: <kbd>植物</kbd> / キャンバスを <kbd>クリック</kbd> でペン切替!</p>
<div>
<span id="modeBadge" class="badge">軌道: 直線</span>
<span id="penBadge" class="badge">ペン: 普通のひも</span>
</div>
<div id="controls">
<button id="btn-normal" class="pen-btn active">普通のひも</button>
<button id="btn-ribbon" class="pen-btn">リボン</button>
<button id="btn-wire" class="pen-btn">有刺鉄線</button>
<button id="btn-chain" class="pen-btn"></button>
<button id="btn-vine" class="pen-btn">植物の蔓</button>
</div>
</div>
<canvas id="gameCanvas" width="900" height="520"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const modeBadge = document.getElementById('modeBadge');
const penBadge = document.getElementById('penBadge');
const mainHeight = 400;
const baselineY = mainHeight / 2;
let currentX = 0;
let currentY = baselineY;
const speed = 2.5;
const drawSpeed = 0.08;
let logicalT = 0;
let patternIndex = 0;
let albumCount = 0;
const patterns = [
{ name: "直線", get: (t) => ({ x: t, y: baselineY }) },
{ name: "波打ち", get: (t) => ({ x: t, y: baselineY + 60 * Math.sin(t * 0.03) }) },
{ name: "ジェットコースター", get: (t) => ({
x: t - 40 * Math.sin(t * 0.06),
y: baselineY - 40 * Math.cos(t * 0.06) + 40
})},
{ name: "ジグザグ・バウンド", get: (t) => ({
x: t,
y: baselineY + 70 * Math.abs(Math.sin(t * 0.025)) - 35
})}
];
let isDrawingShape = false;
let shapeTheta = 0;
let shapeParams = null;
let basePosition = null;
// ペンの状態管理
const penModes = ['normal', 'ribbon', 'wire', 'chain', 'vine'];
let currentModeIndex = 0;
let penMode = penModes[currentModeIndex];
let currentHue = 0;
let needsJump = true;
// リボン用
let ribbonPrevNx = 0;
let ribbonPrevNy = 0;
// 鉄線・鎖・蔓の蓄積距離
let barbDist = 0;
const barbInterval = 25;
const wireColor = '#2c3e50';
let chainDist = 0;
let chainIndex = 0;
const chainInterval = 12;
let vineDist = 0;
const vineInterval = 20;
// --- UIとモードの連動 ---
const buttons = {
'normal': document.getElementById('btn-normal'),
'ribbon': document.getElementById('btn-ribbon'),
'wire': document.getElementById('btn-wire'),
'chain': document.getElementById('btn-chain'),
'vine': document.getElementById('btn-vine')
};
const badgeColors = { 'normal': '#3498db', 'ribbon': '#e84393', 'wire': '#7f8c8d', 'chain': '#95a5a6', 'vine': '#27ae60' };
const badgeTexts = { 'normal': '普通のひも', 'ribbon': 'パステルリボン', 'wire': '有刺鉄線', 'chain': '鎖(チェーン)', 'vine': '植物の蔓(つる)' };
function setPenMode(mode) {
penMode = mode;
currentModeIndex = penModes.indexOf(mode);
needsJump = true;
Object.values(buttons).forEach(btn => btn.classList.remove('active'));
buttons[mode].classList.add('active');
penBadge.innerText = `ペン: ${badgeTexts[mode]}`;
penBadge.style.background = badgeColors[mode];
}
Object.keys(buttons).forEach(mode => {
buttons[mode].addEventListener('click', () => setPenMode(mode));
});
canvas.addEventListener('mousedown', () => {
const nextMode = penModes[(currentModeIndex + 1) % penModes.length];
setPenMode(nextMode);
});
function drawDivider() {
ctx.save();
ctx.strokeStyle = 'rgba(0, 0, 0, 0.15)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, mainHeight);
ctx.lineTo(canvas.width, mainHeight);
ctx.stroke();
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
ctx.font = '14px sans-serif';
ctx.fillText('🌸 標本アルバム(4コース1周でリセット) 🌿', 10, mainHeight + 22);
ctx.restore();
}
function drawBarb(x, y, angle) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.strokeStyle = wireColor;
ctx.lineWidth = 1.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'miter';
ctx.beginPath(); ctx.moveTo(-3, -5); ctx.lineTo(3, 5); ctx.stroke();
ctx.beginPath(); ctx.moveTo(3, -5); ctx.lineTo(-3, 5); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-4, 0); ctx.lineTo(4, 0); ctx.stroke();
ctx.restore();
}
// 線の描画エンジン
function drawSegment(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
const dist = Math.hypot(dx, dy);
if (dist < 0.01) return;
if (penMode === 'normal') {
ctx.strokeStyle = `hsl(${currentHue}, 80%, 55%)`;
ctx.lineWidth = 3;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
needsJump = false;
} else if (penMode === 'ribbon') {
const angle = Math.atan2(dy, dx);
const twistPhase = isDrawingShape ? shapeTheta * 4 : logicalT * 0.03;
const thicknessFactor = Math.cos(twistPhase);
const width = Math.max(1.5, Math.abs(thicknessFactor) * 16);
const nx = Math.cos(angle - Math.PI / 2) * width / 2;
const ny = Math.sin(angle - Math.PI / 2) * width / 2;
if (needsJump) { ribbonPrevNx = nx; ribbonPrevNy = ny; needsJump = false; }
const tX1 = x1 + ribbonPrevNx, tY1 = y1 + ribbonPrevNy;
const bX1 = x1 - ribbonPrevNx, bY1 = y1 - ribbonPrevNy;
const tX2 = x2 + nx, tY2 = y2 + ny;
const bX2 = x2 - nx, bY2 = y2 - ny;
const isFront = thicknessFactor >= 0;
const grad = ctx.createLinearGradient(tX2, tY2, bX2, bY2);
if (isFront) {
grad.addColorStop(0, `hsl(${currentHue}, 90%, 75%)`);
grad.addColorStop(0.5, `hsl(${currentHue}, 90%, 90%)`);
grad.addColorStop(1, `hsl(${currentHue}, 90%, 70%)`);
} else {
grad.addColorStop(0, `hsl(${currentHue}, 60%, 65%)`);
grad.addColorStop(0.5, `hsl(${currentHue}, 60%, 75%)`);
grad.addColorStop(1, `hsl(${currentHue}, 60%, 60%)`);
}
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(tX1, tY1); ctx.lineTo(tX2, tY2); ctx.lineTo(bX2, bY2); ctx.lineTo(bX1, bY1);
ctx.closePath(); ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
ctx.lineWidth = 0.5; ctx.stroke();
ribbonPrevNx = nx; ribbonPrevNy = ny;
} else if (penMode === 'wire') {
const angle = Math.atan2(dy, dx);
ctx.strokeStyle = wireColor;
ctx.lineWidth = 2;
ctx.lineJoin = 'round'; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
barbDist += dist;
if (barbDist >= barbInterval) {
drawBarb(x2, y2, angle);
barbDist = barbDist % barbInterval;
}
needsJump = false;
} else if (penMode === 'chain') {
chainDist += dist;
while (chainDist >= chainInterval) {
const overshoot = chainDist - chainInterval;
const ratio = 1 - (overshoot / dist);
const linkX = x1 + dx * ratio;
const linkY = y1 + dy * ratio;
const angle = Math.atan2(dy, dx);
ctx.save();
ctx.translate(linkX, linkY);
ctx.rotate(angle);
ctx.lineWidth = 2;
ctx.strokeStyle = '#7f8c8d';
if (chainIndex % 2 === 0) {
ctx.beginPath();
ctx.ellipse(0, 0, 7, 3.5, 0, 0, Math.PI * 2);
ctx.stroke();
} else {
ctx.fillStyle = '#95a5a6';
ctx.beginPath();
ctx.ellipse(0, 0, 2.5, 5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
ctx.restore();
chainIndex++;
chainDist -= chainInterval;
}
needsJump = false;
} else if (penMode === 'vine') {
// --- 植物の蔓(つる)と和紙フラワーの描画処理 ---
const angle = Math.atan2(dy, dx);
if (isDrawingShape && !shapeParams.isLeaf) {
// 【和紙フラワーモード】
// 進行度(shapeTheta)に合わせて色相を回転させ、カラフルなグラデーションに
const washHue = (currentHue + shapeTheta * 60) % 360;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// 1. 和紙の毛羽立ち・繊維(ランダムに散らした細い線)
for (let i = 0; i < 4; i++) {
// 座標をわずかにズレさせることで、フワフワの繊維感を出す
const ox1 = x1 + (Math.random() - 0.5) * 12;
const oy1 = y1 + (Math.random() - 0.5) * 12;
const ox2 = x2 + (Math.random() - 0.5) * 12;
const oy2 = y2 + (Math.random() - 0.5) * 12;
ctx.beginPath();
ctx.moveTo(ox1, oy1);
ctx.lineTo(ox2, oy2);
// 半透明にして重ねる
ctx.strokeStyle = `hsla(${washHue}, 90%, 75%, 0.4)`;
ctx.lineWidth = Math.random() * 2 + 1;
ctx.stroke();
}
// 2. 和紙の芯となる柔らかい太線
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = `hsla(${washHue}, 80%, 85%, 0.8)`;
ctx.lineWidth = 3;
ctx.stroke();
} else {
// 【通常の蔓・葉っぱモード】
// 葉っぱを描く時は葉っぱの色、軌道は蔓の緑色
ctx.strokeStyle = (isDrawingShape && shapeParams.isLeaf) ? `hsl(${currentHue}, 80%, 45%)` : '#27ae60';
ctx.lineWidth = 3;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
// 軌道上を走っている時だけ、たまに小さな芽(双葉)を落とす
if (!isDrawingShape) {
vineDist += dist;
if (vineDist >= vineInterval) {
ctx.save();
ctx.translate(x2, y2);
ctx.rotate(angle);
// 左右どちらかにランダムで葉をつける
const side = (Math.random() > 0.5) ? 1 : -1;
// 葉っぱ
ctx.fillStyle = '#2ecc71';
ctx.beginPath();
ctx.ellipse(0, side * 6, 5, 2.5, side * Math.PI/4, 0, Math.PI*2);
ctx.fill();
// 葉っぱと蔓を繋ぐ細い茎
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, side * 4);
ctx.lineWidth = 1.5;
ctx.strokeStyle = '#27ae60';
ctx.stroke();
ctx.restore();
// 少しランダム性を持たせてリセット
vineDist = Math.random() * -10;
}
}
}
needsJump = false;
}
}
// ミニチュア描画(アルバム)
function drawMiniature(params, index) {
ctx.save();
if (penMode === 'wire') {
ctx.strokeStyle = wireColor;
} else if (penMode === 'ribbon') {
ctx.strokeStyle = `hsl(${currentHue}, 90%, 75%)`;
} else if (penMode === 'chain') {
ctx.strokeStyle = '#95a5a6';
ctx.setLineDash([3, 2]);
} else if (penMode === 'vine') {
// 和紙フラワーの場合はカラフルな代表色、葉っぱの場合は緑色
ctx.strokeStyle = params.isLeaf ? '#27ae60' : `hsl(${(currentHue + 90) % 360}, 80%, 75%)`;
if (!params.isLeaf) ctx.lineWidth = 3; // 花はフワッとするように少し太く
} else {
ctx.strokeStyle = `hsl(${currentHue}, 80%, 55%)`;
}
if (penMode !== 'vine' || params.isLeaf) {
ctx.lineWidth = 2;
}
ctx.lineJoin = 'round'; ctx.lineCap = 'round';
const cols = Math.floor((canvas.width - 20) / 40);
const col = index % cols;
const row = Math.floor(index / cols);
const centerX = 30 + col * 40;
const centerY = mainHeight + 50 + row * 40;
if (centerY > canvas.height - 10) { ctx.restore(); return; }
ctx.beginPath();
let theta = 0;
const miniDrawSpeed = 0.1;
const maxT = params.isLeaf ? Math.PI : params.maxTheta;
const size = 15;
while (theta <= maxT + miniDrawSpeed) {
let r = 0;
if (params.isLeaf) {
if (params.type === 0) r = size * Math.sin(theta) * (0.3 + 0.7 * Math.pow(Math.cos(theta), 2));
else if (params.type === 1) r = size * Math.sin(theta) * (1 - 0.2 * Math.sin(theta));
else if (params.type === 2) r = size * 1.5 * Math.pow(Math.sin(theta), 5);
else if (params.type === 3) r = size * Math.sin(theta) * (1 + 0.4 * Math.sin(3 * theta));
else if (params.type === 4) r = size * 1.2 * Math.sin(theta) * (1 + 0.06 * Math.sin(25 * theta));
} else { r = size * Math.sin(params.n * theta); }
const x = centerX + r * Math.cos(theta);
const y = centerY + r * Math.sin(theta);
if (theta === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
theta += miniDrawSpeed;
}
ctx.stroke();
ctx.restore();
}
function update() {
const currentPattern = patterns[patternIndex % patterns.length];
let nextX, nextY;
if (!isDrawingShape) {
logicalT += speed;
const basePos = currentPattern.get(logicalT);
nextX = basePos.x;
nextY = basePos.y;
drawSegment(currentX, currentY, nextX, nextY);
currentX = nextX;
currentY = nextY;
if (currentX > canvas.width + 50) {
logicalT = 0;
patternIndex++;
const nextPattern = patterns[patternIndex % patterns.length];
modeBadge.innerText = `軌道: ${nextPattern.name}`;
ctx.clearRect(0, 0, canvas.width, mainHeight);
barbDist = 0; chainDist = 0; vineDist = 0;
if (patternIndex % patterns.length === 0) {
ctx.clearRect(0, mainHeight, canvas.width, canvas.height - mainHeight);
drawDivider();
albumCount = 0;
}
const startPos = nextPattern.get(logicalT);
currentX = startPos.x;
currentY = startPos.y;
needsJump = true;
}
} else {
shapeTheta += drawSpeed;
let r = 0;
let maxTheta = Math.PI;
if (shapeParams.isLeaf) {
const { type, size } = shapeParams;
maxTheta = Math.PI;
if (type === 0) r = size * Math.sin(shapeTheta) * (0.3 + 0.7 * Math.pow(Math.cos(shapeTheta), 2));
else if (type === 1) r = size * Math.sin(shapeTheta) * (1 - 0.2 * Math.sin(shapeTheta));
else if (type === 2) r = size * 1.8 * Math.pow(Math.sin(shapeTheta), 5);
else if (type === 3) r = size * Math.sin(shapeTheta) * (1 + 0.4 * Math.sin(3 * shapeTheta));
else if (type === 4) r = size * 1.2 * Math.sin(shapeTheta) * (1 + 0.06 * Math.sin(25 * shapeTheta));
} else {
const { n, size, maxTheta: mTheta } = shapeParams;
maxTheta = mTheta;
r = size * Math.sin(n * shapeTheta);
}
nextX = basePosition.x + r * Math.cos(shapeTheta);
nextY = basePosition.y + r * Math.sin(shapeTheta);
drawSegment(currentX, currentY, nextX, nextY);
currentX = nextX;
currentY = nextY;
if (shapeTheta >= maxTheta) {
isDrawingShape = false;
drawSegment(currentX, currentY, basePosition.x, basePosition.y);
currentX = basePosition.x;
currentY = basePosition.y;
drawMiniature(shapeParams, albumCount);
albumCount++;
}
}
requestAnimationFrame(update);
}
window.addEventListener('keydown', (e) => {
if (isDrawingShape) return;
const isLeaf = e.code.startsWith('Numpad');
if (isLeaf) {
let numStr = e.code.replace('Numpad', '');
let num = parseInt(numStr);
if (isNaN(num)) num = 1;
const type = (num - 1 + 5) % 5;
const leafHues = [100, 90, 75, 120, 150];
currentHue = leafHues[type];
isDrawingShape = true;
shapeTheta = 0;
shapeParams = { isLeaf: true, type: type, size: 60 };
basePosition = { x: currentX, y: currentY };
} else {
if (e.key.length !== 1) return;
const charCode = e.key.charCodeAt(0);
let n = (charCode % 5) + 2;
let maxTheta = Math.PI * (n % 2 === 0 ? 2 : 1);
let size = (charCode % 5) * 10 + 30;
currentHue = (charCode * 137) % 360;
isDrawingShape = true;
shapeTheta = 0;
shapeParams = { isLeaf: false, n: n, size: size, maxTheta: maxTheta };
basePosition = { x: currentX, y: currentY };
}
});
drawDivider();
modeBadge.innerText = `軌道: ${patterns[0].name}`;
update();
</script>
</body>
</html>


■ 今日の学び

お絵描き系のゲームを作るたびに思うのですが……

私は「絵を描くこと」は大好きなのに、
決して「絵が上手」というタイプではありません。

だからこそ今回は、
「自分でもアートな絵を描いてみたい!」という気持ちを形にしたくて、
このゲームを作りました。

操作は本当にシンプルで、
流れる線に合わせてキーボードを「ポンッ」と押すだけ。

でも、押すタイミングや選ぶキーによって、
花びらの形も色も変わっていきます。

一筆書きでスッと現れて、軌道が進むとやがて消えてしまう……

まさに その瞬間だけの“一期一会アート” になりました。

テストプレイのつもりが、気がつくとしばらく無心で遊んでいて、

「ポチ…( ˘⊖˘) 。o( 🌸 )」と、
ただただ花を咲かせる時間が心地よかったです。

絵の技術がなくても、指先ひとつで自分だけのアートが生まれる。

プログラミングって、そんなふうに
“できない”を“できる”に変えてくれる魔法のキャンバス なんだなと、
改めて感じました。

みなさんもぜひ、無心になってお花や葉っぱを咲かせてみてくださいね。

あなたが咲かせた作品も、いつか見られたら嬉しいですヽ(*^v^*)ノ

■ 次回は、目に見えない“香り”をテーマにしたゲームです。

花の香りを頼りに、蝶がふわりと飛んでいきます。

途中には、わずかに“危険な香り”を放つ障害物も……。

香りを感じ取りながら、
障害物につかまらずに花までたどり着きましょう。

ゴールすると、香りをテーマにした短歌がそっと現れます。

ぜひ楽しみにしていてくださいね。(*^v^)ノシ

次の実験はこちら  
  (見えないものの表現)
「香りの導きと、想いの短歌」



 ✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

~このブログについて~

AIと一緒に遊び、
100の実験でゲームを育てるブログ

🌈初めての方へ
🌱このブログについて
🌳実験一覧
🍃プライバシーポリシー


AIと作る100のゲーム実験室(*^v^*)をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む