@@ -1,63 +1,42 @@ | |||
<template> | |||
<div> | |||
<main class="inline-block bg-white bg-opacity-50 m-10 relative z-10 top-0 left-0"> | |||
<!-- <ZoomRect /> --> | |||
<StoryEditor :markers="story[0].markers" /> | |||
</main> | |||
<aside class="fixed top-0 left-0 w-screen h-screen" ref="openSeadragonElt"></aside> | |||
</div> | |||
<StoryList /> | |||
</template> | |||
<script setup lang="ts"> | |||
import OpenSeadragon from 'openseadragon' | |||
import { onMounted, ref, provide } from 'vue' | |||
import story from '@/assets/story.json' | |||
const Poulpatore = '../public/deepzoom/poulpatore/dz/info.json' | |||
const Vignemale = '../public/deepzoom/vignemale/dz/info.json' | |||
const Cells = 'https://verrochi92.github.io/axolotl/data/W255B_0.dzi' | |||
import ZoomRect from './components/tools/ZoomRect.vue' | |||
import StoryEditor from './components/tools/StoryEditor.vue' | |||
import TestSortable from './components/tools/TestSortable.vue' | |||
const Viewer = ref() | |||
const openSeadragonElt = ref() | |||
provide('Viewer', Viewer) | |||
const initViewer = () => { | |||
Viewer.value = OpenSeadragon({ | |||
element: openSeadragonElt.value, | |||
animationTime: 0.4, | |||
prefixUrl: 'https://openseadragon.github.io/openseadragon/images/', | |||
showNavigator: false, | |||
sequenceMode: false, | |||
tileSources: Cells, | |||
showNavigationControl: false, | |||
drawer: 'canvas', | |||
preventDefaultAction: true, | |||
visibilityRatio: 1, | |||
crossOriginPolicy: 'Anonymous', | |||
gestureSettingsMouse: { | |||
scrollToZoom: true, | |||
clickToZoom: false, | |||
dblClickToZoom: true, | |||
dragToPan: true, | |||
}, | |||
}) | |||
import StoryList from './components/StoryList.vue' | |||
</script> | |||
<style> | |||
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap'); | |||
.bebas-neue-regular { | |||
font-family: 'Bebas Neue', sans-serif; | |||
font-weight: 400; | |||
font-style: normal; | |||
} | |||
// on hover of openseadragon element, zoom to the center of the image | |||
.noto-sans-400 { | |||
font-family: 'Noto Sans', sans-serif; | |||
font-optical-sizing: auto; | |||
font-weight: 400; | |||
font-style: normal; | |||
font-variation-settings: 'wdth' 100; | |||
} | |||
onMounted(() => { | |||
initViewer() | |||
}) | |||
</script> | |||
.text-6xl { | |||
font-size: 90px; | |||
} | |||
.main { | |||
margin: 0 auto; | |||
max-width: 860px; | |||
} | |||
.z-article-title { | |||
z-index: 50; | |||
} | |||
<style scoped> | |||
button { | |||
@apply border-2 border-black px-4 py-2 rounded-xl; | |||
.z-article-image { | |||
z-index: 49; | |||
} | |||
</style> |
@@ -0,0 +1,128 @@ | |||
[ | |||
{ | |||
"id": 0, | |||
"name": "Poulpatore", | |||
"author": "Ricardo Prosety", | |||
"url": "../public/deepzoom/poulpatore/dz/info.json", | |||
"displayMode": "ARTICLE", | |||
"markers": [ | |||
{ | |||
"id": 0, | |||
"name": "first", | |||
"order": 0, | |||
"position": { | |||
"x": 0.6, | |||
"y": 0.7 | |||
}, | |||
"zoom": 4, | |||
"annotation": "<b>lorem</b> ipsum dolor sit amet" | |||
}, | |||
{ | |||
"id": 1, | |||
"name": "second", | |||
"order": 1, | |||
"position": { | |||
"x": 0.68, | |||
"y": 0.3 | |||
}, | |||
"zoom": 8, | |||
"annotation": "<b>second</b> annotation" | |||
}, | |||
{ | |||
"id": 2, | |||
"name": "third", | |||
"order": 2, | |||
"position": { | |||
"x": 0.5, | |||
"y": 0.2 | |||
}, | |||
"zoom": 7, | |||
"annotation": "<b>third</b>" | |||
} | |||
] | |||
}, | |||
{ | |||
"id": 1, | |||
"name": "Vignemale", | |||
"author": "Amandine MONTAZEAU", | |||
"url": "../public/deepzoom/vignemale/dz/info.json", | |||
"displayMode": "ARTICLE", | |||
"markers": [ | |||
{ | |||
"id": 0, | |||
"name": "first", | |||
"order": 0, | |||
"position": { | |||
"x": 0.6, | |||
"y": 0.7 | |||
}, | |||
"zoom": 4, | |||
"annotation": "<b>lorem</b> ipsum dolor sit amet" | |||
}, | |||
{ | |||
"id": 1, | |||
"name": "second", | |||
"order": 1, | |||
"position": { | |||
"x": 0.68, | |||
"y": 0.3 | |||
}, | |||
"zoom": 8, | |||
"annotation": "<b>second</b> annotation" | |||
}, | |||
{ | |||
"id": 2, | |||
"name": "third", | |||
"order": 2, | |||
"position": { | |||
"x": 0.5, | |||
"y": 0.2 | |||
}, | |||
"zoom": 7, | |||
"annotation": "<b>third</b>" | |||
} | |||
] | |||
}, | |||
{ | |||
"id": 2, | |||
"name": "Cells", | |||
"author": "Sairam Bandarupalli", | |||
"url": "https://verrochi92.github.io/axolotl/data/W255B_0.dzi", | |||
"displayMode": "ARTICLE", | |||
"markers": [ | |||
{ | |||
"id": 0, | |||
"name": "first", | |||
"order": 0, | |||
"position": { | |||
"x": 0.6, | |||
"y": 0.7 | |||
}, | |||
"zoom": 4, | |||
"annotation": "<b>lorem</b> ipsum dolor sit amet" | |||
}, | |||
{ | |||
"id": 1, | |||
"name": "second", | |||
"order": 1, | |||
"position": { | |||
"x": 0.68, | |||
"y": 0.3 | |||
}, | |||
"zoom": 8, | |||
"annotation": "<b>second</b> annotation" | |||
}, | |||
{ | |||
"id": 2, | |||
"name": "third", | |||
"order": 2, | |||
"position": { | |||
"x": 0.5, | |||
"y": 0.2 | |||
}, | |||
"zoom": 7, | |||
"annotation": "<b>third</b>" | |||
} | |||
] | |||
} | |||
] |
@@ -1,42 +0,0 @@ | |||
[ | |||
{ | |||
"id": 0, | |||
"title": "Poulpatore", | |||
"url": "https://verrochi92.github.io/axolotl/data/W255B_0.dzi", | |||
"markers": [ | |||
{ | |||
"id": 0, | |||
"name": "first", | |||
"order": 0, | |||
"position": { | |||
"x": 0.6, | |||
"y": 0.7 | |||
}, | |||
"zoom": 4, | |||
"annotation": "<b>lorem</b> ipsum dolor sit amet" | |||
}, | |||
{ | |||
"id": 1, | |||
"name": "second", | |||
"order": 1, | |||
"position": { | |||
"x": 0.68, | |||
"y": 0.3 | |||
}, | |||
"zoom": 8, | |||
"annotation": "<b>second</b> annotation" | |||
}, | |||
{ | |||
"id": 2, | |||
"name": "third", | |||
"order": 2, | |||
"position": { | |||
"x": 0.5, | |||
"y": 0.2 | |||
}, | |||
"zoom": 7, | |||
"annotation": "<b>third</b>" | |||
} | |||
] | |||
} | |||
] |
@@ -1,33 +0,0 @@ | |||
<template> | |||
<article class="group hover:bg-sky-500 hover:cursor-pointer p-4"> | |||
<figure class="mb-8"> | |||
<figcaption class="text-center text-black z-article-title relative"> | |||
<h2 class="bebas-neue-regular uppercase text-6xl"> | |||
william turner | |||
</h2> | |||
<p class="noto-sans-400 text-2xl capitalize tracking-widest"> | |||
the burning of the houses | |||
</p> | |||
</figcaption> | |||
<img class="-mt-20 z-article-image" src="https://files.erudi.fr/virages/turner.jpg" alt=""> | |||
</figure> | |||
<footer class="footer"> | |||
<h3 class="bebas-neue-regular uppercase text-5xl"> | |||
Romantisme | |||
</h3> | |||
<section class="flex mt-6"> | |||
<span class="bebas-neue-regular uppercase text-5xl mt-4"> | |||
1834 | |||
</span> | |||
<div class="noto-sans-400 text-2xl ml-6"> | |||
<div class="mb-2"> | |||
paysage urbain | |||
</div> | |||
<div class="text-slate-600"> | |||
huile sur toile | |||
</div> | |||
</div> | |||
</section> | |||
</footer> | |||
</article> | |||
</template> |
@@ -0,0 +1,43 @@ | |||
<template> | |||
<article class="story-article p-4 -mt-9 md:-mt-12 z-article-image"> | |||
<figure class="mb-4 lg:mb-8"> | |||
<figcaption class="text-center text-black z-article-title relative"> | |||
<h2 | |||
class="bebas-neue-regular uppercase text-5xl md:text-8xl bg-white inline-block px-4 py-1 md:py-2" | |||
> | |||
{{ story.name }} | |||
</h2> | |||
<p class="noto-sans-400 text-md md:text-3xl capitalize tracking-widest px-4 py-2"> | |||
{{ story.author }} | |||
</p> | |||
</figcaption> | |||
</figure> | |||
<!-- <footer class="footer"> | |||
<h3 class="bebas-neue-regular uppercase text-5xl">Romantisme</h3> | |||
<section class="flex mt-6"> | |||
<span class="bebas-neue-regular uppercase text-5xl mt-4"> 1834 </span> | |||
<div class="noto-sans-400 text-2xl ml-6"> | |||
<div class="mb-2">paysage urbain</div> | |||
<div class="text-slate-600">huile sur toile</div> | |||
</div> | |||
</section> | |||
</footer> --> | |||
</article> | |||
</template> | |||
<script setup lang="ts"> | |||
import type { Story } from '@/types/virages' | |||
defineProps<{ story: Story }>() | |||
</script> | |||
<style lang="scss"> | |||
.story-article { | |||
p { | |||
.display-mode-EDITOR &, | |||
.display-mode-PLAYER & { | |||
@apply inline-block text-white; | |||
text-shadow: black 2px 2px 3px; | |||
} | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,110 @@ | |||
<template> | |||
<section :class="'story display-mode-' + story.displayMode"> | |||
<aside | |||
v-if="story.displayMode === 'EDITOR'" | |||
class="story-tools fixed inline-block bg-white bg-opacity-50 m-10 z-10 top-0 left-0 w-7 h-7 md:w-auto md:h-auto overflow-hidden" | |||
> | |||
<Editor :markers="props.story.markers" /> | |||
</aside> | |||
<main class="story-openseadragon" ref="openSeadragonElt"></main> | |||
<Article :story="props.story" /> | |||
</section> | |||
</template> | |||
<script setup lang="ts"> | |||
import OpenSeadragon from 'openseadragon' | |||
import { onMounted, ref, provide } from 'vue' | |||
import type { Story } from '@/types/virages' | |||
import Article from './StoryArticle.vue' | |||
import Editor from './tools/StoryEditor.vue' | |||
const props = defineProps<{ story: Story }>() | |||
const Viewer = ref() | |||
const openSeadragonElt = ref() | |||
provide('Viewer', Viewer) | |||
const initViewer = () => { | |||
Viewer.value = OpenSeadragon({ | |||
element: openSeadragonElt.value, | |||
animationTime: 0.4, | |||
prefixUrl: 'https://openseadragon.github.io/openseadragon/images/', | |||
showNavigator: false, | |||
sequenceMode: false, | |||
tileSources: props.story.url, | |||
showNavigationControl: false, | |||
drawer: 'canvas', | |||
preventDefaultAction: true, | |||
visibilityRatio: 1, | |||
crossOriginPolicy: 'Anonymous', | |||
gestureSettingsMouse: { | |||
scrollToZoom: true, | |||
clickToZoom: false, | |||
dblClickToZoom: false, | |||
dragToPan: true, | |||
}, | |||
defaultZoomLevel: 1.5, | |||
}) | |||
} | |||
onMounted(() => { | |||
initViewer() | |||
}) | |||
</script> | |||
<style lang="scss"> | |||
.story { | |||
transition: padding 0.5s ease-in-out; | |||
@apply p-8; | |||
&.display-mode-EDITOR, | |||
&.display-mode-PLAYER { | |||
@apply p-0; | |||
.story-openseadragon { | |||
@apply top-0 left-0 w-screen; | |||
position: relative !important; | |||
} | |||
} | |||
&.display-mode-HIDDEN { | |||
@apply p-0 h-0; | |||
} | |||
&.display-mode-ARTICLE { | |||
@apply w-full bg-white hover:bg-slate-200 hover:cursor-pointer; | |||
.openseadragon-canvas { | |||
pointer-events: none !important; | |||
touch-action: none !important; | |||
} | |||
} | |||
} | |||
.story-openseadragon { | |||
transition: height 0.5s ease-in-out; | |||
height: 33vh; | |||
.display-mode-EDITOR &, | |||
.display-mode-PLAYER & { | |||
@apply h-screen; | |||
} | |||
.display-mode-HIDDEN & { | |||
@apply h-0; | |||
} | |||
} | |||
.story-article { | |||
position: relative; | |||
top: 0; | |||
.display-mode-PLAYER & { | |||
position: absolute; | |||
top: 50%; | |||
left: 50%; | |||
transform: translate(-50%, -50%); | |||
} | |||
.display-mode-EDITOR & { | |||
@apply p-0 h-0 w-0 overflow-hidden opacity-0; | |||
} | |||
.display-mode-HIDDEN & { | |||
@apply p-0 h-0 w-0 overflow-hidden; | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,51 @@ | |||
<template> | |||
<button | |||
v-if="isAppModeFullscreen" | |||
@click="closeStories()" | |||
class="bg-white hover:text-lg transition text-black h-8 w-8 rounded-full fixed right-4 top-4 z-50" | |||
> | |||
x | |||
</button> | |||
<StoryCanvas | |||
v-for="story in stories" | |||
:key="story.id" | |||
:story="story" | |||
@click="setOpenStory(story)" | |||
/> | |||
</template> | |||
<script setup lang="ts"> | |||
import { onMounted, ref } from 'vue' | |||
import type { Story } from '@/types/virages' | |||
import StoryCanvas from './StoryContainer.vue' | |||
import datas from '@/assets/stories.json' | |||
const stories = ref<Story[]>(datas) | |||
const isAppModeFullscreen = ref(false) | |||
const setOpenStory = (story: Story) => { | |||
stories.value.forEach((story) => { | |||
story.displayMode = 'HIDDEN' | |||
}) | |||
story.displayMode = 'EDITOR' | |||
isAppModeFullscreen.value = true | |||
} | |||
const closeStories = () => { | |||
stories.value.forEach((story) => { | |||
story.displayMode = 'ARTICLE' | |||
}) | |||
isAppModeFullscreen.value = false | |||
} | |||
const keyboardShortcut = () => { | |||
// if press on escpae key | |||
window.addEventListener('keydown', (e) => { | |||
if (e.key !== 'Escape') return | |||
closeStories() | |||
}) | |||
} | |||
onMounted(() => { | |||
keyboardShortcut() | |||
}) | |||
</script> |
@@ -166,6 +166,7 @@ const keyboardShortcut = () => { | |||
onMounted(() => { | |||
nextTick(() => { | |||
Viewer?.value.clearOverlays() | |||
loadStory(props.markers) | |||
createMarkerOnClick() | |||
editMarkerOnDragOrZoom() | |||
@@ -1,74 +0,0 @@ | |||
<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,12 +0,0 @@ | |||
import { ref, computed } from 'vue' | |||
import { defineStore } from 'pinia' | |||
export const useCounterStore = defineStore('counter', () => { | |||
const count = ref(0) | |||
const doubleCount = computed(() => count.value * 2) | |||
function increment() { | |||
count.value++ | |||
} | |||
return { count, doubleCount, increment } | |||
}) |
@@ -0,0 +1,41 @@ | |||
import type { Story } from '@/types/virages' | |||
import { defineStore } from 'pinia' | |||
import { ref, computed } from 'vue' | |||
export const useStories = defineStore('stories', { | |||
state: (): Story => ({ | |||
stories: [], | |||
/** @type {'all' | 'finished' | 'unfinished'} */ | |||
filter: 'all', | |||
// type will be automatically inferred to number | |||
nextId: 0, | |||
}), | |||
getters: { | |||
finishedStories(state) { | |||
// autocompletion! ✨ | |||
return state.stories.filter((todo) => todo.isFinished) | |||
}, | |||
unfinishedStories(state) { | |||
return state.stories.filter((todo) => !todo.isFinished) | |||
}, | |||
/** | |||
* @returns {{ text: string, id: number, isFinished: boolean }[]} | |||
*/ | |||
filteredStories(state) { | |||
if (this.filter === 'finished') { | |||
// call other getters with autocompletion ✨ | |||
return this.finishedStories | |||
} else if (this.filter === 'unfinished') { | |||
return this.unfinishedStories | |||
} | |||
return this.stories | |||
}, | |||
}, | |||
actions: { | |||
// any amount of arguments, return a promise or not | |||
addStory(text) { | |||
// you can directly mutate the state | |||
this.stories.push({ text, id: this.nextId++, isFinished: false }) | |||
}, | |||
}, | |||
}) |
@@ -1,15 +1,21 @@ | |||
export interface Story { | |||
id: number | |||
name: string | |||
author: string | |||
url: string | |||
markers: Marker[] | |||
displayMode: string // 'ARTICLE' | 'EDITOR' | 'PLAYER' | 'HIDDEN' | |||
date_art_creation: Date | |||
} | |||
export interface Marker { | |||
id: number | |||
order: number | |||
name: string | |||
position: OpenSeadragon.Point | |||
position: { | |||
x: number | |||
y: number | |||
} | |||
zoom: number | |||
annotation: string | |||
} | |||
export interface Story { | |||
id: number | |||
title: string | |||
url: string | |||
markers: Marker[] | |||
} |