Compare commits
6 Commits
main
...
b46ed80a00
| Author | SHA1 | Date | |
|---|---|---|---|
|
b46ed80a00
|
|||
|
3bede11393
|
|||
|
84e3a14677
|
|||
|
04537eadb8
|
|||
|
c3a8904f4d
|
|||
|
a0d90eedca
|
@@ -4,35 +4,20 @@ class IPUBElement extends HTMLElement {
|
||||
static observedAttributes = ["id"];
|
||||
|
||||
connectedCallback() {
|
||||
this.ensureID();
|
||||
this.#ensureID();
|
||||
}
|
||||
|
||||
attributeChangedCallback(_name, _oldValue, _newValue) {
|
||||
this.ensureID();
|
||||
this.#ensureID();
|
||||
}
|
||||
|
||||
ensureID() {
|
||||
if (this.id) {
|
||||
return;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#ensureID() {
|
||||
if (!this.id) {
|
||||
this.id = hashFromHTML(this);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,49 +28,84 @@ class IPUBBackground extends IPUBElement {
|
||||
/**
|
||||
* @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,
|
||||
static #instancesOnScreen = {
|
||||
/** @type {Map<IPUBBody, Set<IPUBBackground>>} */
|
||||
map: new Map(),
|
||||
/**
|
||||
* @param {IPUBBackground} background
|
||||
*/
|
||||
add(background) {
|
||||
const body = getAncestor(background, IPUBBody.elementName);
|
||||
if (!body) {
|
||||
console.error(
|
||||
`IPUBBackground: ${background.id} does not have a valid ipub-body ancestor`,
|
||||
);
|
||||
instance.fade(perc);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return new IntersectionObserver((entries) =>
|
||||
entries.forEach((e) => {
|
||||
let instance = e.target.parentElement;
|
||||
let set = this.map.get(body);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
body.addEventListener("scroll", () => {
|
||||
for (const instance of set.values()) {
|
||||
const perc = getPercentageInView(
|
||||
instance.querySelector("img") || instance,
|
||||
);
|
||||
instance.fade(perc);
|
||||
}
|
||||
});
|
||||
}
|
||||
set.add(background);
|
||||
this.map.set(body, set);
|
||||
},
|
||||
/**
|
||||
* @param {IPUBBackground} background
|
||||
*/
|
||||
remove(background) {
|
||||
const body = getAncestor(background, IPUBBody.elementName);
|
||||
if (!body) {
|
||||
console.error(
|
||||
`IPUBBackground: ${background.id} does not have a valid ipub-body ancestor`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
instance.tagName.toLowerCase() !==
|
||||
IPUBBackground.elementName.toLowerCase()
|
||||
) {
|
||||
instance = instance.parentElement;
|
||||
}
|
||||
const set = this.map.get(body);
|
||||
if (!set) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
instance.tagName.toLowerCase() !==
|
||||
IPUBBackground.elementName.toLowerCase()
|
||||
) {
|
||||
console.error(
|
||||
"IPUBBackground: malformed <ipub-background> element",
|
||||
e.target,
|
||||
);
|
||||
return;
|
||||
}
|
||||
set.delete(background);
|
||||
|
||||
if (e.intersectionRatio > 0 && instance.id) {
|
||||
instancesOnScreen.set(instance.id, instance);
|
||||
} else if (instance.id) {
|
||||
instancesOnScreen.delete(instance.id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
})();
|
||||
if (set.size === 0) {
|
||||
this.map.delete(body);
|
||||
}
|
||||
},
|
||||
};
|
||||
static addToScreen() {}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static #observer = new IntersectionObserver((entries) => {
|
||||
for (const { intersectionRatio, target: image } of entries) {
|
||||
const instance = getAncestor(image, IPUBBackground.elementName);
|
||||
|
||||
if (!instance) {
|
||||
console.error(
|
||||
"IPUBBackground: malformed <ipub-background> element",
|
||||
image,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (intersectionRatio > 0 && instance.id) {
|
||||
IPUBBackground.#instancesOnScreen.add(instance);
|
||||
} else if (instance.id) {
|
||||
IPUBBackground.#instancesOnScreen.remove(instance);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@@ -141,8 +161,6 @@ class IPUBBackground extends IPUBElement {
|
||||
* @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;
|
||||
@@ -159,8 +177,102 @@ class IPUBBackground extends IPUBElement {
|
||||
}
|
||||
}
|
||||
|
||||
class IPUBContent extends IPUBElement {
|
||||
static elementName = "ipub-content";
|
||||
class IPUBBody extends IPUBElement {
|
||||
static elementName = "ipub-body";
|
||||
|
||||
static defineContentElements() {
|
||||
for (const e of [
|
||||
IPUBBackground,
|
||||
IPUBImage,
|
||||
IPUBInteraction,
|
||||
]) {
|
||||
console.info(`IPUBBody: Defining custom element <${e.elementName}>`);
|
||||
globalThis.customElements.define(e.elementName, e);
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.setAttribute("aria-busy", "true");
|
||||
|
||||
console.log("IPUBBody: defining custom element <ipub-cover>");
|
||||
globalThis.customElements.define(IPUBCover.elementName, IPUBCover);
|
||||
|
||||
/** @type {IPUBCover} */
|
||||
const cover = this.querySelector("ipub-cover");
|
||||
if (!cover) {
|
||||
IPUBBody.defineContentElements();
|
||||
}
|
||||
cover.onclose = IPUBBody.defineContentElements;
|
||||
|
||||
this.setAttribute("aria-busy", "false");
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.addEventListener("load", () => {
|
||||
console.info("IPUB: Starting IPUB elements");
|
||||
|
||||
console.log("IPUB: Defining custom element <ipub-body>");
|
||||
globalThis.customElements.define(IPUBBody.elementName, IPUBBody);
|
||||
});
|
||||
|
||||
class IPUBCover extends IPUBElement {
|
||||
static elementName = "ipub-cover";
|
||||
|
||||
/**
|
||||
* @type {() => void} callback
|
||||
*/
|
||||
onclose = () => {};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
console.debug("IPUBCover: Setting up cover");
|
||||
this.setAttribute("aria-busy", "true");
|
||||
|
||||
const dialog = this.querySelector("dialog");
|
||||
dialog.show();
|
||||
|
||||
// HACK: Test if we can autoplay interactions, soundtracks, etc
|
||||
|
||||
/** @type {HTMLMediaElement | null} */
|
||||
const media =
|
||||
this.parentElement.querySelector("audio") ??
|
||||
this.parentElement.querySelector("video");
|
||||
|
||||
if (!media) {
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const pastVolume = media.volume;
|
||||
media.volume = 0.1; // don't let the user hear the test audio
|
||||
|
||||
media
|
||||
.play()
|
||||
.then(() => {
|
||||
media.pause();
|
||||
media.volume = pastVolume;
|
||||
media.currentTime = 0;
|
||||
|
||||
console.debug("IPUBCover: Can autoplay interactions, removing cover");
|
||||
this.onclose();
|
||||
})
|
||||
.catch(() => {
|
||||
console.debug(
|
||||
"IPUBCover: Cannot autoplay interactions, covering content",
|
||||
);
|
||||
|
||||
dialog.parentElement.addEventListener("click", () => {
|
||||
dialog.close();
|
||||
this.onclose();
|
||||
});
|
||||
|
||||
this.setAttribute("aria-busy", "false");
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class IPUBImage extends IPUBElement {
|
||||
@@ -171,72 +283,44 @@ class IPUBInteraction extends IPUBElement {
|
||||
static elementName = "ipub-interaction";
|
||||
}
|
||||
|
||||
globalThis.addEventListener("load", () => {
|
||||
console.info("IPUB: STARTING DEFINITIONS");
|
||||
|
||||
[IPUBBackground, IPUBContent, IPUBImage, IPUBInteraction].forEach((e) => {
|
||||
console.info(`IPUB: Defining custom element <${e.elementName}>`);
|
||||
globalThis.customElements.define(e.elementName, e);
|
||||
});
|
||||
|
||||
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 {Readonly<Element>} el
|
||||
* @param {number} [length=6]
|
||||
* @returns {string}
|
||||
*
|
||||
* @copyright This function contains code from a hash algorithm by
|
||||
* {@link https://stackoverflow.com/a/16348977|Joe Freeman} and
|
||||
* {@link https://stackoverflow.com/a/3426956|Cristian Sanchez} at
|
||||
* StackOverflow, licensed under {@link https://creativecommons.org/licenses/by-sa/3.0/|CC BY-SA 3.0}.
|
||||
*/
|
||||
function hashFromHTML(el, length = 6) {
|
||||
const hexLength = length / 2;
|
||||
if (hexLength % 2 !== 0) {
|
||||
hexLength + 1;
|
||||
}
|
||||
|
||||
let hash = 0;
|
||||
for (const char of el.innerHTML) {
|
||||
hash = char.charCodeAt(0) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
let hex = "";
|
||||
for (let i = 0; i < hexLength; i++) {
|
||||
hex += ((hash >> (i * 8)) & 0xff).toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
return hex.substring(0, length);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,8 +9,19 @@
|
||||
</script>
|
||||
</head>
|
||||
<body xmlns:epub="http://www.idpf.org/2007/ops" class="body">
|
||||
<ipub-content style="--ipub-padding: 10%;">
|
||||
<main>
|
||||
<ipub-body style="--ipub-padding: 10%;">
|
||||
<ipub-cover>
|
||||
<dialog>
|
||||
<header>
|
||||
<h1>Test comic</h1>
|
||||
<form method="dialog">
|
||||
<p>Click anywhere to
|
||||
<button>start Reading</button></p>
|
||||
</form>
|
||||
</header>
|
||||
</dialog>
|
||||
</ipub-cover>
|
||||
<main id="content">
|
||||
<ipub-background id="background0001">
|
||||
<img src="../images/background0001.jpg" width="100" height="100" />
|
||||
</ipub-background>
|
||||
@@ -25,60 +36,35 @@
|
||||
rel="external nofollow noopener noreferrer" target="_blank" />
|
||||
</ipub-interaction>
|
||||
</ipub-image>
|
||||
<section data-ipub-element="page" id="page02">
|
||||
<span data-ipub-element="image">
|
||||
<img src="../images/image0002.png" />
|
||||
</span>
|
||||
<!--
|
||||
This in the UI would be an "Area Interaction". The editor would first place
|
||||
the first top-left point, and then the bottom-right one, to select an area/size
|
||||
of the interaction.
|
||||
|
||||
The element wound not have a "action" per say, but would have a "on screen" trigger,
|
||||
which in itself would have the action "play sound".
|
||||
-->
|
||||
<audio data-ipub-element="interaction" data-ipub-trigger="on-screen" controls="true"
|
||||
volume="0" style="--ipub-x: 20%; --ipub-y: 25%; --ipub-width: 50%; --ipub-height: 50%;"
|
||||
id="int-audio0001">
|
||||
<source src="../audios/audio0001.wav.disable" />
|
||||
</audio>
|
||||
</section>
|
||||
<ipub-image>
|
||||
<img src="../images/image0002.png" />
|
||||
</ipub-image>
|
||||
<ipub-background id="background0002">
|
||||
<picture>
|
||||
<img src="../images/background0002.jpg" />
|
||||
</picture>
|
||||
</ipub-background>
|
||||
<section data-ipub-element="page" id="page03">
|
||||
<span data-ipub-element="image">
|
||||
<img src="../images/image0003.png" />
|
||||
</span>
|
||||
</section>
|
||||
<section data-ipub-element="page" id="page04">
|
||||
<span data-ipub-element="image">
|
||||
<img src="../images/image0004.png" />
|
||||
</span>
|
||||
</section>
|
||||
<ipub-image>
|
||||
<img src="../images/image0003.png" />
|
||||
</ipub-image>
|
||||
<ipub-image>
|
||||
<img src="../images/image0004.png" />
|
||||
</ipub-image>
|
||||
<ipub-background id="background0003">
|
||||
<picture>
|
||||
<img src="../images/background0003.jpg" />
|
||||
</picture>
|
||||
</ipub-background>
|
||||
<section data-ipub-element="page" id="page02">
|
||||
<span data-ipub-element="image">
|
||||
<img src="../images/image0002.png" />
|
||||
</span>
|
||||
</section>
|
||||
<section data-ipub-element="page" id="page04">
|
||||
<span data-ipub-element="image">
|
||||
<img src="../images/image0003.png" />
|
||||
</span>
|
||||
</section>
|
||||
<section data-ipub-element="page" id="page04">
|
||||
<span data-ipub-element="image">
|
||||
<img src="../images/image0004.png" />
|
||||
</span>
|
||||
</section>
|
||||
<ipub-image>
|
||||
<img src="../images/image0002.png" />
|
||||
</ipub-image>
|
||||
<ipub-image>
|
||||
<img src="../images/image0003.png" />
|
||||
</ipub-image>
|
||||
<ipub-image>
|
||||
<img src="../images/image0004.png" />
|
||||
</ipub-image>
|
||||
</main>
|
||||
</ipub-content>
|
||||
</ipub-body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,9 +8,45 @@
|
||||
margin: 0;
|
||||
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
overflow: clip;
|
||||
display: flex;
|
||||
|
||||
--z-cover: 9;
|
||||
ipub-cover > dialog[open] {
|
||||
--ipub-accent-color: #fff;
|
||||
z-index: var(--z-cover);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
backdrop-filter: blur(1rem);
|
||||
background-image: linear-gradient(
|
||||
rgba(from var(--ipub-accent-color) r g b / 0) 0%,
|
||||
rgba(from var(--ipub-accent-color) r g b / 0.5)
|
||||
calc(100% + calc(var(--ipub-fade, 50%) * -1))
|
||||
);
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ipub-content {
|
||||
ipub-body {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
overflow: scroll;
|
||||
&:has(ipub-cover > dialog[open]) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
--ipub-padding: 0%;
|
||||
--ipub-gap: 0%;
|
||||
--ipub-padding-x: var(--ipub-padding, 0%);
|
||||
@@ -146,44 +182,3 @@ ipub-interaction {
|
||||
--ipub-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-ipub-element="image"] {
|
||||
width: var(--ipub-width, unset);
|
||||
height: var(--ipub-height, unset);
|
||||
background-image: var(--ipub-image, unset);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
display: block;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-ipub-element="interaction"] {
|
||||
position: absolute;
|
||||
left: var(--ipub-x, 0%);
|
||||
top: var(--ipub-y, 0%);
|
||||
border-radius: var(--ipub-radius, unset);
|
||||
width: var(--ipub-width, unset);
|
||||
height: var(--ipub-height, unset);
|
||||
transform: translate(
|
||||
var(--ipub-origin-offset-x, 0%),
|
||||
var(--ipub-origin-offset-y, 0%)
|
||||
);
|
||||
aspect-ratio: var(--ipub-ratio, unset);
|
||||
/*
|
||||
* The opacity would be, by default, zero. Here it is 0.3 for easier debugging and
|
||||
* showing of the example ebook
|
||||
*/
|
||||
background-color: red;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
a[data-ipub-element="interaction"] {
|
||||
/* The text inside the interaction anchor are for accessibility purposes */
|
||||
font-size: 0px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user