Files
tools-app/src/composables/useQrDetection.js

175 lines
4.5 KiB
JavaScript

import { ref, onMounted, onUnmounted } from 'vue'
export function useQrDetection(videoRef, overlayCanvasRef) {
let barcodeDetector = null // must be plain variable, NOT a Vue ref (Proxy breaks native private fields)
const isDetecting = ref(false)
const error = ref('')
let scanRafId = null
// Function to initialize detector
const initDetector = async () => {
if (!barcodeDetector) {
if ('BarcodeDetector' in window) {
try {
const formats = await window.BarcodeDetector.getSupportedFormats()
if (formats.includes('qr_code')) {
barcodeDetector = new window.BarcodeDetector({ formats: ['qr_code'] })
} else {
barcodeDetector = new window.BarcodeDetector()
}
} catch (e) {
barcodeDetector = new window.BarcodeDetector()
}
} else {
error.value = 'Barcode Detection API not supported on this device/browser.'
}
}
}
const paintDetections = (codes) => {
const canvas = overlayCanvasRef.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
// 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 startDetection = async (onDetectCallback) => {
error.value = ''
try {
await initDetector()
if (!barcodeDetector) {
if (!error.value) error.value = 'Barcode Detector failed to initialize'
return
}
isDetecting.value = true
const detectLoop = async () => {
const video = videoRef.value
if (!isDetecting.value) return
if (!video || video.readyState < 2) {
scanRafId = requestAnimationFrame(detectLoop)
return
}
try {
const codes = await barcodeDetector.detect(video)
paintDetections(codes)
if (codes.length > 0 && onDetectCallback) {
onDetectCallback(codes)
}
} catch (e) {
// Silent catch for intermittent detection frames failing
}
if (isDetecting.value) {
scanRafId = requestAnimationFrame(detectLoop)
}
}
detectLoop() // start loop
} catch (e) {
error.value = `Detection error: ${e.message}`
}
}
const stopDetection = () => {
isDetecting.value = false
if (scanRafId) cancelAnimationFrame(scanRafId)
// Clear canvas
if (overlayCanvasRef.value) {
const ctx = overlayCanvasRef.value.getContext('2d')
ctx.clearRect(0, 0, overlayCanvasRef.value.width, overlayCanvasRef.value.height)
}
}
onUnmounted(() => {
stopDetection()
})
return {
error,
isDetecting,
startDetection,
stopDetection
}
}