見出し画像

マトリックス&ニキシー管風の時計WEBアプリ

今回ご紹介するWEBアプリは、映画マトリックス風の起動画面とニキシー管ライクな時計アプリです。こちらも、Claude 3.5の支援で作成しています。

機能としてはシンプルで、日付の表示/非表示、タイマー、背景の有無、動きの有無等があります。後付けでスマホ向けにも対応させました。実用性というよりアートですね。

設定パネルは展開時に設定値がローカルストレージに保存されます。
時計をクリックするか、設定パネル外をクリックすることで閉じることができます。

えもニキシー時計

↓以下単体で動作します。

↓自ドメインで公開されているバージョン。

動きのイメージ(スマホ)
スマホに限っては、スワイプ操作で閉じられるようにしました。
ただし、バグがあり再表示ができない場合があります。その際は再読み込みで対応となります。

ちょっとしたポイントとして、ランダムで下部に映画マトリックスの名言が表示されます。また、設定パネルを閉じたタイミングでも表示されるようにしました。
(30秒〜3分の間でランダム表示)

下部のメッセージ。なんとなく、マトリックスから語りかけられているようでワクワクします。
この辺書き換えると、自分の好きな名言を差し込めます。

このアプリ作成には、トータルで約1時間かかりました。主に、某ゲームのイベントのマッチング待ち(試合開始まで1分かかる!)の間に作業しました。(スキマ時間の有効活用🦑)

失敗ポイント

時計を押して設定パネルの表示/非表示を制御することは最初から考えていましたが、ユーザーがぱっと分からないかと思い、赤い薬、青い薬システムを実装しました。これはClaude 3.5側のアイデアです。

しかし、かえって動線が複雑になり、そもそも「時計を押して」の誘導の解決にならないので削除しました。

10秒待機すると、上部に表示される仕様でした。

結局、初期状態で設定パネルは展開され、画面外を触ると閉じる仕様にしました。時計を押して分からなくても、ページを再読み込みすると設定パネルが表示されるので、それで良しとしました

成功ポイント

JavaScriptのリファクタリングは英語で頼んでみました。なんとなく、これまでより綺麗になっている気がします。ただ、作りがかなりシンプルなので単純な比較はできません。仮にうまくリファクタリングできない場合、英語で頼むのも手かもしれません。

また、最初から着地点が明白だったので、大きな問題に直面することもありませんでした。往々にして、「やっぱりこれを入れよう!」と思いつきでやると、ぐちゃぐちゃになるので、そうなったらいっそ作り直した方がいいのかもしれませんね。

コード全量

規模や内容にもよりますが、HTMLやCSS、JavaScriptを含めて約1000行くらいなら1、2時間で見ることができそうです。ただし、それを超えると幾何級数的に時間がかかる気がします。つまり、2000行程度だから4時間?ではなく、2、3日はかかるかもしれません。(どこまで動きを担保するかにもよりますがね。)

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>えもニキシー管時計</title>
    <meta name="description"
        content="えもニキシー管時計は、サイバーパンクスタイルのインタラクティブな時計アプリケーションです。カスタマイズ可能なテーマとマトリックス風の背景効果を楽しめます。">
    <meta name="keywords" content="ニキシー管時計, マトリックス効果, サイバーパンク, インタラクティブ時計, Webアプリ">
    <meta name="author" content="Your Name">
    <meta property="og:title" content="えもニキシー管時計">
    <meta property="og:description" content="サイバーパンクスタイルのインタラクティブな時計アプリケーション。カスタマイズ可能なテーマとマトリックス風の背景効果を楽しめます。">
    <meta property="og:type" content="website">
    <meta property="og:url" content="https://matrix.tanu-ai.blog">
    <meta property="og:image" content="https://matrix.tanu-ai.blog/matrix-nixie-clock-image.png">
    <meta property="og:image:width" content="1200">
    <meta property="og:image:height" content="630">
    <meta property="og:image:alt" content="えもニキシー管時計のスクリーンショット">
    <link rel="canonical" href="https://matrix.tanu-ai.blog">
    <style>
        body,
        html {
            margin: 0;
            padding: 0;
            height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            font-family: 'Courier New', monospace;
            overflow: hidden;
            color: #fff;
            transition: background-color 1s ease;

        }

        #matrix-rain {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 10;
            opacity: 1;
            /* 初期状態で表示 */
        }

        #clock-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            z-index: 20;
            opacity: 0;
            transition: opacity 1s ease-in;
            padding: 20px;
            border-radius: 10px;
            background-color: transparent;
            /* 完全に透明に */
        }

        #clock,
        #date {
            font-size: 5em;
            perspective: 1000px;
            cursor: pointer;
        }

        #date {
            font-size: 2em;
            margin-top: 10px;
        }

        .nixie-tube {
            display: inline-flex;
            justify-content: center;
            align-items: center;
            width: 1em;
            height: 1.5em;
            margin: 0 0.1em;
            background-color: rgba(30, 30, 30, 0.8);
            /* 設定パネルのみ半透明に */
            border-radius: 0.2em;
            position: relative;
            overflow: hidden;
            box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5), 0 0 20px var(--glow-color, rgba(255, 140, 0, 0.3));
            transform-style: preserve-3d;
            transform: rotateX(5deg) rotateY(-5deg);
            transition: transform 0.3s ease;
        }

        .nixie-tube:hover {
            transform: rotateX(0deg) rotateY(0deg) scale(1.05);
        }

        .nixie-tube::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0.1) 100%);
            z-index: 1;
        }

        .nixie-tube::after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: radial-gradient(ellipse at center, var(--glow-color, rgba(255, 140, 0, 0.2)) 0%, rgba(255, 140, 0, 0) 70%);
            z-index: 2;
            opacity: 0.8;
        }

        .nixie-digit,
        .nixie-colon {
            position: relative;
            z-index: 3;
            color: var(--digit-color, #ff8c00);
            animation: candleFlicker 4s infinite alternate;
        }

        @keyframes candleFlicker {

            0%,
            100% {
                opacity: calc(1 - var(--flicker-intensity, 0.15));
                text-shadow: 0 0 10px var(--digit-color, #ff8c00), 0 0 20px var(--digit-color, #ff8c00);
            }

            25% {
                opacity: calc(0.9 - var(--flicker-intensity, 0.15));
                text-shadow: 0 0 8px var(--digit-color, #ff8c00), 0 0 16px var(--digit-color, #ff8c00);
            }

            50% {
                opacity: calc(0.95 - var(--flicker-intensity, 0.15));
                text-shadow: 0 0 12px var(--digit-color, #ff8c00), 0 0 24px var(--digit-color, #ff8c00);
            }

            75% {
                opacity: calc(0.85 - var(--flicker-intensity, 0.15));
                text-shadow: 0 0 9px var(--digit-color, #ff8c00), 0 0 18px var(--digit-color, #ff8c00);
            }
        }

        #controls {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin-top: 20px;
            background-color: rgba(30, 30, 30, 0.8);
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 15px rgba(255, 140, 0, 0.3);
            transition: opacity 0.5s ease, transform 0.5s ease;
        }

        #controls.hidden {
            opacity: 0;
            transform: translateY(20px);
            pointer-events: none;
        }

        #controls label {
            margin: 10px 0;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        #controls input[type="range"] {
            -webkit-appearance: none;
            width: 200px;
            height: 10px;
            background: #333;
            outline: none;
            border-radius: 5px;
            margin-top: 10px;
        }

        #controls input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 20px;
            height: 20px;
            background: #ff8c00;
            cursor: pointer;
            border-radius: 50%;
        }

        #controls select,
        #controls input[type="checkbox"] {
            background-color: #333;
            color: #ff8c00;
            border: none;
            padding: 5px 10px;
            border-radius: 5px;
            margin-top: 10px;
            cursor: pointer;
        }

        .switch {
            position: relative;
            display: inline-block;
            width: 60px;
            height: 34px;
            margin-top: 10px;
        }

        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 34px;
        }

        .slider:before {
            position: absolute;
            content: "";
            height: 26px;
            width: 26px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }

        input:checked+.slider {
            background-color: var(--digit-color, #ff8c00);
        }

        input:checked+.slider:before {
            transform: translateX(26px);
        }

        /*ボタンデザイン*/
        .nixie-button {
            background-color: rgba(30, 30, 30, 0.8);
            color: var(--digit-color, #ff8c00);
            border: 2px solid var(--digit-color, #ff8c00);
            padding: 10px 20px;
            margin: 5px;
            font-size: 1em;
            cursor: pointer;
            transition: all 0.3s ease;
            border-radius: 5px;
            text-shadow: 0 0 5px var(--digit-color, #ff8c00);
            box-shadow: 0 0 10px rgba(255, 140, 0, 0.3);
        }

        .nixie-button:hover {
            background-color: var(--digit-color, #ff8c00);
            color: #1a1a1a;
            text-shadow: none;
        }

        .nixie-button:active {
            transform: scale(0.98);
        }

        #timer-controls {
            display: none;
            justify-content: center;
            gap: 10px;
            margin-top: 20px;
        }

        /*映画マトリクスの名言を吐く*/
        #matrix-quote {
            position: absolute;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            font-family: 'Courier New', monospace;
            color: #00ff00;
            text-shadow: 0 0 5px #00ff00;
            opacity: 0;
            transition: opacity 2s ease-in-out;
            text-align: center;
            max-width: 80%;
            font-size: 1.2em;
            z-index: 30;
        }

        /*フッター*/
        .footer {
            margin-top: 20px;
            padding-top: 10px;
            border-top: 1px solid var(--digit-color, #ff8c00);
            font-size: 0.8em;
            text-align: center;
            color: var(--digit-color, #ff8c00);
            opacity: 0.7;
            transition: opacity 0.3s ease;
        }

        .footer:hover {
            opacity: 1;
        }

        .footer p {
            margin: 5px 0;
        }

        /* スマートフォン向けのスタイル */
        @media screen and (max-width: 768px) {

            .swipe-indicator {
                width: 40%;
                height: 5px;
                background-color: #888;
                border-radius: 2.5px;
                margin: 10px auto;
            }

            body {
                display: flex;
                flex-direction: column;
                justify-content: flex-start;
                padding-top: 20px;
            }

            #clock-container {
                margin-bottom: 60px;
                /* 設定パネル用のスペースを確保 */
            }

            #clock {
                font-size: 3em;
            }

            #date {
                font-size: 1.5em;
                /* 日付のサイズを小さく */
            }

            .nixie-tube {
                width: 0.8em;
                height: 1.2em;
                margin: 0 0.05em;
            }

            #controls {
                position: fixed;
                bottom: 0;
                left: 0;
                width: 100%;
                max-height: 80vh;
                overflow-y: auto;
                transition: transform 0.3s ease-in-out;
                z-index: 1000;
                background-color: rgba(30, 30, 30, 0.9);
                transform: translateY(0);
                /* 初期状態で表示 */
                -webkit-overflow-scrolling: touch;
                /* iOSのスムーズスクロール */
            }

            #controls.hidden {
                transform: translateY(100%) !important;
            }

            #controls label {
                font-size: 0.5em;
            }

            .nixie-button {
                padding: 8px 16px;
                font-size: 0.6em;
            }

            #matrix-quote {
                font-size: 0.7em;
                bottom: 10px;
            }
        }

        /* さらに小さい画面向け */
        @media screen and (max-width: 480px) {
            #clock {
                font-size: 2.5em;
            }

            #date {
                font-size: 1.2em;
            }

            .nixie-tube {
                width: 0.7em;
                height: 1em;
            }
        }

        @media screen and (max-width: 768px) {
            #controls {
                position: fixed;
                bottom: -100%;
                left: 0;
                width: 100%;
                max-height: 80vh;
                overflow-y: auto;
                transition: bottom 0.3s ease-in-out;
                z-index: 1000;
            }

            #controls.show {
                bottom: 0;
            }
        }
    </style>
