melpomene/melpomene.js

617 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");
// ===========================
// STATES GLOBAL VARIABLES
// ===========================
var 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;
}
var CURRENT_ZOOM = 0;
var CURRENT_PAGE = 1;
var CURRENT_WIDTH = 0;
var CURRENT_HEIGHT = 0;
var CURRENT_X = 0;
var CURRENT_Y = 0;
var IS_PAGE_MODE = false;
var MOUSEWHELL_WAIT = false;
// =============
// UTILITIES
// =============
// 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;
}
}
}
function getLastZoomOfPage(pageNumber)
{
let res = null;
for (let zoomIdx = 0; zoomIdx < PAGES_ZOOMS.length; zoomIdx += 1)
{
if (PAGES_ZOOMS[zoomIdx][0] === pageNumber)
{
res = zoomIdx;
}
if (res !== null && PAGES_ZOOMS[zoomIdx][0] !== pageNumber)
{
break;
}
}
return res;
}
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);
const readingProgress = (CURRENT_PAGE - 1) * progressPerPage + getCurrentZoomIndexForPage() * progressPerZoom;
return 100 * readingProgress;
}
function updateProgressBar()
{
PROGRESS_BAR.style.width = getReadingProgressPercent() + "%";
}
// 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 pageRatio(pageNumber)
{
return READER_PAGES.children[pageNumber - 1].naturalWidth / READER_PAGES.children[pageNumber - 1].naturalHeight;
}
function pageMaxHeight()
{
let maxHeight = 0;
for (let i = 0; i < READER_PAGES.children.length; i += 1)
{
if(READER_PAGES.children[i].naturalHeight > maxHeight)
{
maxHeight = READER_PAGES.children[i].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;
}
// =========
// ACTIONS
// =========
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 i = 0; i < READER_PAGES.children.length; i += 1)
{
const img = READER_PAGES.children[i];
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
);
}
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(index)
{
moveReaderDisplayToArea(PAGES_ZOOMS[index][0], PAGES_ZOOMS[index][1], PAGES_ZOOMS[index][2], PAGES_ZOOMS[index][3], PAGES_ZOOMS[index][4]);
CURRENT_ZOOM = index;
}
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 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;
}
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;
}
}
else
{
if (toNext && CURRENT_ZOOM < PAGES_ZOOMS.length - 1)
{
moveReaderDisplayToZoom(CURRENT_ZOOM + 1);
}
else if (!toNext && CURRENT_ZOOM > 0)
{
moveReaderDisplayToZoom(CURRENT_ZOOM - 1);
}
}
updateProgressBar();
}
// =============
// 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;
}
else
{
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 }
);