@@ -1,197 +1,5 @@ | |||
# virages.io | |||
## ROADMAP | |||
1. define a Marker in the viewport | |||
- x | |||
- y | |||
2. define a ZoomArea | |||
- x | |||
- y | |||
- w | |||
- h | |||
3. define a Mask | |||
- Trace | |||
## TOOLS | |||
DSIGN SYSTEMS | |||
- StoryLite | |||
https://atlas-viewer-storybook.netlify.app/#/stories/testing-ensuremouseeventsareaccuratewhenboxchanges | |||
- micr.io | |||
https://i.micr.io/aoxENNv/en/poulpatore-bigjpg | |||
BACK OFFICE | |||
- https://atlas-viewer-storybook.netlify.app/#/stories/annotations-selectiondemo | |||
- https://github.com/altert/OpenseadragonFabricjsOverlay | |||
- https://dash.micr.io/u:ErWRfBqNb1ef0QEnIRPzkQ/tets/@aoxENNv/en | |||
FRONT OFFICE | |||
- https://canvas-panel.digirati.com/#/about | |||
- https://canvas-panel.digirati.com/#/examples/fullpage?manifest=https://stephenwf.github.io/ocean-liners.json | |||
- https://github.com/digirati-co-uk/canvas-panel/tree/master | |||
CONCURENTS | |||
- https://micr.io/ | |||
- https://archief.ntr.nl/tuinderlusten/en.html | |||
LOGO | |||
Virages est un outils pour les historiens de l'art qui veulent expliquer/analyser des images (peintures) en très haute résolution | |||
Concevez un logo minimaliste, moderne pour virages.io, incorporant un V, les couleurs CMJN & RGB. | |||
ws apps : | |||
- logo virages | |||
ALL: https://storiiies-editor.cogapp.com/ | |||
https://github.com/IIIF/awesome-iiif/tree/66f8c724ee7fdb44f750ed4d7cedad449bb5f7a3 | |||
FRONT: OpenSeaDragon | |||
FRONT OFFICE: https://annotorious.github.io/guides/annotorious-in-vue/ | |||
BACK: vips (Zoomable or ZoomHub) | |||
BACK office : https://annotorious.github.io/getting-started/ | |||
https://github.com/IIIF/awesome-iiif | |||
IIIF | |||
Giga pixel | |||
- [ ] load a picture from a props on mounted in a 2d canvas | |||
- [ ] display that picture with a good ratio and cover the full window | |||
- [ ] a method focusOn(x, y, zoom) will change the scale of the image targetting a specific point | |||
- [ ] at any moment the picture is showing in a good ratio even when the window is resized | |||
Watch an erotic dream world where desire, colorful fantasy and poetic body parts intertwine. | |||
https://vimeo.com/ondemand/uneruedanssalongueur | |||
calvitie | |||
forme de nudité | |||
ex : | |||
- Hunter Thomson | |||
- Gérard jugnot | |||
- Sébastien Tellier | |||
qu'apporte l'analyse | |||
artistique ? | |||
quel est son sens ? | |||
l'analyse peint une âme à l'oeuvre, elle renvoit la passion à l'objet d'art. | |||
En ce sens l'analyse peut enfermer l'oeuvre dans un cadre, dans des lignes droites comme elle peut démontrer son aura par l'interprétation voir la surpasser par la sur-interprétation. | |||
un film aléatoire | |||
https://shop.gandi.net/en/6514bca2-b7cc-11ec-87b3-00163eada87b/domain/suggest/7cf99682-e92f-45d9-a87c-01d4d07999ca?search=msledchildren.com | |||
- [ ] display one picture !!!!!!!!! | |||
- [ ] 2 dyn canvas size of the window | |||
- [ ] 3 setView (position, zoom, duration) | |||
- [ ] 4 crop selection | |||
- [ ] 5 filters | |||
- [ ] 6 journey | |||
- [ ] 7 UI : https://www.nextrembrandt.com/ | |||
- [ ] display 2 pictures | |||
- [ ] overlay pictures | |||
- [ ] freeway mode | |||
- [ ] possible effect on art click https://tympanus.net/codrops/2023/03/14/fullscreen-clip-animation/ | |||
# IA & painting Art: | |||
- [ ] IA shcema to painting : https://github.com/alexjc/neural-doodle | |||
- [ ] Depth Estimation of a painting | |||
- [ ] Object Detection in painting | |||
- [ ] Perspective Detection in painting | |||
- [ ] Inpaint in ... well painting :) | |||
### rembrandt: | |||
https://www.youtube.com/results?search_query=tu+delft+rembrandt | |||
### Depth map | |||
Inpainting / Outpainting -> youtube | |||
https://github.com/AUTOMATIC1111/stable-diffusion-webui | |||
https://github.com/docker-mailserver/docker-mailserver | |||
https://tresjs.org/examples/load-textures.html | |||
https://atroposjs.com/docs/vue#whats-next | |||
- découpe d'une image avec clip - d'abord basic ellipse ou cercle | |||
MODULES: | |||
- errance [tuiles + zoom + full screen] | |||
- histoire [tuiles + scroll + zoom] | |||
- analyse [click + zoom + animation | |||
- épaisseurs [3d] | |||
## Pourquoi virages alors que l'art génératif existe ? | |||
Si visuellement l’art génératif illustre très bien un article, même en tâtonnant il est impossible d'avoir exactement ce que l'on désir. | |||
C'est l'occasion à l'art visuel traditionnel de mettre en avant ce qui fait sa spécificité, l'intention de son auteur. | |||
Le mot artiste ne veut rien dire. | |||
Il n'y a pas d'artiste, | |||
Il n'y a que de l'art et leurs auteurs. | |||
### MARCHÉ : | |||
https://www.tumblr.com | |||
https://behance.net | |||
https://vitali-studio.com | |||
https://issuu.com | |||
https://cargo.site | |||
https://www.artsy.net/ | |||
https://www.flickr.com | |||
### THEMES: | |||
plutôt que perdre du temps sur du web design fait et refait, | |||
pourquoi ne pas créer des thèmes sur des modèles comme : | |||
- https://www.apple.com/fr/newsroom/ | |||
- https://www.louvre.fr/en/what-s-on/exhibitions/leonardo-da-vinci#exhibition-overview | |||
- https://salvatormundirevisited.com/ | |||
### CSS | |||
backdrop-filter | |||
Filter color on hover : | |||
https://cosmicmagazine.com.au/ | |||
Folio for photographie & paintings | |||
OU | |||
CMS pour portfolio au plus généralement site créatif (EvilSpins) | |||
Virag.es is a fullstack "framework" since it's a CMS | |||
Virag.es back should be build with the best back (prisma nest) framework & vue | |||
## Photo | |||
- meta | |||
- raw compat | |||
- correction ... | |||
## Paintings | |||
- explorable table | |||
- compte-fils / thread counter (https://codesandbox.io/s/github/WebsiteBeaver/vue-magnifier) | |||
- video ? | |||
- animation half per half (comme pour Lucie 3) | |||
- VHS effect : https://www.ssion.com/ | |||
- painting light (filmer une peinture avec peu de lumière et avec une lumière à fond) | |||
## Lister toutes les références & symbole dans une page qui s'appelerai Mythologie | |||
## DESIGN / FEATURES : | |||
- mobile first -> scroll first | |||
@@ -2,9 +2,9 @@ | |||
<section :class="'story 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" | |||
class="story-tools fixed inline-block bg-white bg-opacity-50 m-10 z-10 top-0 left-0" | |||
> | |||
<Editor :markers="props.story.markers" /> | |||
<Editor :story="props.story" /> | |||
</aside> | |||
<main class="story-openseadragon" ref="openSeadragonElt"></main> | |||
<Article :story="props.story" /> | |||
@@ -16,9 +16,9 @@ import OpenSeadragon from 'openseadragon' | |||
import '@/assets/plugins/openseadragon-scalebar.js' | |||
import '@/assets/plugins/openseadragon-bookmark-url.js' | |||
import { onMounted, ref, provide, onUnmounted } from 'vue' | |||
import type { Story } from '@/types/virages' | |||
import type { Story, ViewerConfiguration } from '@/types/virages' | |||
import Article from './StoryArticle.vue' | |||
import Editor from './tools/StoryEditor.vue' | |||
import Editor from './player/StoryEditor.vue' | |||
const props = defineProps<{ story: Story }>() | |||
const Viewer = ref() | |||
@@ -26,26 +26,30 @@ const openSeadragonElt = ref() | |||
provide('Viewer', Viewer) | |||
const defaultViewerConfiguration: ViewerConfiguration = { | |||
prefixUrl: 'https://openseadragon.github.io/openseadragon/images/', | |||
crossOriginPolicy: 'Anonymous', | |||
animationTime: 2, | |||
showNavigator: false, | |||
sequenceMode: false, | |||
showNavigationControl: false, | |||
drawer: 'canvas', | |||
preventDefaultAction: true, | |||
visibilityRatio: 0.5, | |||
defaultZoomLevel: 1.2, | |||
gestureSettingsMouse: { | |||
scrollToZoom: true, | |||
clickToZoom: false, | |||
dblClickToZoom: false, | |||
dragToPan: true, | |||
}, | |||
} | |||
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: 0.5, | |||
crossOriginPolicy: 'Anonymous', | |||
gestureSettingsMouse: { | |||
scrollToZoom: true, | |||
clickToZoom: false, | |||
dblClickToZoom: false, | |||
dragToPan: true, | |||
}, | |||
defaultZoomLevel: 1.2, | |||
...defaultViewerConfiguration, | |||
}) | |||
} | |||
@@ -1,15 +1,15 @@ | |||
<template> | |||
<div class="story-list"> | |||
<StoryContainer | |||
v-for="story in stories" | |||
v-for="story in store.stories" | |||
:key="story.id" | |||
:story="story" | |||
@click="openStory(story)" | |||
@click="store.openStory(story)" | |||
/> | |||
</div> | |||
<button | |||
v-if="isAppModeFullscreen" | |||
@click="closeStories()" | |||
v-if="store.isStoryOpen" | |||
@click="store.closeStories()" | |||
class="bg-white hover:text-lg transition text-black h-8 w-8 rounded-full fixed right-4 top-4 z-50" | |||
> | |||
x | |||
@@ -17,39 +17,22 @@ | |||
</template> | |||
<script setup lang="ts"> | |||
import { onMounted, ref } from 'vue' | |||
import type { Story } from '@/types/virages' | |||
import { onMounted } from 'vue' | |||
import StoryContainer from './StoryContainer.vue' | |||
import datas from '@/assets/stories.json' | |||
import { useStoryStore } from '@/stores/story' | |||
const stories = ref<Story[]>(datas) | |||
const isAppModeFullscreen = ref(false) | |||
const openStory = (story: Story) => { | |||
stories.value.forEach((story) => { | |||
story.displayMode = 'HIDDEN' | |||
}) | |||
story.displayMode = 'EDITOR' | |||
isAppModeFullscreen.value = true | |||
document.body.classList.add('app-fullscreen') | |||
} | |||
const closeStories = () => { | |||
stories.value.forEach((story) => { | |||
story.displayMode = 'ARTICLE' | |||
}) | |||
isAppModeFullscreen.value = false | |||
document.body.classList.remove('app-fullscreen') | |||
} | |||
const store = useStoryStore() | |||
const keyboardShortcut = () => { | |||
// if press on escpae key | |||
window.addEventListener('keydown', (e) => { | |||
if (e.key !== 'Escape') return | |||
closeStories() | |||
store.closeStories() | |||
}) | |||
} | |||
onMounted(() => { | |||
store.getStories() | |||
keyboardShortcut() | |||
}) | |||
</script> | |||
@@ -29,21 +29,25 @@ | |||
</template> | |||
<script setup lang="ts"> | |||
import type { Marker } from '@/types/virages' | |||
import type { Story, Marker } from '@/types/virages' | |||
import { onMounted, ref, inject, nextTick, type Ref, computed } from 'vue' | |||
import OpenSeadragon from 'openseadragon' | |||
import { useIsMobile } from '@/composable/IsMobile' | |||
import { useViewer } from '@/composable/Viewer' | |||
import { useStoryStore } from '@/stores/story' | |||
const props = defineProps<{ markers: Marker[] }>() | |||
const props = defineProps<{ story: Story }>() | |||
const Viewer = inject<Ref<OpenSeadragon.Viewer>>('Viewer') | |||
const isAddingPoint = ref<boolean>(false) | |||
const selectedMarker = ref<Marker>({} as Marker) | |||
const textInputMarkerName = ref<HTMLInputElement | null>(null) | |||
const { zoomTo } = useViewer(Viewer) | |||
const store = useStoryStore() | |||
const isAMarkerSelected = computed(() => { | |||
return selectedMarker.value.id !== undefined | |||
}) | |||
const sortedMarkers = computed(() => { | |||
return [...props.markers].sort((a, b) => a.order - b.order) | |||
return [...props.story.markers].sort((a, b) => a.order - b.order) | |||
}) | |||
const goHome = () => { | |||
@@ -74,9 +78,12 @@ const selectMarker = (marker: Marker) => { | |||
) | |||
Viewer?.value.viewport.fitBoundsWithConstraints(markerRectangle, false) | |||
selectedMarker.value = marker | |||
// nextTick(() => { | |||
// textInputMarkerName.value?.focus() | |||
// }) | |||
nextTick(() => { | |||
const isMobile = useIsMobile() | |||
if (!isMobile) { | |||
textInputMarkerName.value?.focus() | |||
} | |||
}) | |||
} | |||
const unselectMarker = () => { | |||
@@ -117,14 +124,15 @@ const createMarkerOnClick = () => { | |||
const zoom = Viewer?.value.viewport.getZoom() | |||
const bounds = Viewer?.value.viewport.getBounds() | |||
const newMarker: Marker = { | |||
id: props.markers.length, | |||
order: props.markers.length, | |||
id: props.story.markers.length, | |||
order: props.story.markers.length, | |||
name: '', | |||
bounds: bounds, | |||
point: new OpenSeadragon.Point(point.x, point.y), | |||
zoom: zoom, | |||
annotation: '', | |||
} | |||
store.addMarker(props.story, newMarker) | |||
injectMarker(newMarker) | |||
isAddingPoint.value = false | |||
document.querySelector('.openseadragon-canvas')?.classList.remove('cursor-crosshair') | |||
@@ -198,12 +206,13 @@ onMounted(() => { | |||
nextTick(() => { | |||
Viewer?.value.clearOverlays() | |||
// Viewer.value.viewport.defaultZoomLevel = 1 | |||
loadStory(props.markers) | |||
loadStory(props.story.markers) | |||
createMarkerOnClick() | |||
editMarkerOnDragOrZoom() | |||
keyboardShortcut() | |||
initScalebar() | |||
initUrl() | |||
zoomTo(6) // COMPOSABLE WORKS | |||
}) | |||
}) | |||
</script> |
@@ -1,17 +0,0 @@ | |||
# Story Editor | |||
## Props: | |||
story: Story (should be a state as Markers are CRUD ?) | |||
## Ref: | |||
selectedMarker: Marker | |||
## Methods: | |||
LoadStory | |||
CreateMarker | |||
SelectMarker | |||
EditMarker | |||
DeleteMarker |
@@ -1,83 +0,0 @@ | |||
<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,13 @@ | |||
import { computed } from 'vue' | |||
export function useIsMobile() { | |||
const userAgent = navigator.userAgent || navigator.vendor || window.opera | |||
// Check for mobile user agents | |||
const isMobile = computed(() => { | |||
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i | |||
return mobileRegex.test(userAgent) | |||
}) | |||
return { isMobile } | |||
} |
@@ -0,0 +1,10 @@ | |||
import type OpenSeadragon from 'openseadragon' | |||
import { type Ref } from 'vue' | |||
export function useViewer(Viewer: Ref<OpenSeadragon.Viewer>) { | |||
const zoomTo = (zoomLevel: number) => { | |||
Viewer.value.viewport.zoomTo(zoomLevel, undefined, false) | |||
} | |||
return { zoomTo } | |||
} |
@@ -1,19 +0,0 @@ | |||
import type { Story } from '@/types/virages' | |||
import { defineStore } from 'pinia' | |||
export const useStories = defineStore('stories', { | |||
state: (): Story => ({ | |||
stories: [], | |||
nextId: 0, | |||
}), | |||
getters: { | |||
finishedStories(state) { | |||
return state.stories.filter((todo) => todo.isFinished) | |||
}, | |||
}, | |||
actions: { | |||
action(data) { | |||
this.stories.push({}) | |||
}, | |||
}, | |||
}) |
@@ -0,0 +1,36 @@ | |||
import type { Story } from '@/types/virages' | |||
import { defineStore } from 'pinia' | |||
import datas from '@/assets/stories.json' | |||
export const useStoryStore = defineStore('story', { | |||
state: () => ({ | |||
stories: [] as Story[], | |||
isStoryOpen: false, | |||
}), | |||
actions: { | |||
getStories() { | |||
this.stories = datas | |||
}, | |||
openStory(story: Story) { | |||
this.stories.forEach((story) => { | |||
story.displayMode = 'HIDDEN' | |||
}) | |||
story.displayMode = 'EDITOR' | |||
this.isStoryOpen = true | |||
document.body.classList.add('app-fullscreen') | |||
}, | |||
closeStories() { | |||
this.stories.forEach((story) => { | |||
story.displayMode = 'ARTICLE' | |||
}) | |||
this.isStoryOpen = false | |||
document.body.classList.remove('app-fullscreen') | |||
}, | |||
getStoryById(id: number) { | |||
return this.stories.find((story) => story.id === id) | |||
}, | |||
addMarker(story: Story, marker: Marker) { | |||
story.markers.push(marker) | |||
}, | |||
}, | |||
}) |
@@ -5,7 +5,7 @@ export interface Story { | |||
url: string // absolute or relative url (../public/deepzoom) | |||
markers: Marker[] | |||
displayMode: string // 'ARTICLE' | 'EDITOR' | 'PLAYER' | 'HIDDEN' | |||
date_art_creation: Date | |||
// date_art_creation: Date | |||
} | |||
export interface Marker { | |||
@@ -26,3 +26,22 @@ export interface Marker { | |||
zoom: number | |||
annotation: string | |||
} | |||
export interface ViewerConfiguration { | |||
prefixUrl: string | |||
crossOriginPolicy: string | |||
animationTime: number | |||
showNavigator: boolean | |||
sequenceMode: boolean | |||
showNavigationControl: boolean | |||
drawer: string | |||
preventDefaultAction: boolean | |||
visibilityRatio: number | |||
defaultZoomLevel: number | |||
gestureSettingsMouse: { | |||
scrollToZoom: boolean | |||
clickToZoom: boolean | |||
dblClickToZoom: boolean | |||
dragToPan: boolean | |||
} | |||
} |