@@ -11,13 +11,15 @@ | |||||
"openseadragon": "^5.0.1", | "openseadragon": "^5.0.1", | ||||
"pinia": "^2.2.6", | "pinia": "^2.2.6", | ||||
"vue": "^3.5.13", | "vue": "^3.5.13", | ||||
"vue-router": "^4.4.5" | |||||
"vue-router": "^4.4.5", | |||||
"vuedraggable": "^4.1.0" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@playwright/test": "^1.49.0", | "@playwright/test": "^1.49.0", | ||||
"@tsconfig/node22": "^22.0.0", | "@tsconfig/node22": "^22.0.0", | ||||
"@types/jsdom": "^21.1.7", | "@types/jsdom": "^21.1.7", | ||||
"@types/node": "^22.9.3", | "@types/node": "^22.9.3", | ||||
"@types/openseadragon": "^3.0.10", | |||||
"@vitejs/plugin-vue": "^5.2.1", | "@vitejs/plugin-vue": "^5.2.1", | ||||
"@vitest/eslint-plugin": "1.1.10", | "@vitest/eslint-plugin": "1.1.10", | ||||
"@vue/eslint-config-prettier": "^10.1.0", | "@vue/eslint-config-prettier": "^10.1.0", | ||||
@@ -1622,6 +1624,12 @@ | |||||
"undici-types": "~6.20.0" | "undici-types": "~6.20.0" | ||||
} | } | ||||
}, | }, | ||||
"node_modules/@types/openseadragon": { | |||||
"version": "3.0.10", | |||||
"resolved": "https://registry.npmjs.org/@types/openseadragon/-/openseadragon-3.0.10.tgz", | |||||
"integrity": "sha512-nNVlu9PUNDVTudkSqVWhHtghCsyzQrJObEtN5zsVN2ghh5HYAC6U6xAcT9NQRfVfZVZkWXEYp2Hw7hQgwRxqgg==", | |||||
"dev": true | |||||
}, | |||||
"node_modules/@types/tough-cookie": { | "node_modules/@types/tough-cookie": { | ||||
"version": "4.0.5", | "version": "4.0.5", | ||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", | "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", | ||||
@@ -5738,6 +5746,11 @@ | |||||
"node": ">=18" | "node": ">=18" | ||||
} | } | ||||
}, | }, | ||||
"node_modules/sortablejs": { | |||||
"version": "1.14.0", | |||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", | |||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" | |||||
}, | |||||
"node_modules/source-map-js": { | "node_modules/source-map-js": { | ||||
"version": "1.2.1", | "version": "1.2.1", | ||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", | ||||
@@ -7718,6 +7731,17 @@ | |||||
"typescript": ">=5.0.0" | "typescript": ">=5.0.0" | ||||
} | } | ||||
}, | }, | ||||
"node_modules/vuedraggable": { | |||||
"version": "4.1.0", | |||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", | |||||
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", | |||||
"dependencies": { | |||||
"sortablejs": "1.14.0" | |||||
}, | |||||
"peerDependencies": { | |||||
"vue": "^3.0.1" | |||||
} | |||||
}, | |||||
"node_modules/w3c-xmlserializer": { | "node_modules/w3c-xmlserializer": { | ||||
"version": "5.0.0", | "version": "5.0.0", | ||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", | "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", | ||||
@@ -4,7 +4,7 @@ | |||||
"private": true, | "private": true, | ||||
"type": "module", | "type": "module", | ||||
"scripts": { | "scripts": { | ||||
"dev": "vite", | |||||
"dev": "vite --host", | |||||
"build": "run-p type-check \"build-only {@}\" --", | "build": "run-p type-check \"build-only {@}\" --", | ||||
"preview": "vite preview", | "preview": "vite preview", | ||||
"test:unit": "vitest", | "test:unit": "vitest", | ||||
@@ -18,13 +18,15 @@ | |||||
"openseadragon": "^5.0.1", | "openseadragon": "^5.0.1", | ||||
"pinia": "^2.2.6", | "pinia": "^2.2.6", | ||||
"vue": "^3.5.13", | "vue": "^3.5.13", | ||||
"vue-router": "^4.4.5" | |||||
"vue-router": "^4.4.5", | |||||
"vuedraggable": "^4.1.0" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@playwright/test": "^1.49.0", | "@playwright/test": "^1.49.0", | ||||
"@tsconfig/node22": "^22.0.0", | "@tsconfig/node22": "^22.0.0", | ||||
"@types/jsdom": "^21.1.7", | "@types/jsdom": "^21.1.7", | ||||
"@types/node": "^22.9.3", | "@types/node": "^22.9.3", | ||||
"@types/openseadragon": "^3.0.10", | |||||
"@vitejs/plugin-vue": "^5.2.1", | "@vitejs/plugin-vue": "^5.2.1", | ||||
"@vitest/eslint-plugin": "1.1.10", | "@vitest/eslint-plugin": "1.1.10", | ||||
"@vue/eslint-config-prettier": "^10.1.0", | "@vue/eslint-config-prettier": "^10.1.0", | ||||
@@ -2,7 +2,7 @@ | |||||
<div> | <div> | ||||
<main class="inline-block bg-white bg-opacity-50 m-10 relative z-10 top-0 left-0"> | <main class="inline-block bg-white bg-opacity-50 m-10 relative z-10 top-0 left-0"> | ||||
<!-- <ZoomRect /> --> | <!-- <ZoomRect /> --> | ||||
<ZoomPoint :markers="story[0].markers" /> | |||||
<StoryEditor :markers="story[0].markers" /> | |||||
</main> | </main> | ||||
<aside class="fixed top-0 left-0 w-screen h-screen" ref="openSeadragonElt"></aside> | <aside class="fixed top-0 left-0 w-screen h-screen" ref="openSeadragonElt"></aside> | ||||
</div> | </div> | ||||
@@ -19,7 +19,8 @@ import Vignemale from '../public/deepzoom/vignemale/dz/info.json' | |||||
const Cells = 'https://verrochi92.github.io/axolotl/data/W255B_0.dzi' | const Cells = 'https://verrochi92.github.io/axolotl/data/W255B_0.dzi' | ||||
import ZoomRect from './components/tools/ZoomRect.vue' | import ZoomRect from './components/tools/ZoomRect.vue' | ||||
import ZoomPoint from './components/tools/ZoomPoint.vue' | |||||
import StoryEditor from './components/tools/StoryEditor.vue' | |||||
import TestSortable from './components/tools/TestSortable.vue' | |||||
const Viewer = ref() | const Viewer = ref() | ||||
const openSeadragonElt = ref() | const openSeadragonElt = ref() | ||||
@@ -31,16 +32,20 @@ const initViewer = () => { | |||||
element: openSeadragonElt.value, | element: openSeadragonElt.value, | ||||
animationTime: 0.4, | animationTime: 0.4, | ||||
prefixUrl: 'https://openseadragon.github.io/openseadragon/images/', | prefixUrl: 'https://openseadragon.github.io/openseadragon/images/', | ||||
showNavigator: true, | |||||
sequenceMode: true, | |||||
showNavigator: false, | |||||
sequenceMode: false, | |||||
tileSources: Poulpatore, | tileSources: Poulpatore, | ||||
showNavigationControl: true, | |||||
showNavigationControl: false, | |||||
drawer: 'canvas', | drawer: 'canvas', | ||||
preventDefaultAction: true, | preventDefaultAction: true, | ||||
homeFillsViewer: true, | |||||
visibilityRatio: 1, | visibilityRatio: 1, | ||||
crossOriginPolicy: 'Anonymous', | crossOriginPolicy: 'Anonymous', | ||||
showZoomControl: true, | |||||
gestureSettingsMouse: { | |||||
scrollToZoom: true, | |||||
clickToZoom: false, | |||||
dblClickToZoom: true, | |||||
dragToPan: true, | |||||
}, | |||||
}) | }) | ||||
} | } | ||||
@@ -5,28 +5,34 @@ | |||||
"url": "https://verrochi92.github.io/axolotl/data/W255B_0.dzi", | "url": "https://verrochi92.github.io/axolotl/data/W255B_0.dzi", | ||||
"markers": [ | "markers": [ | ||||
{ | { | ||||
"id": 0, | |||||
"name": "first", | |||||
"order": 0, | "order": 0, | ||||
"position": { | "position": { | ||||
"x": 0.2, | |||||
"y": 0.2 | |||||
"x": 0.6, | |||||
"y": 0.7 | |||||
}, | }, | ||||
"zoom": 4, | "zoom": 4, | ||||
"annotation": "<b>lorem</b> ipsum dolor sit amet" | "annotation": "<b>lorem</b> ipsum dolor sit amet" | ||||
}, | }, | ||||
{ | { | ||||
"id": 1, | |||||
"name": "second", | |||||
"order": 1, | "order": 1, | ||||
"position": { | "position": { | ||||
"x": 0.2, | |||||
"x": 0.68, | |||||
"y": 0.3 | "y": 0.3 | ||||
}, | }, | ||||
"zoom": 3, | |||||
"zoom": 8, | |||||
"annotation": "<b>second</b> annotation" | "annotation": "<b>second</b> annotation" | ||||
}, | }, | ||||
{ | { | ||||
"id": 2, | |||||
"name": "third", | |||||
"order": 2, | "order": 2, | ||||
"position": { | "position": { | ||||
"x": 0.1, | |||||
"y": 0.1 | |||||
"x": 0.5, | |||||
"y": 0.2 | |||||
}, | }, | ||||
"zoom": 7, | "zoom": 7, | ||||
"annotation": "<b>third</b>" | "annotation": "<b>third</b>" | ||||
@@ -0,0 +1,17 @@ | |||||
# Story Editor | |||||
## Props: | |||||
story: Story (should be a state as Markers are CRUD ?) | |||||
## Ref: | |||||
selectedMarker: Marker | |||||
## Methods: | |||||
LoadStory | |||||
CreateMarker | |||||
SelectMarker | |||||
EditMarker | |||||
DeleteMarker |
@@ -0,0 +1,198 @@ | |||||
<template> | |||||
<nav class="[&>*]:m-3 text-black w-48"> | |||||
<div class="flex flex-col ui"> | |||||
<button @click="goHome">home</button> | |||||
<button @click="addMarker">Add Marker</button> | |||||
<div class="flex flex-col my-6"> | |||||
<button | |||||
v-for="marker in sortedMarkers" | |||||
:key="marker.order" | |||||
@click="selectMarker(marker)" | |||||
:class="marker.id === selectedMarker.id ? 'text-green-500 text-bold' : ''" | |||||
> | |||||
{{ marker.name }} | |||||
</button> | |||||
</div> | |||||
<div v-if="isSelectionModeOn"> | |||||
<input | |||||
ref="textInputMarkerName" | |||||
type="text" | |||||
v-model="selectedMarker.name" | |||||
class="w-40 my-4" | |||||
/> | |||||
<textarea v-model="selectedMarker.annotation" class="w-40 h-40"></textarea> | |||||
<button @click="saveMarker">Save</button> | |||||
<button @click="unselectMarker">Cancel</button> | |||||
</div> | |||||
</div> | |||||
</nav> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
import type { Marker } from '@/types/virages' | |||||
import { onMounted, ref, inject, nextTick, type Ref, computed } from 'vue' | |||||
import OpenSeadragon, { imageFormatSupported } from 'openseadragon' | |||||
const props = defineProps<{ markers: Marker[] }>() | |||||
const Viewer = inject<Ref<OpenSeadragon.Viewer>>('Viewer') | |||||
const isEditMode = ref<boolean>(false) | |||||
const selectedMarker = ref<Marker>({} as Marker) | |||||
const textInputMarkerName = ref<HTMLInputElement | null>(null) | |||||
const isSelectionModeOn = computed(() => { | |||||
return selectedMarker.value.id !== undefined | |||||
}) | |||||
const sortedMarkers = computed(() => { | |||||
return [...props.markers].sort((a, b) => a.order - b.order) | |||||
}) | |||||
const goHome = () => { | |||||
Viewer?.value.viewport.goHome(false) | |||||
} | |||||
const zoomPlus = () => { | |||||
Viewer?.value.viewport.zoomBy(1.2) | |||||
Viewer?.value.viewport.applyConstraints() | |||||
} | |||||
const zoomMinus = () => { | |||||
Viewer?.value.viewport.zoomBy(0.8) | |||||
Viewer?.value.viewport.applyConstraints() | |||||
} | |||||
const addMarker = () => { | |||||
isEditMode.value = true | |||||
document.querySelector('.openseadragon-canvas').classList.add('cursor-crosshair') | |||||
} | |||||
const saveMarker = () => { | |||||
unselectMarker() | |||||
} | |||||
const selectMarker = (marker: Marker) => { | |||||
document.querySelectorAll('.marker-selected').forEach((el) => { | |||||
el.classList.remove('marker-selected') | |||||
}) | |||||
const theMarker = document.querySelector('.marker-id-' + marker.id) | |||||
theMarker.classList.add('marker-selected') | |||||
Viewer?.value.viewport.zoomTo(marker.zoom, undefined, false) | |||||
Viewer?.value.viewport.panTo(new OpenSeadragon.Point(marker.position.x, marker.position.y)) | |||||
selectedMarker.value = marker | |||||
nextTick(() => { | |||||
textInputMarkerName.value?.focus() | |||||
}) | |||||
} | |||||
const unselectMarker = () => { | |||||
selectedMarker.value = {} as Marker | |||||
document.querySelector('.marker-selected')?.classList.remove('marker-selected') | |||||
} | |||||
const injectMarker = (marker: Marker) => { | |||||
const overlay = document.createElement('button') | |||||
overlay.className = `marker-id-${marker.id} marker` | |||||
overlay.title = marker.name | |||||
overlay.onfocus = function () { | |||||
selectMarker(marker) | |||||
} | |||||
overlay.addEventListener('click', function () { | |||||
selectMarker(marker) | |||||
}) | |||||
overlay.addEventListener('mouseover', function () { | |||||
Viewer?.value.setMouseNavEnabled(false) | |||||
Viewer.value.gestureSettingsMouse.dragToPan = false | |||||
}) | |||||
overlay.addEventListener('mouseout', function () { | |||||
Viewer?.value.setMouseNavEnabled(true) | |||||
}) | |||||
// Injection de l'overlay | |||||
Viewer?.value.addOverlay({ | |||||
element: overlay, | |||||
location: new OpenSeadragon.Point(marker.position.x, marker.position.y), | |||||
}) | |||||
} | |||||
const createMarkerOnClick = () => { | |||||
Viewer?.value.addHandler('canvas-click', (e) => { | |||||
if (isEditMode.value) { | |||||
const point = Viewer?.value.viewport.pointFromPixel(e.position) | |||||
const zoom = Viewer?.value.viewport.getZoom() | |||||
const newMarker: Marker = { | |||||
id: props.markers.length, // WIP | |||||
order: props.markers.length, // WIP | |||||
annotation: '', | |||||
position: new OpenSeadragon.Point(point.x, point.y), | |||||
zoom: zoom, | |||||
} | |||||
injectMarker(newMarker) | |||||
// isEditMode.value = false | |||||
document.querySelector('.openseadragon-canvas').classList.remove('cursor-crosshair') | |||||
selectMarker(newMarker) | |||||
} else { | |||||
unselectMarker() | |||||
} | |||||
}) | |||||
} | |||||
const editMarkerOnDragOrZoom = () => { | |||||
Viewer?.value.addHandler('canvas-drag', (event) => { | |||||
if (isSelectionModeOn.value) { | |||||
Viewer.value.gestureSettingsMouse.dragToPan = false | |||||
const point = Viewer?.value.viewport.pointFromPixel(event.position) | |||||
selectedMarker.value.position = point | |||||
const overlay = document.querySelector('.marker-id-' + selectedMarker.value.id) | |||||
Viewer?.value.updateOverlay(overlay as Element, point) | |||||
} | |||||
}) | |||||
Viewer?.value.addHandler('canvas-release', () => { | |||||
Viewer.value.gestureSettingsMouse.dragToPan = true | |||||
}) | |||||
Viewer?.value.addHandler('zoom', (event) => { | |||||
if (isSelectionModeOn.value) { | |||||
selectedMarker.value.zoom = event.zoom | |||||
} | |||||
}) | |||||
} | |||||
const loadStory = (markers: Marker[]) => { | |||||
markers.forEach((marker) => { | |||||
injectMarker(marker) | |||||
}) | |||||
} | |||||
const keyboardShortcut = () => { | |||||
// if press on escpae key | |||||
window.addEventListener('keydown', (e) => { | |||||
unselectMarker() | |||||
goHome() | |||||
}) | |||||
} | |||||
onMounted(() => { | |||||
nextTick(() => { | |||||
loadStory(props.markers) | |||||
createMarkerOnClick() | |||||
editMarkerOnDragOrZoom() | |||||
keyboardShortcut() | |||||
}) | |||||
}) | |||||
</script> | |||||
<style> | |||||
.ui button { | |||||
@apply border-2 border-black px-4 py-2 rounded-xl bg-slate-300 hover:bg-neutral-100 m-1; | |||||
} | |||||
.ui input, | |||||
.ui textarea { | |||||
@apply border-2 border-black px-4 py-2; | |||||
} | |||||
.marker { | |||||
@apply w-8 h-8 rounded-full shadow-2xl bg-green-500 bg-opacity-50 hover:bg-green-600 hover:bg-opacity-100 hover:cursor-pointer pointer-events-auto hover:scale-150 transition-all border-2 border-white border-8 transform -translate-y-1/2 -translate-x-1/2; | |||||
} | |||||
.marker-selected { | |||||
@apply border-blue-400 border-8 bg-white animate-pulse hover:cursor-move hover:bg-white; | |||||
} | |||||
</style> |
@@ -0,0 +1,83 @@ | |||||
<template> | |||||
<nav class="[&>*]:m-3 text-black"> | |||||
<div class="flex flex-col ui"> | |||||
<h2>Current Marker</h2> | |||||
{{ currentMarker }} | |||||
<textarea :value="currentMarker.annotation"></textarea> | |||||
</div> | |||||
</nav> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
import type { Marker } from '@/types/virages' | |||||
import { onMounted, ref, inject, nextTick } from 'vue' | |||||
import OpenSeadragon from 'openseadragon' | |||||
const props = defineProps<{ markers: Marker[] }>() | |||||
const Viewer = inject('Viewer') | |||||
const isEditMode = ref<boolean>(false) | |||||
const currentMarker = ref<Marker>({} as Marker) | |||||
const injectMarker = (marker: Marker) => { | |||||
const overlay = document.createElement('button') | |||||
overlay.className = `marker-id-${marker.id} w-8 h-8 rounded-full shadow-2xl bg-green-500 bg-opacity-50 hover:bg-green-600 hover:bg-opacity-100 z-index-20 hover:cursor-pointer pointer-events-auto hover:scale-150 transition-all border-2 border-white border-8 transform -translate-y-1/2 -translate-x-1/2` | |||||
overlay.addEventListener('click', function () { | |||||
Viewer?.value.viewport.zoomTo(marker.zoom, undefined, false) | |||||
Viewer?.value.viewport.panTo(new OpenSeadragon.Point(marker.position.x, marker.position.y)) | |||||
selectMarker(marker) | |||||
}) | |||||
overlay.addEventListener('mouseover', function () { | |||||
Viewer?.value.setMouseNavEnabled(false) | |||||
}) | |||||
overlay.addEventListener('mouseout', function () { | |||||
Viewer?.value.setMouseNavEnabled(true) | |||||
}) | |||||
// Injection de l'overlay | |||||
Viewer?.value.addOverlay({ | |||||
element: overlay, | |||||
location: new OpenSeadragon.Point(marker.position.x, marker.position.y), | |||||
}) | |||||
} | |||||
const createMarkerOnClick = () => { | |||||
Viewer.value.addHandler('canvas-click', (e) => { | |||||
if (!isEditMode.value) return | |||||
const point = Viewer.value.viewport.pointFromPixel(e.position) | |||||
const zoom = Viewer.value.viewport.getZoom() | |||||
const newMarker: Marker = { | |||||
id: props.markers.length, // WIP | |||||
order: props.markers.length, // WIP | |||||
annotation: '', | |||||
position: { | |||||
x: point.x, | |||||
y: point.y, | |||||
}, | |||||
zoom: zoom, | |||||
} | |||||
injectMarker(newMarker) | |||||
isEditMode.value = false | |||||
}) | |||||
} | |||||
const loadStory = (markers: Marker[]) => { | |||||
markers.forEach((marker) => { | |||||
injectMarker(marker) | |||||
}) | |||||
} | |||||
onMounted(() => { | |||||
nextTick(() => { | |||||
Viewer.value.zoomPerClick = true | |||||
createMarkerOnClick() | |||||
loadStory(props.markers) | |||||
}) | |||||
}) | |||||
</script> | |||||
<style> | |||||
.ui button { | |||||
@apply border-2 border-black px-4 py-2 rounded-xl bg-slate-300; | |||||
} | |||||
</style> |
@@ -0,0 +1,74 @@ | |||||
<template> | |||||
<div> | |||||
<draggable | |||||
v-model="markers" | |||||
item-key="id" | |||||
@end="onDragEnd" | |||||
animation="200" | |||||
class="list" | |||||
ghost-class="dragging" | |||||
> | |||||
<template #item="{ element }"> | |||||
<div> | |||||
<!-- Un seul élément parent ici --> | |||||
<button | |||||
@click="selectMarker(element)" | |||||
:class="element.id === selectedMarker?.id ? 'text-green-500' : ''" | |||||
> | |||||
{{ element.name }} | |||||
</button> | |||||
</div> | |||||
</template> | |||||
</draggable> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { ref } from 'vue' | |||||
import draggable from 'vuedraggable' | |||||
export default { | |||||
components: { draggable }, | |||||
setup() { | |||||
const markers = ref([ | |||||
{ id: 1, name: 'Marker 1', order: 2 }, | |||||
{ id: 2, name: 'Marker 2', order: 1 }, | |||||
{ id: 3, name: 'Marker 3', order: 3 }, | |||||
]) | |||||
const selectedMarker = ref(null) | |||||
const selectMarker = (marker) => { | |||||
selectedMarker.value = marker | |||||
} | |||||
const onDragEnd = () => { | |||||
// Optionnel : Mettre à jour les ordres si nécessaire | |||||
markers.value.forEach((marker, index) => { | |||||
marker.order = index + 1 | |||||
}) | |||||
} | |||||
return { | |||||
markers, | |||||
selectedMarker, | |||||
selectMarker, | |||||
onDragEnd, | |||||
} | |||||
}, | |||||
} | |||||
</script> | |||||
<style scoped> | |||||
.list { | |||||
display: flex; | |||||
flex-direction: column; | |||||
gap: 10px; | |||||
} | |||||
.dragging { | |||||
background: rgba(0, 0, 0, 0.1); | |||||
} | |||||
.text-green-500 { | |||||
color: green; | |||||
} | |||||
</style> |
@@ -1,86 +0,0 @@ | |||||
<template> | |||||
<nav class="[&>*]:m-3 text-black"> | |||||
<div class="flex flex-col"> | |||||
<h2>Current Marker</h2> | |||||
{{ currentMarker }} | |||||
<textarea :value="currentMarker.annotation" @input="updateAnnotation"></textarea> | |||||
<div> | |||||
<label for="editMode">Place point on click </label> | |||||
<input id="editMode" type="checkbox" v-model="isEditMode" /> | |||||
</div> | |||||
</div> | |||||
</nav> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
import type { Marker } from '@/types/virages' | |||||
import { onMounted, ref, inject, nextTick } from 'vue' | |||||
import OpenSeadragon from 'openseadragon' | |||||
const props = defineProps<{ markers: Marker[] }>() | |||||
const Viewer = inject('Viewer') | |||||
const isEditMode = ref<boolean>(false) | |||||
const currentMarker = ref<Marker>({} as Marker) | |||||
const placePoint = (point, zoom, annotation = '') => { | |||||
const overlay = document.createElement('button') | |||||
overlay.className = | |||||
'w-8 h-8 rounded-full shadow-2xl bg-green-500 bg-opacity-50 hover:bg-green-600 hover:bg-opacity-100 z-index-20 hover:cursor-pointer pointer-events-auto hover:scale-150 transition-all border-2 border-white border-8' | |||||
// Sauvegarde les données dans les attributs "data-*" | |||||
overlay.setAttribute('data-zoom', zoom) | |||||
overlay.setAttribute('data-position-x', point.x) | |||||
overlay.setAttribute('data-position-y', point.y) | |||||
overlay.setAttribute('data-annotation', annotation) | |||||
overlay.addEventListener('click', function (e) { | |||||
const zoom = Number(overlay.getAttribute('data-zoom')) | |||||
const x = Number(overlay.getAttribute('data-position-x')) | |||||
const y = Number(overlay.getAttribute('data-position-y')) | |||||
const annotation = overlay.getAttribute('data-annotation') | |||||
Viewer.value.viewport.zoomTo(zoom, false) | |||||
Viewer.value.viewport.panTo(new OpenSeadragon.Point(x, y)) | |||||
currentMarker.value.annotation = annotation | |||||
}) | |||||
overlay.addEventListener('mouseover', function (e) { | |||||
Viewer.value.setMouseNavEnabled(false) | |||||
}) | |||||
overlay.addEventListener('mouseout', function (e) { | |||||
Viewer.value.setMouseNavEnabled(true) | |||||
}) | |||||
// Injection de l'overlay | |||||
Viewer.value.addOverlay({ | |||||
element: overlay, | |||||
location: point, | |||||
}) | |||||
} | |||||
const placeAPointOnClick = () => { | |||||
Viewer.value.addHandler('canvas-click', (e) => { | |||||
if (!isEditMode.value) return | |||||
const point = Viewer.value.viewport.pointFromPixel(e.position) | |||||
const zoom = Viewer.value.viewport.getZoom() | |||||
placePoint(point, zoom) | |||||
isEditMode.value = false | |||||
}) | |||||
} | |||||
const loadStory = (markers: Marker[]) => { | |||||
markers.forEach((marker) => { | |||||
placePoint( | |||||
new OpenSeadragon.Point(marker.position.x, marker.position.y), | |||||
marker.zoom, | |||||
marker.annotation, | |||||
) | |||||
}) | |||||
} | |||||
onMounted(() => { | |||||
nextTick(() => { | |||||
Viewer.value.zoomPerClick = true | |||||
placeAPointOnClick() | |||||
loadStory(props.markers) | |||||
}) | |||||
}) | |||||
</script> |
@@ -1,5 +1,7 @@ | |||||
export interface Marker { | export interface Marker { | ||||
id: number | |||||
order: number | order: number | ||||
name: string | |||||
position: OpenSeadragon.Point | position: OpenSeadragon.Point | ||||
zoom: number | zoom: number | ||||
annotation: string | annotation: string | ||||
@@ -6,13 +6,10 @@ import vueDevTools from 'vite-plugin-vue-devtools' | |||||
// https://vite.dev/config/ | // https://vite.dev/config/ | ||||
export default defineConfig({ | export default defineConfig({ | ||||
plugins: [ | |||||
vue(), | |||||
vueDevTools(), | |||||
], | |||||
plugins: [vue(), vueDevTools()], | |||||
resolve: { | resolve: { | ||||
alias: { | alias: { | ||||
'@': fileURLToPath(new URL('./src', import.meta.url)) | |||||
'@': fileURLToPath(new URL('./src', import.meta.url)), | |||||
}, | }, | ||||
}, | }, | ||||
}) | }) |