6 Commits

3 changed files with 405 additions and 101 deletions

View File

@@ -1,7 +1,185 @@
"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", () => {
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>} */
const onScreenMap = new Map();
@@ -9,14 +187,14 @@ globalThis.addEventListener("load", () => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.intersectionRatio > 0) {
console.debug(
`IntersectionObserver: adding element #${e.target.id} to onScreenMap`,
);
// 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`,
);
// console.debug(
// `IntersectionObserver: removing element #${e.target.id} to onScreenMap`,
// );
onScreenMap.delete(e.target.id);
}
});
@@ -31,7 +209,7 @@ globalThis.addEventListener("load", () => {
document.addEventListener("scroll", async () => {
for (const [id, element] of onScreenMap) {
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";
@@ -52,7 +230,7 @@ async function playIpubElement(element) {
/** @type {HTMLAudioElement} */
const audio = element;
await audio.play();
// await audio.play();
break;
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<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" />
<!-- <script type="module" src="../scripts/ipub.js" fetchpriority="high"></script> -->
<script defer="true" src="../scripts/ipub.js" fetchpriority="high">
@@ -9,89 +9,76 @@
</script>
</head>
<body xmlns:epub="http://www.idpf.org/2007/ops" class="body">
<main data-ipub-element="content">
<section data-ipub-element="page" id="page01">
<span data-ipub-element="image">
<ipub-content style="--ipub-padding: 10%;">
<main>
<ipub-background id="background0001">
<img src="../images/background0001.jpg" width="100" height="100" />
</ipub-background>
<ipub-image>
<img src="../images/image0001.png" />
</span>
<!--
This in the UI would be an "Point Interaction" or just "Interaction". The
editor can just place it on some point the page, and adjust it's size.
<ipub-interaction style="--ipub-y:88.5%;--ipub-x:6%" circle="">
<a href="https://krita.org" referrerpolicy="same-origin"
rel="external nofollow noopener noreferrer" target="_blank" />
</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,
to make sure they don't open malicious links.
-->
<!--
The "rel" will have "nofollow", "noopener" and "noreferrer" when the link
is to a domain different from the project's one.
-->
<a data-ipub-element="interaction" data-ipub-variant="point"
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;"
id="int-httpsguzone" href="https://krita.org" target="_blank" referrerpolicy="same-origin"
rel="external nofollow noopener noreferrer">
<!--
This would be generated if the editor doesn't specify a accessibility text,
the in quotations text would be fetched from the site's title when the link is created
if possible.
-->
Go to "Krita | Digital Paiting. Creative Freedom"</a>
<!--
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 "go to page".
-->
<a data-ipub-element="interaction" data-ipub-variant="area"
style="--ipub-x:76%;--ipub-y:90%;--ipub-width:11.5%;--ipub-height:8%;" id="int-httpsguzone"
href="section0001.xhtml#page03">
<!--
This would be generated if the editor doesn't specify a accessibility text.
The in quotations text would be the title of the page if it has one, otherwise
it's ID is used (RFC, we could just place the text as "Go to page", since the IDs.
may not be human-readable).
-->
Go to page "page03"</a>
<!--
TODO: Analyse if area and point interactions should be saved as the same type of element
and if the "data-ipub-variant" should be a thing. This pretty much depends on how much
we want the editor to "guess" what controls to provide the user with.
-->
</section>
<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>
<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>
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-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-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>
</main>
</ipub-content>
</body>
</html>

View File

@@ -1,22 +1,164 @@
.body {
-epub-writing-mode: horizontal-tb;
-webkit-writing-mode: horizontal-tb;
/* direction: ltr; */
direction: rtl;
direction: ltr;
/* direction: rtl; */
writing-mode: horizontal-tb;
position: relative;
margin: 0;
max-width: 100vw;
}
[data-ipub-element="page"] {
display: flex;
flex-direction: column;
ipub-content {
--ipub-padding: 0%;
--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;
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"] {
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"] {
@@ -44,7 +186,4 @@ a[data-ipub-element="interaction"] {
font-size: 0px;
}
img {
max-width: 100%;
max-height: 100%;
}