refactor: extract logic from QrScanner and UrlCleaner to composables
This commit is contained in:
174
src/composables/useQrDetection.js
Normal file
174
src/composables/useQrDetection.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useQrDetection(videoRef, overlayCanvasRef) {
|
||||
const barcodeDetector = ref(null)
|
||||
const isDetecting = ref(false)
|
||||
const error = ref('')
|
||||
let scanRafId = null
|
||||
|
||||
// Function to initialize detector
|
||||
const initDetector = async () => {
|
||||
if (!barcodeDetector.value) {
|
||||
if ('BarcodeDetector' in window) {
|
||||
try {
|
||||
// Formats are optional, but specifying qr_code might be faster
|
||||
const formats = await window.BarcodeDetector.getSupportedFormats()
|
||||
if (formats.includes('qr_code')) {
|
||||
barcodeDetector.value = new window.BarcodeDetector({ formats: ['qr_code'] })
|
||||
} else {
|
||||
barcodeDetector.value = new window.BarcodeDetector()
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback
|
||||
barcodeDetector.value = 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.value) {
|
||||
if (!error.value) error.value = 'Barcode Detector failed to initialize'
|
||||
return
|
||||
}
|
||||
|
||||
isDetecting.value = true
|
||||
|
||||
const detectLoop = async () => {
|
||||
if (!isDetecting.value || !videoRef.value || videoRef.value.paused || videoRef.value.ended) {
|
||||
scanRafId = requestAnimationFrame(detectLoop)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const codes = await barcodeDetector.value.detect(videoRef.value)
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user