| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Webcam Brightness Meter</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .brightness-meter { |
| background: linear-gradient(90deg, #000000 0%, #333333 20%, #666666 40%, #999999 60%, #cccccc 80%, #ffffff 100%); |
| } |
| .brightness-indicator { |
| transition: left 0.3s ease; |
| } |
| .pulse { |
| animation: pulse 2s infinite; |
| } |
| @keyframes pulse { |
| 0% { |
| box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); |
| } |
| 70% { |
| box-shadow: 0 0 0 10px rgba(255, 255, 255, 0); |
| } |
| 100% { |
| box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); |
| } |
| } |
| #chart-container { |
| height: 200px; |
| width: 100%; |
| position: relative; |
| } |
| #historyChart { |
| width: 100% !important; |
| height: 200px !important; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-900 text-white min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <header class="text-center mb-8"> |
| <h1 class="text-4xl font-bold mb-2">Webcam Brightness Meter</h1> |
| <p class="text-gray-400">Measure ambient light levels using your webcam</p> |
| </header> |
|
|
| <main class="max-w-4xl mx-auto"> |
| <div class="bg-gray-800 rounded-xl p-6 shadow-lg mb-8"> |
| <div class="flex flex-col md:flex-row gap-6"> |
| <div class="w-full md:w-1/2"> |
| <div class="relative aspect-video bg-black rounded-lg overflow-hidden mb-4"> |
| <video id="webcam" autoplay playsinline class="w-full h-full object-cover"></video> |
| <div id="overlay" class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center"> |
| <div class="text-center p-4"> |
| <i class="fas fa-camera text-4xl mb-2"></i> |
| <p>Webcam access required</p> |
| </div> |
| </div> |
| </div> |
| <div class="flex justify-center gap-4"> |
| <button id="startBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"> |
| <i class="fas fa-play"></i> Start |
| </button> |
| <button id="stopBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg flex items-center gap-2" disabled> |
| <i class="fas fa-stop"></i> Stop |
| </button> |
| </div> |
| </div> |
|
|
| <div class="w-full md:w-1/2"> |
| <div class="bg-gray-700 p-4 rounded-lg h-full"> |
| <h2 class="text-xl font-semibold mb-4">Brightness Analysis</h2> |
| |
| <div class="mb-6"> |
| <div class="flex justify-between mb-2"> |
| <span>0%</span> |
| <span id="brightnessValue" class="text-xl font-bold">--</span> |
| <span>100%</span> |
| </div> |
| <div class="brightness-meter h-4 rounded-full relative"> |
| <div id="brightnessIndicator" class="brightness-indicator absolute top-0 -ml-2 -mt-2 w-4 h-8 bg-white rounded-full shadow-lg"></div> |
| </div> |
| </div> |
|
|
| <div class="grid grid-cols-2 gap-4"> |
| <div class="bg-gray-600 p-3 rounded-lg"> |
| <div class="text-sm text-gray-300 mb-1">Light Level</div> |
| <div id="lightLevel" class="text-2xl font-bold">--</div> |
| </div> |
| <div class="bg-gray-600 p-3 rounded-lg"> |
| <div class="text-sm text-gray-300 mb-1">Status</div> |
| <div id="lightStatus" class="text-2xl font-bold">Inactive</div> |
| </div> |
| </div> |
|
|
| <div class="mt-6 bg-gray-600 p-3 rounded-lg"> |
| <div class="text-sm text-gray-300 mb-1">Tips</div> |
| <ul class="text-sm list-disc pl-5 space-y-1"> |
| <li>Point your webcam at the area you want to measure</li> |
| <li>For best results, avoid direct light sources in frame</li> |
| <li>Values update every second</li> |
| </ul> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="bg-gray-800 rounded-xl p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-4">Brightness History</h2> |
| <div id="chart-container"> |
| <canvas id="historyChart"></canvas> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <script> |
| |
| const video = document.getElementById('webcam'); |
| const overlay = document.getElementById('overlay'); |
| const startBtn = document.getElementById('startBtn'); |
| const stopBtn = document.getElementById('stopBtn'); |
| const brightnessValue = document.getElementById('brightnessValue'); |
| const brightnessIndicator = document.getElementById('brightnessIndicator'); |
| const lightLevel = document.getElementById('lightLevel'); |
| const lightStatus = document.getElementById('lightStatus'); |
| |
| |
| let stream = null; |
| let interval = null; |
| let brightnessHistory = []; |
| const maxHistory = 30; |
| let chart = null; |
| let isVideoReady = false; |
| |
| |
| function initChart() { |
| const ctx = document.getElementById('historyChart').getContext('2d'); |
| chart = new Chart(ctx, { |
| type: 'line', |
| data: { |
| labels: Array(maxHistory).fill(''), |
| datasets: [{ |
| label: 'Brightness', |
| data: brightnessHistory, |
| borderColor: 'rgb(59, 130, 246)', |
| backgroundColor: 'rgba(59, 130, 246, 0.1)', |
| tension: 0.3, |
| fill: true, |
| pointRadius: 0 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| animation: { |
| duration: 0 |
| }, |
| scales: { |
| y: { |
| min: 0, |
| max: 100, |
| ticks: { |
| callback: function(value) { |
| return value + '%'; |
| } |
| } |
| }, |
| x: { |
| display: false |
| } |
| }, |
| plugins: { |
| legend: { |
| display: false |
| }, |
| tooltip: { |
| callbacks: { |
| label: function(context) { |
| return 'Brightness: ' + context.parsed.y.toFixed(1) + '%'; |
| } |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| function updateChart() { |
| if (chart) { |
| chart.data.datasets[0].data = brightnessHistory; |
| chart.update('none'); |
| } |
| } |
| |
| |
| function calculateBrightness() { |
| if (!isVideoReady || video.videoWidth === 0 || video.videoHeight === 0) { |
| throw new Error('Video not ready for measurement'); |
| } |
| |
| const canvas = document.createElement('canvas'); |
| canvas.width = video.videoWidth; |
| canvas.height = video.videoHeight; |
| const ctx = canvas.getContext('2d'); |
| |
| ctx.drawImage(video, 0, 0, canvas.width, canvas.height); |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| const data = imageData.data; |
| |
| let brightnessSum = 0; |
| const pixelCount = data.length / 4; |
| |
| for (let i = 0; i < data.length; i += 4) { |
| |
| const r = data[i]; |
| const g = data[i + 1]; |
| const b = data[i + 2]; |
| const brightness = 0.299 * r + 0.587 * g + 0.114 * b; |
| brightnessSum += brightness; |
| } |
| |
| const avgBrightness = (brightnessSum / pixelCount) / 2.55; |
| return Math.round(avgBrightness * 10) / 10; |
| } |
| |
| |
| function updateBrightnessUI(brightness) { |
| brightnessValue.textContent = `${brightness}%`; |
| brightnessIndicator.style.left = `${brightness}%`; |
| |
| |
| lightLevel.textContent = `${brightness}%`; |
| |
| |
| let status = ''; |
| let statusColor = ''; |
| |
| if (brightness < 20) { |
| status = 'Very Dark'; |
| statusColor = 'text-purple-400'; |
| } else if (brightness < 40) { |
| status = 'Dark'; |
| statusColor = 'text-blue-400'; |
| } else if (brightness < 60) { |
| status = 'Moderate'; |
| statusColor = 'text-green-400'; |
| } else if (brightness < 80) { |
| status = 'Bright'; |
| statusColor = 'text-yellow-400'; |
| } else { |
| status = 'Very Bright'; |
| statusColor = 'text-red-400'; |
| } |
| |
| lightStatus.textContent = status; |
| lightStatus.className = `text-2xl font-bold ${statusColor}`; |
| |
| |
| brightnessHistory.push(brightness); |
| if (brightnessHistory.length > maxHistory) { |
| brightnessHistory.shift(); |
| } |
| |
| updateChart(); |
| } |
| |
| |
| async function startMeasuring() { |
| try { |
| |
| if (interval) { |
| clearInterval(interval); |
| interval = null; |
| } |
| |
| |
| if (!stream) { |
| await initWebcam(); |
| } |
| |
| |
| if (!isVideoReady) { |
| await new Promise((resolve) => { |
| const checkReady = () => { |
| if (video.videoWidth > 0 && video.videoHeight > 0) { |
| isVideoReady = true; |
| resolve(); |
| } else { |
| setTimeout(checkReady, 100); |
| } |
| }; |
| checkReady(); |
| }); |
| } |
| |
| overlay.classList.add('hidden'); |
| startBtn.disabled = true; |
| stopBtn.disabled = false; |
| |
| |
| const brightness = calculateBrightness(); |
| updateBrightnessUI(brightness); |
| |
| |
| interval = setInterval(() => { |
| try { |
| const brightness = calculateBrightness(); |
| updateBrightnessUI(brightness); |
| } catch (err) { |
| console.error('Error during measurement:', err); |
| |
| stopMeasuring(); |
| overlay.classList.remove('hidden'); |
| overlay.innerHTML = ` |
| <div class="text-center p-4"> |
| <i class="fas fa-exclamation-triangle text-4xl mb-2 text-red-500"></i> |
| <p>Measurement error</p> |
| <p class="text-sm text-gray-300 mt-2">${err.message}</p> |
| <button id="retryBtn" class="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"> |
| <i class="fas fa-sync-alt mr-2"></i> Try Again |
| </button> |
| </div> |
| `; |
| document.getElementById('retryBtn').addEventListener('click', startMeasuring); |
| } |
| }, 1000); |
| } catch (err) { |
| console.error('Error starting measurements:', err); |
| overlay.classList.remove('hidden'); |
| overlay.innerHTML = ` |
| <div class="text-center p-4"> |
| <i class="fas fa-exclamation-triangle text-4xl mb-2 text-red-500"></i> |
| <p>Error starting measurements</p> |
| <p class="text-sm text-gray-300 mt-2">${err.message}</p> |
| <button id="retryBtn" class="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"> |
| <i class="fas fa-sync-alt mr-2"></i> Try Again |
| </button> |
| </div> |
| `; |
| document.getElementById('retryBtn').addEventListener('click', startMeasuring); |
| } |
| } |
| |
| |
| function stopMeasuring() { |
| clearInterval(interval); |
| interval = null; |
| startBtn.disabled = false; |
| stopBtn.disabled = true; |
| overlay.classList.remove('hidden'); |
| |
| |
| if (stream) { |
| stream.getTracks().forEach(track => track.stop()); |
| video.srcObject = null; |
| stream = null; |
| isVideoReady = false; |
| } |
| |
| |
| brightnessValue.textContent = '--'; |
| brightnessIndicator.style.left = '0%'; |
| lightLevel.textContent = '--'; |
| lightStatus.textContent = 'Inactive'; |
| lightStatus.className = 'text-2xl font-bold'; |
| } |
| |
| |
| async function initWebcam() { |
| try { |
| |
| if (stream) { |
| stream.getTracks().forEach(track => track.stop()); |
| } |
| |
| stream = await navigator.mediaDevices.getUserMedia({ video: true }); |
| video.srcObject = stream; |
| isVideoReady = false; |
| |
| |
| await new Promise((resolve) => { |
| video.onloadedmetadata = () => { |
| isVideoReady = true; |
| resolve(); |
| }; |
| }); |
| |
| |
| if (!chart) { |
| initChart(); |
| } |
| |
| return true; |
| } catch (err) { |
| console.error('Error accessing webcam:', err); |
| overlay.classList.remove('hidden'); |
| overlay.innerHTML = ` |
| <div class="text-center p-4"> |
| <i class="fas fa-exclamation-triangle text-4xl mb-2 text-red-500"></i> |
| <p>Could not access webcam</p> |
| <p class="text-sm text-gray-300 mt-2">${err.message}</p> |
| <button id="retryBtn" class="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"> |
| <i class="fas fa-sync-alt mr-2"></i> Try Again |
| </button> |
| </div> |
| `; |
| document.getElementById('retryBtn').addEventListener('click', initWebcam); |
| return false; |
| } |
| } |
| |
| |
| startBtn.addEventListener('click', startMeasuring); |
| stopBtn.addEventListener('click', stopMeasuring); |
| |
| |
| window.addEventListener('beforeunload', () => { |
| if (stream) { |
| stream.getTracks().forEach(track => track.stop()); |
| } |
| if (interval) { |
| clearInterval(interval); |
| } |
| }); |
| |
| |
| document.addEventListener('DOMContentLoaded', initChart); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Doobdeedoo/doobidoo" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |