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 f465564546
3 changed files with 196 additions and 8 deletions

View File

@@ -1,8 +1,111 @@
"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
* @implements {IPUBElementOnScreen}
*/
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 !== IPUBBackground.elementName) {
instance = instance.parentElement;
}
if (instance.tagName !== IPUBBackground.elementName) {
console.error(
"IPUBBackground: malformed <ipub-background> element",
e.target,
);
return;
}
if (e.intersectionRatio > 0) {
instancesOnScreen.set(instance.id, instance);
} else {
instancesOnScreen.delete(instance.id);
}
}),
);
})();
constructor() {
super();
}
connectedCallback() {
if (!this.id) {
console.warn(
`IPUB: ipub-background has not ID, assigning one based on innerHTML`,
this,
);
this.id = hashString(this.innerHTML);
}
console.debug(`IPUB: Added ipub-background#${this.id} to page`);
const image = this.querySelector("img");
if (this.hasAttribute("fade") && image) {
console.debug(`IPUB: Added ipub-background#${this.id} to observer`);
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(`${this.id} is ${perc} on screen`);
if (!this.style.getPropertyValue("--ipub-fade")) {
this.style.setProperty("--ipub-fade", `${perc}%`);
} else 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();

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,102 @@
.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[sticky],
[data-ipub-element="sticky-background"] {
top: 0;
left: 0;
width: 0;
height: 0;
position: sticky;
align-self: start;
}
ipub-background {
--ipub-width: 100vw;
--ipub-height: 100vh;
&[sticky] {
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 +124,4 @@ a[data-ipub-element="interaction"] {
font-size: 0px;
}
img {
max-width: 100%;
max-height: 100%;
}