Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Javascript/Virtual Mirror/README.md
Original file line number Diff line number Diff line change
@@ -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 `<video>` element.
CSS is used to flip the live preview horizontally using:

```css
video {
transform: scaleX(-1);
}
49 changes: 49 additions & 0 deletions Javascript/Virtual Mirror/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Webcam Mirror</title>
<link rel="stylesheet" href="styles/styles.css">
</head>
<body>
<div class="app">
<header>
<img src="resources/camera.svg" alt="Camera Icon" width="36" height="36" />
<div>
<h1>Webcam Mirror</h1>
<div class="hint">Uses your webcam and flips the preview horizontally like a real mirror.</div>
</div>
</header>

<div class="controls">
<button id="startBtn">Start Camera</button>
<button id="stopBtn" disabled>Stop Camera</button>
<select id="deviceSelect" aria-label="Choose camera"></select>
<label class="row" style="align-items:center"><input type="checkbox" id="mirrorToggle" checked> Mirror preview</label>
<button id="captureBtn" disabled>Capture Photo</button>
<a id="downloadLink" style="display:none" download="mirror-snapshot.png">Download snapshot</a>
</div>

<div class="stage">
<div class="viewer">
<video id="video" autoplay playsinline muted></video>
</div>

<aside class="sidebar">
<div class="row"><div class="label">Snapshot</div></div>
<canvas id="canvas" width="320" height="240" aria-hidden></canvas>
<div class="row" style="margin-top:8px"><div class="hint">Captured image respects the mirror setting so it looks like a real mirror photo.</div></div>
<div style="height:8px"></div>
<div class="row"><div class="label">Status:</div><div id="status" class="hint">Idle</div></div>
</aside>
</div>

<footer>
Permission to access the camera will be requested. If nothing happens, check your browser permissions or try a different camera from the dropdown.
</footer>
</div>

<script src="scripts/script.js"></script>
</body>
</html>
15 changes: 15 additions & 0 deletions Javascript/Virtual Mirror/resources/camera.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
138 changes: 138 additions & 0 deletions Javascript/Virtual Mirror/scripts/script.js
Original file line number Diff line number Diff line change
@@ -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 = '<option disabled>No cameras found</option>';
}catch(err){
console.error('Cannot list devices', err);
deviceSelect.innerHTML = '<option disabled>Unable to enumerate devices</option>';
}
}

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();
});
116 changes: 116 additions & 0 deletions Javascript/Virtual Mirror/styles/styles.css
Original file line number Diff line number Diff line change
@@ -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)
}
Loading