diff --git a/src/app/routes/receiver.tsx b/src/app/routes/receiver.tsx index 09598a4..4fe29ff 100644 --- a/src/app/routes/receiver.tsx +++ b/src/app/routes/receiver.tsx @@ -384,13 +384,7 @@ export function ReceiverPage() { const capabilities = track.getCapabilities() as any; if (capabilities?.zoom) { setHasZoomSupport(true); - const idealZoom = Math.min(2, capabilities.zoom.max ?? 2); - try { - await track.applyConstraints({ advanced: [{ zoom: idealZoom }] } as any); - setZoomLevel(idealZoom); - } catch { - // ignore - } + setZoomLevel(1); } else { setHasZoomSupport(false); setZoomLevel(1); @@ -511,7 +505,7 @@ export function ReceiverPage() { }, []); - // ── Capture frame from camera (software crop + optional camera zoom) ─────── + // ── Capture the full camera frame ───────────────────────────────────────── const captureFrame = useCallback(() => { const video = videoRef.current; const canvas = canvasRef.current; @@ -538,20 +532,11 @@ export function ReceiverPage() { canvas.width = cw; canvas.height = ch; - // Crop region: center 50% of the frame (2× software zoom) - // If camera zoom is active, the video already shows a zoomed view, - // so we crop less aggressively. - const cropRatio = zoomLevel > 1 ? 0.6 : 0.5; - const cropW = vw * cropRatio; - const cropH = vh * cropRatio; - const sx = (vw - cropW) / 2; - const sy = (vh - cropH) / 2; - - ctx.drawImage(video, sx, sy, cropW, cropH, 0, 0, cw, ch); + ctx.drawImage(video, 0, 0, vw, vh, 0, 0, cw, ch); const imageData = ctx.getImageData(0, 0, cw, ch); worker.postMessage({ type: 'frame', imageData, realtime: true }); - }, [zoomLevel]); + }, []); // ── Download recovered file ────────────────────────────────────────────── const handleDownload = useCallback(() => { @@ -634,21 +619,17 @@ export function ReceiverPage() {
{/* Corner markers */} -
-
-
-
+
+
+
+
@@ -711,8 +692,8 @@ export function ReceiverPage() {

{hasZoomSupport - ? 'Camera zoom is active. The dashed square shows the scan region.' - : 'Software crop is applied to the center 50% of the frame (dashed square).'} + ? 'Full-frame scan is active. Use Zoom only if the QR codes are too small.' + : 'Full-frame scan is active.'}

{error &&
⚠ {error}
}
diff --git a/src/app/routes/sender.tsx b/src/app/routes/sender.tsx index 783e224..e256e07 100644 --- a/src/app/routes/sender.tsx +++ b/src/app/routes/sender.tsx @@ -14,6 +14,7 @@ import { // ─── Types ─────────────────────────────────────────────────────────────────── type InputMode = 'text' | 'file'; +type ParallelQRCount = 1 | 2 | 4; interface GifResult { gifData: ArrayBuffer; @@ -22,17 +23,23 @@ interface GifResult { frameCount: number; frameRateFps: number; frameDelayMs: number; + parallelCount: ParallelQRCount; } interface LiveTransfer { packets: Uint8Array[]; width: number; height: number; + tileWidth: number; + tileHeight: number; + columns: number; + rows: number; version: number; eccLevel: QRTransferProfile['eccLevel']; symbolSize: number; scale: number; - frameCount: number; + displayFrameCount: number; + parallelCount: ParallelQRCount; } interface FrameCache { @@ -47,6 +54,8 @@ type CSSProps = Record; const MIN_FRAME_RATE_FPS = 2; const MAX_FRAME_RATE_FPS = 60; const DEFAULT_FRAME_RATE_FPS = 30; +const DEFAULT_PARALLEL_QR_COUNT: ParallelQRCount = 1; +const PARALLEL_QR_COUNTS: ParallelQRCount[] = [1, 2, 4]; const LIVE_TARGET_PX = 360; const QR_QUIET_ZONE_MODULES = 4; const FRAME_CACHE_LIMIT = 120; @@ -125,9 +134,10 @@ const S = { maxWidth: '100%', }), fullscreenPreview: { - width: 'min(100vw, 100vh)', - height: 'min(100vw, 100vh)', + width: '100vw', + height: '100vh', borderRadius: 0, + objectFit: 'contain', } as CSSProps, infoGrid: { display: 'grid', @@ -207,6 +217,7 @@ export function SenderPage() { const [status, setStatus] = useState(''); const [qrProfileId, setQrProfileId] = useState(DEFAULT_QR_PROFILE_ID); const [frameRateFps, setFrameRateFps] = useState(DEFAULT_FRAME_RATE_FPS); + const [parallelQRCount, setParallelQRCount] = useState(DEFAULT_PARALLEL_QR_COUNT); const [liveTransfer, setLiveTransfer] = useState(null); const [gifResult, setGifResult] = useState(null); const [stats, setStats] = useState<{ originalSize: number; preprocessedSize: number; frameCount: number; totalGenerations: number } | null>(null); @@ -240,9 +251,9 @@ export function SenderPage() { if (!transfer || !canvas) return; try { - const frameIndex = liveFrameIndexRef.current % transfer.frameCount; + const frameIndex = liveFrameIndexRef.current % transfer.displayFrameCount; drawLiveFrame(canvas, transfer, frameIndex, frameCacheRef.current); - liveFrameIndexRef.current = (frameIndex + 1) % transfer.frameCount; + liveFrameIndexRef.current = (frameIndex + 1) % transfer.displayFrameCount; scheduleNextLiveFrame(); } catch (err: any) { clearPlaybackTimer(); @@ -262,9 +273,9 @@ export function SenderPage() { liveFrameIndexRef.current = 0; } - const frameIndex = liveFrameIndexRef.current % transfer.frameCount; + const frameIndex = liveFrameIndexRef.current % transfer.displayFrameCount; drawLiveFrame(canvas, transfer, frameIndex, frameCacheRef.current); - liveFrameIndexRef.current = (frameIndex + 1) % transfer.frameCount; + liveFrameIndexRef.current = (frameIndex + 1) % transfer.displayFrameCount; scheduleNextLiveFrame(); }, [clearPlaybackTimer, scheduleNextLiveFrame]); @@ -339,6 +350,11 @@ export function SenderPage() { } }, [startLivePlayback]); + const handleParallelQRCountChange = useCallback((value: string) => { + setParallelQRCount(normalizeParallelQRCount(Number(value))); + resetOutput(); + }, [resetOutput]); + const handleGenerate = useCallback(async () => { resetOutput(); @@ -406,8 +422,16 @@ export function SenderPage() { frameCount: encoded.stats.frameCount, totalGenerations: encoded.totalGenerations, }); - setLiveTransfer(createLiveTransfer(encoded.packets, selectedQRProfile)); - setStatus(`Live QR running (${encoded.stats.frameCount} frames). Preparing GIF download…`); + const nextLiveTransfer = createLiveTransfer( + encoded.packets, + selectedQRProfile, + parallelQRCount, + ); + setLiveTransfer(nextLiveTransfer); + setStatus( + `Live QR running (${encoded.stats.frameCount} packets, ` + + `${nextLiveTransfer.parallelCount} per tick). Preparing GIF download…`, + ); // ── Step 2: GIF worker ───────────────────────────────────────── const outputFrameRateFps = frameRateFpsRef.current; @@ -429,6 +453,7 @@ export function SenderPage() { frameCount: e.data.frameCount, frameRateFps: outputFrameRateFps, frameDelayMs: outputFrameDelayMs, + parallelCount: parallelQRCount, }); } else if (e.data.type === 'error') { reject(new Error(e.data.message)); @@ -442,6 +467,7 @@ export function SenderPage() { frameDelayMs: outputFrameDelayMs, qrVersion: selectedQRProfile.version, eccLevel: selectedQRProfile.eccLevel, + parallelCount: parallelQRCount, }, ); }); @@ -455,7 +481,7 @@ export function SenderPage() { } finally { setBusy(false); } - }, [mode, text, file, resetOutput, qrProfileId]); + }, [mode, text, file, resetOutput, qrProfileId, parallelQRCount]); const handleDownload = useCallback(() => { if (!gifResult) return; @@ -554,6 +580,24 @@ export function SenderPage() { Fast
+
+
+ Parallel QR + {parallelQRCount} per tick +
+ +