Ver a proveniência

feat: plugins scaler + url, fix: animate on bounds

master
valere há 3 meses
ascendente
cometimento
c13a447760
9 ficheiros alterados com 1045 adições e 143 eliminações
  1. +113
    -0
      src/assets/plugins/openseadragon-bookmark-url.js
  2. +544
    -0
      src/assets/plugins/openseadragon-scalebar.js
  3. +45
    -90
      src/assets/stories.json
  4. +15
    -3
      src/components/StoryContainer.vue
  5. +4
    -4
      src/components/StoryList.vue
  6. +260
    -0
      src/components/tools/StoryDrawRect.vue
  7. +53
    -20
      src/components/tools/StoryEditor.vue
  8. +2
    -24
      src/stores/stories.ts
  9. +9
    -2
      src/types/virages.d.ts

+ 113
- 0
src/assets/plugins/openseadragon-bookmark-url.js Ver ficheiro

@@ -0,0 +1,113 @@
// OpenSeadragon Bookmark URL plugin 0.0.4

import OpenSeadragon from 'openseadragon'
;(function ($) {
$.Viewer.prototype.bookmarkUrl = function (options) {
options = options || {}
var trackPage = options.trackPage || false

var self = this

var updateTimeout

var parseHash = function () {
var params = {}
var hash = window.location.hash.replace(/^#/, '')
if (hash) {
var parts = hash.split('&')
parts.forEach(function (part) {
var subparts = part.split('=')
var key = subparts[0]
var value = parseFloat(subparts[1])
if (!key || isNaN(value)) {
console.error('bad hash param', part)
} else {
params[key] = value
}
})
}

return params
}

var updateUrl = function () {
// We only update once it's settled, so we're not constantly flashing the URL.
clearTimeout(updateTimeout)
updateTimeout = setTimeout(function () {
var zoom = self.viewport.getZoom()
var pan = self.viewport.getCenter()
var page = self.currentPage()
var oldUrl = location.pathname + location.hash
var search = location.search
var url = location.pathname + search + '#zoom=' + zoom + '&x=' + pan.x + '&y=' + pan.y
if (trackPage) {
url = url + '&page=' + page
}
history.replaceState({}, '', url)

if (url !== oldUrl) {
self.raiseEvent('bookmark-url-change', { url: location.href })
}
}, 100)
}

var useParams = function (params) {
var zoom = self.viewport.getZoom()
var pan = self.viewport.getCenter()
var page = self.currentPage()

if (trackPage && params.page !== undefined && params.page !== page) {
self.goToPage(params.page)
self.addOnceHandler('open', function () {
if (params.zoom !== undefined) {
self.viewport.zoomTo(params.zoom, null, true)
}
if (
params.x !== undefined &&
params.y !== undefined &&
(params.x !== pan.x || params.y !== pan.y)
) {
self.viewport.panTo(new $.Point(params.x, params.y), true)
}
})
} else {
if (params.zoom !== undefined && params.zoom !== zoom) {
self.viewport.zoomTo(params.zoom, null, true)
}
if (
params.x !== undefined &&
params.y !== undefined &&
(params.x !== pan.x || params.y !== pan.y)
) {
self.viewport.panTo(new $.Point(params.x, params.y), true)
}
}
}

var params = parseHash()

if (this.world.getItemCount() === 0) {
this.addOnceHandler('open', function () {
useParams(params)
})
} else {
useParams(params)
}

this.addHandler('zoom', updateUrl)
this.addHandler('pan', updateUrl)
if (trackPage) {
this.addHandler('page', updateUrl)
}

// Note that out own replaceState calls don't trigger hashchange events, so this is only if
// the user has modified the URL (by pasting one in, for instance).
window.addEventListener(
'hashchange',
function () {
useParams(parseHash())
},
false,
)
}
})(OpenSeadragon)

+ 544
- 0
src/assets/plugins/openseadragon-scalebar.js Ver ficheiro

@@ -0,0 +1,544 @@
/*
* This software was developed at the National Institute of Standards and
* Technology by employees of the Federal Government in the course of
* their official duties. Pursuant to title 17 Section 105 of the United
* States Code this software is not subject to copyright protection and is
* in the public domain. This software is an experimental system. NIST assumes
* no responsibility whatsoever for its use by other parties, and makes no
* guarantees, expressed or implied, about its quality, reliability, or
* any other characteristic. We would appreciate acknowledgement if the
* software is used.
*/
import OpenSeadragon from 'openseadragon'
;(function ($) {
$.Viewer.prototype.scalebar = function (options) {
if (!this.scalebarInstance) {
options = options || {}
options.viewer = this
this.scalebarInstance = new $.Scalebar(options)
} else {
this.scalebarInstance.refresh(options)
}
}

$.ScalebarType = {
NONE: 0,
MICROSCOPY: 1,
MAP: 2,
}

$.ScalebarLocation = {
NONE: 0,
TOP_LEFT: 1,
TOP_RIGHT: 2,
BOTTOM_RIGHT: 3,
BOTTOM_LEFT: 4,
}

/**
*
* @class Scalebar
* @param {Object} options
* @param {OpenSeadragon.Viewer} options.viewer The viewer to attach this
* Scalebar to.
* @param {OpenSeadragon.ScalebarType} options.type The scale bar type.
* Default: microscopy
* @param {Integer} options.pixelsPerMeter The pixels per meter of the
* zoomable image at the original image size. If null, the scale bar is not
* displayed. default: null
* @param {Integer} options.referenceItemIdx Specify the item from
* viewer.world to which options.pixelsPerMeter is refering.
* default: 0
* @param (String} options.minWidth The minimal width of the scale bar as a
* CSS string (ex: 100px, 1em, 1% etc...) default: 150px
* @param {OpenSeadragon.ScalebarLocation} options.location The location
* of the scale bar inside the viewer. default: bottom left
* @param {Integer} options.xOffset Offset location of the scale bar along x.
* default: 5
* @param {Integer} options.yOffset Offset location of the scale bar along y.
* default: 5
* @param {Boolean} options.stayInsideImage When set to true, keep the
* scale bar inside the image when zooming out. default: true
* @param {String} options.color The color of the scale bar using a color
* name or the hexadecimal format (ex: black or #000000) default: black
* @param {String} options.fontColor The font color. default: black
* @param {String} options.backgroundColor The background color. default: none
* @param {String} options.fontSize The font size. default: not set
* @param {String} options.fontFamily The font-family. default: not set
* @param {String} options.barThickness The thickness of the scale bar in px.
* default: 2
* @param {function} options.sizeAndTextRenderer A function which will be
* called to determine the size of the scale bar and it's text content.
* The function must have 2 parameters: the PPM at the current zoom level
* and the minimum size of the scale bar. It must return an object containing
* 2 attributes: size and text containing the size of the scale bar and the text.
* default: $.ScalebarSizeAndTextRenderer.METRIC_LENGTH
*/
$.Scalebar = function (options) {
options = options || {}
if (!options.viewer) {
throw new Error('A viewer must be specified.')
}
this.viewer = options.viewer

this.divElt = document.createElement('div')
this.viewer.container.appendChild(this.divElt)
this.divElt.style.position = 'relative'
this.divElt.style.margin = '0'
this.divElt.style.pointerEvents = 'none'

this.setMinWidth(options.minWidth || '150px')

this.setDrawScalebarFunction(options.type || $.ScalebarType.MICROSCOPY)
this.color = options.color || 'black'
this.fontColor = options.fontColor || 'black'
this.backgroundColor = options.backgroundColor || 'none'
this.fontSize = options.fontSize || ''
this.fontFamily = options.fontFamily || ''
this.barThickness = options.barThickness || 2
this.pixelsPerMeter = options.pixelsPerMeter || null
this.referenceItemIdx = options.referenceItemIdx || 0
this.location = options.location || $.ScalebarLocation.BOTTOM_LEFT
this.xOffset = options.xOffset || 5
this.yOffset = options.yOffset || 5
this.stayInsideImage = isDefined(options.stayInsideImage) ? options.stayInsideImage : true
this.sizeAndTextRenderer =
options.sizeAndTextRenderer || $.ScalebarSizeAndTextRenderer.METRIC_LENGTH

var self = this
this.viewer.addHandler('open', function () {
self.refresh()
})
this.viewer.addHandler('animation', function () {
self.refresh()
})
this.viewer.addHandler('resize', function () {
self.refresh()
})
}

$.Scalebar.prototype = {
updateOptions: function (options) {
if (!options) {
return
}
if (isDefined(options.type)) {
this.setDrawScalebarFunction(options.type)
}
if (isDefined(options.minWidth)) {
this.setMinWidth(options.minWidth)
}
if (isDefined(options.color)) {
this.color = options.color
}
if (isDefined(options.fontColor)) {
this.fontColor = options.fontColor
}
if (isDefined(options.backgroundColor)) {
this.backgroundColor = options.backgroundColor
}
if (isDefined(options.fontSize)) {
this.fontSize = options.fontSize
}
if (isDefined(options.fontFamily)) {
this.fontFamily = options.fontFamily
}
if (isDefined(options.barThickness)) {
this.barThickness = options.barThickness
}
if (isDefined(options.pixelsPerMeter)) {
this.pixelsPerMeter = options.pixelsPerMeter
}
if (isDefined(options.referenceItemIdx)) {
this.referenceItemIdx = options.referenceItemIdx
}
if (isDefined(options.location)) {
this.location = options.location
}
if (isDefined(options.xOffset)) {
this.xOffset = options.xOffset
}
if (isDefined(options.yOffset)) {
this.yOffset = options.yOffset
}
if (isDefined(options.stayInsideImage)) {
this.stayInsideImage = options.stayInsideImage
}
if (isDefined(options.sizeAndTextRenderer)) {
this.sizeAndTextRenderer = options.sizeAndTextRenderer
}
},
setDrawScalebarFunction: function (type) {
if (!type) {
this.drawScalebar = null
} else if (type === $.ScalebarType.MAP) {
this.drawScalebar = this.drawMapScalebar
} else {
this.drawScalebar = this.drawMicroscopyScalebar
}
},
setMinWidth: function (minWidth) {
this.divElt.style.width = minWidth
// Make sure to display the element before getting is width
this.divElt.style.display = ''
this.minWidth = this.divElt.offsetWidth
},
/**
* Refresh the scalebar with the options submitted.
* @param {Object} options
* @param {OpenSeadragon.ScalebarType} options.type The scale bar type.
* Default: microscopy
* @param {Integer} options.pixelsPerMeter The pixels per meter of the
* zoomable image at the original image size. If null, the scale bar is not
* displayed. default: null
* @param {Integer} options.referenceItemIdx Specify the item from
* viewer.world to which options.pixelsPerMeter is refering.
* default: 0
* @param (String} options.minWidth The minimal width of the scale bar as a
* CSS string (ex: 100px, 1em, 1% etc...) default: 150px
* @param {OpenSeadragon.ScalebarLocation} options.location The location
* of the scale bar inside the viewer. default: bottom left
* @param {Integer} options.xOffset Offset location of the scale bar along x.
* default: 5
* @param {Integer} options.yOffset Offset location of the scale bar along y.
* default: 5
* @param {Boolean} options.stayInsideImage When set to true, keep the
* scale bar inside the image when zooming out. default: true
* @param {String} options.color The color of the scale bar using a color
* name or the hexadecimal format (ex: black or #000000) default: black
* @param {String} options.fontColor The font color. default: black
* @param {String} options.backgroundColor The background color. default: none
* @param {String} options.fontSize The font size. default: not set
* @param {String} options.barThickness The thickness of the scale bar in px.
* default: 2
* @param {function} options.sizeAndTextRenderer A function which will be
* called to determine the size of the scale bar and it's text content.
* The function must have 2 parameters: the PPM at the current zoom level
* and the minimum size of the scale bar. It must return an object containing
* 2 attributes: size and text containing the size of the scale bar and the text.
* default: $.ScalebarSizeAndTextRenderer.METRIC_LENGTH
*/
refresh: function (options) {
this.updateOptions(options)

if (!this.viewer.isOpen() || !this.drawScalebar || !this.pixelsPerMeter || !this.location) {
this.divElt.style.display = 'none'
return
}
this.divElt.style.display = ''

var viewport = this.viewer.viewport
var tiledImage = this.viewer.world.getItemAt(this.referenceItemIdx)
var zoom = tiledImageViewportToImageZoom(tiledImage, viewport.getZoom(true))
var currentPPM = zoom * this.pixelsPerMeter
var props = this.sizeAndTextRenderer(currentPPM, this.minWidth)

this.drawScalebar(props.size, props.text)
var location = this.getScalebarLocation()
this.divElt.style.left = location.x + 'px'
this.divElt.style.top = location.y + 'px'
},
drawMicroscopyScalebar: function (size, text) {
this.divElt.style.fontSize = this.fontSize
this.divElt.style.fontFamily = this.fontFamily
this.divElt.style.textAlign = 'center'
this.divElt.style.color = this.fontColor
this.divElt.style.border = 'none'
this.divElt.style.borderBottom = this.barThickness + 'px solid ' + this.color
this.divElt.style.backgroundColor = this.backgroundColor
this.divElt.innerHTML = text
this.divElt.style.width = size + 'px'
},
drawMapScalebar: function (size, text) {
this.divElt.style.fontSize = this.fontSize
this.divElt.style.fontFamily = this.fontFamily
this.divElt.style.textAlign = 'center'
this.divElt.style.color = this.fontColor
this.divElt.style.border = this.barThickness + 'px solid ' + this.color
this.divElt.style.borderTop = 'none'
this.divElt.style.backgroundColor = this.backgroundColor
this.divElt.innerHTML = text
this.divElt.style.width = size + 'px'
},
/**
* Compute the location of the scale bar.
* @returns {OpenSeadragon.Point}
*/
getScalebarLocation: function () {
if (this.location === $.ScalebarLocation.TOP_LEFT) {
var x = 0
var y = 0
if (this.stayInsideImage) {
var pixel = this.viewer.viewport.pixelFromPoint(new $.Point(0, 0), true)
if (!this.viewer.wrapHorizontal) {
x = Math.max(pixel.x, 0)
}
if (!this.viewer.wrapVertical) {
y = Math.max(pixel.y, 0)
}
}
return new $.Point(x + this.xOffset, y + this.yOffset)
}
if (this.location === $.ScalebarLocation.TOP_RIGHT) {
var barWidth = this.divElt.offsetWidth
var container = this.viewer.container
var x = container.offsetWidth - barWidth
var y = 0
if (this.stayInsideImage) {
var pixel = this.viewer.viewport.pixelFromPoint(new $.Point(1, 0), true)
if (!this.viewer.wrapHorizontal) {
x = Math.min(x, pixel.x - barWidth)
}
if (!this.viewer.wrapVertical) {
y = Math.max(y, pixel.y)
}
}
return new $.Point(x - this.xOffset, y + this.yOffset)
}
if (this.location === $.ScalebarLocation.BOTTOM_RIGHT) {
var barWidth = this.divElt.offsetWidth
var barHeight = this.divElt.offsetHeight
var container = this.viewer.container
var x = container.offsetWidth - barWidth
var y = container.offsetHeight - barHeight
if (this.stayInsideImage) {
var pixel = this.viewer.viewport.pixelFromPoint(
new $.Point(1, 1 / this.viewer.source.aspectRatio),
true,
)
if (!this.viewer.wrapHorizontal) {
x = Math.min(x, pixel.x - barWidth)
}
if (!this.viewer.wrapVertical) {
y = Math.min(y, pixel.y - barHeight)
}
}
return new $.Point(x - this.xOffset, y - this.yOffset)
}
if (this.location === $.ScalebarLocation.BOTTOM_LEFT) {
var barHeight = this.divElt.offsetHeight
var container = this.viewer.container
var x = 0
var y = container.offsetHeight - barHeight
if (this.stayInsideImage) {
var pixel = this.viewer.viewport.pixelFromPoint(
new $.Point(0, 1 / this.viewer.source.aspectRatio),
true,
)
if (!this.viewer.wrapHorizontal) {
x = Math.max(x, pixel.x)
}
if (!this.viewer.wrapVertical) {
y = Math.min(y, pixel.y - barHeight)
}
}
return new $.Point(x + this.xOffset, y - this.yOffset)
}
},
/**
* Get the rendered scalebar in a canvas.
* @returns {Element} A canvas containing the scalebar representation
*/
getAsCanvas: function () {
var canvas = document.createElement('canvas')
canvas.width = this.divElt.offsetWidth
canvas.height = this.divElt.offsetHeight
var context = canvas.getContext('2d')
context.fillStyle = this.backgroundColor
context.fillRect(0, 0, canvas.width, canvas.height)
context.fillStyle = this.color
context.fillRect(0, canvas.height - this.barThickness, canvas.width, canvas.height)
if (this.drawScalebar === this.drawMapScalebar) {
context.fillRect(0, 0, this.barThickness, canvas.height)
context.fillRect(canvas.width - this.barThickness, 0, this.barThickness, canvas.height)
}
context.font = window.getComputedStyle(this.divElt).font
context.textAlign = 'center'
context.textBaseline = 'middle'
context.fillStyle = this.fontColor
var hCenter = canvas.width / 2
var vCenter = canvas.height / 2
context.fillText(this.divElt.textContent, hCenter, vCenter)
return canvas
},
/**
* Get a copy of the current OpenSeadragon canvas with the scalebar.
* @returns {Element} A canvas containing a copy of the current OpenSeadragon canvas with the scalebar
*/
getImageWithScalebarAsCanvas: function () {
var imgCanvas = this.viewer.drawer.canvas
var newCanvas = document.createElement('canvas')
newCanvas.width = imgCanvas.width
newCanvas.height = imgCanvas.height
var newCtx = newCanvas.getContext('2d')
newCtx.drawImage(imgCanvas, 0, 0)
var scalebarCanvas = this.getAsCanvas()
var location = this.getScalebarLocation()
newCtx.drawImage(scalebarCanvas, location.x, location.y)
return newCanvas
},
}

$.ScalebarSizeAndTextRenderer = {
/**
* Metric length. From nano meters to kilometers.
*/
METRIC_LENGTH: function (ppm, minSize) {
return getScalebarSizeAndTextForMetric(ppm, minSize, 'm')
},
/**
* Imperial length. Choosing the best unit from thou, inch, foot and mile.
*/
IMPERIAL_LENGTH: function (ppm, minSize) {
var maxSize = minSize * 2
var ppi = ppm * 0.0254
if (maxSize < ppi * 12) {
if (maxSize < ppi) {
var ppt = ppi / 1000
return getScalebarSizeAndText(ppt, minSize, 'th')
}
return getScalebarSizeAndText(ppi, minSize, 'in')
}
var ppf = ppi * 12
if (maxSize < ppf * 2000) {
return getScalebarSizeAndText(ppf, minSize, 'ft')
}
var ppmi = ppf * 5280
return getScalebarSizeAndText(ppmi, minSize, 'mi')
},
/**
* Astronomy units. Choosing the best unit from arcsec, arcminute, and degree
*/
ASTRONOMY: function (ppa, minSize) {
var maxSize = minSize * 2
if (maxSize < ppa * 60) {
return getScalebarSizeAndText(ppa, minSize, '"', false, '')
}
var ppminutes = ppa * 60
if (maxSize < ppminutes * 60) {
return getScalebarSizeAndText(ppminutes, minSize, "\'", false, '')
}
var ppd = ppminutes * 60
return getScalebarSizeAndText(ppd, minSize, '&#176', false, '')
},
/**
* Standard time. Choosing the best unit from second (and metric divisions),
* minute, hour, day and year.
*/
STANDARD_TIME: function (pps, minSize) {
var maxSize = minSize * 2
if (maxSize < pps * 60) {
return getScalebarSizeAndTextForMetric(pps, minSize, 's', false)
}
var ppminutes = pps * 60
if (maxSize < ppminutes * 60) {
return getScalebarSizeAndText(ppminutes, minSize, 'minute', true)
}
var pph = ppminutes * 60
if (maxSize < pph * 24) {
return getScalebarSizeAndText(pph, minSize, 'hour', true)
}
var ppd = pph * 24
if (maxSize < ppd * 365.25) {
return getScalebarSizeAndText(ppd, minSize, 'day', true)
}
var ppy = ppd * 365.25
return getScalebarSizeAndText(ppy, minSize, 'year', true)
},
/**
* Generic metric unit. One can use this function to create a new metric
* scale. For example, here is an implementation of energy levels:
* function(ppeV, minSize) {
* return OpenSeadragon.ScalebarSizeAndTextRenderer.METRIC_GENERIC(
* ppeV, minSize, "eV");
* }
*/
METRIC_GENERIC: getScalebarSizeAndTextForMetric,
}

// Missing TiledImage.viewportToImageZoom function in OSD 2.0.0
function tiledImageViewportToImageZoom(tiledImage, viewportZoom) {
var ratio =
(tiledImage._scaleSpring.current.value * tiledImage.viewport._containerInnerSize.x) /
tiledImage.source.dimensions.x
return ratio * viewportZoom
}

function getScalebarSizeAndText(ppm, minSize, unitSuffix, handlePlural, spacer) {
spacer = spacer === undefined ? ' ' : spacer
var value = normalize(ppm, minSize)
var factor = roundSignificand((value / ppm) * minSize, 3)
var size = value * minSize
var plural = handlePlural && factor > 1 ? 's' : ''
return {
size: size,
text: factor + spacer + unitSuffix + plural,
}
}

function getScalebarSizeAndTextForMetric(ppm, minSize, unitSuffix) {
var value = normalize(ppm, minSize)
var factor = roundSignificand((value / ppm) * minSize, 3)
var size = value * minSize
var valueWithUnit = getWithUnit(factor, unitSuffix)
return {
size: size,
text: valueWithUnit,
}
}

function normalize(value, minSize) {
var significand = getSignificand(value)
var minSizeSign = getSignificand(minSize)
var result = getSignificand(significand / minSizeSign)
if (result >= 5) {
result /= 5
}
if (result >= 4) {
result /= 4
}
if (result >= 2) {
result /= 2
}
return result
}

function getSignificand(x) {
return x * Math.pow(10, Math.ceil(-log10(x)))
}

function roundSignificand(x, decimalPlaces) {
var exponent = -Math.ceil(-log10(x))
var power = decimalPlaces - exponent
var significand = x * Math.pow(10, power)
// To avoid rounding problems, always work with integers
if (power < 0) {
return Math.round(significand) * Math.pow(10, -power)
}
return Math.round(significand) / Math.pow(10, power)
}

function log10(x) {
return Math.log(x) / Math.log(10)
}

function getWithUnit(value, unitSuffix) {
if (value < 0.000001) {
return value * 1000000000 + ' n' + unitSuffix
}
if (value < 0.001) {
return value * 1000000 + ' μ' + unitSuffix
}
if (value < 1) {
return value * 1000 + ' m' + unitSuffix
}
if (value >= 1000) {
return value / 1000 + ' k' + unitSuffix
}
return value + ' ' + unitSuffix
}

function isDefined(variable) {
return typeof variable !== 'undefined'
}
})(OpenSeadragon)

+ 45
- 90
src/assets/stories.json Ver ficheiro

@@ -1,127 +1,82 @@
[
{
"id": 0,
"name": "Poulpatore",
"author": "Ricardo Prosety",
"url": "../public/deepzoom/poulpatore/dz/info.json",
"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
"bounds": {
"x": 0.32376980664954275,
"y": 0.283813392419349,
"width": 0.09524470945402953,
"height": 0.028126953260643097,
"degrees": 0
},
"zoom": 8,
"annotation": "<b>second</b> annotation"
},
{
"id": 2,
"name": "third",
"order": 2,
"position": {
"x": 0.5,
"y": 0.2
"zoom": 9,
"point": {
"x": 0.3580975873485992,
"y": 0.2968103267302634
},
"zoom": 7,
"annotation": "<b>third</b>"
"annotation": "<b>lorem</b> ipsum dolor sit amet"
}
]
},
{
"id": 1,
"name": "Vignemale",
"author": "Amandine MONTAZEAU",
"url": "../public/deepzoom/vignemale/dz/info.json",
"id": 0,
"name": "Bathypolypus",
"author": "arcticus",
"url": "../public/deepzoom/poulpatore/dz/info.json",
"displayMode": "ARTICLE",
"markers": [
{
"id": 0,
"name": "first",
"order": 0,
"position": {
"x": 0.6,
"y": 0.7
"bounds": {
"degrees": 0,
"height": 0.028126953260643097,
"width": 0.09524470945402953,
"x": 0.32376980664954275,
"y": 0.283813392419349
},
"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": 9,
"point": {
"x": 0.3580975873485992,
"y": 0.2968103267302634
},
"zoom": 7,
"annotation": "<b>third</b>"
"annotation": "<b>lorem</b> ipsum dolor sit amet"
}
]
},
{
"id": 2,
"name": "Cells",
"author": "Sairam Bandarupalli",
"url": "https://verrochi92.github.io/axolotl/data/W255B_0.dzi",
"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
"bounds": {
"degrees": 0,
"height": 0.028126953260643097,
"width": 0.09524470945402953,
"x": 0.32376980664954275,
"y": 0.283813392419349
},
"zoom": 8,
"annotation": "<b>second</b> annotation"
},
{
"id": 2,
"name": "third",
"order": 2,
"position": {
"x": 0.5,
"y": 0.2
"zoom": 9,
"point": {
"x": 0.3580975873485992,
"y": 0.2968103267302634
},
"zoom": 7,
"annotation": "<b>third</b>"
"annotation": "<b>lorem</b> ipsum dolor sit amet"
}
]
}


+ 15
- 3
src/components/StoryContainer.vue Ver ficheiro

@@ -13,7 +13,9 @@

<script setup lang="ts">
import OpenSeadragon from 'openseadragon'
import { onMounted, ref, provide } from 'vue'
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 Article from './StoryArticle.vue'
import Editor from './tools/StoryEditor.vue'
@@ -35,7 +37,7 @@ const initViewer = () => {
showNavigationControl: false,
drawer: 'canvas',
preventDefaultAction: true,
visibilityRatio: 1,
visibilityRatio: 0.5,
crossOriginPolicy: 'Anonymous',
gestureSettingsMouse: {
scrollToZoom: true,
@@ -43,13 +45,23 @@ const initViewer = () => {
dblClickToZoom: false,
dragToPan: true,
},
defaultZoomLevel: 1.5,
defaultZoomLevel: 1.2,
})
}

