■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つのコースを描き終えると、
画面下の 押し花アルバム がリセットされて新しいページになります。
どんなコースに、どんなお花を咲かせるか。
組み合わせは無限大です!(´▽`*)
ぜひ、お気に入りの植物ラインを作って遊んでみてくださいね。
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。
▼ここからコード▼(クリックで開く)
<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^*)





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