620 lines
14 KiB
JavaScript
620 lines
14 KiB
JavaScript
/* Melpomene webcomic reader JS */
|
|
/* Version 1.0.0_RC1 */
|
|
/* CC-BY-NC-SA : https://git.aribaud.net/caribaud/melpomene/ */
|
|
|
|
"use strict";
|
|
|
|
// ============
|
|
// CONTROLS
|
|
// ============
|
|
|
|
const MOVE_NEXT = "ArrowRight";
|
|
const MOVE_BACK = "ArrowLeft";
|
|
const TOGGLE_FULLSCREEN = "F";
|
|
const TOGGLE_PROGRESSBAR = "P";
|
|
const TOGGLE_VIEW_MODE = "V";
|
|
|
|
|
|
// ========================
|
|
// NAVIGATION CONSTANTS
|
|
// ========================
|
|
|
|
const MOUSEWHELL_MIN_DELAY = 50;
|
|
const DELAY_BEFORE_HIDDING_CONTROLS = 4000;
|
|
|
|
// ====================
|
|
// STATES CONSTANTS
|
|
// ====================
|
|
|
|
const MELPOMENE_VERSION = "1.0.0_UNSTABLE";
|
|
|
|
const READER_FRAME = document.getElementById("melpomene");
|
|
const READER_CONTENT_FRAME = document.getElementById("melpomene-content-frame");
|
|
const READER_PAGES = document.getElementById("melpomene-pages");
|
|
const FOCUS_OVERLAY_HEIGHT = document.getElementById("melpomene-focus");
|
|
const FOCUS_OVERLAY_WIDTH = document.getElementById("melpomene-focus-col");
|
|
const HELP_CONTROLS = document.getElementById("melpomene-help-menu");
|
|
const PROGRESS_BAR_CONTAINER = document.getElementById("melpomene-progress-container");
|
|
const PROGRESS_BAR = document.getElementById("melpomene-progress-bar");
|
|
const PROGRESS_BAR_PAGES = document.getElementById("melpomene-progress-sections");
|
|
const VERSION_DISPLAY = document.getElementById("melpomene-version");
|
|
|
|
// ====================
|
|
// INDEX CONSTANTS
|
|
// ====================
|
|
|
|
const ZOOM_PAGE_INDEX = 0;
|
|
const ZOOM_WIDTH_INDEX = 1;
|
|
const ZOOM_HEIGHT_INDEX = 2;
|
|
const ZOOM_X_INDEX = 3;
|
|
const ZOOM_Y_INDEX = 4;
|
|
|
|
// ===========================
|
|
// STATES GLOBAL VARIABLES
|
|
// ===========================
|
|
|
|
let PAGES_ZOOMS;
|
|
// The variable ZOOMS can either be defined by another JS file or contructed at init
|
|
if (typeof PAGES_ZOOMS === "undefined")
|
|
{
|
|
PAGES_ZOOMS = null;
|
|
}
|
|
|
|
let CURRENT_ZOOM = 0;
|
|
let CURRENT_PAGE = 1;
|
|
let CURRENT_WIDTH = 0;
|
|
let CURRENT_HEIGHT = 0;
|
|
let CURRENT_X = 0;
|
|
let CURRENT_Y = 0;
|
|
|
|
let IS_PAGE_MODE = false;
|
|
let MOUSEWHELL_WAIT = false;
|
|
|
|
// =============
|
|
// UTILITIES
|
|
// =============
|
|
|
|
// Dimensions utilites
|
|
// -------------------
|
|
|
|
function getPagesCount()
|
|
{
|
|
return READER_PAGES.childElementCount;
|
|
}
|
|
|
|
function pageOriginalHeight(pageNumber)
|
|
{
|
|
return READER_PAGES.children[pageNumber - 1].naturalHeight;
|
|
}
|
|
|
|
function pageOriginalWidth(pageNumber)
|
|
{
|
|
return READER_PAGES.children[pageNumber - 1].naturalWidth;
|
|
}
|
|
|
|
function readerFrameRatio()
|
|
{
|
|
return READER_CONTENT_FRAME.clientWidth / READER_CONTENT_FRAME.clientHeight;
|
|
}
|
|
|
|
function pageMaxHeight()
|
|
{
|
|
let maxHeight = 0;
|
|
|
|
for (let pageIdx = 0; pageIdx < READER_PAGES.children.length; pageIdx += 1)
|
|
{
|
|
if (READER_PAGES.children[pageIdx].naturalHeight > maxHeight)
|
|
{
|
|
maxHeight = READER_PAGES.children[pageIdx].naturalHeight;
|
|
}
|
|
}
|
|
|
|
return maxHeight;
|
|
}
|
|
|
|
function pageVerticalOffset(pageNumber)
|
|
{
|
|
return (pageMaxHeight() - pageOriginalHeight(pageNumber)) / 2;
|
|
}
|
|
|
|
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 += 1)
|
|
{
|
|
totalWidth += READER_PAGES.children[idx].naturalWidth;
|
|
}
|
|
|
|
return totalWidth;
|
|
}
|
|
|
|
// 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 (let idx = 0; idx < READER_PAGES.children.length; idx += 1)
|
|
{
|
|
const zoomsRawData = READER_PAGES.children[idx].dataset.zooms;
|
|
|
|
// ";" separates zooms data, "," separates values
|
|
// We add the page number (adding 1 because of indexing)
|
|
const zooms = zoomsRawData.split(";").map(
|
|
(zoom) => [idx + 1].concat(
|
|
zoom.split(",").map(
|
|
(value) => parseFloat(value)
|
|
)
|
|
)
|
|
);
|
|
|
|
PAGES_ZOOMS = PAGES_ZOOMS.concat(zooms);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getFirstZoomOfPage(pageNumber)
|
|
{
|
|
for (let zoomIdx = 0; zoomIdx < PAGES_ZOOMS.length; zoomIdx += 1)
|
|
{
|
|
if (PAGES_ZOOMS[zoomIdx][0] === pageNumber)
|
|
{
|
|
return zoomIdx;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function getZoomCountForPage(pageNumber)
|
|
{
|
|
return PAGES_ZOOMS.filter((zoom) => zoom[0] === pageNumber).length;
|
|
}
|
|
|
|
function getCurrentZoomIndexForPage()
|
|
{
|
|
const previousZoomsCount = PAGES_ZOOMS.filter((zoom) => zoom[0] < CURRENT_PAGE).length;
|
|
return CURRENT_ZOOM - previousZoomsCount + 1;
|
|
}
|
|
|
|
function getReadingProgressPercent()
|
|
{
|
|
const progressPerPage = 1 / getPagesCount();
|
|
|
|
if (IS_PAGE_MODE)
|
|
{
|
|
return 100 * progressPerPage * CURRENT_PAGE;
|
|
}
|
|
|
|
const progressPerZoom = progressPerPage / getZoomCountForPage(CURRENT_PAGE);
|
|
|
|
|
|
let readingProgress = (CURRENT_PAGE - 1) * progressPerPage;
|
|
readingProgress += getCurrentZoomIndexForPage() * progressPerZoom;
|
|
|
|
return 100 * readingProgress;
|
|
}
|
|
|
|
function updateProgressBar()
|
|
{
|
|
PROGRESS_BAR.style.width = `${getReadingProgressPercent()}%`;
|
|
}
|
|
|
|
// =========
|
|
// ACTIONS
|
|
// =========
|
|
|
|
function updateFocusByWidth(width)
|
|
{
|
|
FOCUS_OVERLAY_WIDTH.style.width = `${width / READER_CONTENT_FRAME.clientWidth * 100}%`;
|
|
FOCUS_OVERLAY_HEIGHT.style.height = "100%";
|
|
}
|
|
|
|
function updateFocusByHeight(height)
|
|
{
|
|
FOCUS_OVERLAY_WIDTH.style.width = "100%";
|
|
FOCUS_OVERLAY_HEIGHT.style.height = `${height / READER_CONTENT_FRAME.clientHeight * 100}%`;
|
|
}
|
|
|
|
function moveReaderDisplayToArea(pageNumber, oWidth, oHeight, oPosx, oPosy)
|
|
{
|
|
// Keep original values for registering
|
|
let width = oWidth;
|
|
let height = oHeight;
|
|
let posx = oPosx;
|
|
let posy = oPosy;
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
// Align the top-left corner of the frame with the page
|
|
READER_PAGES.style.transform = `translate(-${previousPagesWidth(pageNumber)}px, -${pageVerticalOffset(pageNumber)}px)`;
|
|
|
|
// 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
|
|
if (width === 0)
|
|
{
|
|
width = pageOriginalWidth(pageNumber);
|
|
}
|
|
|
|
if (height === 0)
|
|
{
|
|
height = pageOriginalHeight(pageNumber);
|
|
}
|
|
|
|
const zoomRatio = width / height;
|
|
|
|
if (readerFrameRatio() > zoomRatio)
|
|
{
|
|
// Frame wider than zoom => scale so heights are the same, offset on x
|
|
const zoomToFrameScaleFactor = READER_CONTENT_FRAME.clientHeight / height;
|
|
|
|
READER_PAGES.style.transform = `scale(${zoomToFrameScaleFactor}) ${READER_PAGES.style.transform}`;
|
|
|
|
const scaledWidth = width * zoomToFrameScaleFactor;
|
|
const offset = (READER_CONTENT_FRAME.clientWidth - scaledWidth) / 2;
|
|
|
|
READER_PAGES.style.transform = `translateX(${offset}px) ${READER_PAGES.style.transform}`;
|
|
|
|
updateFocusByWidth(scaledWidth);
|
|
}
|
|
|
|
else
|
|
{
|
|
// Frame narower than zoom => scale so left/right match, offset on y
|
|
const zoomToFrameScaleFactor = READER_CONTENT_FRAME.clientWidth / width;
|
|
|
|
READER_PAGES.style.transform = `scale(${zoomToFrameScaleFactor}) ${READER_PAGES.style.transform}`;
|
|
|
|
const scaledHeight = height * zoomToFrameScaleFactor;
|
|
const offset = (READER_CONTENT_FRAME.clientHeight - scaledHeight) / 2;
|
|
|
|
READER_PAGES.style.transform = `translateY(${offset}px) ${READER_PAGES.style.transform}"`;
|
|
|
|
updateFocusByHeight(scaledHeight);
|
|
}
|
|
|
|
// Use values before global offset / scale
|
|
CURRENT_PAGE = pageNumber;
|
|
CURRENT_WIDTH = oWidth;
|
|
CURRENT_HEIGHT = oHeight;
|
|
CURRENT_X = oPosx;
|
|
CURRENT_Y = oPosy;
|
|
}
|
|
|
|
function refreshReaderDisplay()
|
|
{
|
|
moveReaderDisplayToArea(
|
|
CURRENT_PAGE,
|
|
CURRENT_WIDTH,
|
|
CURRENT_HEIGHT,
|
|
CURRENT_X,
|
|
CURRENT_Y
|
|
);
|
|
}
|
|
|
|
function moveReaderDisplayToPage(pageNumber)
|
|
{
|
|
moveReaderDisplayToArea(pageNumber, 0, 0, 0, 0);
|
|
}
|
|
|
|
function moveReaderDisplayToZoom(zoomIdx)
|
|
{
|
|
moveReaderDisplayToArea(
|
|
PAGES_ZOOMS[zoomIdx][ZOOM_PAGE_INDEX],
|
|
PAGES_ZOOMS[zoomIdx][ZOOM_WIDTH_INDEX],
|
|
PAGES_ZOOMS[zoomIdx][ZOOM_HEIGHT_INDEX],
|
|
PAGES_ZOOMS[zoomIdx][ZOOM_X_INDEX],
|
|
PAGES_ZOOMS[zoomIdx][ZOOM_Y_INDEX]
|
|
);
|
|
|
|
CURRENT_ZOOM = zoomIdx;
|
|
}
|
|
|
|
function toggleViewMode()
|
|
{
|
|
if (IS_PAGE_MODE)
|
|
{
|
|
if (CURRENT_ZOOM === null)
|
|
{
|
|
moveReaderDisplayToZoom(getFirstZoomOfPage(CURRENT_PAGE));
|
|
}
|
|
|
|
else
|
|
{
|
|
moveReaderDisplayToZoom(CURRENT_ZOOM);
|
|
}
|
|
|
|
IS_PAGE_MODE = false;
|
|
}
|
|
|
|
else
|
|
{
|
|
moveReaderDisplayToPage(CURRENT_PAGE);
|
|
IS_PAGE_MODE = true;
|
|
}
|
|
|
|
updateProgressBar();
|
|
}
|
|
|
|
function moveReader(toNext)
|
|
{
|
|
if (IS_PAGE_MODE)
|
|
{
|
|
if (toNext && CURRENT_PAGE < getPagesCount())
|
|
{
|
|
moveReaderDisplayToPage(CURRENT_PAGE + 1);
|
|
CURRENT_ZOOM = null;
|
|
}
|
|
|
|
else if (!toNext && CURRENT_PAGE > 1)
|
|
{
|
|
moveReaderDisplayToPage(CURRENT_PAGE - 1);
|
|
CURRENT_ZOOM = null;
|
|
}
|
|
|
|
updateProgressBar();
|
|
}
|
|
|
|
// Zoom mode
|
|
else
|
|
{
|
|
if (toNext && CURRENT_ZOOM < PAGES_ZOOMS.length - 1)
|
|
{
|
|
moveReaderDisplayToZoom(CURRENT_ZOOM + 1);
|
|
}
|
|
|
|
else if (!toNext && CURRENT_ZOOM > 0)
|
|
{
|
|
moveReaderDisplayToZoom(CURRENT_ZOOM - 1);
|
|
}
|
|
|
|
updateProgressBar();
|
|
}
|
|
}
|
|
|
|
function initReader()
|
|
{
|
|
VERSION_DISPLAY.innerText = VERSION_DISPLAY.innerText.replace("Unknown version", MELPOMENE_VERSION);
|
|
|
|
loadZoomsFromImgTagsIfRequired();
|
|
moveReaderDisplayToZoom(0);
|
|
|
|
// Smoothly show pictures when they intersect with the viewport
|
|
const 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";
|
|
}
|
|
});
|
|
},
|
|
{
|
|
root: READER_CONTENT_FRAME,
|
|
rootMargin: "-10px"
|
|
}
|
|
);
|
|
|
|
for (let pageIndex = 0; pageIndex < READER_PAGES.children.length; pageIndex += 1)
|
|
{
|
|
const img = READER_PAGES.children[pageIndex];
|
|
visibilityObserver.observe(img);
|
|
|
|
PROGRESS_BAR_PAGES.appendChild(document.createElement("div"));
|
|
}
|
|
|
|
READER_PAGES.style.display = "flex";
|
|
|
|
setTimeout(
|
|
() => { READER_PAGES.hidden = false; },
|
|
"300"
|
|
);
|
|
|
|
setTimeout(
|
|
() =>
|
|
{
|
|
HELP_CONTROLS.style.opacity = null;
|
|
HELP_CONTROLS.style.transform = null;
|
|
},
|
|
DELAY_BEFORE_HIDDING_CONTROLS
|
|
);
|
|
}
|
|
|
|
|
|
// =============
|
|
// CALLBACKS
|
|
// =============
|
|
|
|
function handleKeyPress(key)
|
|
{
|
|
if (key === MOVE_NEXT)
|
|
{
|
|
moveReader(true);
|
|
}
|
|
|
|
else if (key === MOVE_BACK)
|
|
{
|
|
moveReader(false);
|
|
}
|
|
|
|
else if (key.toUpperCase() === TOGGLE_FULLSCREEN)
|
|
{
|
|
if (document.fullscreenElement === null)
|
|
{
|
|
READER_FRAME.requestFullscreen();
|
|
}
|
|
|
|
else
|
|
{
|
|
document.exitFullscreen();
|
|
}
|
|
}
|
|
|
|
else if (key.toUpperCase() === TOGGLE_PROGRESSBAR)
|
|
{
|
|
if (PROGRESS_BAR_CONTAINER.hidden === true)
|
|
{
|
|
PROGRESS_BAR_CONTAINER.hidden = false;
|
|
}
|
|
|
|
else
|
|
{
|
|
PROGRESS_BAR_CONTAINER.hidden = true;
|
|
}
|
|
|
|
refreshReaderDisplay();
|
|
}
|
|
|
|
else if (key.toUpperCase() === TOGGLE_VIEW_MODE)
|
|
{
|
|
toggleViewMode();
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
if (MOUSEWHELL_WAIT)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MOUSEWHELL_WAIT = true;
|
|
setTimeout(
|
|
() => { MOUSEWHELL_WAIT = false; },
|
|
MOUSEWHELL_MIN_DELAY
|
|
);
|
|
|
|
if (event.deltaY > 0)
|
|
{
|
|
moveReader(true, false);
|
|
}
|
|
|
|
else
|
|
{
|
|
moveReader(false, false);
|
|
}
|
|
}
|
|
|
|
// ======
|
|
// INIT
|
|
// ======
|
|
|
|
window.addEventListener(
|
|
"load",
|
|
(_event) => { initReader(); }
|
|
);
|
|
|
|
addEventListener(
|
|
"resize",
|
|
(_event) => { refreshReaderDisplay(); }
|
|
);
|
|
|
|
addEventListener(
|
|
"keydown",
|
|
(event) =>
|
|
{
|
|
handleKeyPress(event.key, event.shiftKey);
|
|
}
|
|
);
|
|
|
|
addEventListener(
|
|
"wheel",
|
|
(event) => { handleMouseWhell(event); },
|
|
{ passive: false }
|
|
);
|