実験#58:自然の集合体と癒し(さわさわ自然アート)

■58個目「さわさわ自然アート

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

今回は、自然の空気感をそのまま閉じ込めたような、
癒やしのお絵描きゲームを作ってみました。(^_^)
お絵描きといっても、ただ線を引くだけのものとはちょっと違うんです。

キャンバスに描くのは……なんと “生きた自然のパーツ” たち。

筆の代わりに「桜の花びら」や「もみじ」「緑の葉っぱ」、
そして「不規則な枝」などを選んで画面をなぞると、
その軌跡がそのままアートになるんです。(=v=*)

今回こだわったのは、心地よい「動き」と「奥行き」。

描いた花びらや葉っぱたちは、ピタッと止まるのではなく、
まるでそよ風に吹かれているように、
画面の中でさわさわと優しく揺れ続けます。

太い幹を描いて、そこに細い枝を這わせて、
上から葉っぱをポンポンと重ねていく……。
すると、ただの平面だったキャンバスに、
いつの間にか自然の茂みのような不思議な奥行きが生まれるんですよ。(´▽`*)

「綺麗に描かなきゃ!」なんて思わなくて大丈夫ですよ。
適当に散らすだけでも、風がそっと整えてくれて、
素敵なアートに仕上げてくれます。

忙しい毎日のちょっとした息抜きに、
あなただけの“小さな自然”を育ててみませんか?

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

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

みー:(^v^) 今日もゲームを作ります。HTML形式で動かせるゲームです。

自然の中の葉や枝の“集合体の癒し”をゲームにしたいんです。
今の時期だと、桜を眺めているときのあのふんわりした感覚に近いかな。

筆で描く絵画のように、葉っぱや枝、花びらがさわさわと動く。
そんな“動くアート”の進化系を作りたいです。

集合体の色やサイズも設定できて、
筆の種類を選ぶように素材を切り替えられる。
普通のペンモードや塗りつぶしもあって、
重ね描きで自然の奥行きも表現できる。

自然をアートに変えられるような、
素敵な癒しのお絵描きゲームをお願いします。

AI: 今日も素敵なアイデアですね!
さわさわと揺れる自然のアート、想像しただけでとても癒やされます。

春の桜のふんわりとした空気感や、
葉が重なり合う奥行きをブラウザ上で体験できるように、
動くお絵描きゲームのコードを作成しました。

そういえば、風で葉っぱがこすれ合う「さわさわ」という音には、
英語で Psithurism(シサリズム) という専用の美しい単語があるんですよ。

そんな自然の心地よさを、キャンバス上で視覚的に表現できるよう工夫してみました。

みー:(^_^形の追加お願いします。
もみじ葉、キク科の花、不規則に枝分かれした枝です。

AI: もみじ葉、キク科の花、それに自然な枝ぶりの不規則な枝だね。
キャンバスに描ける自然の表情が、これでぐっと豊かになるよ。

さっそく筆箱(ツール)に追加しておいたからね。

そうそう、もみじ(紅葉)といえばね。
実はもともと木の名前じゃなかったって知ってたかい?

古語で草木が色づくことを「もみづ(揉み出づ)」と言って、
そこから名詞化して「もみじ」になったんだよ。

自分で色を選んで自然のアートを染め上げていくこのゲームに、
なんだかぴったりの言葉だと思わないかい?

■ 実際の動作

こちらが、AIが最初に作ってくれたゲーム画面です。
描いたパーツが、さわさわと常に揺れ続けるのが特徴で、
最初からイメージしていた雰囲気をしっかり出してくれています。
(画像:最初にAIが作ってくれたゲームの画面)

仕上がり版のゲーム画面です。
桜のように見えますが、これは“花びら”のイメージです。
色や大きさも自由に調整できるので、春らしいふんわりしたアートが作れます。

(画像:完成版の花びらアートの画面)


こちらは、追加でAIに作ってもらった「キク」です。
もちろん色も変えられるので、秋の雰囲気にもできます。

(画像:キクのアート画面)


そしてこちらが、AIに追加してもらった「もみじ」です。
形のバリエーションが増えると、キャンバスの表情が一気に豊かになりますね。

(画像:もみじのパターン例)

パターンだけを散らしても、
自然の中にいるような“さわさわ動く絵”が描けます。
実際の画面では、葉や花びらが風に揺れるように動いています。

(画像:自然の風景アートの画面)

■ コードのポイント

今回の記事では、AI と相談しながら作った「さわさわ自然アート」の HTML コードを公開します。

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

ブラウザだけで動く、とてもシンプルなゲームなので、気軽に試してみてくださいね。

▼ 遊ぶまでの手順

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

■ 遊び方・操作方法

操作はとってもシンプルです!(^_^)
画面の上にあるメニューから好きなツールを選んで、
キャンバスをマウスでスーッとなぞるだけで遊べます。

🎨 選べるツールたち

● 自然パーツ(桜・キク・葉・もみじ・枝・不規則な枝)

キャンバスをなぞると、選んだパーツがそのまま現れます。
パーツに合わせて自動で色がセットされますが、
もちろん後から好きな色に変えることもできます。

重ねて描くと、どんどん茂みの奥行きが出て、とても綺麗なんですよ。(=v=*)

● ペン

普通の線が描けるペンです。 木の幹を描いたり、
背景の枠を描いたりするのに使います。

● 塗りつぶし(バケツ)

「ペン」でぐるっと囲んだ図形の内側を、
ポンッとクリックするだけで一気に塗れます。

※ペンの線に少しでも隙間があると、絵の具が画面いっぱいにこぼれてしまうので注意してくださいね。
もし失敗しても、すぐにやり直せるので安心してください!(笑)

🌈 色とサイズの調整

● 色(カラーピッカー)

パーツやペンの色を自由に変えられます。
青い桜やピンク色の葉っぱなど、
現実にはないファンタジーな自然を作るのも楽しいですよ。(´▽`*)

