feat(ipub): sticky background implementation via web components

This commit is contained in:
Guz
2025-10-16 15:02:34 -03:00
parent 60c9d3624a
commit 007de6b9f1
3 changed files with 216 additions and 16 deletions

View File

@@ -1,22 +1,146 @@
"use strict";
/**
* @param {string} str
* @returns {string}
*/
function hashString(str) {
return Array.from(str).reduce(
(s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0,
0,
);
}
class IPUBBackground extends HTMLElement {
static elementName = "ipub-background";
static observedAttributes = ["sticky", "fade", "id"];
/**
* @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) {
console.debug("IPUBBackground: attribute changed", name);
switch (name) {
case "id": {
if (!this.id) {
console.warn(
`IPUBBackground: no ID specified, assigning one based on innerHTML`,
this,
);
this.id = hashString(this.innerHTML);
}
break;
}
case "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);
}
}
break;
}
}
}
/**
* @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}%`);
}
}
}
globalThis.addEventListener("load", () => {
console.log("IPUB SCRIPT LOADED");
customElements.define(IPUBBackground.elementName, IPUBBackground);
/** @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`,
);
// 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 +155,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 +176,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">
@@ -10,6 +10,9 @@
</head>
<body xmlns:epub="http://www.idpf.org/2007/ops" class="body">
<main data-ipub-element="content">
<ipub-background id="background0001" sticky="">
<img src="../images/background0001.jpg" width="100" height="100" />
</ipub-background>
<section data-ipub-element="page" id="page01">
<span data-ipub-element="image">
<img src="../images/image0001.png" />
@@ -77,6 +80,11 @@
<source src="../audios/audio0001.wav.disable" />
</audio>
</section>
<ipub-background sticky="" fade="" 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" />

View File

@@ -1,22 +1,93 @@
.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"] {
[data-ipub-element="content"] {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
}
[data-ipub-element="content"] > [data-ipub-element="page"] {
margin: 5% 10%;
}
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);
}
}
position: relative;
}
[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 +115,4 @@ a[data-ipub-element="interaction"] {
font-size: 0px;
}
img {
max-width: 100%;
max-height: 100%;
}