Files
comicverse/.epub/example/OEBPS/scripts/ipub.js

249 lines
6.0 KiB
JavaScript
Raw Normal View History

"use strict";
class IPUBElement extends HTMLElement {
static observedAttributes = ["id"];
connectedCallback() {
this.ensureID();
}
attributeChangedCallback(_name, _oldValue, _newValue) {
this.ensureID();
}
ensureID() {
if (this.id) {
return;
}
// INFO: Hash algorithm by Joe Freeman & Cristian Sanchez at StackOverflow:
// https://stackoverflow.com/a/16348977
// https://stackoverflow.com/a/3426956
//
// Licensed under CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/)
let hash = 0;
this.innerHTML.split("").forEach((char) => {
hash = char.charCodeAt(0) + ((hash << 5) - hash);
});
let id = "";
for (let i = 0; i < 3; i++) {
id += ((hash >> (i * 8)) & 0xff).toString(16).padStart(2, "0");
}
this.id = id;
}
}
class IPUBBackground extends IPUBElement {
static elementName = "ipub-background";
static observedAttributes = ["sticky", "fade"].concat(
super.observedAttributes,
);
/**
* @private
*/
static #observer = (() => {
/** @type {Map<string, IPUBBackground>} */
const instancesOnScreen = new Map();
document.addEventListener("scroll", () => {
for (const [_, instance] of instancesOnScreen) {
const perc = getPercentageInView(
instance.querySelector("img") || instance,
);
instance.fade(perc);
}
});
return new IntersectionObserver((entries) =>
entries.forEach((e) => {
let instance = e.target.parentElement;
if (
instance.tagName.toLowerCase() !==
IPUBBackground.elementName.toLowerCase()
) {
instance = instance.parentElement;
}
if (
instance.tagName.toLowerCase() !==
IPUBBackground.elementName.toLowerCase()
) {
console.error(
"IPUBBackground: malformed <ipub-background> element",
e.target,
);
return;
}
if (e.intersectionRatio > 0 && instance.id) {
instancesOnScreen.set(instance.id, instance);
} else if (instance.id) {
instancesOnScreen.delete(instance.id);
}
}),
);
})();
attributeChangedCallback(name, _oldValue, _newValue) {
super.attributeChangedCallback();
if (name === "fade") {
const image = this.querySelector("img");
if (image) {
console.debug(`IPUBBackground: ipub-background#${this.id} to observer`);
if (this.hasAttribute("fade")) {
IPUBBackground.#observer.observe(image);
} else {
IPUBBackground.#observer.unobserve(image);
}
const perc = getPercentageInView(image);
if (perc > 0) {
this.fade(perc);
}
}
}
}
/**
* @param {number} perc
* @throws {Error}
* @returns {void | Promise<void>}
*/
fade(perc) {
console.debug(`IPUBBackground: ${this.id} is ${perc} on screen`);
if (!this.style.getPropertyValue("--ipub-fade")) {
this.style.setProperty("--ipub-fade", `${perc}%`);
return;
}
const currentPerc = this.style.getPropertyValue("--ipub-fade");
if (currentPerc === "100%" && perc >= 100) {
return;
}
if (perc % 10 === 0) {
this.style.setProperty("--ipub-fade", `${perc}%`);
}
}
}
class IPUBContent extends IPUBElement {
2025-10-17 15:35:18 -03:00
static elementName = "ipub-content";
}
class IPUBImage extends IPUBElement {
2025-10-17 15:36:39 -03:00
static elementName = "ipub-image";
}
globalThis.addEventListener("load", () => {
2025-10-17 15:35:18 -03:00
console.info("IPUB: STARTING DEFINITIONS");
2025-10-17 15:36:39 -03:00
[IPUBBackground, IPUBContent, IPUBImage].forEach((e) => {
2025-10-17 15:35:18 -03:00
console.info(`IPUB: Defining custom element <${e.elementName}>`);
globalThis.customElements.define(e.elementName, e);
});
2025-10-17 15:35:18 -03:00
console.info("IPUB: FINISHED DEFINITIONS");
/** @type {Map<string, Element>} */
const onScreenMap = new Map();
const observer = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.intersectionRatio > 0) {
// console.debug(
// `IntersectionObserver: adding element #${e.target.id} to onScreenMap`,
// );
onScreenMap.set(e.target.id, e.target);
} else {
// console.debug(
// `IntersectionObserver: removing element #${e.target.id} to onScreenMap`,
// );
onScreenMap.delete(e.target.id);
}
});
});
for (const element of document.querySelectorAll(
`[data-ipub-trigger="on-screen"]`,
)) {
observer.observe(element);
}
document.addEventListener("scroll", async () => {
for (const [id, element] of onScreenMap) {
const perc = getPercentageInView(element);
// console.debug(`Element #${id} is now ${perc}% on screen`);
const played = element.getAttribute("data-ipub-trigger-played") == "true";
if (perc >= 100 && !played) {
await playIpubElement(element);
element.setAttribute("data-ipub-trigger-played", "true");
}
}
});
});
/**
* @param {Element} element
*/
async function playIpubElement(element) {
switch (element.tagName) {
case "audio": {
/** @type {HTMLAudioElement} */
const audio = element;
// await audio.play();
break;
}
default:
break;
}
}
/**
* @param {Element} element
* @returns {number}
*/
function getPercentageInView(element) {
const viewTop = globalThis.pageYOffset;
const viewBottom = viewTop + globalThis.innerHeight;
const rect = element.getBoundingClientRect();
const elementTop = rect.y + viewTop;
const elementBottom = rect.y + rect.height + viewTop;
if (viewTop > elementBottom || viewBottom < elementTop) {
return 0;
}
if (
(viewTop < elementTop && viewBottom > elementBottom) ||
(elementTop < viewTop && elementBottom > viewBottom)
) {
return 100;
}
let inView = rect.height;
if (elementTop < viewTop) {
inView = rect.height - (globalThis.pageYOffset - elementTop);
}
if (elementBottom > viewBottom) {
inView = inView - (elementBottom - viewBottom);
}
return Math.round((inView / globalThis.innerHeight) * 100);
}