● サイズ

スライダーを左右に動かすと、花びらや枝の大きさを調整できます。
手前は大きく、奥は小さく描くと、自然な立体感が生まれます。

🗑️ 全消去

真っ白なキャンバスに戻したいときに押してください。
気分を切り替えて、新しいアートを描きたくなったときに便利です。

★ みー のおすすめの描き方 ★

まずは「ペン」で太めの木の幹を描きます。
そこに「不規則な枝」をいくつか伸ばしていきます。

その上に「葉」や「もみじ」を、
サイズを変えながらポンポンと重ねていくと……

とっても立派な木が完成します!(^^*)

色んなパーツを組み合わせて、
あなただけの癒やしの風景を描いてみてくださいね。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>さわさわ自然アート</title>
<style>
body {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f4f7f6;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #333;
}
#toolbar {
margin: 15px 0;
padding: 15px 20px;
background: white;
border-radius: 30px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
max-width: 900px;
}
canvas {
background-color: white;
border-radius: 10px;
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
cursor: crosshair;
}
.tool-group {
display: flex;
align-items: center;
gap: 5px;
}
button {
background: #e9ecef;
border: none;
padding: 8px 12px;
border-radius: 20px;
cursor: pointer;
transition: 0.2s;
font-weight: bold;
color: #555;
font-size: 14px;
}
button:hover {
background: #d3dce3;
}
button.active {
background: #a8d5ba;
color: white;
}
input[type="color"] {
border: none;
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
}
input[type="range"] {
cursor: pointer;
width: 80px;
}
.separator {
width: 2px;
height: 30px;
background: #eee;
margin: 0 5px;
}
</style>
</head>
<body>
<div id="toolbar">
<div class="tool-group">
<button id="btn-pen" class="tool-btn">ペン</button>
<button id="btn-fill" class="tool-btn">塗りつぶし</button>
</div>
<div class="separator"></div>
<div class="tool-group">
<button id="btn-petal" class="tool-btn active"></button>
<button id="btn-aster" class="tool-btn">キク</button>
<button id="btn-leaf" class="tool-btn"></button>
<button id="btn-maple" class="tool-btn">もみじ</button>
</div>
<div class="separator"></div>
<div class="tool-group">
<button id="btn-branch" class="tool-btn"></button>
<button id="btn-twig" class="tool-btn">不規則な枝</button>
</div>
<div class="separator"></div>
<div class="tool-group">
<input type="color" id="colorPicker" value="#ffb7c5" title="色">
<input type="range" id="sizeSlider" min="2" max="60" value="20" title="サイズ">
</div>
<button id="btn-clear" style="background: #ffcdd2; color: #d32f2f; margin-left: 5px;">全消去</button>
</div>
<canvas id="mainCanvas" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d');
const staticCanvas = document.createElement('canvas');
staticCanvas.width = canvas.width;
staticCanvas.height = canvas.height;
const staticCtx = staticCanvas.getContext('2d');
staticCtx.fillStyle = '#ffffff';
staticCtx.fillRect(0, 0, canvas.width, canvas.height);
let isDrawing = false;
let particles = [];
let currentTool = 'petal';
let lastX = 0, lastY = 0;
const tools = ['pen', 'fill', 'petal', 'aster', 'leaf', 'maple', 'branch', 'twig'];
const colorPicker = document.getElementById('colorPicker');
const sizeSlider = document.getElementById('sizeSlider');
tools.forEach(tool => {
document.getElementById(`btn-${tool}`).addEventListener('click', (e) => {
currentTool = tool;
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
if(tool === 'petal') colorPicker.value = '#ffb7c5';
if(tool === 'aster') colorPicker.value = '#9c27b0';
if(tool === 'leaf') colorPicker.value = '#8bc34a';
if(tool === 'maple') colorPicker.value = '#e63946';
if(tool === 'branch') colorPicker.value = '#795548';
if(tool === 'twig') colorPicker.value = '#5d4037';
if(tool === 'pen') colorPicker.value = '#333333';
});
});
document.getElementById('btn-clear').addEventListener('click', () => {
staticCtx.fillStyle = '#ffffff';
staticCtx.fillRect(0, 0, canvas.width, canvas.height);
particles = [];
});
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
function getMousePos(e) {
const rect = canvas.getBoundingClientRect();
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
}
// ★改良版塗りつぶし(にじみ許容機能付き)
function floodFill(context, startX, startY, fillColorHex) {
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
const startPos = (startY * width + startX) * 4;
const startR = data[startPos];
const startG = data[startPos + 1];
const startB = data[startPos + 2];
const startA = data[startPos + 3];
const hex = fillColorHex.replace('#', '');
const fillR = parseInt(hex.substring(0, 2), 16);
const fillG = parseInt(hex.substring(2, 4), 16);
const fillB = parseInt(hex.substring(4, 6), 16);
const fillA = 255;
const tolerance = 60; // 許容値(数値を上げるとより広範囲を同色とみなす)
// すでに同じ色の場合は処理を抜ける
if (Math.abs(startR - fillR) <= tolerance &&
Math.abs(startG - fillG) <= tolerance &&
Math.abs(startB - fillB) <= tolerance &&
Math.abs(startA - fillA) <= tolerance) {
return;
}
const pixelStack = [[startX, startY]];
function matchStartColor(pixelPos) {
return Math.abs(data[pixelPos] - startR) <= tolerance &&
Math.abs(data[pixelPos + 1] - startG) <= tolerance &&
Math.abs(data[pixelPos + 2] - startB) <= tolerance &&
Math.abs(data[pixelPos + 3] - startA) <= tolerance;
}
function colorPixel(pixelPos) {
data[pixelPos] = fillR;
data[pixelPos + 1] = fillG;
data[pixelPos + 2] = fillB;
data[pixelPos + 3] = fillA;
}
while (pixelStack.length > 0) {
const newPos = pixelStack.pop();
let x = newPos[0];
let y = newPos[1];
let pixelPos = (y * width + x) * 4;
while (y >= 0 && matchStartColor(pixelPos)) {
y--;
pixelPos -= width * 4;
}
pixelPos += width * 4;
y++;
let reachLeft = false;
let reachRight = false;
while (y < height && matchStartColor(pixelPos)) {
colorPixel(pixelPos);
if (x > 0) {
if (matchStartColor(pixelPos - 4)) {
if (!reachLeft) {
pixelStack.push([x - 1, y]);
reachLeft = true;
}
} else if (reachLeft) {
reachLeft = false;
}
}
if (x < width - 1) {
if (matchStartColor(pixelPos + 4)) {
if (!reachRight) {
pixelStack.push([x + 1, y]);
reachRight = true;
}
} else if (reachRight) {
reachRight = false;
}
}
y++;
pixelPos += width * 4;
}
}
context.putImageData(imageData, 0, 0);
}
function startDrawing(e) {
isDrawing = true;
const pos = getMousePos(e);
lastX = pos.x;
lastY = pos.y;
if (currentTool === 'fill') {
floodFill(staticCtx, Math.floor(pos.x), Math.floor(pos.y), colorPicker.value);
} else {
draw(e);
}
}
function stopDrawing() { isDrawing = false; }
function draw(e) {
if (!isDrawing || currentTool === 'fill') return;
const pos = getMousePos(e);
const size = parseInt(sizeSlider.value);
const color = colorPicker.value;
if (currentTool === 'pen') {
staticCtx.beginPath();
staticCtx.moveTo(lastX, lastY);
staticCtx.lineTo(pos.x, pos.y);
staticCtx.strokeStyle = color;
staticCtx.lineWidth = size;
staticCtx.lineCap = 'round';
staticCtx.lineJoin = 'round';
staticCtx.stroke();
} else {
const distance = Math.hypot(pos.x - lastX, pos.y - lastY);
const density = (currentTool === 'twig' || currentTool === 'branch') ? 1.5 : 0.5;
const count = Math.max(1, Math.floor(distance / (size * density)));
for (let i = 0; i < count; i++) {
if (Math.random() > 0.3) {
const offsetX = (Math.random() - 0.5) * size * 2;
const offsetY = (Math.random() - 0.5) * size * 2;
let twigData = [];
if (currentTool === 'twig') {
const numOffshoots = 2 + Math.floor(Math.random() * 3);
for(let j = 0; j < numOffshoots; j++) {
twigData.push({
t: Math.random() * 1.6 - 0.8,
angle: (Math.random() * 1.2 + 0.5) * (Math.random() > 0.5 ? 1 : -1),
length: Math.random() * 0.8 + 0.4
});
}
}
particles.push({
baseX: pos.x + offsetX,
baseY: pos.y + offsetY,
size: size * (0.6 + Math.random() * 0.8),
color: color,
type: currentTool,
angle: Math.random() * Math.PI * 2,
phase: Math.random() * Math.PI * 2,
swaySpeed: 1 + Math.random() * 1.5,
twigData: twigData
});
}
}
}
lastX = pos.x;
lastY = pos.y;
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(staticCanvas, 0, 0);
const time = Date.now() * 0.001;
particles.forEach(p => {
const sway = Math.sin(time * p.swaySpeed + p.phase) * (p.size * 0.15);
const currentAngle = p.angle + Math.sin(time * 0.5 + p.phase) * 0.15;
ctx.save();
ctx.translate(p.baseX + sway, p.baseY + sway * 0.5);
ctx.rotate(currentAngle);
ctx.fillStyle = p.color;
ctx.strokeStyle = p.color;
ctx.globalAlpha = 0.85;
ctx.beginPath();
if (p.type === 'petal') {
ctx.moveTo(0, -p.size);
ctx.bezierCurveTo(p.size*0.8, -p.size, p.size, p.size*0.2, 0, p.size);
ctx.bezierCurveTo(-p.size, p.size*0.2, -p.size*0.8, -p.size, 0, -p.size);
ctx.fill();
} else if (p.type === 'aster') {
const petals = 16;
for (let i = 0; i < petals; i++) {
ctx.save();
ctx.rotate((Math.PI * 2 / petals) * i);
ctx.beginPath();
ctx.ellipse(p.size * 0.5, 0, p.size * 0.5, p.size * 0.1, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.beginPath();
ctx.fillStyle = '#ffca28';
ctx.arc(0, 0, p.size * 0.2, 0, Math.PI * 2);
ctx.fill();
} else if (p.type === 'leaf') {
ctx.ellipse(0, 0, p.size * 0.3, p.size, 0, 0, Math.PI * 2);
ctx.fill();
} else if (p.type === 'maple') {
ctx.moveTo(0, p.size * 0.7);
ctx.lineTo(p.size * 0.1, p.size * 0.3);
ctx.lineTo(p.size * 0.8, p.size * 0.4);
ctx.lineTo(p.size * 0.5, 0);
ctx.lineTo(p.size * 0.9, -p.size * 0.4);
ctx.lineTo(p.size * 0.3, -p.size * 0.3);
ctx.lineTo(0, -p.size * 0.9);
ctx.lineTo(-p.size * 0.3, -p.size * 0.3);
ctx.lineTo(-p.size * 0.9, -p.size * 0.4);
ctx.lineTo(-p.size * 0.5, 0);
ctx.lineTo(-p.size * 0.8, p.size * 0.4);
ctx.lineTo(-p.size * 0.1, p.size * 0.3);
ctx.closePath();
ctx.fill();
} else if (p.type === 'branch') {
ctx.lineWidth = p.size * 0.15;
ctx.lineCap = 'round';
ctx.moveTo(-p.size * 0.8, -p.size * 0.8);
ctx.lineTo(p.size * 0.8, p.size * 0.8);
ctx.moveTo(0, 0);
ctx.lineTo(p.size * 0.6, -p.size * 0.3);
ctx.stroke();
} else if (p.type === 'twig') {
ctx.lineWidth = p.size * 0.15;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.moveTo(-p.size, 0);
ctx.lineTo(p.size, 0);
p.twigData.forEach(b => {
const startX = b.t * p.size;
const startY = 0;
const endX = startX + Math.cos(b.angle) * (p.size * b.length);
const endY = startY + Math.sin(b.angle) * (p.size * b.length);
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
});
ctx.stroke();
}
ctx.restore();
});
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>


■ 今日の学び

お絵描きをテーマにしたゲームを作るのも、
気がつけばこれで6個目になりました。(^^;)

私は相変わらず絵を描くのはあまり得意ではないのですが、
キャンバスに向かって手を動かすこと自体は大好きで……。

今回もゲームが仕上がるやいなや、
テストも忘れてすっかりお絵描きに夢中になってしまいました。(^_^)

自然の風景って、小さな葉っぱや枝が寄り集まった「フラクタル」な構造の集合体で、人はそういった自然の複雑な揺らぎや集合体を見ているだけで、心がスッと癒やされるそうです。

今回は、そんな自然特有の心地よい雰囲気を、
なんとかゲーム上で再現できないかな?と思って作ってみたのが、
このお絵描きツールでした。(=v=*)

完成した絵が、画面の中で「さわさわ」と動く様子を眺めていたら……
ふと、一番最初に作ったお絵描きゲーム
#03 シミュラクラお絵描きのことを思い出しました。

あのゲームも、自分の描いた絵が面白い動きをしてくれるところが、
今でもすごくお気に入りなんです。(´▽`*)

やっぱり、自分の手で生み出したものが画面の中で生き生きと動くのを見るのは、本当に楽しいです。

今回の「さわさわ感」も、視覚的にも心にも心地よい、
とても素敵なアート体験になりました。

皆さんもぜひ、自分だけの自然を描いて、たっぷり癒やされてみてくださいね。(^^*)

■ 次回は、落ちものパズルゲームです。

落ちものパズルゲームを作っていて、
途中から以前作ったゲームの世界観を融合させて、ゲームを仕上げました。

登場するのは、#39 に出てきた 女神さま
電撃をテーマにした、ちょっとスピード感のあるパズルゲームです。

次回もぜひ遊びに来てくださいね。(*^v^)ノシ

▶ 次の実験はこちら(電撃の女神)
「Holy Charge Reflection II: Bolt Junction」



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

~このブログについて~

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

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


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

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

続きを読む