175 lines
4.5 KiB
JavaScript
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
|
|
}
|
|
}
|