melpomene/melpomene.js

514 lines
14 KiB
JavaScript
Raw Normal View History

2023-05-21 21:07:40 +02:00
/* Melpomene webcomic reader JS */
2023-05-26 19:32:31 +02:00
/* Version 1.0.0_RC1 */
2023-05-19 18:15:34 +02:00
/* CC-BY-NC-SA : https://git.aribaud.net/caribaud/melpomene/ */
2023-04-16 21:35:32 +02:00
//============
// CONTROLS
//============
MOVE_NEXT = "ArrowRight"
MOVE_BACK = "ArrowLeft"
TOGGLE_FULLSCREEN = "F"
2023-04-17 20:45:46 +02:00
TOGGLE_PROGRESSBAR = "P"
2023-04-17 20:59:32 +02:00
TOGGLE_VIEW_MODE = "V"
2023-04-16 21:35:32 +02:00
//========================
// NAVIGATION CONSTANTS
//========================
2023-04-15 13:12:49 +02:00
PAGE_TRANSITION_SPEED = "1.5s"
2023-04-16 21:35:32 +02:00
MOUSEWHELL_MIN_DELAY = 50
2023-04-16 22:44:17 +02:00
DELAY_BEFORE_HIDDING_CONTROLS = 4000;
2023-04-15 13:12:49 +02:00
2023-04-16 21:35:32 +02:00
//====================
// STATES CONSTANTS
//====================
2023-04-15 13:12:49 +02:00
2023-05-26 19:32:31 +02:00
MELPOMENE_VERSION = "1.0.0_RC1"
2023-05-26 19:19:38 +02:00
READER_FRAME = document.getElementById("melpomene")
READER_CONTENT_FRAME = document.getElementById("melpomene-content-frame")
READER_PAGES = document.getElementById("melpomene-pages")
FOCUS_OVERLAY_HEIGHT = document.getElementById("melpomene-focus")
FOCUS_OVERLAY_WIDTH = document.getElementById("melpomene-focus-col")
HELP_CONTROLS = document.getElementById("melpomene-help-menu")
PROGRESS_BAR_CONTAINER = document.getElementById("melpomene-progress-container")
PROGRESS_BAR = document.getElementById("melpomene-progress-bar")
PROGRESS_BAR_PAGES = document.getElementById("melpomene-progress-sections")
VERSION_DISPLAY = document.getElementById("melpomene-version")
2023-04-15 13:12:49 +02:00
//===========================
// STATES GLOBAL VARIABLES
//===========================
// The variable ZOOMS can either be defined by another JS file or contructed at init
if (typeof PAGES_ZOOMS == 'undefined') {
PAGES_ZOOMS = null
}
2023-04-15 13:12:49 +02:00
CURRENT_ZOOM = 0
CURRENT_PAGE = 1
CURRENT_WIDTH = 0
CURRENT_HEIGHT = 0
CURRENT_X = 0
CURRENT_Y = 0
IS_PAGE_MODE = false
2023-04-16 17:43:37 +02:00
MOUSEWHELL_WAIT = false
2023-04-16 21:35:32 +02:00
// =============
// UTILITIES
// =============
2023-04-15 13:12:49 +02:00
// Zooms utilites
// --------------
function globalZoomScale(){
if (READER_PAGES.dataset.globalZoomScale != undefined){
return parseFloat(READER_PAGES.dataset.globalZoomScale)
}
return 1.0
}
function globalZoomOffsetX(){
if (READER_PAGES.dataset.globalZoomOffset != undefined){
return parseFloat(READER_PAGES.dataset.globalZoomOffset.split(',')[0])
}
return 0.0
}
function globalZoomOffsetY(){
if (READER_PAGES.dataset.globalZoomOffset != undefined){
return parseFloat(READER_PAGES.dataset.globalZoomOffset.split(',')[1])
}
return 0.0
}
function loadZoomsFromImgTagsIfRequired(){
// Zooms may be defined by another JS file
if (PAGES_ZOOMS == null){
PAGES_ZOOMS = []
// parse the data-zooms of each img and and the page number info
for (var i = 0; i < READER_PAGES.children.length; i++) {
zooms_raw_data = READER_PAGES.children[i].dataset.zooms
// ';' separates zooms data, ',' separates values
// We add the page number (adding 1 because of indexing)
zooms = zooms_raw_data.split(";").map(
zoom => [i + 1].concat(
zoom.split(',').map(
value => parseFloat(value)
)
)
)
PAGES_ZOOMS = PAGES_ZOOMS.concat(zooms)
}
}
}
2023-04-15 13:12:49 +02:00
function getFirstZoomOfPage(pageNumber){
for (var zoom_idx = 0; zoom_idx < PAGES_ZOOMS.length; zoom_idx++){
if (PAGES_ZOOMS[zoom_idx][0] == pageNumber) {
2023-04-15 13:12:49 +02:00
return zoom_idx
}
}
}
function getLastZoomOfPage(pageNumber){
let res = null
for (var zoom_idx = 0; zoom_idx < PAGES_ZOOMS.length; zoom_idx++){
if (PAGES_ZOOMS[zoom_idx][0] == pageNumber) {
2023-04-15 13:12:49 +02:00
res = zoom_idx
}
if (res != null && PAGES_ZOOMS[zoom_idx][0] != pageNumber) {
2023-04-15 13:12:49 +02:00
break
}
}
return res
}
2023-04-17 19:33:41 +02:00
function getZoomCountForPage(pageNumber) {
return PAGES_ZOOMS.filter(zoom => zoom[0] == pageNumber).length
2023-04-17 19:33:41 +02:00
}
function getCurrentZoomIndexForPage() {
previousZoomsCount = PAGES_ZOOMS.filter(zoom => zoom[0] < CURRENT_PAGE).length
2023-04-17 19:33:41 +02:00
return CURRENT_ZOOM - previousZoomsCount + 1
}
function getReadingProgressPercent() {
progressPerPage = 1 / getPagesCount()
if (IS_PAGE_MODE){
return 100 * progressPerPage * CURRENT_PAGE
}
progressPerZoom = progressPerPage / getZoomCountForPage(CURRENT_PAGE)
readingProgress = (CURRENT_PAGE - 1) * progressPerPage + getCurrentZoomIndexForPage() * progressPerZoom
return 100 * readingProgress
}
function updateProgressBar(){
PROGRESS_BAR.style.width = getReadingProgressPercent() + "%"
}
// Dimensions utilites
// -------------------
2023-04-15 13:12:49 +02:00
function getPagesCount() {
return READER_PAGES.childElementCount
}
2023-05-25 21:31:22 +02:00
function pageOriginalHeight(pageNumber) {
return READER_PAGES.children[pageNumber - 1].height
2023-04-15 13:12:49 +02:00
}
2023-05-25 21:31:22 +02:00
function pageOriginalWidth(pageNumber) {
return READER_PAGES.children[pageNumber - 1].width
2023-04-15 13:12:49 +02:00
}
function readerFrameRatio() {
2023-04-17 20:45:46 +02:00
return READER_CONTENT_FRAME.clientWidth / READER_CONTENT_FRAME.clientHeight
}
2023-05-25 21:31:22 +02:00
function pageRatio(pageNumber) {
return READER_PAGES.children[pageNumber - 1].width / READER_PAGES.children[pageNumber - 1].height
2023-04-15 13:12:49 +02:00
}
2023-05-25 21:31:22 +02:00
function pageMaxHeight(){
let max_height = 0
for (var i = 0; i < READER_PAGES.children.length; i++) {
if(READER_PAGES.children[i].height > max_height){
max_height = READER_PAGES.children[i].height
}
}
return max_height
}
2023-05-25 21:31:22 +02:00
function pageVerticalOffset(pageNumber) {
return ( pageMaxHeight() - pageOriginalHeight(pageNumber) ) / 2
}
2023-05-25 21:31:22 +02:00
function previousPagesWidth(pageNumber) {
// The width of all previous pages relative to the provided index
let totalWidth = 0
for (let idx = 0; idx < pageNumber - 1; idx++){
totalWidth = totalWidth + READER_PAGES.children[idx].width
}
return totalWidth
}
2023-04-15 13:12:49 +02:00
// =========
// ACTIONS
// =========
function initReader(){
loadZoomsFromImgTagsIfRequired()
2023-04-15 13:12:49 +02:00
moveReaderDisplayToZoom(0)
2023-04-15 18:14:57 +02:00
// Smoothly show pictures when they intersect with the viewport
let visibilityObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1
entry.target.style.visibility = "visible"
} else {
entry.target.style.opacity = 0
entry.target.style.visibility = "hidden"
}
});
2023-04-17 20:45:46 +02:00
}, { root: READER_CONTENT_FRAME, rootMargin: "-10px" });
2023-04-15 18:14:57 +02:00
for (var i = 0; i < READER_PAGES.children.length; i++) {
let img = READER_PAGES.children[i];
2023-04-15 18:14:57 +02:00
visibilityObserver.observe(img)
2023-04-22 14:31:26 +02:00
PROGRESS_BAR_PAGES.appendChild(document.createElement("div"))
}
READER_PAGES.style.display = "flex"
2023-04-15 18:14:57 +02:00
2023-04-15 17:38:46 +02:00
setTimeout(() => {
READER_PAGES.hidden = false
}, "300")
2023-04-16 22:24:59 +02:00
setTimeout(() => {
HELP_CONTROLS.style.opacity = null;
2023-04-17 20:32:13 +02:00
HELP_CONTROLS.style.transform = null;
2023-04-16 22:24:59 +02:00
}, DELAY_BEFORE_HIDDING_CONTROLS)
2023-04-15 13:12:49 +02:00
}
function moveReaderDisplayToArea(pageNumber, width, height, posx, posy){
// Keep original values for registering
o_width = width
o_height = height
o_posx = posx
o_posy = posy
// Apply global offsets before scales if we are displaying a zoom
// Pages display uses width & height = 0
if (width != 0 || height != 0){
width = width * globalZoomScale()
height = height * globalZoomScale()
posx = (posx + globalZoomOffsetX()) * globalZoomScale()
posy = (posy + globalZoomOffsetY()) * globalZoomScale()
}
// reduce width if offset sent us outside of page
if (posx < 0) {
width = width + posx
posx = 0
}
if ((posx + width) > pageOriginalWidth(pageNumber)) {
width = pageOriginalWidth(pageNumber) - posx
}
// reduce height if offset sent us outside of page
if (posy < 0) {
height = height + posy
posy = 0
}
if ((posy + height) > pageOriginalHeight(pageNumber)) {
height = pageOriginalHeight(pageNumber) - posy
}
2023-05-25 21:31:22 +02:00
// Align the top-left corner of the frame with the page
READER_PAGES.style.transform = "translate(-" + previousPagesWidth(pageNumber) + "px, -" + pageVerticalOffset(pageNumber) + "px )"
2023-05-22 00:19:12 +02:00
// Then move so the top-left point of the zoom match the frame top-left
READER_PAGES.style.transform = "translate(" + (- posx) + "px, " + (-posy) + "px )" + READER_PAGES.style.transform
// Then, scale so the zoom would fit the frame, and center the zoom
2023-04-15 13:12:49 +02:00
if (width == 0){
2023-05-25 21:31:22 +02:00
width = pageOriginalWidth(pageNumber)
2023-04-15 13:12:49 +02:00
}
if (height == 0){
2023-05-25 21:31:22 +02:00
height = pageOriginalHeight(pageNumber)
2023-04-15 13:12:49 +02:00
}
zoomRatio = width / height
2023-04-15 13:12:49 +02:00
if (readerFrameRatio() > zoomRatio) {
2023-05-22 00:19:12 +02:00
// Frame wider than zoom => scale so heights are the same, offset on x
var zoomToFrameScaleFactor = READER_CONTENT_FRAME.clientHeight / height
READER_PAGES.style.transform = "scale(" + zoomToFrameScaleFactor + ")" + READER_PAGES.style.transform
var scaledWidth = width * zoomToFrameScaleFactor
var offset = (READER_CONTENT_FRAME.clientWidth - scaledWidth) / 2
READER_PAGES.style.transform = "translateX(" + offset + "px)" + READER_PAGES.style.transform
updateFocusByWidth(scaledWidth)
2023-04-15 13:12:49 +02:00
} else {
2023-05-22 00:19:12 +02:00
// Frame narower than zoom => scale so left/right match, offset on y
var zoomToFrameScaleFactor = READER_CONTENT_FRAME.clientWidth / width
READER_PAGES.style.transform = "scale(" + zoomToFrameScaleFactor + ")" + READER_PAGES.style.transform
var scaledHeight = height * zoomToFrameScaleFactor
var offset = (READER_CONTENT_FRAME.clientHeight - scaledHeight) / 2
READER_PAGES.style.transform = "translateY(" + offset + "px)" + READER_PAGES.style.transform
updateFocusByHeight(scaledHeight)
2023-04-15 13:12:49 +02:00
}
// Use values before global offset / scale
2023-04-15 13:12:49 +02:00
CURRENT_PAGE = pageNumber
CURRENT_WIDTH = o_width
CURRENT_HEIGHT = o_height
CURRENT_X = o_posx
CURRENT_Y = o_posy
2023-04-15 13:12:49 +02:00
}
2023-04-17 20:45:46 +02:00
function refreshReaderDisplay() {
moveReaderDisplayToArea(CURRENT_PAGE, CURRENT_WIDTH, CURRENT_HEIGHT, CURRENT_X, CURRENT_Y)
}
2023-04-15 13:12:49 +02:00
function moveReaderDisplayToPage(pageNumber) {
moveReaderDisplayToArea(pageNumber, 0, 0, 0, 0)
}
function moveReaderDisplayToZoom(index) {
moveReaderDisplayToArea(PAGES_ZOOMS[index][0], PAGES_ZOOMS[index][1], PAGES_ZOOMS[index][2], PAGES_ZOOMS[index][3], PAGES_ZOOMS[index][4])
2023-04-15 13:12:49 +02:00
CURRENT_ZOOM = index
}
function updateFocusByWidth(width){
2023-05-19 15:42:39 +02:00
FOCUS_OVERLAY_WIDTH.style.width = (width / READER_CONTENT_FRAME.clientWidth * 100) + "%"
FOCUS_OVERLAY_HEIGHT.style.height = "100%"
2023-04-15 13:12:49 +02:00
}
function updateFocusByHeight(height){
2023-05-19 15:42:39 +02:00
FOCUS_OVERLAY_WIDTH.style.width = "100%"
FOCUS_OVERLAY_HEIGHT.style.height = (height / READER_CONTENT_FRAME.clientHeight * 100) + "%"
2023-04-15 13:12:49 +02:00
}
2023-04-17 20:59:32 +02:00
function toggleViewMode() {
if (IS_PAGE_MODE){
if (CURRENT_ZOOM != null){
moveReaderDisplayToZoom(CURRENT_ZOOM)
} else {
moveReaderDisplayToZoom(getFirstZoomOfPage(CURRENT_PAGE))
}
IS_PAGE_MODE = false
} else {
moveReaderDisplayToPage(CURRENT_PAGE)
IS_PAGE_MODE = true
}
2023-04-17 21:03:31 +02:00
updateProgressBar()
2023-04-17 20:59:32 +02:00
}
function moveReader(to_next) {
2023-04-15 13:12:49 +02:00
2023-04-17 20:59:32 +02:00
if (IS_PAGE_MODE){
if (to_next && CURRENT_PAGE < getPagesCount()) {
moveReaderDisplayToPage(CURRENT_PAGE + 1)
CURRENT_ZOOM = null
2023-04-15 13:12:49 +02:00
}
2023-04-17 20:59:32 +02:00
else if (!to_next && CURRENT_PAGE > 1) {
moveReaderDisplayToPage(CURRENT_PAGE - 1)
CURRENT_ZOOM = null
2023-04-15 13:12:49 +02:00
}
} else {
2023-04-17 20:59:32 +02:00
if (to_next && CURRENT_ZOOM < PAGES_ZOOMS.length - 1) {
2023-04-17 20:59:32 +02:00
moveReaderDisplayToZoom(CURRENT_ZOOM + 1)
}
2023-04-15 13:12:49 +02:00
2023-04-17 20:59:32 +02:00
else if (!to_next && CURRENT_ZOOM > 0) {
moveReaderDisplayToZoom(CURRENT_ZOOM - 1)
2023-04-15 13:12:49 +02:00
}
}
2023-04-17 19:33:41 +02:00
updateProgressBar()
2023-04-15 13:12:49 +02:00
}
2023-04-16 21:35:32 +02:00
// =============
// CALLBACKS
// =============
2023-04-17 20:59:32 +02:00
function handleKeyPress(key){
2023-04-15 13:12:49 +02:00
2023-04-16 21:35:32 +02:00
if (key == MOVE_NEXT) {
2023-04-17 20:59:32 +02:00
moveReader(true)
2023-04-15 13:12:49 +02:00
}
2023-04-16 21:35:32 +02:00
else if (key == MOVE_BACK) {
2023-04-17 20:59:32 +02:00
moveReader(false)
2023-04-15 13:12:49 +02:00
}
2023-04-16 17:43:37 +02:00
2023-04-16 21:35:32 +02:00
else if (key.toUpperCase() == TOGGLE_FULLSCREEN){
2023-04-16 17:43:37 +02:00
if (document.fullscreenElement == null){
READER_FRAME.requestFullscreen();
} else {
document.exitFullscreen();
}
}
2023-04-17 20:45:46 +02:00
else if (key.toUpperCase() == TOGGLE_PROGRESSBAR){
if (PROGRESS_BAR_CONTAINER.hidden == true) {
PROGRESS_BAR_CONTAINER.hidden = false
} else {
PROGRESS_BAR_CONTAINER.hidden = true
}
refreshReaderDisplay();
}
2023-04-17 20:59:32 +02:00
else if (key.toUpperCase() == TOGGLE_VIEW_MODE) {
toggleViewMode()
}
2023-04-16 17:43:37 +02:00
}
2023-04-15 13:12:49 +02:00
2023-05-26 23:32:11 +02:00
function handleMouseWhell(event){
// Only handle scroll event if the target is the nav controls
// to avoid preventing page scrolling.
// Do disable page scrolling when we do prev/next, though
if (! READER_FRAME.contains(event.target)){
return
}
event.preventDefault()
event.stopPropagation()
2023-04-16 17:43:37 +02:00
if (MOUSEWHELL_WAIT){
return
} else {
MOUSEWHELL_WAIT = true
setTimeout(() => {
MOUSEWHELL_WAIT = false
}, MOUSEWHELL_MIN_DELAY)
}
2023-05-26 23:32:11 +02:00
if (event.deltaY > 0) {
2023-04-16 17:43:37 +02:00
moveReader(true, false)
}
else {
moveReader(false, false)
}
2023-04-15 13:12:49 +02:00
}
// ======
// INIT
// ======
window.addEventListener("load", (event) => {
2023-05-26 19:19:38 +02:00
VERSION_DISPLAY.innerText = VERSION_DISPLAY.innerText.replace("Unknown version", MELPOMENE_VERSION)
2023-04-15 13:12:49 +02:00
initReader()
});
addEventListener("resize", (event) => {
2023-04-17 20:45:46 +02:00
refreshReaderDisplay();
2023-04-15 13:12:49 +02:00
});
addEventListener("keydown", (event) => {
2023-04-17 18:44:24 +02:00
handleKeyPress(event.key, event.shiftKey)
2023-04-16 17:43:37 +02:00
});
addEventListener("wheel", (event) => {
2023-05-26 23:32:11 +02:00
handleMouseWhell(event)
}, { passive:false });