diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..9284e55 --- /dev/null +++ b/README.txt @@ -0,0 +1,5 @@ +Navigation : + - Right arrow : focus next panel + - Left arrow : focus previous panel + - Ctrl + Right arrow : focus next page + - Ctrl + Left arrow : focus current / previous page diff --git a/comic_reader.css b/comic_reader.css new file mode 100644 index 0000000..f8c8467 --- /dev/null +++ b/comic_reader.css @@ -0,0 +1,82 @@ +#reader-frame { + overflow: hidden; + position: relative; +} + +#reader-pages { + position: absolute; + display: flex; + flex-direction: row; + left: 0px; + right: 0px; + height: 100%; + transition: all 1.5s ease; +} + +#reader-pages > img { + display: inline-block; + opacity:0; + transition: all 1.5s ease; + flex-shrink: 0; +} + +#focus-overlay, #focus-overlay * { + transition: all 1.5s ease; +} + +#focus-overlay, #nav-controls { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + display: grid; + + grid-template-areas: + "left top right" + "left center right" + "left bottom right"; +} + +#focus-overlay { + grid-template-columns: 50% auto 50%; + grid-template-rows: 50% auto 50%; +} + +#nav-controls { + grid-template-columns: 1fr 15em 1fr; + grid-template-rows: auto; +} + +#nav-controls > div { + cursor: pointer; +} + +.top { + grid-area: top; +} + +.left { + grid-area: left; +} + +.right { + grid-area: right; +} + +.top { + grid-area: top; +} + +.bottom { + grid-area: bottom; +} + +#focus-overlay > *:not(.center) { + background-color: rgba(0, 0, 0, 0.85); +} + +#focus-overlay > .center { + box-shadow: inset 0px 0px 5px 5px rgba(0, 0, 0, 0.85); +} diff --git a/comic_reader.js b/comic_reader.js new file mode 100644 index 0000000..d7d7509 --- /dev/null +++ b/comic_reader.js @@ -0,0 +1,254 @@ + +PAGE_TRANSITION_SPEED = "1.5s" + +READER_FRAME = document.getElementById("reader-frame") + +READER_PAGES = document.getElementById("reader-pages") + +FOCUS_OVERLAY = document.getElementById("focus-overlay") + +CURRENT_ZOOM = 0 + +CURRENT_PAGE = 1 + +CURRENT_WIDTH = 0 +CURRENT_HEIGHT = 0 +CURRENT_X = 0 +CURRENT_Y = 0 + +IS_PAGE_MODE = false + +// =========== +// UTILITIES +// =========== + +function getFirstZoomOfPage(pageNumber){ + + for (var zoom_idx = 0; zoom_idx < zooms.length; zoom_idx++){ + if (zooms[zoom_idx][0] == pageNumber) { + return zoom_idx + } + } +} + +function getLastZoomOfPage(pageNumber){ + let res = null + + for (var zoom_idx = 0; zoom_idx < zooms.length; zoom_idx++){ + if (zooms[zoom_idx][0] == pageNumber) { + res = zoom_idx + } + + if (res != null && zooms[zoom_idx][0] != pageNumber) { + break + } + } + + return res +} + +function getReaderFrameRatio() { + return READER_FRAME.clientWidth / READER_FRAME.clientHeight +} + +function getPagesCount() { + return READER_PAGES.childElementCount +} + +function getPagesHeight() { + return READER_PAGES.dataset.pagesHeight +} + +function getPagesWidth() { + return READER_PAGES.dataset.pagesWidth +} + +function getPagesRatio() { + return READER_PAGES.dataset.pagesWidth / READER_PAGES.dataset.pagesHeight +} + + +// ========= +// ACTIONS +// ========= + +function initReader(){ + moveReaderDisplayToZoom(0) +} + + +function moveReaderDisplayToArea(pageNumber, width, height, posx, posy){ + + if (width == 0){ + width = getPagesWidth() + } + + if (height == 0){ + height = getPagesHeight() + } + + ratio = width / height + + base_scale_factor = getPagesHeight() / READER_FRAME.clientHeight // This is the factor at 100% height + + if (ratio < getReaderFrameRatio()) { + + scale_factor = READER_FRAME.clientHeight / height + + centering_padding = (READER_FRAME.clientWidth - width * scale_factor) / 2 + + pages_negative_padding = getPagesWidth() * scale_factor * (pageNumber - 1) + + READER_PAGES.style.height = base_scale_factor * scale_factor * 100 + "%" + + READER_PAGES.style.left = (- posx * scale_factor + centering_padding - pages_negative_padding) + "px" + + READER_PAGES.style.top = -posy * scale_factor + "px" + + updateFocusByWidth(width * scale_factor) + + } else { + + scale_factor = READER_FRAME.clientWidth / width + + pages_negative_padding = getPagesWidth() * scale_factor * (pageNumber - 1) + + scaled_height = height * scale_factor + + READER_PAGES.style.height = base_scale_factor * scale_factor * 100 + "%" + + READER_PAGES.style.left = (- posx * scale_factor - pages_negative_padding) + "px" + + READER_PAGES.style.top = -posy * scale_factor + (READER_FRAME.clientHeight - scaled_height)/2 + "px" + + updateFocusByHeight(scaled_height) + + } + + CURRENT_PAGE = pageNumber + CURRENT_WIDTH = width + CURRENT_HEIGHT = height + CURRENT_X = posx + CURRENT_Y = posy +} + + +function moveReaderDisplayToPage(pageNumber) { + moveReaderDisplayToArea(pageNumber, 0, 0, 0, 0) +} + +function moveReaderDisplayToZoom(index) { + + moveReaderDisplayToArea(zooms[index][0], zooms[index][1], zooms[index][2], zooms[index][3], zooms[index][4]) + + CURRENT_ZOOM = index +} + +function updateFocusByWidth(width){ + + side_width = (READER_FRAME.clientWidth - width) / 2 + + FOCUS_OVERLAY.style.gridTemplateColumns = side_width +"px auto " + side_width + "px" + FOCUS_OVERLAY.style.gridTemplateRows = "0px auto 0px"; +} + +function updateFocusByHeight(height){ + + side_width = (READER_FRAME.clientHeight - height) / 2 + + FOCUS_OVERLAY.style.gridTemplateRows = side_width +"px auto " + side_width + "px" + FOCUS_OVERLAY.style.gridTemplateColumns = "0px auto 0px"; +} + +function moveReader(to_next, move_page) { + + if (move_page){ + + if (IS_PAGE_MODE){ + if (to_next && CURRENT_PAGE < getPagesCount()) { + moveReaderDisplayToPage(CURRENT_PAGE + 1) + IS_PAGE_MODE = true + } + + else if (CURRENT_PAGE > 1) { + moveReaderDisplayToPage(CURRENT_PAGE - 1) + IS_PAGE_MODE = true + } + } + + else { + + if (to_next && CURRENT_PAGE < getPagesCount()) { + moveReaderDisplayToPage(CURRENT_PAGE + 1) + IS_PAGE_MODE = true + } + + else { + moveReaderDisplayToPage(CURRENT_PAGE) + IS_PAGE_MODE = true + } + + } + + } else { + + if (IS_PAGE_MODE){ + + if (to_next) { + moveReaderDisplayToZoom(getFirstZoomOfPage(CURRENT_PAGE)) + IS_PAGE_MODE = false + } + else { + if (CURRENT_PAGE == 1) { + moveReaderDisplayToZoom(0) + IS_PAGE_MODE = false + } else { + moveReaderDisplayToZoom(getLastZoomOfPage(CURRENT_PAGE - 1)) + IS_PAGE_MODE = false + } + } + + } else { + + if (to_next && CURRENT_ZOOM < zooms.length - 1) { + moveReaderDisplayToZoom(CURRENT_ZOOM + 1) + IS_PAGE_MODE = false + } + + else if (CURRENT_ZOOM > 0) { + moveReaderDisplayToZoom(CURRENT_ZOOM - 1) + IS_PAGE_MODE = false + } + + } + } + +} + +function handleKeyPress(key, has_ctrl){ + + if (key == "ArrowRight") { + moveReader(true, has_ctrl) + } + + else if (key == "ArrowLeft") { + moveReader(false, has_ctrl) + } + +} + +// ====== +// INIT +// ====== + +window.addEventListener("load", (event) => { + initReader() +}); + +addEventListener("resize", (event) => { + moveReaderDisplayToArea(CURRENT_PAGE, CURRENT_WIDTH, CURRENT_HEIGHT, CURRENT_X, CURRENT_Y) +}); + +addEventListener("keydown", (event) => { + handleKeyPress(event.key, event.ctrlKey) +}); \ No newline at end of file diff --git a/comic_reader_test.css b/comic_reader_test.css new file mode 100644 index 0000000..fa501ad --- /dev/null +++ b/comic_reader_test.css @@ -0,0 +1,20 @@ + + +html, body { + margin: 0px; + box-sizing: border-box; +} + +body { + padding: 1em; + height: 100vh; + background-color: whitesmoke; +} + +#reader-frame { + height: 100%; + width: 100%; + border: 2px solid; + box-sizing: border-box; + background-color: black; +} \ No newline at end of file diff --git a/comic_reader_test_high_res.html b/comic_reader_test_high_res.html new file mode 100644 index 0000000..df7c818 --- /dev/null +++ b/comic_reader_test_high_res.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + +
+
+ + + + + + + + + + + + +
+ +
+
+
+
+
+
+
+ + +
+ + + + + + + \ No newline at end of file diff --git a/zooms_generator.py b/zooms_generator.py new file mode 100644 index 0000000..341022b --- /dev/null +++ b/zooms_generator.py @@ -0,0 +1,45 @@ +import sys +import re +import xml.etree.ElementTree as ET + +from pathlib import Path + + +def extract_zooms(src_folder, dest_file): + folder = Path(src_folder) + + zooms = {} + + for svg_path in folder.glob("*.svg"): + print(svg_path.name) + match = re.search("P(\d+)", svg_path.name) + if match: + + page_idx = int(match.group(1)) + + zooms[page_idx] = [] + + tree = ET.parse(svg_path) + root = tree.getroot() + + for area in root.findall('.//{*}rect'): + zooms[page_idx].append([ + float(area.get("width")), + float(area.get("height")), + float(area.get("x")), + float(area.get("y")), + ]) + + with open(dest_file, "w") as data_file: + data_file.write("zooms = [\n") + for page_idx in sorted(zooms.keys()): + for zoom in zooms[page_idx]: + + if zoom[2] < 0 or zoom[3] < 0 : + print(f"WARNING: negative pos x / pos y in page {page_idx} for zoom {zoom} (is the rectangle flipped?)") + + data_file.write(f" {[page_idx] + zoom},\n") + data_file.write("]\n") + +if __name__ == "__main__": + extract_zooms(sys.argv[1], sys.argv[2])