6 Commits

3 changed files with 405 additions and 101 deletions

View File

@@ -1,7 +1,185 @@
"use strict"; "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 = ["nofade"].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);
}
}),
);
})();
connectedCallback() {
super.connectedCallback();
const image = this.querySelector("img");
if (!image) {
console.error("IPUBBackground: missing <img> element inside background");
return;
}
// INFO: We don't need to fade the first background
if (this.matches(":first-of-type") || this.hasAttribute("nofade")) {
IPUBBackground.#observer.unobserve(image);
return;
}
IPUBBackground.#observer.observe(image);
const perc = getPercentageInView(image);
if (perc > 0) {
this.fade(perc);
}
}
attributeChangedCallback(name, _oldValue, _newValue) {
super.attributeChangedCallback();
if (name !== "nofade") {
return;
}
const image = this.querySelector("img");
if (!image) {
console.error("IPUBBackground: missing <img> element inside background");
return;
}
// INFO: We don't need to fade the first background
if (this.matches(":first-of-type") || this.hasAttribute("nofade")) {
IPUBBackground.#observer.unobserve(image);
return;
}
IPUBBackground.#observer.observe(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 {
static elementName = "ipub-content";
}
class IPUBImage extends IPUBElement {
static elementName = "ipub-image";
}
class IPUBInteraction extends IPUBElement {
static elementName = "ipub-interaction";
}
globalThis.addEventListener("load", () => { globalThis.addEventListener("load", () => {
console.log("IPUB SCRIPT LOADED"); 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>} */ /** @type {Map<string, Element>} */
const onScreenMap = new Map(); const onScreenMap = new Map();
@@ -9,14 +187,14 @@ globalThis.addEventListener("load", () => {
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
entries.forEach((e) => { entries.forEach((e) => {
if (e.intersectionRatio > 0) { if (e.intersectionRatio > 0) {
console.debug( // console.debug(
`IntersectionObserver: adding element #${e.target.id} to onScreenMap`, // `IntersectionObserver: adding element #${e.target.id} to onScreenMap`,
); // );
onScreenMap.set(e.target.id, e.target); onScreenMap.set(e.target.id, e.target);
} else { } else {
console.debug( // console.debug(
`IntersectionObserver: removing element #${e.target.id} to onScreenMap`, // `IntersectionObserver: removing element #${e.target.id} to onScreenMap`,
); // );
onScreenMap.delete(e.target.id); onScreenMap.delete(e.target.id);
} }
}); });
@@ -31,7 +209,7 @@ globalThis.addEventListener("load", () => {
document.addEventListener("scroll", async () => { document.addEventListener("scroll", async () => {
for (const [id, element] of onScreenMap) { for (const [id, element] of onScreenMap) {
const perc = getPercentageInView(element); const perc = getPercentageInView(element);
console.debug(`Element #${id} is now ${perc}% on screen`); // console.debug(`Element #${id} is now ${perc}% on screen`);
const played = element.getAttribute("data-ipub-trigger-played") == "true"; const played = element.getAttribute("data-ipub-trigger-played") == "true";
@@ -52,7 +230,7 @@ async function playIpubElement(element) {
/** @type {HTMLAudioElement} */ /** @type {HTMLAudioElement} */
const audio = element; const audio = element;
await audio.play(); // await audio.play();
break; break;
} }

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<meta name="x-ipub-version" content="1.0" /> <meta name="x-ipub-version" content="0.1" />
<link href="../styles/stylesheet.css" rel="stylesheet" type="text/css" /> <link href="../styles/stylesheet.css" rel="stylesheet" type="text/css" />
<!-- <script type="module" src="../scripts/ipub.js" fetchpriority="high"></script> --> <!-- <script type="module" src="../scripts/ipub.js" fetchpriority="high"></script> -->
<script defer="true" src="../scripts/ipub.js" fetchpriority="high"> <script defer="true" src="../scripts/ipub.js" fetchpriority="high">
@@ -9,89 +9,76 @@
</script> </script>
</head> </head>
<body xmlns:epub="http://www.idpf.org/2007/ops" class="body"> <body xmlns:epub="http://www.idpf.org/2007/ops" class="body">
<main data-ipub-element="content"> <ipub-content style="--ipub-padding: 10%;">
<section data-ipub-element="page" id="page01"> <main>
<span data-ipub-element="image"> <ipub-background id="background0001">
<img src="../images/background0001.jpg" width="100" height="100" />
</ipub-background>
<ipub-image>
<img src="../images/image0001.png" /> <img src="../images/image0001.png" />
</span> <ipub-interaction style="--ipub-y:88.5%;--ipub-x:6%" circle="">
<!-- <a href="https://krita.org" referrerpolicy="same-origin"
This in the UI would be an "Point Interaction" or just "Interaction". The rel="external nofollow noopener noreferrer" target="_blank" />
editor can just place it on some point the page, and adjust it's size. </ipub-interaction>
<ipub-interaction style="--ipub-y:93.5%;--ipub-x:81.5%;--ipub-size:13%;">
<a href="https://guz.one" referrerpolicy="same-origin"
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 action is "open link", this action should have a warning to the reader, The element wound not have a "action" per say, but would have a "on screen" trigger,
to make sure they don't open malicious links. which in itself would have the action "play sound".
--> -->
<!-- <audio data-ipub-element="interaction" data-ipub-trigger="on-screen" controls="true"
The "rel" will have "nofollow", "noopener" and "noreferrer" when the link volume="0" style="--ipub-x: 20%; --ipub-y: 25%; --ipub-width: 50%; --ipub-height: 50%;"
is to a domain different from the project's one. id="int-audio0001">
--> <source src="../audios/audio0001.wav.disable" />
<a data-ipub-element="interaction" data-ipub-variant="point" </audio>
style="--ipub-x:6%;--ipub-y:88.5%;--ipub-width:10%;--ipub-radius:100%;--ipub-origin-offset-x:-50%;--ipub-origin-offset-y:-50%;--ipub-ratio:1/1;" </section>
id="int-httpsguzone" href="https://krita.org" target="_blank" referrerpolicy="same-origin" <ipub-background id="background0002">
rel="external nofollow noopener noreferrer"> <picture>
<!-- <img src="../images/background0002.jpg" />
</picture>
This would be generated if the editor doesn't specify a accessibility text, </ipub-background>
the in quotations text would be fetched from the site's title when the link is created <section data-ipub-element="page" id="page03">
if possible. <span data-ipub-element="image">
--> <img src="../images/image0003.png" />
Go to "Krita | Digital Paiting. Creative Freedom"</a> </span>
<!-- </section>
This in the UI would be an "Area Interaction". The editor would first place <section data-ipub-element="page" id="page04">
the first top-left point, and then the bottom-right one, to select an area/size <span data-ipub-element="image">
of the interaction. <img src="../images/image0004.png" />
</span>
The action is "go to page". </section>
--> <ipub-background id="background0003">
<a data-ipub-element="interaction" data-ipub-variant="area" <picture>
style="--ipub-x:76%;--ipub-y:90%;--ipub-width:11.5%;--ipub-height:8%;" id="int-httpsguzone" <img src="../images/background0003.jpg" />
href="section0001.xhtml#page03"> </picture>
<!-- </ipub-background>
This would be generated if the editor doesn't specify a accessibility text. <section data-ipub-element="page" id="page02">
The in quotations text would be the title of the page if it has one, otherwise <span data-ipub-element="image">
it's ID is used (RFC, we could just place the text as "Go to page", since the IDs. <img src="../images/image0002.png" />
may not be human-readable). </span>
--> </section>
Go to page "page03"</a> <section data-ipub-element="page" id="page04">
<!-- <span data-ipub-element="image">
TODO: Analyse if area and point interactions should be saved as the same type of element <img src="../images/image0003.png" />
and if the "data-ipub-variant" should be a thing. This pretty much depends on how much </span>
we want the editor to "guess" what controls to provide the user with. </section>
--> <section data-ipub-element="page" id="page04">
</section> <span data-ipub-element="image">
<section data-ipub-element="page" id="page02"> <img src="../images/image0004.png" />
<span data-ipub-element="image"> </span>
<img src="../images/image0002.png" /> </section>
</span> </main>
<!-- </ipub-content>
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>
<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>
<section data-ipub-element="page" id="page02">
<span data-ipub-element="image">
<img src="../images/image0002.png" />
</span>
</section>
</main>
</body> </body>
</html> </html>

View File

@@ -1,22 +1,164 @@
.body { .body {
-epub-writing-mode: horizontal-tb; -epub-writing-mode: horizontal-tb;
-webkit-writing-mode: horizontal-tb; -webkit-writing-mode: horizontal-tb;
/* direction: ltr; */ direction: ltr;
direction: rtl; /* direction: rtl; */
writing-mode: horizontal-tb; writing-mode: horizontal-tb;
position: relative;
margin: 0;
max-width: 100vw; max-width: 100vw;
} }
[data-ipub-element="page"] { ipub-content {
display: flex; --ipub-padding: 0%;
flex-direction: column; --ipub-gap: 0%;
--ipub-padding-x: var(--ipub-padding, 0%);
--ipub-padding-y: var(--ipub-padding, 0%);
--ipub-padding-t: var(--ipub-padding-y, 0%);
--ipub-padding-r: var(--ipub-padding-x, 0%);
--ipub-padding-b: var(--ipub-padding-y, 0%);
--ipub-padding-l: var(--ipub-padding-x, 0%);
& > article,
& > main,
& > section {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
& > *:first-child:not(ipub-background),
& > ipub-background:first-child + *:first-of-type {
margin-top: var(--ipub-padding-t);
}
& > *:not(ipub-background) {
margin-top: calc(var(--ipub-gap) / 2);
margin-right: var(--ipub-padding-r);
margin-left: var(--ipub-padding-l);
margin-bottom: calc(var(--ipub-gap) / 2);
}
& > *:last-child:not(ipub-background),
& > ipub-background:last-child + *:last-of-type {
margin-bottom: var(--ipub-padding-b);
}
}
}
ipub-background {
--ipub-width: 100vw;
--ipub-height: 100vh;
&[sticky] {
display: inline-block;
top: 0;
left: 0;
width: 0;
height: 0;
position: sticky;
align-self: start;
}
&[fade] img {
/* For testing */
/* background-image: linear-gradient( */
/* rgba(266, 0, 0, 1) 0%, */
/* rgba(0, 266, 0, 1) calc(100% + calc(var(--ipub-fade, 100%) * -1)), */
/* rgba(266, 0, 266, 1) 100% */
/* ) !important; */
--mask: linear-gradient(
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) calc(100% + calc(var(--ipub-fade, 100%) * -1))
) !important;
/* background-image: var(--mask); */
mask-image: var(--mask);
-webkit-mask-image: var(--mask);
}
& > picture {
display: block;
width: var(--ipub-width);
height: var(--ipub-height);
& > img {
object-fit: cover;
width: 100%;
height: 100%;
}
}
/* Support standalone img element */
& > img {
display: block;
object-fit: cover;
width: var(--ipub-width);
height: var(--ipub-height);
}
}
ipub-image {
position: relative; position: relative;
display: block;
flex-direction: column;
width: var(--ipub-width, unset);
height: var(--ipub-height, unset);
img {
max-width: 100%;
max-height: 100%;
}
}
ipub-interaction {
position: absolute;
--ipub-x: 0px;
--ipub-y: 0px;
--ipub-size: 10%;
--ipub-width: var(--ipub-size, unset);
--ipub-height: unset;
--ipub-ratio: 1/1;
left: var(--ipub-x);
top: var(--ipub-y);
width: var(--ipub-width);
height: var(--ipub-height);
aspect-ratio: var(--ipub-ratio, unset);
transform: translate(var(--ipub-offset-x, -50%), var(--ipub-offset-y, -50%));
& > * {
display: block;
width: 100%;
height: 100%;
border-radius: var(--ipub-radius, 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;
}
&[circle] {
--ipub-radius: 100%;
}
} }
[data-ipub-element="image"] { [data-ipub-element="image"] {
width: var(--ipub-width, unset); width: var(--ipub-width, unset);
height: 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"] { [data-ipub-element="interaction"] {
@@ -44,7 +186,4 @@ a[data-ipub-element="interaction"] {
font-size: 0px; font-size: 0px;
} }
img {
max-width: 100%;
max-height: 100%;
} }