Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Phoenix Camera Pro — v2.3</title> | |
| <style> | |
| :root{--bg:#070507;--gold:#f6b24a;--muted:rgba(255,255,255,0.68);--glass:rgba(6,6,6,0.45)} | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| html,body{height:100%;background:var(--bg);color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial} | |
| .camera-view{width:100vw;height:100vh;position:relative;overflow:hidden;background:linear-gradient(180deg,#000 0%, #070507 100%);touch-action:none} | |
| #cameraVideo{width:100%;height:100%;object-fit:cover;display:block;transition:transform .18s ease,filter .2s} | |
| #aiOverlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;opacity:0;transition:opacity .38s ease} | |
| .top-bar{position:absolute;top:14px;left:12px;right:12px;display:flex;align-items:center;justify-content:space-between;z-index:120;backdrop-filter:blur(12px);background:var(--glass);padding:6px;border-radius:12px} | |
| .top-left,.top-right{display:flex;gap:10px;align-items:center} | |
| .icon-btn{min-width:44px;height:44px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:transparent;border:1px solid rgba(255,255,255,0.04);cursor:pointer;color:var(--muted);transition:all .14s;position:relative} | |
| .icon-btn.active{color:var(--gold);box-shadow:0 6px 20px rgba(246,178,74,0.06)} | |
| .icon-label{position:absolute;left:50%;transform:translateX(-50%);bottom:-34px;background:rgba(0,0,0,0.6);padding:6px 8px;border-radius:10px;font-size:12px;opacity:0;pointer-events:none;color:#fff;white-space:nowrap;transition:opacity .18s,transform .18s} | |
| .icon-label.show{opacity:1;transform:translateX(-50%) translateY(-4px)} | |
| .mode-strip{position:absolute;left:0;right:0;bottom:170px;display:flex;justify-content:center;z-index:80} | |
| .mode-list{display:flex;gap:18px;overflow:auto;padding:6px 12px;background:transparent;border-radius:999px} | |
| .mode-item{padding:8px 10px;font-size:13px;color:rgba(255,255,255,0.78);cursor:pointer;text-transform:uppercase;letter-spacing:.6px} | |
| .mode-item.active{color:#fff;border-bottom:2px solid var(--gold);font-weight:700} | |
| .bottom-bar{position:absolute;left:0;right:0;bottom:70px;display:flex;justify-content:space-between;align-items:center;padding:8px 18px;z-index:120;backdrop-filter:blur(10px);background:var(--glass);border-radius:14px;margin:0 12px} | |
| .thumb{width:56px;height:56px;border-radius:12px;overflow:hidden;border:2px solid rgba(255,255,255,0.06);background:#111;background-size:cover;background-position:center;cursor:pointer} | |
| .shutter-wrap{display:flex;align-items:center;gap:18px} | |
| .shutter-btn{width:84px;height:84px;border-radius:50%;background:linear-gradient(180deg,#fff,#f1e9d6);display:flex;align-items:center;justify-content:center;border:6px solid rgba(246,178,74,0.12);box-shadow:0 10px 30px rgba(0,0,0,0.6);cursor:pointer;position:relative} | |
| .shutter-inner{width:46px;height:46px;border-radius:50%;background:#fff;border:6px solid rgba(0,0,0,0.06);box-shadow:inset 0 2px 6px rgba(0,0,0,0.12);transition:background .14s, box-shadow .14s} | |
| .shutter-inner.recording{background:#ff3b30;box-shadow:0 0 0 6px rgba(255,59,48,0.08) inset} | |
| .switch-btn{width:56px;height:56px;border-radius:12px;background:transparent;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255,255,255,0.04);cursor:pointer} | |
| .center-focus{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:96px;height:96px;border-radius:50%;display:flex;align-items:center;justify-content:center;pointer-events:none;z-index:95} | |
| .focus-ring{position:absolute;width:64px;height:64px;border-radius:50%;border:2px solid rgba(246,178,74,0.9);opacity:0;transform:scale(.6);transition:all .34s} | |
| .focus-ring.active{opacity:1;transform:scale(1);animation:focusPulse .9s ease-out} | |
| @keyframes focusPulse{0%{transform:scale(.8);opacity:.9}50%{transform:scale(1.05)}100%{transform:scale(1);opacity:.9}} | |
| .zoom-indicator{position:absolute;right:22px;top:50%;transform:translateY(-50%);font-size:15px;color:var(--gold);z-index:140;opacity:0;transition:opacity .25s} | |
| .zoom-indicator.active{opacity:1} | |
| .flash-effect{position:absolute;inset:0;background:#fff;opacity:0;pointer-events:none;z-index:150} | |
| .flash-effect.on{animation:flashAnim .5s ease} | |
| @keyframes flashAnim{0%{opacity:0}10%{opacity:1}60%{opacity:1}100%{opacity:0}} | |
| .settings-page{position:absolute;top:0;right:0;width:100%;height:100%;background:linear-gradient(180deg,rgba(5,3,2,0.98),rgba(9,6,3,0.98));transform:translateX(100%);transition:transform .45s cubic-bezier(.2,.9,.2,1);z-index:200;display:flex;flex-direction:column} | |
| .settings-page.open{transform:translateX(0)} | |
| .settings-header{display:flex;align-items:center;padding:18px;border-bottom:1px solid rgba(255,255,255,0.04)} | |
| .settings-title{flex:1;text-align:center;font-size:18px;color:var(--gold);font-weight:700} | |
| .settings-list{padding:12px 8px;overflow:auto} | |
| .option{display:flex;align-items:center;justify-content:space-between;padding:14px;border-bottom:1px solid rgba(255,255,255,0.03)} | |
| .option label{font-size:15px;color:#fff} | |
| .select, .toggle{background:transparent;color:#fff;border:1px solid rgba(255,255,255,0.06);padding:8px;border-radius:8px} | |
| .info-overlay{position:absolute;left:12px;top:12px;padding:8px 12px;background:rgba(0,0,0,0.45);border-radius:12px;font-size:13px;z-index:130} | |
| .grid-overlay{position:absolute;inset:0;pointer-events:none;z-index:85;display:none} | |
| .grid-overlay.on{display:block} | |
| .grid-line{position:absolute;background:rgba(255,255,255,0.12)} | |
| .grid-line.v{width:1px;height:100%} | |
| .grid-line.h{height:1px;width:100%} | |
| .recording-indicator{position:absolute;left:50%;top:14px;transform:translateX(-50%);background:rgba(0,0,0,0.5);padding:8px 12px;border-radius:18px;display:flex;align-items:center;gap:8px;font-weight:600;color:#fff;z-index:210;opacity:0;transition:opacity .2s} | |
| .recording-indicator.on{opacity:1} | |
| .record-dot{width:12px;height:12px;border-radius:50%;background:#ff3b30;box-shadow:0 0 8px rgba(255,59,48,0.8)} | |
| .timer-text{font-family:monospace} | |
| .toast-center{position:fixed;left:50%;top:48%;transform:translate(-50%,-50%) scale(.98);background:rgba(0,0,0,0.75);color:#fff;padding:10px 16px;border-radius:18px;font-size:15px;z-index:900;opacity:0;pointer-events:none;transition:opacity .28s ease,transform .28s ease} | |
| .toast-center.show{opacity:1;transform:translate(-50%,-50%) scale(1)} | |
| .toast-center.hide{opacity:0;transform:translate(-50%,-50%) scale(.98)} | |
| .lunid-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.9);z-index:10000;display:none;align-items:center;justify-content:center;overflow-y:auto} | |
| .lunid-modal-overlay.show{display:flex} | |
| .lunid-modal-close{position:absolute;top:16px;right:16px;background:none;border:none;color:#fff;font-size:28px;cursor:pointer} | |
| </style> | |
| <script src="/static/lunid-auth.js"></script> | |
| </head> | |
| <body> | |
| <div id="lunidModalOverlay" class="lunid-modal-overlay"> | |
| <button class="lunid-modal-close" onclick="closeLunidModal()">×</button> | |
| <div id="lunidAuthContainer" style="width:100%;max-width:400px"></div> | |
| </div> | |
| <div class="camera-view" id="cameraRoot"> | |
| <video id="cameraVideo" autoplay playsinline></video> | |
| <canvas id="cameraCanvas" style="display:none"></canvas> | |
| <canvas id="aiOverlay"></canvas> | |
| <div class="flash-effect" id="flashEffect"></div> | |
| <div class="top-bar"> | |
| <div class="top-left"> | |
| <button class="icon-btn" onclick="if(window.parent !== window) { window.parent.postMessage({action:'closeApp'}, '*'); } else { window.location.href='index.html'; }" title="Back to Home"> | |
| <svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:currentColor"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg> | |
| <div class="icon-label">Home</div> | |
| </button> | |
| <button class="icon-btn" id="flashBtn" title="Flash"> | |
| <svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:currentColor"><path d="M7 2v11h3v9l7-12h-4l4-8z"/></svg> | |
| <div class="icon-label">Flash</div> | |
| </button> | |
| <button class="icon-btn" id="hdrBtn" title="HDR"> | |
| <svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:currentColor"><path d="M12 6l-2 4-4 .5 3 2.8L8 18l4-2 4 2-1-4.7L20 10.5 16 10z"/></svg> | |
| <div class="icon-label">HDR</div> | |
| </button> | |
| <button class="icon-btn" id="aiBtn" title="AI Enhance">AI<div class="icon-label">AI Enhance</div></button> | |
| </div> | |
| <div class="top-right"> | |
| <div id="signInBtnContainer" style="display:flex;align-items:center"></div> | |
| <button class="icon-btn" id="filterBtn" title="Filter"> | |
| <svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:currentColor"><path d="M10 4H4v6h2V6h4V4zm10 0h-6v2h4v4h2V4zM4 14v6h6v-2H6v-4H4zm14 6v-4h-2v4h-4v2h6v-2z"/></svg> | |
| <div class="icon-label">Filter</div> | |
| </button> | |
| <button class="icon-btn" id="menuBtn" title="Settings"> | |
| <svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:currentColor"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm8.94 1.06l-1.43-.82a7.98 7.98 0 0 0-.9-1.56l.77-1.5L18.3 4l-1.5.77a7.98 7.98 0 0 0-1.56-.9L14 2.06 12 2l-1.99.06L9.17 3.4a7.98 7.98 0 0 0-1.56.9L6.11 3.54 4.8 4.8l.77 1.5c-.35.5-.62 1.06-.9 1.56L3.39 9.59 4.82 11l1.5-.77c.5.35 1.06.62 1.56.9L9.17 13l.84 1.99L12 16l1.99-.06.84-1.99c.5-.28 1.06-.55 1.56-.9l1.5.77 1.43-1.43-.77-1.5c.35-.5.62-1.06.9-1.56l1.43-.82z"/></svg> | |
| <div class="icon-label">Settings</div> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="info-overlay" id="infoOverlay">PHOTO</div> | |
| <div class="center-focus"><div class="focus-ring" id="focusRing"></div></div> | |
| <div class="zoom-indicator" id="zoomIndicator">1.0x</div> | |
| <div class="mode-strip"><div class="mode-list" id="modeList"> | |
| <div class="mode-item">Short Video</div> | |
| <div class="mode-item">Video</div> | |
| <div class="mode-item active">Photo</div> | |
| <div class="mode-item">Portrait</div> | |
| <div class="mode-item">Night</div> | |
| <div class="mode-item">Pro</div> | |
| </div></div> | |
| <div class="grid-overlay" id="gridOverlay"> | |
| <div class="grid-line v" style="left:33.33%"></div> | |
| <div class="grid-line v" style="left:66.66%"></div> | |
| <div class="grid-line h" style="top:33.33%"></div> | |
| <div class="grid-line h" style="top:66.66%"></div> | |
| </div> | |
| <div class="bottom-bar"> | |
| <div class="thumb" id="thumbPreview" onclick="localStorage.setItem('openGallery', 'true'); if(window.parent !== window) { window.parent.postMessage({action:'closeApp'}, '*'); } else { window.location.href='index.html'; }"></div> | |
| <div class="shutter-wrap"> | |
| <button class="shutter-btn" id="shutterBtn" title="Capture"><div class="shutter-inner" id="shutterInner"></div></button> | |
| </div> | |
| <button class="switch-btn" id="switchBtn" title="Switch Camera"> | |
| <svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:currentColor"><path d="M12 6v3l4-4-4-4v3c-4.97 0-9 4.03-9 9 0 1.66.43 3.22 1.18 4.57L5.1 20.6C3.95 18.88 3.2 16.55 3.2 14c0-5.52 4.48-10 10-10zm6.8 1.4L20.9 3.4C22.05 5.12 22.8 7.45 22.8 10c0 5.52-4.48 10-10 10v-3l-4 4 4 4v-3c4.97 0 9-4.03 9-9 0-1.66-.43-3.22-1.18-4.57z"/></svg> | |
| </button> | |
| </div> | |
| <div class="settings-page" id="settingsPage"> | |
| <div class="settings-header"> | |
| <button class="icon-btn" id="settingsBack"><svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:currentColor"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg></button> | |
| <div class="settings-title">Camera Settings</div> | |
| <div style="width:42px"></div> | |
| </div> | |
| <div class="settings-list"> | |
| <div class="option"><label>Photo Resolution</label><select id="photoRes" class="select"><option value="720">720p</option><option value="1080" selected>1080p</option><option value="2160">4K</option></select></div> | |
| <div class="option"><label>Video Quality</label><select id="videoQuality" class="select"><option value="480">480p</option><option value="720">720p</option><option value="1080" selected>1080p</option></select></div> | |
| <div class="option"><label>Flash Mode</label><select id="flashMode" class="select"><option value="auto">Auto</option><option value="on">On</option><option value="off">Off</option></select></div> | |
| <div class="option"><label>Grid Lines</label><input type="checkbox" id="gridToggle" class="toggle" /></div> | |
| <div class="option"><label>Default Filter</label><select id="defaultFilter" class="select"><option value="none">Normal</option><option value="warm">Warm</option><option value="vintage">Vintage</option><option value="bw">Black & White</option></select></div> | |
| <div class="option"><label>Shutter Sound</label><input type="checkbox" id="shutterToggle" class="toggle" checked /></div> | |
| <div class="option"><label>AI Enhancement</label><input type="checkbox" id="aiToggleSetting" class="toggle" /></div> | |
| <div class="option"><label>HDR Mode</label><input type="checkbox" id="hdrToggleSetting" class="toggle" /></div> | |
| </div> | |
| </div> | |
| <div class="recording-indicator" id="recordingIndicator"><div class="record-dot"></div><div class="timer-text" id="recordTimer">00:00</div></div> | |
| </div> | |
| <script> | |
| let cameraStream=null,currentFacing='environment',mediaRecorder=null,recordedChunks=[],isRecording=false; | |
| let flashMode='auto',shutterSound=true,gridOn=false,defaultFilter='none'; | |
| let zoomLevel=1,lastTouchDistance=null,zoomTimeout=null; | |
| let recordStartTime=0,recordTimerInterval=null; | |
| let aiEnabled=false,hdrEnabled=false; | |
| const video=document.getElementById('cameraVideo'); | |
| const canvas=document.getElementById('cameraCanvas'); | |
| const flashEl=document.getElementById('flashEffect'); | |
| const focusRing=document.getElementById('focusRing'); | |
| const infoOverlay=document.getElementById('infoOverlay'); | |
| const thumb=document.getElementById('thumbPreview'); | |
| const zoomIndicator=document.getElementById('zoomIndicator'); | |
| const recordingIndicator=document.getElementById('recordingIndicator'); | |
| const recordTimerEl=document.getElementById('recordTimer'); | |
| function formatTime(ms){const s=Math.floor(ms/1000);const mm=Math.floor(s/60).toString().padStart(2,'0');const ss=(s%60).toString().padStart(2,'0');return `${mm}:${ss}`;} | |
| async function startCamera(){ | |
| try{ | |
| if(cameraStream){cameraStream.getTracks().forEach(t=>t.stop());cameraStream=null} | |
| const photoRes=parseInt(document.getElementById('photoRes').value,10)||1080; | |
| const constraints={audio:false,video:{facingMode:currentFacing,width:{ideal:photoRes},height:{ideal:Math.floor(photoRes*16/9)}}}; | |
| cameraStream=await navigator.mediaDevices.getUserMedia(constraints); | |
| video.srcObject=cameraStream;await video.play();updateInfo();applyFilter();updateThumb(); | |
| }catch(e){console.error(e);showToast('Camera access denied');} | |
| } | |
| function updateInfo(){const w=video.videoWidth||0;const h=video.videoHeight||0;infoOverlay.textContent=`${w}×${h} • ${getActiveMode().toUpperCase()}`;} | |
| function getActiveMode(){const active=document.querySelector('.mode-item.active');return active?active.textContent.trim().toLowerCase():'photo';} | |
| function applyFilter(){ | |
| defaultFilter=document.getElementById('defaultFilter').value; | |
| let filter='none'; | |
| switch(defaultFilter){ | |
| case 'warm':filter='sepia(.12) saturate(1.1) contrast(1.03)';break; | |
| case 'vintage':filter='sepia(.22) contrast(.95) saturate(.9)';break; | |
| case 'bw':filter='grayscale(1) contrast(1.05)';break; | |
| } | |
| if(aiEnabled)filter=(filter==='none'?'':'filter+')+' brightness(1.15) contrast(1.1) saturate(1.05)'; | |
| if(hdrEnabled)filter=(filter==='none'?'':'filter+'')+' contrast(1.15) brightness(1.05)'; | |
| video.style.filter=filter; | |
| } | |
| document.getElementById('defaultFilter').addEventListener('change',applyFilter); | |
| document.getElementById('gridToggle').addEventListener('change',e=>{gridOn=e.target.checked;document.getElementById('gridOverlay').classList.toggle('on',gridOn);}); | |
| document.getElementById('shutterToggle').addEventListener('change',e=>{shutterSound=e.target.checked;}); | |
| document.getElementById('flashMode').addEventListener('change',e=>{flashMode=e.target.value;}); | |
| document.getElementById('menuBtn').addEventListener('click',()=>document.getElementById('settingsPage').classList.add('open')); | |
| document.getElementById('settingsBack').addEventListener('click',()=>document.getElementById('settingsPage').classList.remove('open')); | |
| document.getElementById('shutterBtn').addEventListener('click',captureFlow); | |
| document.getElementById('switchBtn').addEventListener('click',async()=>{currentFacing=currentFacing==='user'?'environment':'user';await startCamera();showToast(`Switched to ${currentFacing==='user'?'front':'rear'}`);}); | |
| document.getElementById('modeList').addEventListener('click',e=>{const m=e.target.closest('.mode-item');if(!m)return;document.querySelectorAll('.mode-item').forEach(x=>x.classList.remove('active'));m.classList.add('active');updateInfo();}); | |
| const cameraRoot=document.getElementById('cameraRoot'); | |
| function getTouchDistance(touches){const dx=touches[0].clientX-touches[1].clientX;const dy=touches[0].clientY-touches[1].clientY;return Math.sqrt(dx*dx+dy*dy);} | |
| function handleTouchStart(e){if(e.touches&&e.touches.length===2)lastTouchDistance=getTouchDistance(e.touches);} | |
| function handleTouchMove(e){if(e.touches&&e.touches.length===2){e.preventDefault();const currentDistance=getTouchDistance(e.touches);if(lastTouchDistance){const delta=currentDistance/lastTouchDistance;zoomLevel=Math.min(5,Math.max(1,zoomLevel*delta));applyZoom();}lastTouchDistance=currentDistance;}} | |
| function handleTouchEnd(e){if(!e.touches||e.touches.length<2)lastTouchDistance=null;} | |
| function applyZoom(){video.style.transform=`scale(${zoomLevel})`;zoomIndicator.textContent=`${zoomLevel.toFixed(1)}x`;zoomIndicator.classList.add('active');clearTimeout(zoomTimeout);zoomTimeout=setTimeout(()=>zoomIndicator.classList.remove('active'),1000);} | |
| cameraRoot.addEventListener('touchstart',handleTouchStart,{passive:false}); | |
| cameraRoot.addEventListener('touchmove',handleTouchMove,{passive:false}); | |
| cameraRoot.addEventListener('touchend',handleTouchEnd); | |
| cameraRoot.addEventListener('click',e=>{if(e.target.closest('.icon-btn')||e.target.closest('.shutter-btn')||e.target.closest('.mode-list')||e.target.closest('.settings-page'))return;const rect=cameraRoot.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;showFocus(x,y);}); | |
| function showFocus(x,y){const ring=focusRing;ring.classList.remove('active');ring.style.left=`${x-32}px`;ring.style.top=`${y-32}px`;void ring.offsetWidth;ring.classList.add('active');setTimeout(()=>ring.classList.remove('active'),900);} | |
| async function measureBrightness(){try{const w=Math.min(160,video.videoWidth||320);const h=Math.min(90,video.videoHeight||180);if(!w||!h)return 255;canvas.width=w;canvas.height=h;const ctx=canvas.getContext('2d');ctx.drawImage(video,0,0,w,h);const data=ctx.getImageData(0,0,w,h).data;let sum=0;for(let i=0;i<data.length;i+=4)sum+=(0.299*data[i]+0.587*data[i+1]+0.114*data[i+2]);return sum/(w*h);}catch(e){return 255;}} | |
| async function captureFlow(){ | |
| const mode=getActiveMode(); | |
| if(mode==='video'||mode==='short video'){toggleRecording();return;} | |
| let simulateFlash=false; | |
| if(flashMode==='on')simulateFlash=true; | |
| else if(flashMode==='auto'){const b=await measureBrightness();if(b<60)simulateFlash=true;} | |
| if(simulateFlash)triggerFlash(); | |
| const dataUrl=await capturePhoto(); | |
| if(!dataUrl)return; | |
| let photos=JSON.parse(localStorage.getItem('photos')||'[]'); | |
| photos.push({data:dataUrl,date:new Date().toISOString(),mode:getActiveMode(),filter:defaultFilter,ai:aiEnabled,hdr:hdrEnabled}); | |
| localStorage.setItem('photos',JSON.stringify(photos)); | |
| showToast('✓ Photo saved'); | |
| updateThumb(); | |
| } | |
| function triggerFlash(){flashEl.classList.remove('on');void flashEl.offsetWidth;flashEl.classList.add('on');setTimeout(()=>flashEl.classList.remove('on'),600);} | |
| async function capturePhoto(){ | |
| try{ | |
| if(!cameraStream){showToast('Camera not ready');return null;} | |
| if(shutterSound&&document.getElementById('shutterToggle').checked){try{const s=new Audio('https://actions.google.com/sounds/v1/camera/camera_shutter_click.ogg');s.play().catch(()=>{});}catch(e){}} | |
| const w=video.videoWidth;const h=video.videoHeight;if(!w||!h)return null; | |
| canvas.width=w;canvas.height=h;const ctx=canvas.getContext('2d'); | |
| const filter=getComputedStyle(video).getPropertyValue('filter'); | |
| ctx.filter=filter==='none'?'none':filter; | |
| ctx.drawImage(video,0,0,w,h); | |
| ctx.fillStyle='rgba(0,0,0,0.4)';ctx.fillRect(8,h-46,320,36); | |
| ctx.fillStyle='#fff';ctx.font='18px sans-serif'; | |
| let modeText=getActiveMode().toUpperCase(); | |
| if(aiEnabled)modeText+=' AI'; | |
| if(hdrEnabled)modeText+=' HDR'; | |
| ctx.fillText(`${new Date().toLocaleString()} • ${modeText}`,14,h-20); | |
| return canvas.toDataURL('image/jpeg',0.95); | |
| }catch(e){console.error(e);showToast('Capture failed');return null;} | |
| } | |
| function updateThumb(){ | |
| try{ | |
| const photos=JSON.parse(localStorage.getItem('photos')||'[]'); | |
| if(photos.length>0&&thumb){ | |
| const last=photos[photos.length-1]; | |
| thumb.style.backgroundImage=`url(${last.data})`; | |
| }else if(thumb){ | |
| thumb.style.backgroundImage='none'; | |
| thumb.style.background='linear-gradient(135deg,#111,#0b0603)'; | |
| } | |
| }catch(e){ | |
| // Silent fail - no error logging | |
| } | |
| } | |
| function toggleRecording(){ | |
| if(isRecording){if(mediaRecorder&&mediaRecorder.state==='recording')mediaRecorder.stop();stopRecordingUI();return;} | |
| if(!cameraStream){showToast('Camera not ready');return;} | |
| try{mediaRecorder=new MediaRecorder(cameraStream,{mimeType:'video/webm;codecs=vp8'});}catch(e){try{mediaRecorder=new MediaRecorder(cameraStream);}catch(err){showToast('Recording not supported');return;}} | |
| recordedChunks=[]; | |
| mediaRecorder.ondataavailable=e=>{if(e.data&&e.data.size)recordedChunks.push(e.data);}; | |
| mediaRecorder.onstop=async()=>{ | |
| const blob=new Blob(recordedChunks,{type:'video/webm'}); | |
| const reader=new FileReader(); | |
| reader.onload=function(){ | |
| const dataUrl=reader.result; | |
| let photos=JSON.parse(localStorage.getItem('photos')||'[]'); | |
| photos.push({data:dataUrl,date:new Date().toISOString(),mode:'video',filter:'none',isVideo:true}); | |
| localStorage.setItem('photos',JSON.stringify(photos)); | |
| showToast('✓ Video saved'); | |
| updateThumb(); | |
| }; | |
| reader.readAsDataURL(blob); | |
| }; | |
| mediaRecorder.start();isRecording=true;startRecordingUI();showToast('Recording...'); | |
| } | |
| function startRecordingUI(){recordingIndicator.classList.add('on');recordStartTime=Date.now();recordTimerEl.textContent='00:00';recordTimerInterval=setInterval(()=>{recordTimerEl.textContent=formatTime(Date.now()-recordStartTime);},500);document.getElementById('shutterInner').classList.add('recording');} | |
| function stopRecordingUI(){recordingIndicator.classList.remove('on');if(recordTimerInterval)clearInterval(recordTimerInterval);recordTimerInterval=null;isRecording=false;document.getElementById('shutterInner').classList.remove('recording');} | |
| function showToast(msg,t=1600){ | |
| const existing=document.querySelector('.toast-center'); | |
| if(existing){existing.remove();clearTimeout(existing._removeTimer);} | |
| const el=document.createElement('div'); | |
| el.className='toast-center'; | |
| el.textContent=msg; | |
| document.body.appendChild(el); | |
| void el.offsetWidth; | |
| el.classList.add('show'); | |
| el._removeTimer=setTimeout(()=>{el.classList.remove('show');el.classList.add('hide');setTimeout(()=>el.remove(),300);},t); | |
| } | |
| const flashBtn=document.getElementById('flashBtn'); | |
| flashBtn.addEventListener('click',()=>{const fm=document.getElementById('flashMode');fm.value=fm.value==='on'?'off':'on';flashMode=fm.value;flashBtn.classList.toggle('active',flashMode==='on');showToast('Flash: '+flashMode);}); | |
| const filterBtn=document.getElementById('filterBtn'); | |
| filterBtn.addEventListener('click',()=>{const sel=document.getElementById('defaultFilter');const next={none:'warm',warm:'vintage',vintage:'bw',bw:'none'}[sel.value];sel.value=next;applyFilter();showToast('Filter: '+next);}); | |
| const aiBtn=document.getElementById('aiBtn'); | |
| aiBtn.addEventListener('click',()=>{aiEnabled=!aiEnabled;aiBtn.classList.toggle('active',aiEnabled);document.getElementById('aiToggleSetting').checked=aiEnabled;applyFilter();showToast(aiEnabled?'🤖 AI Enhance On':'AI Enhance Off');}); | |
| const hdrBtn=document.getElementById('hdrBtn'); | |
| hdrBtn.addEventListener('click',()=>{hdrEnabled=!hdrEnabled;hdrBtn.classList.toggle('active',hdrEnabled);document.getElementById('hdrToggleSetting').checked=hdrEnabled;applyFilter();showToast(hdrEnabled?'✨ HDR On':'HDR Off');}); | |
| document.getElementById('aiToggleSetting').addEventListener('change',e=>{aiEnabled=e.target.checked;aiBtn.classList.toggle('active',aiEnabled);applyFilter();showToast(aiEnabled?'🤖 AI Enhance On':'AI Enhance Off');}); | |
| document.getElementById('hdrToggleSetting').addEventListener('change',e=>{hdrEnabled=e.target.checked;hdrBtn.classList.toggle('active',hdrEnabled);applyFilter();showToast(hdrEnabled?'✨ HDR On':'HDR Off');}); | |
| ['photoRes','videoQuality'].forEach(id=>document.getElementById(id).addEventListener('change',()=>{startCamera();})); | |
| [...document.querySelectorAll('.icon-btn')].forEach(b=>{b.addEventListener('mouseenter',()=>{const label=b.querySelector('.icon-label');if(label){label.classList.add('show');setTimeout(()=>label.classList.remove('show'),900);}});b.addEventListener('touchstart',()=>{const label=b.querySelector('.icon-label');if(label){label.classList.add('show');setTimeout(()=>label.classList.remove('show'),900);}},{passive:true});}); | |
| video.addEventListener('loadedmetadata',updateInfo); | |
| window.addEventListener('load',startCamera); | |
| window.addEventListener('beforeunload',()=>{if(cameraStream)cameraStream.getTracks().forEach(t=>t.stop());if(recordTimerInterval)clearInterval(recordTimerInterval);}); | |
| function initSignInButton() { | |
| const style = document.createElement('style'); | |
| style.textContent = LunIDAuth.getStyles(); | |
| document.head.appendChild(style); | |
| if (window.lunidAuth) { | |
| lunidAuth.createSignInButton('signInBtnContainer', { | |
| onClick: (isAuth, user) => showLunidModal() | |
| }); | |
| } | |
| } | |
| function showLunidModal() { | |
| document.getElementById('lunidModalOverlay').classList.add('show'); | |
| if (window.lunidAuth) { | |
| lunidAuth.createAccountPickerUI('lunidAuthContainer', { | |
| appName: 'Camera', | |
| onContinue: (user) => closeLunidModal(), | |
| onCancel: closeLunidModal | |
| }); | |
| } | |
| } | |
| function closeLunidModal() { | |
| document.getElementById('lunidModalOverlay').classList.remove('show'); | |
| } | |
| initSignInButton(); | |
| </script> | |
| </body> | |
| </html> |