Project structure:

LitGeiger/
├── static/
│   ├── css/
│   │   └── styles.css
│   ├── images/
│   │   └── geiger.webp
│   ├── js/
│   │   └── app.js
├── templates/
│   └── index2.html
└── main.py


Project files:

main.py
```py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def main():
    return render_template("index2.html")

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8081)
```


static/css/styles.css
```css
body {
    font-family: Arial, sans-serif;
    text-align: center;
    padding: 0;
    background-color: #f0f0f0;
    margin: 0;
}

h1 {
    color: #333;
}

.main-screen {
    position: fixed;
    width: 100%;
    height: 100vh;
    display: flex;
}

#radiationLevel {
    font-weight: bold;
    color: #ffffff; /* Default color set to white */
    font-size: 90px;
    opacity: 0.5;
    filter: blur(0.7px);
}

#radiationSlider {
    width: 100%;
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    opacity: 0.1;
}

#startButton {
    position: absolute;
    z-index: 2;
    left: 21%;
    top: 57%;
    width: 12%;
    height: 6%;
    background: none;
    border: none;
    outline: none;
    cursor: pointer; /* Added for better UX */
}

#startButton:hover {
    background-color: #4cae4c;
}

.controls {
    z-index: 2;
    position: absolute;
    left: 10%;
    bottom: 12%;
}

.controls label {
    font-size: 16px;
    margin-left: 10px;
}

.background-img {
    width: 100%;
    height: 100vh;
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
}

.screen {
    display: flex;
    position: absolute;
    width: 61%;
    height: 35%; /* Increased height to accommodate the graph */
    background: none;
    left: 20%;
    top: 15%;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

.radiationLevel {
    color: #ffffff; /* Initial color */
    font-size: 90px;
    opacity: 0.5;
    filter: blur(0.7px);
    transition: color 0.5s; /* Smooth color transition */
}

#radiationGraph {
    width: 100%;
    height: 100px;
    margin-top: 20px;
    display: block;
    background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
    border-radius: 5px; /* Rounded corners for aesthetics */
}
```


