|
|
|
|
@@ -8,7 +8,7 @@ const scannedCodes = ref([])
|
|
|
|
|
const hasMultipleCameras = ref(false)
|
|
|
|
|
const isFullscreen = ref(false)
|
|
|
|
|
const videoAspect = ref(1)
|
|
|
|
|
const isFront = computed(() => facingMode.value === 'user')
|
|
|
|
|
const isMirrored = ref(false)
|
|
|
|
|
const wrapperRef = ref(null)
|
|
|
|
|
const bgCanvas = ref(null)
|
|
|
|
|
let bgRafId = null
|
|
|
|
|
@@ -19,6 +19,95 @@ let stream = null
|
|
|
|
|
let scanRafId = null
|
|
|
|
|
let barcodeDetector = null
|
|
|
|
|
|
|
|
|
|
const overlayCanvas = ref(null)
|
|
|
|
|
|
|
|
|
|
const paintDetections = (codes) => {
|
|
|
|
|
const canvas = overlayCanvas.value
|
|
|
|
|
const video = videoRef.value
|
|
|
|
|
|
|
|
|
|
if (!canvas || !video) return
|
|
|
|
|
|
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
|
|
|
const { width, height } = canvas.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
// Update canvas size if needed (to match CSS size)
|
|
|
|
|
if (canvas.width !== width || canvas.height !== height) {
|
|
|
|
|
canvas.width = width
|
|
|
|
|
canvas.height = height
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, width, height)
|
|
|
|
|
|
|
|
|
|
if (!codes || codes.length === 0) return
|
|
|
|
|
|
|
|
|
|
const vw = video.videoWidth
|
|
|
|
|
const vh = video.videoHeight
|
|
|
|
|
if (!vw || !vh) return
|
|
|
|
|
|
|
|
|
|
// Calculate object-fit: cover scaling
|
|
|
|
|
const videoRatio = vw / vh
|
|
|
|
|
const canvasRatio = width / height
|
|
|
|
|
|
|
|
|
|
let drawWidth, drawHeight, startX, startY
|
|
|
|
|
|
|
|
|
|
if (canvasRatio > videoRatio) {
|
|
|
|
|
// Canvas is wider than video (video cropped top/bottom)
|
|
|
|
|
drawWidth = width
|
|
|
|
|
drawHeight = width / videoRatio
|
|
|
|
|
startX = 0
|
|
|
|
|
startY = (height - drawHeight) / 2
|
|
|
|
|
} else {
|
|
|
|
|
// Canvas is taller than video (video cropped left/right)
|
|
|
|
|
drawHeight = height
|
|
|
|
|
drawWidth = height * videoRatio
|
|
|
|
|
startY = 0
|
|
|
|
|
startX = (width - drawWidth) / 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scale = drawWidth / vw
|
|
|
|
|
// Canvas is mirrored via CSS if isMirrored is true, so no manual coordinate mirroring needed
|
|
|
|
|
|
|
|
|
|
// Styles
|
|
|
|
|
const styles = getComputedStyle(document.documentElement)
|
|
|
|
|
const accent = styles.getPropertyValue('--primary-accent').trim() || '#00f2fe'
|
|
|
|
|
|
|
|
|
|
ctx.lineWidth = 4
|
|
|
|
|
ctx.strokeStyle = accent
|
|
|
|
|
ctx.fillStyle = accent
|
|
|
|
|
|
|
|
|
|
codes.forEach(code => {
|
|
|
|
|
const points = code.cornerPoints
|
|
|
|
|
if (!points || points.length < 4) return
|
|
|
|
|
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
|
|
|
|
|
const transform = (p) => {
|
|
|
|
|
let x = p.x * scale + startX
|
|
|
|
|
let y = p.y * scale + startY
|
|
|
|
|
return { x, y }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const p0 = transform(points[0])
|
|
|
|
|
ctx.moveTo(p0.x, p0.y)
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i < points.length; i++) {
|
|
|
|
|
const p = transform(points[i])
|
|
|
|
|
ctx.lineTo(p.x, p.y)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.closePath()
|
|
|
|
|
ctx.stroke()
|
|
|
|
|
|
|
|
|
|
// Draw corners
|
|
|
|
|
points.forEach(p => {
|
|
|
|
|
const tp = transform(p)
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
ctx.arc(tp.x, tp.y, 4, 0, Math.PI * 2)
|
|
|
|
|
ctx.fill()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateVideoAspect = () => {
|
|
|
|
|
if (videoRef.value && videoRef.value.videoWidth && videoRef.value.videoHeight) {
|
|
|
|
|
videoAspect.value = videoRef.value.videoWidth / videoRef.value.videoHeight
|
|
|
|
|
@@ -110,6 +199,23 @@ const startScan = async () => {
|
|
|
|
|
|
|
|
|
|
stream = await navigator.mediaDevices.getUserMedia(constraints)
|
|
|
|
|
|
|
|
|
|
// Detect actual facing mode to mirror front camera correctly
|
|
|
|
|
const videoTrack = stream.getVideoTracks()[0]
|
|
|
|
|
if (videoTrack) {
|
|
|
|
|
const settings = videoTrack.getSettings()
|
|
|
|
|
if (settings.facingMode) {
|
|
|
|
|
isMirrored.value = settings.facingMode === 'user'
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback: check label for desktop cameras or assume requested mode
|
|
|
|
|
const label = videoTrack.label ? videoTrack.label.toLowerCase() : ''
|
|
|
|
|
if (label.includes('front') || label.includes('facetime') || label.includes('macbook')) {
|
|
|
|
|
isMirrored.value = true
|
|
|
|
|
} else {
|
|
|
|
|
isMirrored.value = facingMode.value === 'user'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (videoRef.value) {
|
|
|
|
|
videoRef.value.srcObject = stream
|
|
|
|
|
// Wait for metadata to play
|
|
|
|
|
@@ -142,6 +248,7 @@ const detectLoop = async () => {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const codes = await barcodeDetector.detect(videoRef.value)
|
|
|
|
|
paintDetections(codes)
|
|
|
|
|
if (codes.length > 0) {
|
|
|
|
|
onDetect(codes)
|
|
|
|
|
}
|
|
|
|
|
@@ -370,7 +477,7 @@ const isUrl = (string) => {
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
class="camera-wrapper"
|
|
|
|
|
:class="{ 'clickable': !isFullscreen, 'is-front': isFront }"
|
|
|
|
|
:class="{ 'clickable': !isFullscreen, 'is-mirrored': isMirrored }"
|
|
|
|
|
:style="desktopFullscreenStyle"
|
|
|
|
|
ref="wrapperRef"
|
|
|
|
|
@click="!isFullscreen && toggleFullscreen()"
|
|
|
|
|
@@ -378,12 +485,14 @@ const isUrl = (string) => {
|
|
|
|
|
<video
|
|
|
|
|
ref="videoRef"
|
|
|
|
|
class="camera-feed"
|
|
|
|
|
:class="{ 'is-front': isFront }"
|
|
|
|
|
:class="{ 'is-mirrored': isMirrored }"
|
|
|
|
|
autoplay
|
|
|
|
|
playsinline
|
|
|
|
|
muted
|
|
|
|
|
></video>
|
|
|
|
|
|
|
|
|
|
<canvas ref="overlayCanvas" class="scan-overlay-canvas" :class="{ 'is-mirrored': isMirrored }"></canvas>
|
|
|
|
|
|
|
|
|
|
<div v-if="error" class="error-overlay">
|
|
|
|
|
<p>{{ error }}</p>
|
|
|
|
|
<button @click="startScan" class="retry-btn">Retry</button>
|
|
|
|
|
@@ -536,7 +645,21 @@ const isUrl = (string) => {
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.camera-feed.is-front {
|
|
|
|
|
.camera-feed.is-mirrored {
|
|
|
|
|
transform: scaleX(-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scan-overlay-canvas {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
z-index: 5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scan-overlay-canvas.is-mirrored {
|
|
|
|
|
transform: scaleX(-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|