Spaces:
Running
Running
<template> | |
<div class="zoom-view"> | |
<div v-if="hasAnnotations" class="zoom-image-container"> | |
<canvas | |
ref="zoomCanvas" | |
class="zoom-canvas" | |
:width="zoomWidth" | |
:height="zoomHeight" | |
></canvas> | |
<div class="zoom-info"> | |
<span>Frame {{ currentFrameNumber }}</span> | |
<span v-if="zoomRegion">{{ Math.round(zoomRegion.width) }}x{{ Math.round(zoomRegion.height) }}px</span> | |
<span v-if="zoomRegion?.type === 'points' && selectedAnnotations.length > 1"> | |
{{ selectedAnnotations.length }} points | |
</span> | |
</div> | |
</div> | |
<div v-else class="no-annotations"> | |
<p>Aucune annotation sur cette frame</p> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { useAnnotationStore } from '@/stores/annotationStore' | |
import { useVideoStore } from '@/stores/videoStore' | |
import { computed, ref, watch, nextTick, onMounted } from 'vue' | |
export default { | |
name: 'ZoomView', | |
mounted() { | |
// Forcer le rafraîchissement quand le composant est monté | |
this.$nextTick(() => { | |
setTimeout(() => { | |
this.updateZoomImage() | |
}, 100) // Petit délai pour s'assurer que la vidéo est prête | |
}) | |
}, | |
setup() { | |
const annotationStore = useAnnotationStore() | |
const videoStore = useVideoStore() | |
const zoomCanvas = ref(null) | |
// Dimensions du canvas de zoom | |
const zoomWidth = 200 | |
const zoomHeight = 300 | |
const getCurrentFrameNumber = () => { | |
const frameRate = annotationStore.currentSession?.frameRate || 30 | |
return Math.round(videoStore.currentTime * frameRate) | |
} | |
const currentFrameNumber = computed(() => getCurrentFrameNumber()) | |
const selectedAnnotations = computed(() => { | |
const currentFrame = getCurrentFrameNumber() | |
const frameAnnotations = annotationStore.getAnnotationsForFrame(currentFrame) || [] | |
return frameAnnotations.filter( | |
annotation => annotation && annotation.objectId === annotationStore.selectedObjectId | |
) | |
}) | |
const hasAnnotations = computed(() => { | |
return selectedAnnotations.value.length > 0 | |
}) | |
const zoomRegion = computed(() => { | |
const annotations = selectedAnnotations.value | |
if (!annotations.length) return null | |
// Séparer rectangles et points | |
const rectangles = annotations.filter(a => a.type === 'rectangle') | |
const points = annotations.filter(a => a.type === 'point') | |
if (rectangles.length > 0) { | |
// Utiliser le premier rectangle trouvé | |
const rect = rectangles[0] | |
return { | |
x: rect.x, | |
y: rect.y, | |
width: rect.width, | |
height: rect.height, | |
type: 'rectangle' | |
} | |
} else if (points.length > 0) { | |
// Calculer le centre moyen des points | |
const avgX = points.reduce((sum, p) => sum + p.x, 0) / points.length | |
const avgY = points.reduce((sum, p) => sum + p.y, 0) / points.length | |
if (points.length === 1) { | |
// Pour un seul point, utiliser une taille fixe raisonnable | |
const fixedSize = 120 | |
return { | |
x: avgX - fixedSize, | |
y: avgY - fixedSize, | |
width: fixedSize * 2, | |
height: fixedSize * 2, | |
type: 'points', | |
centerX: avgX, | |
centerY: avgY | |
} | |
} else { | |
// Pour plusieurs points, calculer la bounding box englobante | |
const minX = Math.min(...points.map(p => p.x)) | |
const maxX = Math.max(...points.map(p => p.x)) | |
const minY = Math.min(...points.map(p => p.y)) | |
const maxY = Math.max(...points.map(p => p.y)) | |
// Calculer les dimensions nécessaires | |
const pointsWidth = maxX - minX | |
const pointsHeight = maxY - minY | |
// Ajouter une marge (minimum 60px de chaque côté) | |
const marginX = Math.max(60, pointsWidth * 0.3) | |
const marginY = Math.max(60, pointsHeight * 0.3) | |
// Calculer les dimensions finales | |
const finalWidth = pointsWidth + marginX * 2 | |
const finalHeight = pointsHeight + marginY * 2 | |
return { | |
x: minX - marginX, | |
y: minY - marginY, | |
width: finalWidth, | |
height: finalHeight, | |
type: 'points', | |
centerX: avgX, | |
centerY: avgY, | |
pointsBounds: { | |
minX, maxX, minY, maxY, | |
pointsWidth, pointsHeight | |
} | |
} | |
} | |
} | |
return null | |
}) | |
const drawAnnotationsOnZoom = (ctx, sourceX, sourceY, sourceWidth, sourceHeight) => { | |
const annotations = selectedAnnotations.value | |
if (!annotations.length) return | |
// Calculer le facteur d'échelle entre la source et le canvas | |
const scaleX = zoomWidth / sourceWidth | |
const scaleY = zoomHeight / sourceHeight | |
annotations.forEach(annotation => { | |
if (annotation.type === 'rectangle') { | |
// Calculer la position du rectangle dans le canvas zoomé | |
const rectX = (annotation.x - sourceX) * scaleX | |
const rectY = (annotation.y - sourceY) * scaleY | |
const rectWidth = annotation.width * scaleX | |
const rectHeight = annotation.height * scaleY | |
// Ne dessiner que si le rectangle est visible dans la zone | |
if (rectX < zoomWidth && rectY < zoomHeight && | |
rectX + rectWidth > 0 && rectY + rectHeight > 0) { | |
// Dessiner le rectangle | |
ctx.strokeStyle = '#00ff00' // Vert pour les rectangles | |
ctx.lineWidth = 2 | |
ctx.setLineDash([5, 3]) // Trait pointillé | |
ctx.strokeRect(rectX, rectY, rectWidth, rectHeight) | |
ctx.setLineDash([]) // Remettre trait plein | |
} | |
} else if (annotation.type === 'point') { | |
// Calculer la position du point dans le canvas zoomé | |
const pointX = (annotation.x - sourceX) * scaleX | |
const pointY = (annotation.y - sourceY) * scaleY | |
// Ne dessiner que si le point est visible dans la zone | |
if (pointX >= 0 && pointX <= zoomWidth && pointY >= 0 && pointY <= zoomHeight) { | |
// Couleur selon le type de point | |
const pointColor = annotation.pointType === 'positive' ? '#00ff00' : '#ff0000' | |
// Dessiner le cercle du point | |
ctx.fillStyle = pointColor | |
ctx.strokeStyle = '#ffffff' | |
ctx.lineWidth = 2 | |
ctx.beginPath() | |
ctx.arc(pointX, pointY, 6, 0, 2 * Math.PI) | |
ctx.fill() | |
ctx.stroke() | |
// Dessiner le symbole + ou - | |
ctx.strokeStyle = '#ffffff' | |
ctx.lineWidth = 2 | |
ctx.beginPath() | |
if (annotation.pointType === 'positive') { | |
// Dessiner + | |
ctx.moveTo(pointX - 3, pointY) | |
ctx.lineTo(pointX + 3, pointY) | |
ctx.moveTo(pointX, pointY - 3) | |
ctx.lineTo(pointX, pointY + 3) | |
} else { | |
// Dessiner - | |
ctx.moveTo(pointX - 3, pointY) | |
ctx.lineTo(pointX + 3, pointY) | |
} | |
ctx.stroke() | |
} | |
} | |
}) | |
// Si c'est une vue centrée sur des points, dessiner une croix de repère au centre | |
if (zoomRegion.value?.type === 'points') { | |
ctx.strokeStyle = '#ffff00' // Jaune pour le centre | |
ctx.lineWidth = 1 | |
ctx.setLineDash([3, 3]) | |
ctx.beginPath() | |
const centerX = zoomWidth / 2 | |
const centerY = zoomHeight / 2 | |
ctx.moveTo(centerX - 15, centerY) | |
ctx.lineTo(centerX + 15, centerY) | |
ctx.moveTo(centerX, centerY - 15) | |
ctx.lineTo(centerX, centerY + 15) | |
ctx.stroke() | |
ctx.setLineDash([]) | |
} | |
} | |
const updateZoomImage = async () => { | |
if (!zoomCanvas.value || !zoomRegion.value) return | |
// Trouver l'élément vidéo | |
const videoElement = document.querySelector('video') | |
if (!videoElement) return | |
const canvas = zoomCanvas.value | |
const ctx = canvas.getContext('2d') | |
// Effacer le canvas | |
ctx.clearRect(0, 0, zoomWidth, zoomHeight) | |
try { | |
// Calculer les coordonnées source dans la vidéo | |
const sourceX = Math.max(0, zoomRegion.value.x) | |
const sourceY = Math.max(0, zoomRegion.value.y) | |
const sourceWidth = Math.min(zoomRegion.value.width, videoElement.videoWidth - sourceX) | |
const sourceHeight = Math.min(zoomRegion.value.height, videoElement.videoHeight - sourceY) | |
// S'assurer que les dimensions sont valides | |
if (sourceWidth <= 0 || sourceHeight <= 0) return | |
// Dessiner la région zoomée sur le canvas | |
ctx.drawImage( | |
videoElement, | |
sourceX, sourceY, sourceWidth, sourceHeight, // Source (région de la vidéo) | |
0, 0, zoomWidth, zoomHeight // Destination (canvas) | |
) | |
// Dessiner les annotations sur l'image zoomée | |
drawAnnotationsOnZoom(ctx, sourceX, sourceY, sourceWidth, sourceHeight) | |
} catch (error) { | |
console.error('Erreur lors de la capture du zoom:', error) | |
// Afficher un message d'erreur sur le canvas | |
ctx.fillStyle = '#666' | |
ctx.fillRect(0, 0, zoomWidth, zoomHeight) | |
ctx.fillStyle = '#fff' | |
ctx.font = '14px Arial' | |
ctx.textAlign = 'center' | |
ctx.fillText('Erreur de capture', zoomWidth / 2, zoomHeight / 2) | |
} | |
} | |
// Watcher pour mettre à jour l'image quand les annotations changent | |
watch([selectedAnnotations, currentFrameNumber], () => { | |
nextTick(() => { | |
updateZoomImage() | |
}) | |
}, { deep: true }) | |
// Watcher pour mettre à jour quand le temps de la vidéo change | |
watch(() => videoStore.currentTime, () => { | |
nextTick(() => { | |
updateZoomImage() | |
}) | |
}) | |
// Hook onMounted pour forcer le rafraîchissement au montage | |
onMounted(() => { | |
nextTick(() => { | |
setTimeout(() => { | |
updateZoomImage() | |
}, 200) // Délai plus long pour s'assurer que tout est prêt | |
}) | |
}) | |
return { | |
selectedAnnotations, | |
hasAnnotations, | |
zoomRegion, | |
currentFrameNumber, | |
zoomCanvas, | |
zoomWidth, | |
zoomHeight, | |
updateZoomImage | |
} | |
} | |
} | |
</script> | |
<style scoped> | |
.zoom-view { | |
height: 100%; | |
padding: 10px; | |
color: white; | |
overflow: auto; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
} | |
.zoom-image-container { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
gap: 8px; | |
} | |
.zoom-canvas { | |
border: 1px solid #555; | |
border-radius: 4px; | |
background: #000; | |
} | |
.zoom-info { | |
display: flex; | |
gap: 12px; | |
font-size: 0.8rem; | |
color: #ccc; | |
} | |
.no-annotations { | |
text-align: center; | |
color: #888; | |
font-style: italic; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
height: 100%; | |
} | |
.no-annotations p { | |
margin: 0; | |
} | |
</style> |