4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-11-05

はじめに

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

デモ

画面:
スクリーンショット 2024-11-05 11.39.12.png

仕様

図形の種類

  1. 一般的な図形
    • 三角形
    • 四角形
    • 五角形
    • 六角形
  2. 文様
    • 七宝文様
    • マンダラ
    • 麻の葉文様
    • 青海波
    • 菊菱
    • 矢絣
    • 市松模様
  3. その他
    • 風車

図形の描画

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

ボタン

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

図形名

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

コード

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>
4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?