HTML5 Canvasでインタラクティブアートを作ってみた

# はじめに
HTML5 Canvasでインタラクティブアートを作りました。

[デモ](http://interactive-art.wealthy-design.com/)

画面：
![スクリーンショット 2024-11-05 11.39.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682035/9fdf44da-4b34-3792-4aa9-4392f6dfbc96.png)



# 仕様

### 図形の種類
1. 一般的な図形
    - 円
    - 三角形
    - 四角形
    - 五角形
    - 六角形
1. 文様
    - 七宝文様
    - マンダラ
    - 麻の葉文様
    - 青海波
    - 菊菱
    - 矢絣
    - 市松模様
1. その他
    - 風車
    - 桜
    - 星

### 図形の描画
画面を右クリックすると、カーソル位置から図形が描画されます。
大きさ、色、動く方向はランダムです。

### ボタン
図形の種類を選択できます。
クリアで、描画された図形を削除できます。

### 図形名
クリック時に描画された図形名が、右上に表示されます。

# コード

```html:interactiveArt.html
<!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;
            overflow: hidden;
            background-color: #f0f0f0; /* 背景色を設定 */
        }
        canvas {
            display: block; /* キャンバスをブロック要素として表示 */
        }
        /* ボタンの配置とスタイル設定 */
        .button-container {
            position: absolute; /* 絶対位置で配置 */
            top: 10px;
            left: 10px;
            z-index: 10; /* 他の要素よりも前面に表示 */
            background: rgba(255, 255, 255, 0.9); /* 半透明の背景 */
            padding: 10px;
            border-radius: 8px;
            box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); /* 影を追加 */
        }
        .button-container button {
            margin: 5px;
            padding: 10px 15px;
            font-size: 14px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            transition: background-color 0.3s, transform 0.2s; /* 背景色と変形のトランジション */
        }
        .button-container button.active {
            background-color: #4CAF50; /* アクティブボタンの色 */
            color: white;
            transform: scale(1.05); /* 拡大表示 */
        }
        .button-container button:hover {
            background-color: #ddd; /* ボタンにホバーしたときの色 */
        }
        .button-container button.clear-button {
            background-color: #FF6347; /* クリアボタンの色 */
            color: white;
        }
        /* 描画された図形の名前を表示するスタイル */
        .shape-name-display {
            position: absolute; /* 絶対位置で配置 */
            top: 10px;
            right: 10px;
            z-index: 10;
            background: rgba(0, 0, 0, 0.7); /* 半透明の黒背景 */
            color: white;
            padding: 10px;
            border-radius: 8px;
            font-size: 16px;
        }
    </style>
</head>
<body>
    <!-- 図形の種類を選択するボタン群 -->
    <div class="button-container">
        <button onclick="setShapeCategory('general')" id="generalBtn">一般的な図形</button>
        <button onclick="setShapeCategory('patterns')" id="patternsBtn">文様</button>
        <button onclick="setShapeCategory('other')" id="otherBtn">その他</button>
        <button onclick="clearCanvas()" class="clear-button">クリア</button>
    </div>
    <!-- 描画された図形の名前を表示する領域 -->
    <div class="shape-name-display" id="shapeNameDisplay">図形が表示されます</div>
    <!-- 描画用のキャンバス -->
    <canvas id="artCanvas"></canvas>
    <script>
        // キャンバスとそのコンテキストの設定
        const canvas = document.getElementById('artCanvas');
        const ctx = canvas.getContext('2d');
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;

        const shapeNameDisplay = document.getElementById('shapeNameDisplay');

        let shapes = []; // 生成された図形を格納する配列
        let currentCategory = 'general'; // 現在の描画カテゴリ

       // 図形名の対応表
       const shapeNames = {
            Circle: '円',
            Triangle: '三角形',
            Square: '四角形',
            Pentagon: '五角形',
            Hexagon: '六角形',
            Shippo: '七宝文様',
            Mandala: 'マンダラ',
            Asanoha: '麻の葉文様',
            Seigaiha: '青海波',
            Kikubishi: '菊菱',
            Yagasuri: '矢絣',
            Ichimatsu: '市松模様',
            Windmill: '風車',
            Star: '星',
            Sakura: '桜'
        };

        // カテゴリボタンの状態を更新する関数
        function updateActiveButton() {
            // すべてのボタンから 'active' クラスを削除し、現在のカテゴリに対応するボタンに 'active' クラスを追加
            document.getElementById('generalBtn').classList.remove('active');
            document.getElementById('patternsBtn').classList.remove('active');
            document.getElementById('otherBtn').classList.remove('active');
            document.getElementById(`${currentCategory}Btn`).classList.add('active');
        }

        // カテゴリを設定する関数
        function setShapeCategory(category) {
            currentCategory = category; // 現在のカテゴリを変更
            updateActiveButton(); // ボタンの状態を更新
        }

        // キャンバスをクリアする関数
        function clearCanvas() {
            shapes = []; // 図形配列をクリア
            ctx.clearRect(0, 0, canvas.width, canvas.height); // キャンバスをクリア
            shapeNameDisplay.textContent = '図形がクリアされました'; // 図形名をリセット
        }

        // ランダムなカラーを生成する関数
        const getRandomColor = () => '#' + Math.floor(Math.random() * 16777215).toString(16);

        // 図形の基底クラス
        class Shape {
            constructor(x, y, size) {
                this.x = x; // 図形のX座標
                this.y = y; // 図形のY座標
                this.size = size; // 図形のサイズ
                this.color = getRandomColor(); // ランダムな色を生成
                this.dx = (Math.random() - 0.5) * 8; // X方向の速度
                this.dy = (Math.random() - 0.5) * 8; // Y方向の速度
                this.rotation = 0; // 図形の回転角度
                this.rotationSpeed = (Math.random() - 0.5) * 0.1; // 回転速度
            }

            // 図形の位置と回転を更新する関数
            update() {
                this.x += this.dx; // X座標の更新
                this.y += this.dy; // Y座標の更新
                this.rotation += this.rotationSpeed; // 回転の更新

                // キャンバスの境界に達した場合、方向を反転
                if (this.x + this.size > canvas.width || this.x - this.size < 0) this.dx = -this.dx;
                if (this.y + this.size > canvas.height || this.y - this.size < 0) this.dy = -this.dy;

                this.draw(); // 図形の描画
            }

            // 描画メソッド（サブクラスでオーバーライドされる）
            draw() {}
        }

        // 各図形クラスの定義
        // 円を描画するクラス
        class Circle extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                ctx.beginPath();
                ctx.arc(0, 0, this.size, 0, Math.PI * 2);
                ctx.fillStyle = this.color;
                ctx.fill();
                ctx.restore();
            }
        }

        // 三角形を描画するクラス
        class Triangle extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                ctx.beginPath();
                ctx.moveTo(0, -this.size / 2);
                ctx.lineTo(-this.size / 2, this.size / 2);
                ctx.lineTo(this.size / 2, this.size / 2);
                ctx.closePath();
                ctx.fillStyle = this.color;
                ctx.fill();
                ctx.restore();
            }
        }

        // 四角形を描画するクラス
        class Square extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                ctx.fillStyle = this.color;
                ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size);
                ctx.restore();
            }
        }

        // 五角形を描画するクラス
        class Pentagon extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                ctx.beginPath();
                for (let i = 0; i < 5; i++) {
                    const angle = (Math.PI * 2 / 5) * i;
                    ctx.lineTo(this.size * Math.cos(angle), this.size * Math.sin(angle));
                }
                ctx.closePath();
                ctx.fillStyle = this.color;
                ctx.fill();
                ctx.restore();
            }
        }

        // 六角形を描画するクラス
        class Hexagon extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                ctx.beginPath();
                for (let i = 0; i < 6; i++) {
                    const angle = (Math.PI / 3) * i;
                    ctx.lineTo(this.size * Math.cos(angle), this.size * Math.sin(angle));
                }
                ctx.closePath();
                ctx.fillStyle = this.color;
                ctx.fill();
                ctx.restore();
            }
        }

        // 七宝文様を描画するクラス
        class Shippo extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);

                const radius = this.size / 2;
                for (let i = 0; i < 4; i++) {
                    ctx.beginPath();
                    ctx.arc(0, 0, radius, 0, Math.PI * 2);
                    ctx.strokeStyle = this.color;
                    ctx.lineWidth = 2;
                    ctx.stroke();
                    ctx.rotate(Math.PI / 2);
                }

                ctx.restore();
            }
        }

        // マンダラを描画するクラス
        class Mandala extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);

                const layers = 5;
                const elementsPerLayer = 8;

                for (let layer = 1; layer <= layers; layer++) {
                    const radius = (this.size / layers) * layer;

                    for (let i = 0; i < elementsPerLayer; i++) {
                        const angle = (i * Math.PI * 2) / elementsPerLayer;

                        ctx.save();
                        ctx.rotate(angle);

                        // 装飾的な要素の描画
                        ctx.beginPath();
                        ctx.moveTo(radius * 0.8, 0);
                        ctx.lineTo(radius, radius * 0.1);
                        ctx.lineTo(radius, -radius * 0.1);
                        ctx.closePath();
                        ctx.fillStyle = this.color;
                        ctx.fill();

                        ctx.restore();
                    }

                    // 円の描画
                    ctx.beginPath();
                    ctx.arc(0, 0, radius, 0, Math.PI * 2);
                    ctx.strokeStyle = this.color;
                    ctx.lineWidth = 1;
                    ctx.stroke();
                }

                ctx.restore();
            }
        }

        // 麻の葉文様を描画するクラス
        class Asanoha extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);

                const unit = this.size / 6;

                for (let i = 0; i < 6; i++) {
                    ctx.save();
                    ctx.rotate(i * Math.PI / 3);

                    // 菱形の描画
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.lineTo(unit * 2, -unit);
                    ctx.lineTo(unit * 4, 0);
                    ctx.lineTo(unit * 2, unit);
                    ctx.closePath();

                    ctx.strokeStyle = this.color;
                    ctx.lineWidth = 2;
                    ctx.stroke();

                    ctx.restore();
                }

                ctx.restore();
            }
        }

        // 青海波文様を描画するクラス
        class Seigaiha extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);

                const radius = this.size / 4;
                const rows = 3;
                const cols = 3;
                const offset = radius * 2;

                for (let row = 0; row < rows; row++) {
                    for (let col = 0; col < cols; col++) {
                        const x = col * offset - (row % 2 ? offset / 2 : 0);
                        const y = row * offset * 0.866; // √3/2 for hexagonal packing

                        for (let i = 3; i > 0; i--) {
                            ctx.beginPath();
                            ctx.arc(x, y, radius * (i / 3), 0, Math.PI);
                            ctx.strokeStyle = this.color;
                            ctx.lineWidth = 2;
                            ctx.stroke();
                        }
                    }
                }

                ctx.restore();
            }
        }

        // 菊菱文様を描画するクラス
        class Kikubishi extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);

                const petalCount = 16;
                const innerRadius = this.size * 0.3;
                const outerRadius = this.size * 0.5;

                ctx.beginPath();
                for (let i = 0; i < petalCount; i++) {
                    const angle = (i * Math.PI * 2) / petalCount;
                    const nextAngle = ((i + 1) * Math.PI * 2) / petalCount;

                    const midAngle = (angle + nextAngle) / 2;

                    ctx.moveTo(0, 0);
                    ctx.quadraticCurveTo(
                        Math.cos(midAngle) * outerRadius * 1.2,
                        Math.sin(midAngle) * outerRadius * 1.2,
                        Math.cos(nextAngle) * outerRadius,
                        Math.sin(nextAngle) * outerRadius
                    );
                }
                ctx.fillStyle = this.color;
                ctx.fill();

                ctx.beginPath();
                for (let i = 0; i < 4; i++) {
                    const angle = (i * Math.PI * 2) / 4;
                    if (i === 0) {
                        ctx.moveTo(Math.cos(angle) * innerRadius, Math.sin(angle) * innerRadius);
                    } else {
                        ctx.lineTo(Math.cos(angle) * innerRadius, Math.sin(angle) * innerRadius);
                    }
                }
                ctx.closePath();
                ctx.strokeStyle = this.color;
                ctx.lineWidth = 2;
                ctx.stroke();

                ctx.restore();
            }
        }

        // 矢絣文様を描画するクラス
        class Yagasuri extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);

                const unitSize = this.size / 4;
                const rows = 3;
                const cols = 3;

                for (let row = 0; row < rows; row++) {
                    for (let col = 0; col < cols; col++) {
                        const x = col * unitSize * 2 - this.size;
                        const y = row * unitSize * 2 - this.size;

                        ctx.beginPath();
                        ctx.moveTo(x, y);
                        ctx.lineTo(x + unitSize, y - unitSize);
                        ctx.lineTo(x + unitSize * 2, y);
                        ctx.lineTo(x + unitSize, y + unitSize);
                        ctx.closePath();
                        ctx.fillStyle = this.color;
                        ctx.fill();
                    }
                }

                ctx.restore();
            }
        }

        // 市松模様を描画するクラス
        class Ichimatsu extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);

                const squareSize = this.size / 4;
                const rows = 4;
                const cols = 4;

                for (let row = 0; row < rows; row++) {
                    for (let col = 0; col < cols; col++) {
                        if ((row + col) % 2 === 0) {
                            const x = col * squareSize - this.size / 2;
                            const y = row * squareSize - this.size / 2;

                            ctx.fillStyle = this.color;
                            ctx.fillRect(x, y, squareSize, squareSize);
                        }
                    }
                }

                ctx.restore();
            }
        }

        // 風車を描画するクラス
        class Windmill extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                ctx.beginPath();
                for (let i = 0; i < 4; i++) {
                    ctx.moveTo(0, 0);
                    ctx.lineTo(this.size, this.size * 0.3);
                    ctx.lineTo(this.size, -this.size * 0.3);
                    ctx.closePath();
                    ctx.rotate(Math.PI / 2);
                }
                ctx.fillStyle = this.color;
                ctx.fill();
                ctx.restore();
            }
        }

        // 星型を描画するクラス
        class Star extends Shape {
            draw() {
                const spikes = 5, outerRadius = this.size, innerRadius = this.size / 2;
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                ctx.beginPath();
                for (let i = 0; i < spikes; i++) {
                    ctx.lineTo(Math.cos((i * 2 * Math.PI) / spikes) * outerRadius, Math.sin((i * 2 * Math.PI) / spikes) * outerRadius);
                    ctx.lineTo(Math.cos(((i * 2 + 1) * Math.PI) / spikes) * innerRadius, Math.sin(((i * 2 + 1) * Math.PI) / spikes) * innerRadius);
                }
                ctx.closePath();
                ctx.fillStyle = this.color;
                ctx.fill();
                ctx.restore();
            }
        }


        // 桜の花を描画するクラス
        class Sakura extends Shape {
            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);

                const petalCount = 5;
                const angle = (Math.PI * 2) / petalCount;

                ctx.fillStyle = this.color;

                for (let i = 0; i < petalCount; i++) {
                    ctx.save();
                    ctx.rotate(i * angle);

                    // 花びらの描画
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.quadraticCurveTo(
                        this.size * 0.3, -this.size * 0.2,
                        this.size * 0.5, 0
                    );
                    ctx.quadraticCurveTo(
                        this.size * 0.3, this.size * 0.2,
                        0, 0
                    );
                    ctx.fill();
                    ctx.restore();
                }

                // 中心の円
                ctx.beginPath();
                ctx.arc(0, 0, this.size * 0.1, 0, Math.PI * 2);
                ctx.fillStyle = '#FFE3E3';
                ctx.fill();

                ctx.restore();
            }
        }

        // 図形のカテゴリごとの配列
        const shapeCategories = {
            general: [Circle, Triangle, Square, Pentagon, Hexagon], // 一般的な図形のカテゴリ
            patterns: [Shippo, Mandala, Asanoha, Seigaiha, Kikubishi, Yagasuri, Ichimatsu], // 文様のカテゴリ
            other: [Windmill, Star, Sakura] // その他の図形のカテゴリ
        };

        // ランダムな形状を生成する関数
        function getRandomShape(x, y) {
            const size = Math.random() * 20 + 10; // ランダムなサイズを決定
            const shapeClasses = shapeCategories[currentCategory]; // 現在のカテゴリの図形を取得
            const ShapeClass = shapeClasses[Math.floor(Math.random() * shapeClasses.length)]; // ランダムに図形クラスを選択
            const shapeInstance = new ShapeClass(x, y, size); // 図形のインスタンスを生成
            shapeNameDisplay.textContent = `図形: ${shapeNames[ShapeClass.name]}`; // 図形名を表示
            return shapeInstance;
        }

        // クリックイベントで図形を追加するイベントリスナー
        canvas.addEventListener('click', (event) => {
            const rect = canvas.getBoundingClientRect();
            const x = event.clientX - rect.left;
            const y = event.clientY - rect.top;
            shapes.push(getRandomShape(x, y)); // クリックした位置に図形を追加
        });

        // アニメーションの実行
        function animate() {
            ctx.clearRect(0, 0, canvas.width, canvas.height); // キャンバスをクリア
            shapes.forEach(shape => shape.update()); // すべての図形を更新
            requestAnimationFrame(animate); // アニメーションフレームのリクエスト
        }

        animate(); // アニメーションを開始

        // ウィンドウリサイズ時にキャンバスサイズを更新
        window.addEventListener('resize', () => {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
        });

        // 初期のアクティブボタンを設定
        updateActiveButton();
    </script>
</body>
</html>
```