From a3d4bee9da780fe28b9b1643a2eec3f2ec0754a4 Mon Sep 17 00:00:00 2001 From: Zain Amjad Date: Sun, 12 Oct 2025 16:03:32 +0100 Subject: [PATCH 1/2] created virtual mirror app --- Javascript/Virtual Mirror/index.html | 49 +++++++ .../Virtual Mirror/resources/camera.svg | 15 ++ Javascript/Virtual Mirror/scripts/script.js | 138 ++++++++++++++++++ Javascript/Virtual Mirror/styles/styles.css | 116 +++++++++++++++ 4 files changed, 318 insertions(+) create mode 100644 Javascript/Virtual Mirror/index.html create mode 100644 Javascript/Virtual Mirror/resources/camera.svg create mode 100644 Javascript/Virtual Mirror/scripts/script.js create mode 100644 Javascript/Virtual Mirror/styles/styles.css diff --git a/Javascript/Virtual Mirror/index.html b/Javascript/Virtual Mirror/index.html new file mode 100644 index 0000000..b868edb --- /dev/null +++ b/Javascript/Virtual Mirror/index.html @@ -0,0 +1,49 @@ + + + + + + Webcam Mirror + + + +
+
+ Camera Icon +
+

Webcam Mirror

+
Uses your webcam and flips the preview horizontally like a real mirror.
+
+
+ +
+ + + + + + +
+ +
+
+ +
+ + +
+ +
+ Permission to access the camera will be requested. If nothing happens, check your browser permissions or try a different camera from the dropdown. +
+
+ + + + diff --git a/Javascript/Virtual Mirror/resources/camera.svg b/Javascript/Virtual Mirror/resources/camera.svg new file mode 100644 index 0000000..95a27a7 --- /dev/null +++ b/Javascript/Virtual Mirror/resources/camera.svg @@ -0,0 +1,15 @@ + diff --git a/Javascript/Virtual Mirror/scripts/script.js b/Javascript/Virtual Mirror/scripts/script.js new file mode 100644 index 0000000..44b7309 --- /dev/null +++ b/Javascript/Virtual Mirror/scripts/script.js @@ -0,0 +1,138 @@ +const startBtn = document.getElementById('startBtn'); +const stopBtn = document.getElementById('stopBtn'); +const video = document.getElementById('video'); +const deviceSelect = document.getElementById('deviceSelect'); +const mirrorToggle = document.getElementById('mirrorToggle'); +const captureBtn = document.getElementById('captureBtn'); +const canvas = document.getElementById('canvas'); +const downloadLink = document.getElementById('downloadLink'); +const status = document.getElementById('status'); + +let stream = null; + +function setStatus(s){ status.textContent = s; } + +async function enumerateCameras(){ + try{ + const devices = await navigator.mediaDevices.enumerateDevices(); + const cams = devices.filter(d => d.kind === 'videoinput'); + deviceSelect.innerHTML = ''; + cams.forEach((c, i)=>{ + const opt = document.createElement('option'); + opt.value = c.deviceId; + opt.text = c.label || `Camera ${i+1}`; + deviceSelect.appendChild(opt); + }); + if(cams.length===0) deviceSelect.innerHTML = ''; + }catch(err){ + console.error('Cannot list devices', err); + deviceSelect.innerHTML = ''; + } +} + +async function startCamera(deviceId){ + stopCamera(); + setStatus('Requesting camera...'); + const constraints = { + audio: false, + video: { + width: {ideal: 1280}, + height: {ideal: 720}, + } + }; + if(deviceId) constraints.video.deviceId = { exact: deviceId }; + try{ + stream = await navigator.mediaDevices.getUserMedia(constraints); + video.srcObject = stream; + startBtn.disabled = true; + stopBtn.disabled = false; + captureBtn.disabled = false; + setStatus('Camera started'); + await enumerateCameras(); // refresh labels (some browsers only expose labels after permission) + applyMirror(); + }catch(err){ + console.error('getUserMedia error', err); + setStatus('Camera error: ' + (err.message || err.name)); + } +} + +function stopCamera(){ + if(stream){ + stream.getTracks().forEach(t => t.stop()); + stream = null; + video.srcObject = null; + startBtn.disabled = false; + stopBtn.disabled = true; + captureBtn.disabled = true; + setStatus('Camera stopped'); + } +} + +function applyMirror(){ + const mirrored = mirrorToggle.checked; + // For the live preview, the easiest and smoothest approach is CSS transform. + // This flips the DOM element visually but does not change the underlying camera frames. + video.style.transform = mirrored ? 'scaleX(-1)' : 'none'; +} + +function captureSnapshot(){ + if(!video || video.readyState < 2) return; + const w = canvas.width = video.videoWidth || 320; + const h = canvas.height = video.videoHeight || 240; + const ctx = canvas.getContext('2d'); + + ctx.save(); + if(mirrorToggle.checked){ + // To make the saved image match the mirrored preview, draw the video flipped on the canvas. + ctx.translate(w, 0); + ctx.scale(-1, 1); + } + // draw the video frame to canvas + ctx.drawImage(video, 0, 0, w, h); + ctx.restore(); + + // Create a download link for the snapshot + canvas.toBlob(blob => { + if(!blob) return; + const url = URL.createObjectURL(blob); + downloadLink.href = url; + downloadLink.style.display = 'inline-block'; + downloadLink.textContent = 'Download snapshot'; + }, 'image/png'); + + setStatus('Snapshot captured'); +} + +// Wire up UI +startBtn.addEventListener('click', async ()=>{ + const selected = deviceSelect.value || null; + await startCamera(selected); +}); +stopBtn.addEventListener('click', ()=>stopCamera()); +mirrorToggle.addEventListener('change', applyMirror); +captureBtn.addEventListener('click', captureSnapshot); + +// If the user changes camera from the dropdown, restart with that device +deviceSelect.addEventListener('change', async ()=>{ + if(deviceSelect.value) await startCamera(deviceSelect.value); +}); + +// On load: try to enumerate devices and set a helpful default +(async function init(){ + if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ + setStatus('getUserMedia not supported in this browser'); + startBtn.disabled = true; + return; + } + await enumerateCameras(); + // Try to pre-select a camera if available + if(deviceSelect.options.length>0 && deviceSelect.options[0].value){ + deviceSelect.selectedIndex = 0; + } + setStatus('Ready β€” click "Start Camera"'); +})(); + +// Optional: stop camera when the page is hidden to be polite with permissions +document.addEventListener('visibilitychange', ()=>{ + if(document.hidden) stopCamera(); +}); \ No newline at end of file diff --git a/Javascript/Virtual Mirror/styles/styles.css b/Javascript/Virtual Mirror/styles/styles.css new file mode 100644 index 0000000..5b25a11 --- /dev/null +++ b/Javascript/Virtual Mirror/styles/styles.css @@ -0,0 +1,116 @@ +:root { + --bg: #0f1724; + --card: #0b1220; + --accent: #06b6d4; + color-scheme: dark +} + + +body { + height: 100%; + margin: 0; + font-family: Inter, system-ui, Arial, Helvetica, sans-serif; + background: linear-gradient(180deg, #071022 0%, #06111a 100%); + color: #e6eef6 +} + +.app { + max-width: 980px; + margin: 28px auto; + padding: 18px; + border-radius: 12px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent); + box-shadow: 0 6px 30px rgba(2, 6, 23, 0.6) +} + +header { + display: flex; + align-items: center; + gap: 14px +} + +h1 { + font-size: 20px; + margin: 0 +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 12px +} + +button, +select, +label { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.08); + padding: 8px 10px; + border-radius: 8px; + color: inherit +} + +button:hover, +select:hover { + border-color: var(--accent); + cursor: pointer +} + +.stage { + display: grid; + grid-template-columns: 1fr 320px; + gap: 14px; + margin-top: 14px +} + +.viewer { + background: #031022; + border-radius: 10px; + padding: 10px; + display: flex; + align-items: center; + justify-content: center; + min-height: 360px +} + +video { + max-width: 100%; + max-height: 100%; + border-radius: 8px +} + +.sidebar { + padding: 10px; + border-radius: 8px; + background: linear-gradient(180deg, #051426, #021018) +} + +.row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px +} + +.label { + font-size: 13px; + color: rgba(255, 255, 255, 0.7) +} + +#canvas { + max-width: 100%; + border-radius: 8px; + background: #081220 +} + +.hint { + font-size: 12px; + color: rgba(255, 255, 255, 0.45) +} + +footer { + margin-top: 12px; + font-size: 12px; + color: rgba(255, 255, 255, 0.45) +} \ No newline at end of file From 35554ddcc201d5ad9fe41ae7020517094d58a11f Mon Sep 17 00:00:00 2001 From: Zain Amjad Date: Mon, 13 Oct 2025 19:41:59 +0100 Subject: [PATCH 2/2] created README.md --- Javascript/Virtual Mirror/README.md | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Javascript/Virtual Mirror/README.md diff --git a/Javascript/Virtual Mirror/README.md b/Javascript/Virtual Mirror/README.md new file mode 100644 index 0000000..0d25220 --- /dev/null +++ b/Javascript/Virtual Mirror/README.md @@ -0,0 +1,33 @@ +# πŸͺž Webcam Mirror β€” JavaScript Demo + +A lightweight web app that uses your device’s **webcam** and flips the video feed **horizontally**, so it behaves just like a real mirror. +Built with plain **HTML**, **CSS**, and **JavaScript** β€” no frameworks required. + +--- + +## πŸ“Έ Features + +- πŸ” **Real mirror effect** β€” flips the camera preview horizontally. +- πŸŽ₯ **Camera selection** β€” choose between multiple cameras (front/back on mobile). +- πŸ–ΌοΈ **Snapshot capture** β€” take photos that respect the mirror orientation. +- πŸ’Ύ **Instant download** β€” download your mirrored photo as a `.png` file. +- βš™οΈ **Toggle mirroring** β€” turn the mirror effect on or off anytime. +- πŸŒ— **Dark UI** β€” modern, clean, and responsive design. + +--- + +## πŸš€ Live Demo + +You can run this app locally by opening the HTML file directly in a browser that supports **`getUserMedia()`** (most modern browsers). + +--- + +## 🧠 How It Works + +The app uses the **WebRTC API** (`navigator.mediaDevices.getUserMedia`) to access your webcam stream and display it in a `