const destroyViewer = () => {
if (Viewer.value) {
Viewer.value.destroy()
}
}

onMounted(() => {
initViewer()
})

onUnmounted(() => {
destroyViewer()
})
</script>

<style lang="scss">


+ 4
- 4
src/components/StoryList.vue Ver ficheiro

@@ -4,7 +4,7 @@
v-for="story in stories"
:key="story.id"
:story="story"
@click="setOpenStory(story)"
@click="openStory(story)"
/>
</div>
<button
@@ -25,7 +25,7 @@ import datas from '@/assets/stories.json'
const stories = ref<Story[]>(datas)
const isAppModeFullscreen = ref(false)

const setOpenStory = (story: Story) => {
const openStory = (story: Story) => {
stories.value.forEach((story) => {
story.displayMode = 'HIDDEN'
})
@@ -56,9 +56,9 @@ onMounted(() => {

<style lang="scss">
.story-list {
@apply flex flex-wrap;
@apply flex flex-wrap max-w-7xl mx-auto;
.app-fullscreen & {
@apply block;
@apply block max-w-full;
}
.story {
@apply grow lg:w-1/3;


+ 260
- 0
src/components/tools/StoryDrawRect.vue Ver ficheiro

@@ -0,0 +1,260 @@
<template>
<nav class="[&>*]:m-3 text-black w-48">
<div class="flex flex-col ui">
<div>
<button @click="drawRect">draw rect</button>
{{ selectedBounds }}
</div>
<button @click="goHome">home</button>
<button @click="addPoint">Add Point</button>
<button @click="addRectangle">Add Rectangle</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="isAMarkerSelected">
<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 from 'openseadragon'

const props = defineProps<{ markers: Marker[] }>()
const Viewer = inject<Ref<OpenSeadragon.Viewer>>('Viewer')
const isAddingPoint = ref<boolean>(false)
const isAddingRectangle = ref<boolean>(false)
const selectedMarker = ref<Marker>({} as Marker)
const textInputMarkerName = ref<HTMLInputElement | null>(null)

const selectedBounds = ref(null)

const isAMarkerSelected = computed(() => {
return selectedMarker.value.id !== undefined
})
const sortedMarkers = computed(() => {
return [...props.markers].sort((a, b) => a.order - b.order)
})

const drawRect = () => {}

function convertZoomPointToBounds(zoom, pointX, pointY) {
const canvasWidth = Viewer?.value.canvas.offsetWidth
const canvasHeight = Viewer?.value.canvas.offsetHeight

const bounds = {
x: (pointX - canvasWidth / 2) / (canvasWidth / 2) / zoom,
y: (pointY - canvasHeight / 2) / (canvasHeight / 2) / zoom,
width: canvasWidth / zoom,
height: canvasHeight / zoom,
}

return bounds
}

const goHome = () => {
Viewer?.value.viewport.goHome(false)
}

const addPoint = () => {
unselectMarker()
isAddingPoint.value = true
document.querySelector('.openseadragon-canvas')?.classList.add('cursor-crosshair')
}

const addRectangle = () => {
unselectMarker()
isAddingRectangle.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.fitBoundsWithConstraints(new OpenSeadragon.Rect(marker.bounds), false)
Viewer?.value.viewport.zoomTo(marker.zoom, undefined, false)
Viewer?.value.viewport.panTo(new OpenSeadragon.Point(marker.point.x, marker.point.y))
console.log('marker', marker)
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.point.x, marker.point.y),
})
}

const createMarkerOnClick = () => {
Viewer?.value.addHandler('canvas-click', (event) => {
// adding point
if (isAddingPoint.value) {
console.log('adding point')
const point = Viewer?.value.viewport.pointFromPixel(event.position)
const zoom = Viewer?.value.viewport.getZoom()
const bounds = Viewer?.value.viewport.getBounds()
console.log('bounds', bounds)
console.log('zoom', zoom)
console.log('point', point)
const newMarker: Marker = {
id: props.markers.length,
order: props.markers.length,
name: '',
bounds: bounds,
point: new OpenSeadragon.Point(point.x, point.y),
zoom: zoom,
annotation: '',
}
injectMarker(newMarker)
isAddingPoint.value = false
document.querySelector('.openseadragon-canvas')?.classList.remove('cursor-crosshair')
selectMarker(newMarker)
} else if (isAddingRectangle.value) {
const point = Viewer?.value.viewport.pointFromPixel(event.position)
const zoom = Viewer?.value.viewport.getZoom()
const bounds = Viewer?.value.viewport.getBounds()
} else if (isAMarkerSelected.value) {
unselectMarker()
}
})
}

const editMarkerOnDragOrZoom = () => {
Viewer?.value.addHandler('canvas-drag', (event) => {
if (isAMarkerSelected.value) {
Viewer.value.gestureSettingsMouse.dragToPan = false
const point = Viewer?.value.viewport.pointFromPixel(event.position)
selectedMarker.value.point = point
selectedMarker.value.bounds = Viewer?.value.viewport.getBounds()
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 (isAMarkerSelected.value) {
selectedMarker.value.zoom = event.zoom
selectedMarker.value.bounds = Viewer?.value.viewport.getBounds()
}
})
}

const loadStory = (markers: Marker[]) => {
markers.forEach((marker) => {
injectMarker(marker)
})
}

const keyboardShortcut = () => {
// if press on escpae key
window.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return
unselectMarker()
goHome()
})
}

const initScalebar = () => {
Viewer.value.scalebar({
type: OpenSeadragon.ScalebarType.MAP,
pixelsPerMeter: 37792223.52,
minWidth: '74px',
location: OpenSeadragon.ScalebarLocation.BOTTOM_RIGHT,
color: 'black',
fontColor: 'black',
backgroundColor: 'rgba(255, 255, 255, 0.5)',
barThickness: 1,
stayInsideImage: false,
xOffset: 20,
yOffset: 20,
})
}

const initUrl = () => {
Viewer.value.bookmarkUrl()
}

onMounted(() => {
nextTick(() => {
Viewer?.value.clearOverlays()
// Viewer.value.viewport.defaultZoomLevel = 1
loadStory(props.markers)
createMarkerOnClick()
editMarkerOnDragOrZoom()
keyboardShortcut()
initScalebar()
initUrl()
})
})
</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>

+ 53
- 20
src/components/tools/StoryEditor.vue Ver ficheiro

@@ -2,7 +2,7 @@
<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>
<button @click="addPoint">Add Point</button>
<div class="flex flex-col my-6">
<button
v-for="marker in sortedMarkers"
@@ -35,7 +35,7 @@ import OpenSeadragon from 'openseadragon'

const props = defineProps<{ markers: Marker[] }>()
const Viewer = inject<Ref<OpenSeadragon.Viewer>>('Viewer')
const isAddingMarker = ref<boolean>(false)
const isAddingPoint = ref<boolean>(false)
const selectedMarker = ref<Marker>({} as Marker)
const textInputMarkerName = ref<HTMLInputElement | null>(null)

@@ -50,9 +50,9 @@ const goHome = () => {
Viewer?.value.viewport.goHome(false)
}

const addMarker = () => {
const addPoint = () => {
unselectMarker()
isAddingMarker.value = true
isAddingPoint.value = true
document.querySelector('.openseadragon-canvas')?.classList.add('cursor-crosshair')
}

@@ -66,12 +66,17 @@ const selectMarker = (marker: Marker) => {
})
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))
const markerRectangle = new OpenSeadragon.Rect(
marker.bounds.x,
marker.bounds.y,
marker.bounds.width,
marker.bounds.height,
)
Viewer?.value.viewport.fitBoundsWithConstraints(markerRectangle, false)
selectedMarker.value = marker
nextTick(() => {
textInputMarkerName.value?.focus()
})
// nextTick(() => {
// textInputMarkerName.value?.focus()
// })
}

