Browse Source

feat(graphic): open story effect

master
valere 3 months ago
parent
commit
669e038616
12 changed files with 420 additions and 222 deletions
  1. +32
    -53
      src/App.vue
  2. +128
    -0
      src/assets/stories.json
  3. +0
    -42
      src/assets/story.json
  4. +0
    -33
      src/components/Article.vue
  5. +43
    -0
      src/components/StoryArticle.vue
  6. +110
    -0
      src/components/StoryContainer.vue
  7. +51
    -0
      src/components/StoryList.vue
  8. +1
    -0
      src/components/tools/StoryEditor.vue
  9. +0
    -74
      src/components/tools/TestSortable.vue
  10. +0
    -12
      src/stores/counter.ts
  11. +41
    -0
      src/stores/stories.ts
  12. +14
    -8
      src/types/virages.d.ts

+ 32
- 53
src/App.vue View File

@@ -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>

+ 128
- 0
src/assets/stories.json View File

@@ -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>"
}
]
}
]

+ 0
- 42
src/assets/story.json View File

@@ -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>"
}
]
}
]

+ 0
- 33
src/components/Article.vue View File

@@ -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>

+ 43
- 0
src/components/StoryArticle.vue View File

@@ -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>

+ 110
- 0
src/components/StoryContainer.vue View File

@@ -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>

+ 51
- 0
src/components/StoryList.vue View File

@@ -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>

+ 1
- 0
src/components/tools/StoryEditor.vue View File

@@ -166,6 +166,7 @@ const keyboardShortcut = () => {

onMounted(() => {
nextTick(() => {
Viewer?.value.clearOverlays()
loadStory(props.markers)
createMarkerOnClick()
editMarkerOnDragOrZoom()


+ 0
- 74
src/components/tools/TestSortable.vue View File

@@ -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>

+ 0
- 12
src/stores/counter.ts View File

@@ -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 }
})

+ 41
- 0
src/stores/stories.ts View File

@@ -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 })
},
},
})

+ 14
- 8
src/types/virages.d.ts View File

@@ -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[]
}

Loading…
Cancel
Save