はじめに
今回は 純粋な JavaScript と CSS を使って、マウスを乗せると色が変わり、さらに何度もホバーすると少しずつ暗くなっていく「お絵描き風のグリッド」を作ってみます。
ライブラリは一切使わず、基本的な DOM 操作とイベントリスナーだけで実現できます。
完成イメージ👇
- ページを開くと 16×16 の白いグリッドが表示される
- マウスを乗せるとランダムな色で塗られる
- 同じマスに繰り返しマウスを当てると、色が少しずつ暗くなり、最大 10 段階まで変化する
- ボタンを押すとグリッドのサイズをリセットできる
PenCodeで完成物を確認👇
HTML
まずは基本の構造です。
ボタンと、グリッドを描画するコンテナを用意しています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Canvas Grid</title>
</head>
<body>
<div id="controls">
<button id="resizeBtn">グリッドサイズの調整</button>
</div>
<div id="container"></div>
</body>
</html>
CSS
グリッドをきれいに並べるために Flexbox を使います。
各マスは .square クラスで制御し、背景色が変わるときに滑らかなアニメーションが付くようにしています。
body {
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
margin: 20px;
background: #f0f0f0;
}
#controls {
margin-bottom: 15px;
}
#container {
display: flex;
flex-wrap: wrap;
width: 960px;
height: 960px;
background: white;
border: 2px solid #333;
}
.square {
box-sizing: border-box;
border: 1px solid #ddd;
user-select: none;
transition: background-color 0.3s;
}
JavaScript
ここがメインの処理です。
- createGrid(size): 指定されたサイズでマスを生成する
- mouseenter イベント: マウスを乗せたとき、ランダムな色をつける & 既に色がある場合は暗さを一段階上げる
- WeakMap を使って「各マスの暗さ」と「元のランダムカラー」を記憶する
- ボタンクリックで新しいサイズのグリッドを生成する
// DOM の主要要素を取得
const container = document.querySelector("#container"); // グリッドを入れる親要素
const resizeBtn = document.querySelector("#resizeBtn"); // グリッドサイズ変更ボタン
// キャンバス(コンテナ)の固定サイズ(px)
const containerSize = 960; // CSS に合わせてキャンバスは 960x960px を想定
let gridSize = 16; // 初期のグリッド分割数(16 × 16)
// 各マス(DOM 要素)に紐づく状態を保存するための WeakMap
// WeakMap を使うと、要素が DOM から消えた時に GC の対象になるためメモリ管理が楽になる
const darknessMap = new WeakMap(); // 各マスの「暗さ(0〜10)」を保存
const colorMap = new WeakMap(); // 各マスの「元のランダム RGB」を保存([r,g,b] の配列)
/**
* グリッドを生成する関数
* @param {number} size - 1辺あたりのマス数(size x size のグリッド)
*/
function createGrid(size) {
// まずは既存のマスを全部削除してリセット(シンプルなやり方)
container.innerHTML = "";
// 1 マスの幅/高さを計算(コンテナの総サイズを分割数で割る)
const squareSize = containerSize / size;
// size * size 個のマスを作るループ
for (let i = 0; i < size * size; i++) {
// マス用の div を作成
const square = document.createElement("div");
square.classList.add("square"); // CSS 側で表示・ボーダー・遷移を定義している想定
// マスのサイズを inline style で指定(px)。CSS の box-sizing: border-box と組み合わせて使う。
square.style.width = squareSize + "px";
square.style.height = squareSize + "px";
// 初期は白で塗る
square.style.backgroundColor = "rgb(255,255,255)";
// 初期状態を WeakMap に登録
darknessMap.set(square, 0); // 暗さは 0(まだ暗くしていない)
colorMap.set(square, null); // まだ色(ランダム RGB)は割り当てていない -> null
// マウスがマスに入ったときの挙動(mouseenter は子要素に伝播しない)
square.addEventListener("mouseenter", () => {
// まだこのマスにランダム色が割り当てられていなければ生成して保存する
if (!colorMap.get(square)) {
// 0〜255 の整数をランダムに生成して RGB 配列を作る
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
colorMap.set(square, [r, g, b]); // このマスの「元色」として保存
}
// 保存してある元色を取得
const [r, g, b] = colorMap.get(square);
// 現在の暗さを取得して 1 段階深くする(最大 10 段階)
let darkness = darknessMap.get(square);
if (darkness < 10) darkness++; // 既定通り最大は 10
darknessMap.set(square, darkness); // 更新して保存
// 暗さに応じた係数を求める
// darkness = 0 -> factor = 1(元の色)
// darkness = 10 -> factor = 0(真っ黒)
const factor = 1 - 0.1 * darkness;
// 元の RGB に係数を掛けて暗くした色を計算(Math.floor で整数化)
const newR = Math.floor(r * factor);
const newG = Math.floor(g * factor);
const newB = Math.floor(b * factor);
// 計算した色で背景色を更新(CSS の transition: background-color が効く)
square.style.backgroundColor = `rgb(${newR},${newG},${newB})`;
});
// コンテナに追加して DOM に表示
container.appendChild(square);
}
}
// 初期表示(ページ読み込み時に既定のグリッドを生成)
createGrid(gridSize);
/**
* ボタンを押したときの処理:ユーザーに新しいグリッド数を入力させて再生成する
*/
resizeBtn.addEventListener("click", () => {
// プロンプトでユーザー入力を受け取る(キャンセルは null)
let userInput = prompt("新しいグリッドサイズを入力してください(最大100):", gridSize);
if (userInput === null) return; // ユーザーがキャンセルしたら何もしない
// parseInt は基数を指定する方が安全(ここでは 10 進数)
userInput = parseInt(userInput, 10);
// 入力チェック:数値であることと、1 以上であること
if (isNaN(userInput) || userInput < 1) {
alert("1以上の数字を入力してください。");
return;
}
// 上限チェック(100 を超えると負荷が大きくなるため制限)
if (userInput > 100) {
alert("最大は100までです。");
return;
}
// グローバルな gridSize を更新して、新しいサイズでグリッドを作り直す
gridSize = userInput;
createGrid(gridSize);
});
実装のポイント
-
WeakMap の活用
- 各マスに「状態」を紐づけるために WeakMap を使用
- DOM 要素をキーにでき、要素が消えると自動的にメモリ解放されるため便利
-
段階的な暗さ
-
darknessを 0〜10 の範囲で管理 -
factor = 1 - 0.1 * darknessによって、元の色を段階的に暗くしていく - マウスを繰り返し当てると少しずつ黒に近づく表現が可能
-
-
動的なグリッドサイズ
- ボタンを押すと任意のサイズのグリッドを再生成可能
- 最大 100×100 まで制限してブラウザの負荷を回避
-
CSS と組み合わせた滑らかなアニメーション
-
.squareにtransition: background-color 0.3sを設定 - 背景色の変更が滑らかになり、見た目が自然
-
まとめ
- JavaScript だけでシンプルなお絵描きアプリ風の挙動を作れる
- WeakMap を使うことで DOM 要素ごとに状態管理が簡単に行える
- Flexbox を使えば、手軽に「お絵描きキャンバス」のような見た目にできる
- ボタン操作でグリッドサイズを自由に変更可能で、インタラクティブ性が高い
GitHub でソースコードを見る
このお絵描きグリッドの完全なソースコードは GitHub に公開しています。
興味のある方は下記リンクからご覧ください。
https://github.com/Cyoukakitsu/JavaScript-30day