実験#47:タイピングアート(言の葉の色庭)

■47個目「言の葉の色庭

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

今回は、タイピングとアートをかけ合わせた、少し不思議で静かなゲームを作ってみました。

皆さんは、自分の紡いだ「言葉」に色があると感じたことはありますか?

このゲームでは、真っ白なキャンバスを“庭”に見立てて、
そこに自分の言葉をそっと植えていくような体験ができます。

キーボードで文章を綴り、パチンとEnterキーでパチンと確定すると……

その言葉の響きに合わせて、額縁の中に柔らかな色が
ポンッ、ポンッと滲み出します。

今回こだわったのは、重なり合う「色」と「大きさ」のバランス、
そして「質感」です。(=v=*)

和紙にインクを落としたようにじわじわ広がる色。

海辺で見つけるシーグラスのように淡く透明な図形。
それらが万華鏡のように重なり合い、ひとつの抽象画を形づくります。

書き終えるころには、世界にたった一つしかない、 あなたの心を描き出したような“色の庭”が完成します。

さらに、シーグラスの色を使ったお絵描きや、 描いた絵を薄くしたり、マウスでそっと削って 新しい形を見つけることもできます。(^ー^

忙しい毎日の合間に、ぼーっと眺めて一息ついて、 自分の言葉と色が溶け合うあたたかい時間を楽しんでいただけたら嬉しいです。

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

■ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。
文章を入力すると、キーボードのボタンごとに色と音が出るタイピングゲーム。
タイトルは「言の葉の色庭」。

普通に文が打てる文章枠があって、その横に絵画枠。
文字と合わせて、ランダムな場所・大きさで、ぼかしのある円や楕円、角のない長方形などがスタンプのように押されていきます。
文章を打ち終えると、唯一無二の抽象画が完成します。

AI: 「言の葉の色庭」、非常に情緒的で素敵なコンセプトですね。
タイピングという日常の動作が、一期一会の抽象画に変わる体験。
あなたの言葉の「響き」と「色」が、キャンバスに重なっていくプログラムを作成しました。

みー:(^_^) インクが滲むようなエフェクト、
図形の角度や縦横比のランダム化、 追加しましょう。

AI: では、「言の葉の色庭」をさらにアーティスティックに進化させましょう。

✨ 今回のこだわりポイント

  • 滲みのアニメーション requestAnimationFrame を使い、描いた図形が外側へ広がりながら薄くなる“インクの滲み”を再現。
  • 和の色彩と響き 彩度を落とした和色を採用し、音は余韻の長い静かな鈴の音に。
  • 不規則性の美 角度・縦横比のランダム化で、同じ文字でも二度と同じ形は出ません。 新登場の「かどなし三角」も庭に新しい表情を与えています。

文章を書き終えたとき、そこにはあなたの言葉が奏でた 優しく深い色の滲みが広がっているはずです。

みー:( ̄v ̄)* ぼかし具合をもう少し抑えたいかな。
文字枠は便箋風、絵画枠は額縁風にできる?

AI: 承知いたしました。さらに詩的で情緒あふれる世界観へ調整します。

🌿 マスターからのささやかな小話

(お客様、ずいぶんと筆が進んでおられるようですね。
この庭を愛でてくださって嬉しい限りです。)

昔々、ある国の庭師は、花の代わりに「詩」を庭に埋めていたそうです。
喜びの言葉を埋めれば鮮やかな大輪が咲き、
悲しみの言葉を埋めれば、露を含んだ青い草木が茂る。

その庭師が最後に残した言葉は、
「色は、沈黙した言葉である」

あなたが今、額縁の中に綴っている色たちも、
いつか誰かが読み解くのを待つ“静かな言葉”なのかもしれませんね。

■ 実際の動作

左側のメモ帳風の枠で文字を入力します。
Enterキーを押すと、心地よい音とともに図形がふわっと描かれていきます。
(画像:ゲーム画面)


文字に合わせた図形や色が、ランダムなサイズ・角度・場所で描かれます。
同じ文章でも、毎回ちがう絵が生まれるのが楽しいところです。
(画像:お絵描きの様子)


「庭を掃く」ボタンを押すと、描いた絵を薄くして
“前の色を残しつつ新しい色を重ねる”ことができます。

(画像:掃く操作の様子)

シーグラスボタンを選ぶと、色とペンの太さを変えて自由にお絵描きもできます。
シーグラスの優しい色合いがとても気に入っています。

(画像:シーグラス色のお絵描き)

さらに文字を入力すると、今の絵の上に新しい図形を追加できます。
「光を刻む」を選ぶと、マウスで絵をそっと削って
光が差し込んだような表現を作ることもできます。

(画像:光を刻む)


■ コードのポイント

今回の記事では、AI と相談しながら作った 「言の葉の色庭」の HTML コードを公開します。

ブラウザだけで動く、とてもシンプルなゲームです。 コードも難しい部分はほとんどないので、気軽に試してみてくださいね。

▼ 遊ぶまでの手順

1. PC の「メモ帳」(※Windowsの場合)や、  
 Mac の「テキストエディット」など、テキストエディタを開きます。
2. 下のコードをコピーして貼り付けます。
3. 「kotonohanoironiwa .html」など、好きな名前で保存します。
 (※拡張子は .html にしてください)
4. 保存したファイルをブラウザで開けば、すぐに遊べます。

🎨 「言の葉の色庭」の楽しみ方

【1】言葉で色を落とす(タイピング)

左側のテキストエリアに、好きな言葉や今の気持ちを入力して
「Enterキー」を押してみてください。(^_^

入力した文字に合わせて、右側のキャンバス(庭)に
ポポンッと色と形が舞い降りていきます。

【2】シーグラスを置く / 光を刻む(マウス操作)

右側のキャンバスをマウスでなぞると、直接絵を描くこともできます。

  • 「シーグラス」モード  
    スライダーで色を選んでなぞると、
    すりガラスのような淡い色が重なっていきます。
  • 「光を刻む」モード  
    色が塗られた場所をなぞると、そこだけ色が削れて  
    下の白い光がふわっと透けて見えます。

ペンの太さも自由に変えられるので、 大きなシーグラスを置いたり、
細い線で光を削り出したりして遊んでみてくださいね。(’v’*)

【3】庭を掃く・保存する

  • 「庭を掃く」ボタン  一気に消えるのではなく、白い霧がかかったように  
    色が少しずつ薄くなっていきます。  
    何度か押して、ちょうどいい淡さを探すのも楽しいです。
  • 「絵画を保存」ボタン  お気に入りの「言の葉の色庭」ができたら、  
    画像として保存して残しておくことができます。ヽ(^_^

文字を打つだけでも、マウスでなぞるだけでも、両方組み合わせてもOK。
ぜひ、あなただけの無限の色の庭を育ててみてくださいね。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>言の葉の色庭 - 無限の彩</title>
<style>
:root {
--bg-color: #f2eee2;
--text-color: #333;
--frame-color: #5d4037;
--frame-shadow: #3e2723;
--accent: #8fb8cc;
--seaglass-preview: transparent;
}
body {
margin: 0; padding: 20px;
font-family: 'Hiragino Mincho ProN', 'Sawarabi Mincho', serif;
background: var(--bg-color);
display: flex; flex-direction: column; align-items: center;
color: var(--text-color);
user-select: none;
min-height: 100vh;
}
h2 { font-weight: 200; letter-spacing: 0.5em; margin-bottom: 5px; }
.hint { font-size: 0.85rem; color: #666; text-align: center; margin-bottom: 15px;}
.main-wrapper {
display: flex; gap: 20px; width: 100%; max-width: 1350px;
height: 65vh; min-height: 400px;
}
.input-area {
flex: 1; display: flex; flex-direction: column;
background: #fffefb; padding: 25px; border-radius: 8px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.08);
position: relative; overflow: hidden; border: 1px solid #e0dbc5;
}
.input-area::before {
content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background-image: linear-gradient(#eee9d5 1px, transparent 1px);
background-size: 100% 2.5em; margin-top: 2.8em; pointer-events: none;
}
textarea {
flex: 1; border: none; font-size: 1.2rem; line-height: 2.5em; outline: none;
resize: none; background: transparent; color: #222; z-index: 1;
}
.canvas-frame {
flex: 1.8; background: var(--frame-color);
border-radius: 4px; padding: 25px;
box-shadow: inset 0 0 15px var(--frame-shadow), 5px 5px 20px rgba(0,0,0,0.2);
position: relative; box-sizing: border-box;
}
.canvas-area {
width: 100%; height: 100%; background: #fff; border-radius: 2px;
overflow: hidden; position: relative; cursor: crosshair;
}
canvas { width: 100%; height: 100%; }
.ui-container {
width: 100%; max-width: 1350px;
margin-top: 20px; display: flex; flex-direction: column; gap: 15px; align-items: center;
}
.ui-row { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; justify-content: center; }
.control-group {
display: flex; align-items: center; gap: 12px;
background: rgba(255,255,255,0.5); padding: 8px 20px; border-radius: 30px;
border: 1px solid rgba(0,0,0,0.05);
}
button {
padding: 8px 22px; border-radius: 20px; border: 1px solid #999;
background: rgba(255,255,255,0.8); color: #444; cursor: pointer;
transition: all 0.2s; font-family: serif; font-size: 0.9rem;
}
button:hover { background: #666; color: white; border-color: #666; }
button.active { background: var(--accent); color: white; border-color: var(--accent); box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
label { font-size: 0.85rem; color: #555; display: flex; align-items: center; gap: 6px;}
input[type="range"] { cursor: pointer; accent-color: var(--accent); }
#seaglass-preview {
width: 20px; height: 20px; border-radius: 4px;
background: var(--seaglass-preview);
border: 1px solid rgba(0,0,0,0.1);
transition: background-color 0.2s;
}
</style>
</head>
<body>
<h2>言の葉の色庭</h2>
<p class="hint">言葉は光、時間は絵筆。無限の大きさ、万華鏡の色彩を綴る。</p>
<div class="main-wrapper">
<div class="input-area">
<textarea id="editor" placeholder="ここに、想いを... (A,B,C,Dは淡い無限の光を放ちます)"></textarea>
</div>
<div class="canvas-frame">
<div class="canvas-area" id="c-container">
<canvas id="garden"></canvas>
</div>
</div>
</div>
<div class="ui-container">
<div class="ui-row">
<div class="control-group">
<button id="mode-seaglass" class="active" onclick="setMode('paint')">シーグラス</button>
<button id="mode-scratch" onclick="setMode('scratch')">光を刻む</button>
</div>
<div class="control-group">
<label>筆の太さ</label>
<input type="range" id="pen-size" min="3" max="150" value="35">
</div>
<div class="control-group">
<label>シーグラスの色 <span id="seaglass-preview"></span></label>
<input type="range" id="seaglass-hue" min="0" max="360" value="180">
</div>
</div>
<div class="ui-row">
<button onclick="fadeGarden()">庭を掃く</button>
<button onclick="saveImage()">絵画を保存</button>
</div>
</div>
<script>
const canvas = document.getElementById('garden');
const ctx = canvas.getContext('2d');
const editor = document.getElementById('editor');
const penSizeInput = document.getElementById('pen-size');
const seaglassHueInput = document.getElementById('seaglass-hue');
const seaglassPreviewSpan = document.getElementById('seaglass-preview');
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let isDrawing = false;
let currentMode = 'paint';
function init() {
const container = document.getElementById('c-container');
canvas.width = container.offsetWidth;
canvas.height = container.offsetHeight;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
updateSeaglassPreview();
}
window.onload = init;
window.onresize = init;
function updateSeaglassPreview() {
const hue = parseInt(seaglassHueInput.value);
const color = `hsla(${hue}, 45%, 80%, 0.5)`;
seaglassPreviewSpan.style.backgroundColor = color;
document.documentElement.style.setProperty('--seaglass-preview', color);
}
seaglassHueInput.addEventListener('input', updateSeaglassPreview);
function setMode(mode) {
currentMode = mode;
document.getElementById('mode-seaglass').classList.toggle('active', mode === 'paint');
document.getElementById('mode-scratch').classList.toggle('active', mode === 'scratch');
}
canvas.addEventListener('mousedown', (e) => { isDrawing = true; draw(e); });
window.addEventListener('mouseup', () => isDrawing = false);
canvas.addEventListener('mousemove', draw);
function draw(e) {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const size = parseInt(penSizeInput.value);
ctx.save();
if (currentMode === 'scratch') {
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.filter = 'blur(3px)';
ctx.fill();
} else {
const hue = parseInt(seaglassHueInput.value);
ctx.globalCompositeOperation = 'source-over';
const s = 45 + (Math.random() * 15 - 5);
const l = 82 + (Math.random() * 10 - 5);
ctx.fillStyle = `hsla(${hue}, ${s}%, ${l}%, 0.12)`;
ctx.filter = 'blur(1px)';
for(let i=0; i<2; i++) {
ctx.beginPath();
const rx = x + (Math.random() - 0.5) * size * 0.4;
const ry = y + (Math.random() - 0.5) * size * 0.4;
ctx.ellipse(rx, ry, size, size * 0.85, Math.random() * Math.PI, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
// ★心地よい音にするための変更
function playGardenTone(text) {
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
const isLightKey = /[ABCDabcd]/.test(text);
// 心地よい和音になるよう、ペンタトニックスケールの周波数を定義
const scale = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25, 783.99, 880.00, 1046.50, 1174.66];
// 文字数が多すぎると音が鳴り止まないので、最大15文字程度で制限
const count = Math.min(text.length, 15);
for (let i = 0; i < count; i++) {
const code = text.charCodeAt(i);
// 文字コードから音階を決定
const noteIndex = code % scale.length;
let freq = scale[noteIndex];
// 光のキーが含まれる場合は少し高音のキラキラした響きに
if (isLightKey) freq *= 1.5;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = isLightKey ? 'triangle' : 'sine';
// ★1文字ずつ音を鳴らすタイミングを 0.08秒 ずつずらす
const startTime = now + (i * 0.08);
osc.frequency.setValueAtTime(freq, startTime);
gain.gain.setValueAtTime(0, startTime);
gain.gain.linearRampToValueAtTime(0.025, startTime + 0.05); // ふんわり立ち上がる
gain.gain.exponentialRampToValueAtTime(0.00001, startTime + 1.5); // ゆっくり消える
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(startTime);
osc.stop(startTime + 1.5);
}
}
function animateStamp(text) {
const isLight = /[ABCDabcd]/.test(text);
const count = Math.min(text.length, 15); // 音の数に合わせて少し上限を増やす
for (let i = 0; i < count; i++) {
const code = text.charCodeAt(i);
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const randSkewed = Math.pow(Math.random(), 6);
const maxSize = isLight ? 1000 : 600;
const minSize = isLight ? 30 : 10;
const baseSize = minSize + (code % 25) + (maxSize * randSkewed);
const angle = Math.random() * Math.PI * 2;
const hue = isLight ? 55 + (Math.random() * 10 - 5) : ((code * 137.5) % 360) + (Math.random() * 20 - 10);
// 図形が出るタイミングも音とだいたい同じになるように調整(80ミリ秒)
setTimeout(() => { drawBleed(x, y, baseSize, angle, hue, code, isLight); }, i * 80);
}
}
function drawBleed(x, y, size, angle, hue, code, isLight) {
let opacity = isLight ? 0.35 + (Math.random() * 0.1) : 0.20 + (Math.random() * 0.1);
let currentSize = size;
const shapeType = code % 2;
function step() {
if (opacity > 0.01) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.globalCompositeOperation = isLight ? 'screen' : 'multiply';
ctx.shadowBlur = isLight ? 15 + size * 0.05 : 2;
ctx.shadowColor = isLight ? `hsla(${hue}, 100%, 95%, ${opacity})` : `hsla(${hue}, 55%, 80%, ${opacity})`;
ctx.fillStyle = isLight ? `hsla(${hue}, 100%, 98%, ${opacity * 0.6})` : `hsla(${hue}, 55%, 85%, ${opacity * 0.25})`;
ctx.beginPath();
if (shapeType === 0) ctx.arc(0, 0, currentSize/2, 0, Math.PI*2);
else ctx.roundRect(-currentSize/2, -currentSize/2, currentSize, currentSize, 12);
ctx.fill();
ctx.restore();
currentSize += 0.8 + (Math.random() * 0.6);
opacity *= 0.92 + (Math.random() * 0.02);
requestAnimationFrame(step);
}
}
step();
}
editor.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.isComposing) {
const lines = editor.value.split('\n');
const lastContent = lines[lines.length - 1] || lines[lines.length - 2];
if (lastContent && lastContent.trim()) {
playGardenTone(lastContent);
animateStamp(lastContent);
}
}
});
function fadeGarden() {
ctx.save();
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
function saveImage() {
const link = document.createElement('a');
link.download = 'iro-niwa_art.png';
link.href = canvas.toDataURL();
link.click();
}
</script>
</body>
</html>


■ 今日の学び

お絵描きのゲームは、この記事で5個目になりました。

今回のテーマは、 以前 #44 の記事で紹介した AI の雑学から生まれています。

作曲家スクリャービンは、 “特定の調性に特定の色を感じる”という独自の感覚を持っていて、 演奏中に会場をその色で照らす 「色光ピアノ」 まで考案したそうです。

その話を聞いてから、 「音と色の共鳴」って素敵だなあ……と思い、 そこからさらに発展して “文字と色、形、音の共鳴” をテーマにしてみました。

私は絵が上手というわけではないけれど、 色や形で遊ぶことがとても好きです。
だからこそ、いろんな表現で楽しめるお絵描きゲームを
これからも作っていきたいと思っています。

好きをぎゅっと詰め込んだお絵描きゲームたちを、
ぜひ楽しんでくださいね。

ちなみに…… 6個目のお絵描きゲームもすでに完成しています。
また近いうちに紹介しますので、お楽しみに。(^^*)


■ 次回は、ファンタジー世界の果物収穫です。

#26「ドミノドラゴン」で登場した、 “中途半端なちょびっと魔法”が
存在する世界が再び舞台になります。

道具に頼りながら、ちょっとだけ魔法を使って果物を収穫する、
不思議でにぎやかなファンタジー収穫ゲームです。

次回もお楽しみに。(*^v^)ノシ

▶ 次の実験はこちら(逃げる果物、拡大する投網)
  「魔法の投網 – ちょびっと魔法と果樹園」




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

コメントを残す

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

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

続きを読む