Download src/lib/components/Scanner/CameraCapture.svelte from Fraser/piclets: direct link, hf CLI and curl.
- Browser
- Download file 4.34 kB
-
https://huggingface.co/spaces/Fraser/piclets/resolve/main/src/lib/components/Scanner/CameraCapture.svelte
- Command line
-
hf download hf://spaces/Fraser/piclets/src/lib/components/Scanner/CameraCapture.svelte
-
curl -L -o CameraCapture.svelte https://huggingface.co/spaces/Fraser/piclets/resolve/main/src/lib/components/Scanner/CameraCapture.svelte
4.34 kB
| <script lang="ts"> | |
| import { onMount, onDestroy } from 'svelte'; | |
| interface Props { | |
| onCapture: (file: File) => void; | |
| } | |
| let { onCapture }: Props = $props(); | |
| let video: HTMLVideoElement; | |
| let stream: MediaStream | null = null; | |
| let error: string | null = $state(null); | |
| let ready = $state(false); | |
| onMount(async () => { | |
| try { | |
| stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { facingMode: 'environment' }, | |
| audio: false, | |
| }); | |
| video.srcObject = stream; | |
| await video.play(); | |
| ready = true; | |
| } catch (err) { | |
| error = | |
| err instanceof DOMException && err.name === 'NotAllowedError' | |
| ? 'Camera permission denied. Please allow camera access in your browser settings.' | |
| : err instanceof Error | |
| ? err.message | |
| : 'Could not access camera.'; | |
| } | |
| }); | |
| onDestroy(() => { | |
| stream?.getTracks().forEach((t) => t.stop()); | |
| }); | |
| function capture() { | |
| if (!ready || !video.videoWidth) return; | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = video.videoWidth; | |
| canvas.height = video.videoHeight; | |
| const ctx = canvas.getContext('2d'); | |
| if (!ctx) return; | |
| ctx.drawImage(video, 0, 0); | |
| canvas.toBlob( | |
| (blob) => { | |
| if (blob) { | |
| const file = new File([blob], 'capture.jpg', { type: 'image/jpeg' }); | |
| onCapture(file); | |
| } | |
| }, | |
| 'image/jpeg', | |
| 0.9, | |
| ); | |
| } | |
| </script> | |
| <div class="camera-container"> | |
| {#if error} | |
| <div class="camera-error"> | |
| <div class="error-icon">📷</div> | |
| <p>{error}</p> | |
| </div> | |
| {:else} | |
| <video | |
| bind:this={video} | |
| class="camera-video" | |
| autoplay | |
| playsinline | |
| muted | |
| ></video> | |
| {#if ready} | |
| <div class="capture-overlay"> | |
| <div class="reticle"></div> | |
| <button class="capture-btn" onclick={capture} aria-label="Take photo"> | |
| <div class="capture-ring"></div> | |
| </button> | |
| <p class="hint">Point at an object and tap to scan</p> | |
| </div> | |
| {:else} | |
| <div class="camera-loading"> | |
| <div class="spinner"></div> | |
| <p>Starting camera…</p> | |
| </div> | |
| {/if} | |
| {/if} | |
| </div> | |
| <style> | |
| .camera-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| background: #000; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .camera-video { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .capture-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: flex-end; | |
| padding-bottom: 2rem; | |
| pointer-events: none; | |
| } | |
| .reticle { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 200px; | |
| height: 200px; | |
| border: 2px solid rgba(255, 255, 255, 0.4); | |
| border-radius: 16px; | |
| pointer-events: none; | |
| } | |
| .capture-btn { | |
| width: 72px; | |
| height: 72px; | |
| border: none; | |
| background: none; | |
| cursor: pointer; | |
| padding: 0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| pointer-events: auto; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| .capture-ring { | |
| width: 64px; | |
| height: 64px; | |
| border: 4px solid white; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.3); | |
| transition: all 0.15s; | |
| } | |
| .capture-btn:active .capture-ring { | |
| transform: scale(0.85); | |
| background: rgba(255, 255, 255, 0.6); | |
| } | |
| .hint { | |
| color: rgba(255, 255, 255, 0.7); | |
| font-size: 0.85rem; | |
| margin-top: 0.75rem; | |
| text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5); | |
| } | |
| .camera-loading { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 1rem; | |
| color: white; | |
| } | |
| .spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 3px solid rgba(255, 255, 255, 0.2); | |
| border-top-color: white; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .camera-error { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 1rem; | |
| color: white; | |
| text-align: center; | |
| padding: 2rem; | |
| } | |
| .error-icon { | |
| font-size: 3rem; | |
| } | |
| </style> | |