static/js/app.js
```js
$(document).ready(function () {
    let radiationLevel = 0;
    let beepTimeout;
    let audioCtx;
    let masterGain;
    let lfo;
    let isAudioInitialized = false;

    // Graph Configuration
    const graphData = [];
    const maxGraphPoints = 100; // Maximum number of data points displayed
    const graphUpdateInterval = 100; // Interval in milliseconds for graph updates

    const canvas = document.getElementById('radiationGraph');
    const ctx = canvas.getContext('2d');

    // Function to resize the canvas based on its container
    function resizeCanvas() {
        canvas.width = canvas.clientWidth;
        canvas.height = canvas.clientHeight;
    }

    // Initial canvas resize
    resizeCanvas();

    // Resize the canvas when the window size changes
    window.addEventListener('resize', resizeCanvas);

    // Function to draw the real-time graph
    function drawGraph() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        // Draw semi-transparent background
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        if (graphData.length < 2) return; // Need at least two points to draw

        // Determine the color based on the latest value
        const latestValue = graphData[graphData.length - 1];
        const graphColor = latestValue > 200 ? '#d9534f' : '#00ff00'; // Red if >200, else Green

        ctx.strokeStyle = graphColor;
        ctx.lineWidth = 2;
        ctx.beginPath();

        const step = canvas.width / maxGraphPoints;
        const maxDisplayValue = 1000; // As per mapping
        const minDisplayValue = 50;

        graphData.forEach((value, index) => {
            const x = index * step;
            // Invert Y axis: Higher values appear at the top
            const y = canvas.height - ((value - minDisplayValue) / (maxDisplayValue - minDisplayValue)) * canvas.height;
            if (index === 0) {
                ctx.moveTo(x, y);
            } else {
                ctx.lineTo(x, y);
            }
        });

        ctx.stroke();
    }

    // Periodically update the graph
    setInterval(drawGraph, graphUpdateInterval);

    // Function to map slider value (0-100) to display value (50-1000)
    function getDisplayValue(level) {
        return Math.round(50 + level * 9.5);
    }

    // Function to update the displayed radiation level and graph data
    function updateDisplay() {
        const displayValue = getDisplayValue(radiationLevel);
        $('#radiationLevel').text(displayValue);

        // Update text color based on the display value
        if (displayValue > 200) {
            $('#radiationLevel').css('color', '#d9534f'); // Red
        } else {
            $('#radiationLevel').css('color', '#ffffff'); // White
        }

        // Update graph data
        graphData.push(displayValue);
        if (graphData.length > maxGraphPoints) {
            graphData.shift(); // Remove the oldest data point to maintain the max points
        }
    }

    // Audio Configuration and Functions
    const beepDuration = 0.05; // Duration of each beep in seconds
    const attackTime = 0.005; // Attack time for ADSR envelope
    const decayTime = 0.015;  // Decay time for ADSR envelope

    // Initialize Audio Context and Nodes
    function initAudio() {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();

        // Create Master Gain Node
        masterGain = audioCtx.createGain();
        masterGain.gain.setValueAtTime(0.8, audioCtx.currentTime); // Set master volume
        masterGain.connect(audioCtx.destination);

        // Create Low-Frequency Oscillator (LFO) for global volume modulation
        lfo = audioCtx.createOscillator();
        const lfoGain = audioCtx.createGain();
        lfo.frequency.setValueAtTime(0.2, audioCtx.currentTime); // LFO frequency: 0.2 Hz
        lfoGain.gain.setValueAtTime(0.05, audioCtx.currentTime);   // LFO amplitude
        lfo.connect(lfoGain);
        lfoGain.connect(masterGain.gain);
        lfo.start();

        isAudioInitialized = true;
    }

    // Function to start a beep sound
    function startBeep() {
        if (!isAudioInitialized) return;

        const oscillator = audioCtx.createOscillator();
        const gainNode = audioCtx.createGain();

        // Set frequency with slight random variation for a natural sound
        const baseFreq = 880;
        const variation = (Math.random() - 0.5) * 20; // +/-10 Hz variation
        oscillator.frequency.setValueAtTime(baseFreq + variation, audioCtx.currentTime);
        oscillator.type = 'sine';

        // Configure ADSR envelope for smooth beep
        const now = audioCtx.currentTime;
        gainNode.gain.setValueAtTime(0, now);
        gainNode.gain.linearRampToValueAtTime(0.5, now + attackTime);
        gainNode.gain.linearRampToValueAtTime(0, now + attackTime + decayTime);

        oscillator.connect(gainNode);
        gainNode.connect(masterGain);
        oscillator.start();
        oscillator.stop(now + beepDuration + decayTime + 0.1); // Ensure oscillator stops after envelope

        // Cleanup after beep ends
        oscillator.onended = function () {
            oscillator.disconnect();
            gainNode.disconnect();
        };
    }

    // Function to schedule beeps based on radiation level
    function scheduleBeep() {
        if (radiationLevel === 0) return;

        // Calculate average interval between beeps (lower interval for higher radiation)
        const averageInterval = 600 / (radiationLevel + 1); // Adjusted formula for interval

        // Introduce randomness to the beep intervals
        const jitter = averageInterval * 0.3;
        const interval = averageInterval + (Math.random() * jitter * 2 - jitter);

        beepTimeout = setTimeout(() => {
            // Determine the chance of overlapping beeps based on radiation level
            const overlapChance = radiationLevel > 80 ? 0.3 :
                radiationLevel > 60 ? 0.2 :
                    radiationLevel > 40 ? 0.1 : 0.05;

            if (Math.random() < overlapChance) {
                // Start multiple beeps simultaneously for higher radiation levels
                const beepCount = Math.floor(Math.random() * 3) + 1; // 1, 2, or 3 beeps
                for (let i = 0; i < beepCount; i++) {
                    startBeep();
                }
            } else {
                startBeep();
            }

            scheduleBeep(); // Schedule the next beep
        }, interval);
    }

    // Function to update beep scheduling
    function updateBeep() {
        clearTimeout(beepTimeout);
        if (radiationLevel > 0) {
            scheduleBeep();
        }
    }

    // Event Handler for Slider Input
    $('#radiationSlider').on('input', function () {
        if (useGyroscope) return; // Ignore slider if gyroscope is active

        const newRadiationLevel = parseInt($(this).val());
        if (newRadiationLevel !== radiationLevel) {
            radiationLevel = newRadiationLevel;
            updateDisplay();
            if (isAudioInitialized) {
                updateBeep();
            }
        }
    });

    // Event Handler for Start Button
    $('#startButton').click(function () {
        if (!isAudioInitialized) {
            initAudio();
            updateBeep();
            $(this).fadeOut(); // Hide the start button after initialization
        }
    });

    // Gyroscope Configuration and Handlers
    let useGyroscope = false;
    let initialOrientation = null;
    let deviceOrientationListener = null;

    $('#gyroscopeCheckbox').on('change', function () {
        useGyroscope = $(this).is(':checked');

        if (useGyroscope) {
            // Initialize Gyroscope
            if (window.DeviceOrientationEvent) {
                // For iOS 13+ devices requiring permission
                if (typeof DeviceOrientationEvent.requestPermission === 'function') {
                    DeviceOrientationEvent.requestPermission()
                        .then(response => {
                            if (response === 'granted') {
                                startGyroscope();
                            } else {
                                alert('Доступ к гироскопу отклонен.');
                                $('#gyroscopeCheckbox').prop('checked', false);
                            }
                        })
                        .catch(() => {
                            alert('Не удалось получить доступ к гироскопу.');
                            $('#gyroscopeCheckbox').prop('checked', false);
                        });
                } else {
                    // For browsers that do not require permission
                    startGyroscope();
                }
            } else {
                alert('Гироскоп не поддерживается вашим устройством или браузером.');
                $('#gyroscopeCheckbox').prop('checked', false);
            }
        } else {
            // Stop Gyroscope
            if (deviceOrientationListener) {
                window.removeEventListener('deviceorientation', deviceOrientationListener);
                deviceOrientationListener = null;
            }
            initialOrientation = null;
            // Re-enable the slider as gyroscope is inactive
            $('#radiationSlider').prop('disabled', false);
        }
    });

    // Function to start gyroscope event listening
    function startGyroscope() {
        initialOrientation = null;

        deviceOrientationListener = function (event) {
            if (!initialOrientation) {
                initialOrientation = {
                    alpha: event.alpha,
                    beta: event.beta,
                    gamma: event.gamma
                };
                return;
            }

            // Calculate changes in orientation
            let deltaAlpha = Math.abs(event.alpha - initialOrientation.alpha);
            let deltaBeta = Math.abs(event.beta - initialOrientation.beta);
            let deltaGamma = Math.abs(event.gamma - initialOrientation.gamma);

            // Handle angle wrapping around 360 degrees
            deltaAlpha = deltaAlpha > 180 ? 360 - deltaAlpha : deltaAlpha;
            deltaBeta = deltaBeta > 180 ? 360 - deltaBeta : deltaBeta;
            deltaGamma = deltaGamma > 180 ? 360 - deltaGamma : deltaGamma;

            // Ignore rotation around Y-axis (gamma)
            deltaAlpha = 0;

            // Calculate overall deviation
            const deviation = Math.sqrt(deltaAlpha * deltaAlpha + deltaBeta * deltaBeta);

            // Scale deviation to 0-100 (assuming max 90 degrees)
            const scaledDeviation = Math.min(Math.floor((deviation / 90) * 100), 100);

            // Update radiation level if it has changed
            if (scaledDeviation !== radiationLevel) {
                radiationLevel = scaledDeviation;
                updateDisplay();
                $('#radiationSlider').val(radiationLevel);
                if (isAudioInitialized) {
                    updateBeep();
                }
            }
        };

        window.addEventListener('deviceorientation', deviceOrientationListener);
        // Disable the slider as gyroscope is now active
        $('#radiationSlider').prop('disabled', true);
    }

    // Cleanup Functions on Page Unload
    $(window).on('beforeunload', function () {
        if (audioCtx) {
            audioCtx.close();
        }
        if (deviceOrientationListener) {
            window.removeEventListener('deviceorientation', deviceOrientationListener);
        }
    });
});
```


templates/index2.html
```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Счетчик Гейгера</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    <!-- jQuery Library -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- Link to the CSS file -->
    <link rel="stylesheet" href="../static/css/styles.css">
</head>
<body>

<div class="main-screen">
    <img class="background-img" src="../static/images/geiger.webp" alt="Geiger Counter Background">
    <div class="screen">
        <!-- Displayed Radiation Level -->
        <div class="radiationLevel" id="radiationLevel">50</div>
        <!-- Slider to adjust Radiation Level -->
        <input type="range" id="radiationSlider" min="0" max="100" value="0">
        <!-- Real-Time Graph -->
        <canvas id="radiationGraph"></canvas>
    </div>

    <br>
    <!-- Start Button -->
    <button id="startButton"></button>
    <div class="controls">
        <!-- Gyroscope Checkbox -->
        <input type="checkbox" id="gyroscopeCheckbox">
        <label for="gyroscopeCheckbox">Гироскоп</label>
    </div>

</div>
<!-- Link to the JavaScript file -->
<script src="../static/js/app.js"></script>

</body>
</html>
```


Сдеалй так, чтобы число и график сами по себе постоянно немного прыгали, не отходя далеко от реального числа, чтобы это было реалистичнее, а также, чтобы график двигался сам по себе