/* Melpomene webcomic reader JS */ /* Version 1.0.0 - UNSTABLE */ /* CC-BY-NC-SA : https://git.aribaud.net/caribaud/melpomene/ */ //============ // CONTROLS //============ MOVE_NEXT = "ArrowRight" MOVE_BACK = "ArrowLeft" TOGGLE_FULLSCREEN = "F" TOGGLE_PROGRESSBAR = "P" TOGGLE_VIEW_MODE = "V" //======================== // NAVIGATION CONSTANTS //======================== PAGE_TRANSITION_SPEED = "1.5s" MOUSEWHELL_MIN_DELAY = 50 DELAY_BEFORE_HIDDING_CONTROLS = 4000; //==================== // STATES CONSTANTS //==================== READER_FRAME = document.getElementById("reader-frame") READER_CONTENT_FRAME = document.getElementById("reader-content-frame") READER_PAGES = document.getElementById("reader-pages") FOCUS_OVERLAY_HEIGHT = document.getElementById("focus-overlay-height") FOCUS_OVERLAY_WIDTH = document.getElementById("focus-overlay-width") HELP_CONTROLS = document.getElementById("help-controls") PROGRESS_BAR_CONTAINER = document.getElementById("reader-progress-container") PROGRESS_BAR = document.getElementById("reader-progress-bar") PROGRESS_BAR_PAGES = document.getElementById("reader-progress-pages") //=========================== // 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 } CURRENT_ZOOM = 0 CURRENT_PAGE = 1 CURRENT_WIDTH = 0 CURRENT_HEIGHT = 0 CURRENT_X = 0 CURRENT_Y = 0 IS_PAGE_MODE = false 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 (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) } } } function getFirstZoomOfPage(pageNumber){ for (var zoom_idx = 0; zoom_idx < PAGES_ZOOMS.length; zoom_idx++){ if (PAGES_ZOOMS[zoom_idx][0] == pageNumber) { 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) { res = zoom_idx } if (res != null && PAGES_ZOOMS[zoom_idx][0] != pageNumber) { break } } return res } function getZoomCountForPage(pageNumber) { return PAGES_ZOOMS.filter(zoom => zoom[0] == pageNumber).length } function getCurrentZoomIndexForPage() { previousZoomsCount = PAGES_ZOOMS.filter(zoom => zoom[0] < CURRENT_PAGE).length 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 // ------------------- function getPagesCount() { return READER_PAGES.childElementCount } function pageOriginalHeight(pageNumber) { return READER_PAGES.children[pageNumber - 1].height } function pageOriginalWidth(pageNumber) { return READER_PAGES.children[pageNumber - 1].width } function readerFrameRatio() { return READER_CONTENT_FRAME.clientWidth / READER_CONTENT_FRAME.clientHeight } function pageRatio(pageNumber) { return READER_PAGES.children[pageNumber - 1].width / READER_PAGES.children[pageNumber - 1].height } 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 } 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++){ totalWidth = totalWidth + READER_PAGES.children[idx].width } return totalWidth } // ========= // ACTIONS // ========= function initReader(){ loadZoomsFromImgTagsIfRequired() moveReaderDisplayToZoom(0) // 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" } }); }, { root: READER_CONTENT_FRAME, rootMargin: "-10px" }); for (var i = 0; i < READER_PAGES.children.length; i++) { let 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, 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 } // 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) } zoomRatio = width / height if (readerFrameRatio() > zoomRatio) { // 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) } else { // 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) } // Use values before global offset / scale CURRENT_PAGE = pageNumber CURRENT_WIDTH = o_width CURRENT_HEIGHT = o_height CURRENT_X = o_posx CURRENT_Y = o_posy } 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(to_next) { if (IS_PAGE_MODE){ if (to_next && CURRENT_PAGE < getPagesCount()) { moveReaderDisplayToPage(CURRENT_PAGE + 1) CURRENT_ZOOM = null } else if (!to_next && CURRENT_PAGE > 1) { moveReaderDisplayToPage(CURRENT_PAGE - 1) CURRENT_ZOOM = null } } else { if (to_next && CURRENT_ZOOM < PAGES_ZOOMS.length - 1) { moveReaderDisplayToZoom(CURRENT_ZOOM + 1) } else if (!to_next && 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(deltaY){ if (MOUSEWHELL_WAIT){ return } else { MOUSEWHELL_WAIT = true setTimeout(() => { MOUSEWHELL_WAIT = false }, MOUSEWHELL_MIN_DELAY) } if (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.deltaY) });