const unselectMarker = () => {
@@ -100,26 +105,29 @@ const injectMarker = (marker: Marker) => {
// Injection de l'overlay
Viewer?.value.addOverlay({
element: overlay,
location: new OpenSeadragon.Point(marker.position.x, marker.position.y),
location: new OpenSeadragon.Point(marker.point.x, marker.point.y),
})
}

const createMarkerOnClick = () => {
Viewer?.value.addHandler('canvas-click', (e) => {
if (isAddingMarker.value) {
const point = Viewer?.value.viewport.pointFromPixel(e.position)
Viewer?.value.addHandler('canvas-click', (event) => {
// adding point
if (isAddingPoint.value) {
const point = Viewer?.value.viewport.pointFromPixel(event.position)
const zoom = Viewer?.value.viewport.getZoom()
const bounds = Viewer?.value.viewport.getBounds()
const newMarker: Marker = {
id: props.markers.length, // WIP
order: props.markers.length, // WIP
id: props.markers.length,
order: props.markers.length,
name: '',
annotation: '',
position: new OpenSeadragon.Point(point.x, point.y),
bounds: bounds,
point: new OpenSeadragon.Point(point.x, point.y),
zoom: zoom,
annotation: '',
}
injectMarker(newMarker)
isAddingMarker.value = false
document.querySelector('.openseadragon-canvas').classList.remove('cursor-crosshair')
isAddingPoint.value = false
document.querySelector('.openseadragon-canvas')?.classList.remove('cursor-crosshair')
selectMarker(newMarker)
} else if (isAMarkerSelected.value) {
unselectMarker()
@@ -132,7 +140,8 @@ const editMarkerOnDragOrZoom = () => {
if (isAMarkerSelected.value) {
Viewer.value.gestureSettingsMouse.dragToPan = false
const point = Viewer?.value.viewport.pointFromPixel(event.position)
selectedMarker.value.position = point
selectedMarker.value.point = point
selectedMarker.value.bounds = Viewer?.value.viewport.getBounds()
const overlay = document.querySelector('.marker-id-' + selectedMarker.value.id)
Viewer?.value.updateOverlay(overlay as Element, point)
}
@@ -145,6 +154,7 @@ const editMarkerOnDragOrZoom = () => {
Viewer?.value.addHandler('zoom', (event) => {
if (isAMarkerSelected.value) {
selectedMarker.value.zoom = event.zoom
selectedMarker.value.bounds = Viewer?.value.viewport.getBounds()
}
})
}
@@ -164,13 +174,36 @@ const keyboardShortcut = () => {
})
}

const initScalebar = () => {
Viewer.value.scalebar({
type: OpenSeadragon.ScalebarType.MAP,
pixelsPerMeter: 1000000,
minWidth: '74px',
location: OpenSeadragon.ScalebarLocation.BOTTOM_RIGHT,
color: 'black',
fontColor: 'black',
backgroundColor: 'rgba(255, 255, 255, 0.5)',
barThickness: 1,
stayInsideImage: false,
xOffset: 20,
yOffset: 20,
})
}

const initUrl = () => {
Viewer.value.bookmarkUrl()
}

onMounted(() => {
nextTick(() => {
Viewer?.value.clearOverlays()
// Viewer.value.viewport.defaultZoomLevel = 1
loadStory(props.markers)
createMarkerOnClick()
editMarkerOnDragOrZoom()
keyboardShortcut()
initScalebar()
initUrl()
})
})
</script>


+ 2
- 24
src/stores/stories.ts Ver ficheiro

@@ -1,41 +1,19 @@
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 })
action(data) {
this.stories.push({})
},
},
})

+ 9
- 2
src/types/virages.d.ts Ver ficheiro

@@ -2,7 +2,7 @@ export interface Story {
id: number
name: string
author: string
url: string
url: string // absolute or relative url (../public/deepzoom)
markers: Marker[]
displayMode: string // 'ARTICLE' | 'EDITOR' | 'PLAYER' | 'HIDDEN'
date_art_creation: Date
@@ -12,7 +12,14 @@ export interface Marker {
id: number
order: number
name: string
position: {
bounds: {
x: number
y: number
width: number
height: number
degrees: number
}
point: {
x: number
y: number
}


Carregando…
Cancelar
Guardar