</head>

<body>
    <canvas id="matrix-rain"></canvas>
    <div id="matrix-quote"></div>
    <div id="clock-container">
        <div id="clock"></div>
        <div id="date"></div>
        <div id="timer-controls">
            <button id="start-stop" class="nixie-button">開始 / 停止</button>
            <button id="reset" class="nixie-button">リセット</button>
        </div>
        <div id="controls">
            <div class="swipe-indicator"></div>
            <label>
                明滅の強度
                <input type="range" id="flicker-intensity" min="0" max="0.5" step="0.01" value="0.15">
            </label>
            <label>
                カラーテーマ
                <select id="color-theme">
                    <option value="classic">クラシック(オレンジ)</option>
                    <option value="neon">ネオン(マルチカラー)</option>
                    <option value="monochrome">モノクローム(緑)</option>
                    <option value="pastel">パステル</option>
                </select>
            </label>
            <label>
                日付を表示
                <div class="switch">
                    <input type="checkbox" id="show-date" checked>
                    <span class="slider"></span>
                </div>
            </label>
            <label>
                タイマーモード
                <div class="switch">
                    <input type="checkbox" id="timer-mode">
                    <span class="slider"></span>
                </div>
            </label>
            <label>
                マトリックス背景
                <div class="switch">
                    <input type="checkbox" id="matrix-background">
                    <span class="slider"></span>
                </div>
            </label>
            <label>
                背景アニメーション
                <div class="switch">
                    <input type="checkbox" id="matrix-animation" checked>
                    <span class="slider"></span>
                </div>
            </label>
            <!-- フッター部分を追加 -->
            <div id="footer" class="footer">
                <p>&copy; 2024 えもニキシー管時計 All Rights Reserved.</p>
                <p>制作: たぬ | 開発支援: Claude 3.5 (Anthropic AI)</p>
                <p>Version 1.0.0 "Cyber Timekeeper" (2024-07-15 18:17)</p>
            </div>
        </div>
    </div>

    <script>
        // Constants
        const CLOCK_UPDATE_INTERVAL = 1000;
        const MATRIX_ANIMATION_INTERVAL = 33;
        const QUOTE_MIN_DELAY = 30000;
        const QUOTE_MAX_DELAY = 180000;

        // DOM Elements
        const canvas = document.getElementById('matrix-rain');
        const ctx = canvas.getContext('2d');
        const clockDiv = document.getElementById('clock');
        const dateDiv = document.getElementById('date');
        const clockContainer = document.getElementById('clock-container');
        const controlsDiv = document.getElementById('controls');
        const timerControlsDiv = document.getElementById('timer-controls');
        const quoteElement = document.getElementById('matrix-quote');
        const flickerIntensitySlider = document.getElementById('flicker-intensity');
        const colorThemeSelect = document.getElementById('color-theme');
        const showDateCheckbox = document.getElementById('show-date');
        const timerModeSwitch = document.getElementById('timer-mode');
        const startStopButton = document.getElementById('start-stop');
        const resetButton = document.getElementById('reset');
        const matrixBackgroundSwitch = document.getElementById('matrix-background');
        const matrixAnimationSwitch = document.getElementById('matrix-animation');

        // Global Variables
        let currentTheme = 'classic';
        let flickerIntensity = 0.15;
        let showDate = true;
        let isTimerMode = false;
        let isTimerRunning = false;
        let timerSeconds = 0;
        let timerInterval;
        let showMatrixBackground = false;
        let animateMatrixBackground = true;
        let matrixInterval;
        let isControlsOpen = false;
        let touchStartY = 0;
        let touchEndY = 0;

        // Matrix Effect Setup
        canvas.height = window.innerHeight;
        canvas.width = window.innerWidth;
        const fontSize = 16;
        const columns = canvas.width / fontSize;
        const rainDrops = Array(Math.floor(columns)).fill(1);

        // Color Themes
        const colorThemes = {
            classic: {
                digits: [
                    { digit: '#ff8c00', glow: 'rgba(255, 140, 0, 0.2)' },
                    { digit: '#ff8c00', glow: 'rgba(255, 140, 0, 0.2)' },
                    { digit: '#ff8c00', glow: 'rgba(255, 140, 0, 0.2)' },
                    { digit: '#ff8c00', glow: 'rgba(255, 140, 0, 0.2)' },
                ],
                background: '#1a1a1a'
            },
            neon: {
                digits: [
                    { digit: '#ff00ff', glow: 'rgba(255, 0, 255, 0.2)' },
                    { digit: '#00ffff', glow: 'rgba(0, 255, 255, 0.2)' },
                    { digit: '#ffff00', glow: 'rgba(255, 255, 0, 0.2)' },
                    { digit: '#00ff00', glow: 'rgba(0, 255, 0, 0.2)' },
                ],
                background: '#000033'
            },
            monochrome: {
                digits: [
                    { digit: '#00ff00', glow: 'rgba(0, 255, 0, 0.2)' },
                    { digit: '#00ff00', glow: 'rgba(0, 255, 0, 0.2)' },
                    { digit: '#00ff00', glow: 'rgba(0, 255, 0, 0.2)' },
                    { digit: '#00ff00', glow: 'rgba(0, 255, 0, 0.2)' },
                ],
                background: '#001a00'
            },
            pastel: {
                digits: [
                    { digit: '#FFB3BA', glow: 'rgba(255, 179, 186, 0.2)' },
                    { digit: '#BAFFC9', glow: 'rgba(186, 255, 201, 0.2)' },
                    { digit: '#BAE1FF', glow: 'rgba(186, 225, 255, 0.2)' },
                    { digit: '#FFFFBA', glow: 'rgba(255, 255, 186, 0.2)' },
                ],
                background: '#2a2a2a'
            },
        };

        // Matrix Quotes
        const matrixQuotes = [
            "There is no spoon.",
            "Wake up, Neo...",
            "Follow the white rabbit.",
            "The Matrix has you...",
            "Knock, knock, Neo.",
            "I know kung fu.",
            "Free your mind!",
            "Welcome to the desert of the real.",
            "Remember... all I'm offering is the truth. Nothing more.",
            "Unfortunately, no one can be told what the Matrix is. You have to see it for yourself."
        ];

        // Functions
        function updateDisplay() {
            const now = new Date();
            const time = now.toTimeString().split(' ')[0];
            const date = now.toLocaleDateString('ja-JP', { year: 'numeric', month: 'long', day: 'numeric' });

            if (isTimerMode) {
                updateTimerDisplay();
            } else {
                updateClockDisplay(time, date);
            }

            dateDiv.style.display = showDate && !isTimerMode ? 'block' : 'none';
            timerControlsDiv.style.display = isTimerMode ? 'block' : 'none';
        }

        function updateTimerDisplay() {
            const hours = Math.floor(timerSeconds / 3600);
            const minutes = Math.floor((timerSeconds % 3600) / 60);
            const seconds = timerSeconds % 60;
            const time = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;

            clockDiv.innerHTML = createNixieDisplay(time);
        }

        function updateClockDisplay(time, date) {
            clockDiv.innerHTML = createNixieDisplay(time);
            dateDiv.innerHTML = createNixieDisplay(date);
        }

        function createNixieDisplay(text) {
            return text.split('').map((char, index) => {
                const color = colorThemes[currentTheme].digits[index % 4];
                if (char === ':') {
                    return `<span class="nixie-tube"><span class="nixie-colon" style="--digit-color: ${color.digit}; --glow-color: ${color.glow};">:</span></span>`;
                } else {
                    return `<span class="nixie-tube" style="--digit-color: ${color.digit}; --glow-color: ${color.glow}; --flicker-intensity: ${flickerIntensity};"><span class="nixie-digit">${char}</span></span>`;
                }
            }).join('');
        }

        function updateBackground() {
            document.body.style.backgroundColor = colorThemes[currentTheme].background;
        }

        function startStopTimer() {
            if (isTimerRunning) {
                clearInterval(timerInterval);
                startStopButton.textContent = '開始';
            } else {
                timerInterval = setInterval(() => {
                    timerSeconds++;
                    updateDisplay();
                }, CLOCK_UPDATE_INTERVAL);
                startStopButton.textContent = '停止';
            }
            isTimerRunning = !isTimerRunning;
        }

        function resetTimer() {
            clearInterval(timerInterval);
            timerSeconds = 0;
            isTimerRunning = false;
            startStopButton.textContent = '開始';
            updateDisplay();
        }

        function updateButtonColors() {
            const buttons = document.querySelectorAll('.nixie-button');
            const color = colorThemes[currentTheme].digits[0].digit;
            buttons.forEach(button => {
                button.style.setProperty('--digit-color', color);
            });
        }

        function toggleMatrixBackground() {
            showMatrixBackground = matrixBackgroundSwitch.checked;
            canvas.style.opacity = showMatrixBackground ? '0.15' : '0';
            if (showMatrixBackground) {
                clearBackground();
                if (animateMatrixBackground && !matrixInterval) {
                    matrixInterval = setInterval(drawMatrix, MATRIX_ANIMATION_INTERVAL);
                }
            } else {
                clearInterval(matrixInterval);
                matrixInterval = null;
                clearBackground();
            }
            saveSettings();
        }

        function toggleMatrixAnimation() {
            animateMatrixBackground = matrixAnimationSwitch.checked;
            if (showMatrixBackground && animateMatrixBackground && !matrixInterval) {
                clearBackground();
                matrixInterval = setInterval(drawMatrix, MATRIX_ANIMATION_INTERVAL);
            } else if (!animateMatrixBackground) {
                clearInterval(matrixInterval);
                matrixInterval = null;
            }
            saveSettings();
        }

        function saveSettings() {
            const settings = {
                theme: currentTheme,
                flickerIntensity: flickerIntensity,
                showDate: showDate,
                isTimerMode: isTimerMode,
                showMatrixBackground: showMatrixBackground,
                showControls: !controlsDiv.classList.contains('hidden'),
                animateMatrixBackground: animateMatrixBackground
            };
            localStorage.setItem('nixieClockSettings', JSON.stringify(settings));
        }

        function loadSettings() {
            const savedSettings = localStorage.getItem('nixieClockSettings');
            if (savedSettings) {
                const settings = JSON.parse(savedSettings);
                // Apply saved settings
                currentTheme = settings.theme;
                flickerIntensity = settings.flickerIntensity;
                showDate = settings.showDate;
                isTimerMode = settings.isTimerMode;
                showMatrixBackground = settings.showMatrixBackground;
                animateMatrixBackground = settings.animateMatrixBackground !== undefined ? settings.animateMatrixBackground : true;

                // Update UI elements
                colorThemeSelect.value = currentTheme;
                flickerIntensitySlider.value = flickerIntensity;
                showDateCheckbox.checked = showDate;
                timerModeSwitch.checked = isTimerMode;
                matrixBackgroundSwitch.checked = showMatrixBackground;
                matrixAnimationSwitch.checked = animateMatrixBackground;

                toggleMatrixBackground();
                toggleMatrixAnimation();
            } else {
                clearBackground();
            }
        }

        function drawMatrix() {
            ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            ctx.fillStyle = '#0F0';
            ctx.font = fontSize + 'px monospace';

            for (let i = 0; i < rainDrops.length; i++) {
                const text = alphabet.charAt(Math.floor(Math.random() * alphabet.length));
                ctx.fillText(text, i * fontSize, rainDrops[i] * fontSize);

                if (rainDrops[i] * fontSize > canvas.height && Math.random() > 0.975) {
                    rainDrops[i] = 0;
                }
                rainDrops[i]++;
            }
        }

        function showRandomQuote() {
            const randomQuote = matrixQuotes[Math.floor(Math.random() * matrixQuotes.length)];
            quoteElement.textContent = randomQuote;
            quoteElement.style.opacity = '1';

            setTimeout(() => {
                quoteElement.style.opacity = '0';
            }, 5000);
        }

        function scheduleNextQuote() {
            const randomDelay = Math.floor(Math.random() * (QUOTE_MAX_DELAY - QUOTE_MIN_DELAY + 1) + QUOTE_MIN_DELAY);

            setTimeout(() => {
                showRandomQuote();
                scheduleNextQuote();
            }, randomDelay);
        }

        function adjustClockSize() {
            if (window.innerWidth < 480) {
                clockDiv.style.fontSize = '2.5em';
            } else if (window.innerWidth < 768) {
                clockDiv.style.fontSize = '3em';
            } else {
                clockDiv.style.fontSize = '5em';
            }
        }

        function toggleControls() {
            isControlsOpen = !isControlsOpen;

            if (window.innerWidth <= 768) {
                controlsDiv.style.transform = isControlsOpen ? 'translateY(0)' : 'translateY(100%)';
            } else {
                controlsDiv.classList.toggle('hidden');
            }

            if (!isControlsOpen) {
                showRandomQuote();
            }
        }

        function initMobileControls() {
            if (window.innerWidth <= 768) {
                controlsDiv.classList.remove('hidden');
                controlsDiv.style.bottom = '-100%';
            }
        }

        function initControls() {
            isControlsOpen = true;

            if (window.innerWidth <= 768) {
                controlsDiv.classList.add('show');
                controlsDiv.style.bottom = '0';
            } else {
                controlsDiv.classList.remove('hidden');
            }
        }

        function handleSwipe() {
            if (touchEndY - touchStartY > 50) {
                if (isControlsOpen) {
                    toggleControls();
                }
            }
        }

        function init() {
            clearBackground();
            matrixInterval = setInterval(drawMatrix, MATRIX_ANIMATION_INTERVAL);

            setTimeout(() => {
                canvas.style.opacity = '0.15';
                clockContainer.style.opacity = '1';
                loadSettings();
                adjustClockSize();
                window.addEventListener('resize', adjustClockSize);
                setInterval(updateDisplay, CLOCK_UPDATE_INTERVAL);
                updateDisplay();
                updateBackground();
                toggleMatrixBackground();
                updateButtonColors();
                scheduleNextQuote();
                initControls();
                initSwipeToClose();
            }, 5000);
        }

        // Event Listeners
        flickerIntensitySlider.addEventListener('input', (e) => {
            flickerIntensity = parseFloat(e.target.value);
            updateDisplay();
            saveSettings();
        });

        colorThemeSelect.addEventListener('change', (e) => {
            currentTheme = e.target.value;
            updateDisplay();
            updateBackground();
            updateButtonColors();
            saveSettings();
        });

        showDateCheckbox.addEventListener('change', (e) => {
            showDate = e.target.checked;
            updateDisplay();
            saveSettings();
        });

        timerModeSwitch.addEventListener('change', (e) => {
            isTimerMode = e.target.checked;
            resetTimer();
            updateDisplay();
            saveSettings();
        });

        startStopButton.addEventListener('click', startStopTimer);
        resetButton.addEventListener('click', resetTimer);

        matrixBackgroundSwitch.addEventListener('change', toggleMatrixBackground);
        matrixAnimationSwitch.addEventListener('change', toggleMatrixAnimation);

        clockDiv.addEventListener('click', (e) => {
            e.stopPropagation();
            console.log('Clock clicked'); // デバッグ用
            toggleControls();
        });

        document.addEventListener('click', (e) => {
            if (isControlsOpen && !controlsDiv.contains(e.target) && e.target !== clockDiv) {
                toggleControls();
            }
        });

        controlsDiv.addEventListener('click', (e) => {
            e.stopPropagation();
        });

        window.addEventListener('resize', adjustClockSize);

        // Touch event listeners for swipe
        controlsDiv.addEventListener('touchstart', function (e) {
            touchStartY = e.touches[0].clientY;
        }, { passive: false });

        controlsDiv.addEventListener('touchmove', function (e) {
            touchEndY = e.touches[0].clientY;
            if (touchStartY - touchEndY > 0) {
                return;
            }
            e.preventDefault();
        }, { passive: false });

        controlsDiv.addEventListener('touchend', function (e) {
            handleSwipe();
        }, { passive: false });

        // Utility functions
        function clearBackground() {
            ctx.fillStyle = 'rgb(0, 0, 0)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
        }

        function initSwipeToClose() {
            // Already set up in the event listeners above
        }

        // Matrix effect setup
        const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン';
        const latin = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        const nums = '0123456789';
        const alphabet = katakana + latin + nums;

        // Initialize the application
        init();
    </script>
</body>

</html>

この記事が気に入ったらサポートをしてみませんか?