16 Commits

Author SHA1 Message Date
e1734a5310 feat(ipub): ipub-track element to position elements outside of content elements 2025-11-04 18:41:49 -03:00
f76be67247 feat(ipub): debug attribute and overlay 2025-11-04 18:40:00 -03:00
c90bff53a3 chore(ipub): move IPUBBody and IPUBCover definitions to top 2025-11-04 18:38:07 -03:00
7d1a21430c feat(ipub): soundtrack support with fade transition 2025-11-04 10:54:36 -03:00
a3c2efd5b0 feat(ipub): make ipub-background containerized into ipub-body 2025-11-04 10:54:36 -03:00
77631f2a6c feat(ipub): ipub-cover, forcing user to interact to enable autoplay 2025-11-03 15:24:21 -03:00
185001308d feat(ipub): use ipub-body to handle custom elements defining 2025-11-03 15:24:20 -03:00
bbb9ad0e35 feat(ipub): ipub-body element
this replaces the ipub-content element, and makes ipub publications
limited/containerized into one element.
2025-11-03 15:24:20 -03:00
6a7abdea6f feat(ipub): make #ensureID private and remove old code 2025-10-30 09:42:34 -03:00
b52e7f165f feat(ipub): change all section images to ipub-images 2025-10-30 09:42:34 -03:00
d775301567 feat(ipub): make sticky and fade backgrounds the default 2025-10-17 19:23:41 -03:00
ba7ca52ed2 feat(ipub): ipub-interaction element 2025-10-17 18:05:12 -03:00
185ca863fe feat(ipub): base elements on IPUBElement
This makes the random hex ID logic shared between all elements
2025-10-17 18:05:11 -03:00
11456db9c4 feat(ipub): ipub-image element 2025-10-17 18:05:11 -03:00
d556b0eefe feat(ipub): ipub-content element 2025-10-17 18:05:11 -03:00
007de6b9f1 feat(ipub): sticky background implementation via web components 2025-10-17 18:01:53 -03:00
3 changed files with 1167 additions and 164 deletions

View File

@@ -1,64 +1,719 @@
"use strict";
globalThis.addEventListener("load", () => {
console.log("IPUB SCRIPT LOADED");
class IPUBElement extends HTMLElement {
/**
* @protected
* @type {Readonly<string[]>}
*/
static observedAttributes = ["id", "debug"];
/** @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);
connectedCallback() {
this.#ensureID();
}
document.addEventListener("scroll", async () => {
for (const [id, element] of onScreenMap) {
const perc = getPercentageInView(element);
console.debug(`Element #${id} is now ${perc}% on screen`);
attributeChangedCallback(name, _oldValue, _newValue) {
switch (name) {
case "id":
this.#ensureID();
break;
case "debug":
if (this.hasAttribute("debug")) {
this.setDebug(true);
} else {
this.setDebug(false);
}
break;
}
}
const played = element.getAttribute("data-ipub-trigger-played") == "true";
/**
* @private
*/
#ensureID() {
if (!this.id) {
this.id = hashFromHTML(this);
}
}
if (perc >= 100 && !played) {
await playIpubElement(element);
element.setAttribute("data-ipub-trigger-played", "true");
/**
* @returns {boolean}
*/
getDebug() {
return this.hasAttribute("debug");
}
/**
* @param {boolean} state
*/
setDebug(state) {
if (state) {
if (!this.hasAttribute("debug")) {
this.setAttribute("debug", "true");
}
if (!this.style.getPropertyValue(IPUBElement.#PROPERTY_DEBUG_COLOR)) {
this.style.setProperty(
IPUBElement.#PROPERTY_DEBUG_COLOR,
`#${hashFromHTML(this)}`,
);
}
} else {
if (!state && this.hasAttribute("debug")) {
this.removeAttribute("debug");
}
if (
!state &&
this.style.getPropertyValue(IPUBElement.#PROPERTY_DEBUG_COLOR)
) {
this.style.removeProperty(IPUBElement.#PROPERTY_DEBUG_COLOR);
}
}
});
getAllDescendants(this)
.filter((el) => el.tagName.startsWith("ipub-"))
.forEach((el) => {
el.setDebug?.(state);
});
}
/**
* @private
* @type {Readonly<string>}
*/
static #PROPERTY_DEBUG_COLOR = "--ipub-debug-color";
}
globalThis.addEventListener("load", () => {
console.info("IPUB: Starting IPUB elements");
console.log("IPUB: Defining custom element <ipub-body>");
globalThis.customElements.define(IPUBBody.elementName, IPUBBody);
});
/**
* @param {Element} element
*/
async function playIpubElement(element) {
switch (element.tagName) {
case "audio": {
/** @type {HTMLAudioElement} */
const audio = element;
class IPUBBody extends IPUBElement {
static elementName = `ipub-body`;
await audio.play();
connectedCallback() {
super.connectedCallback();
break;
this.setAttribute("aria-busy", "true");
// TODO?: Move IPUBCover's "can-play" logic to here
console.log("IPUBBody: Defining custom element <ipub-cover>");
globalThis.customElements.define(IPUBCover.elementName, IPUBCover);
/** @type {IPUBCover} */
const cover = this.querySelector("ipub-cover");
if (!cover) {
// TODO: automatically create IPUBCover element if it doesn't exists
console.error("IPUBBody: Document doesn't has <ipub-cover> element");
this.#initElements();
this.setAttribute("aria-busy", "false");
return;
}
default:
break;
cover.onclose = () => {
this.#initElements();
};
cover.cover();
this.setAttribute("aria-busy", "false");
}
/**
* @private
*/
#initElements() {
for (const e of [
IPUBAudio,
IPUBBackground,
IPUBImage,
IPUBInteraction,
IPUBSoundtrack,
IPUBTrack,
IPUBTrackItem,
IPUBTrackItemPosition,
]) {
console.info(`IPUBBody: Defining custom element <${e.elementName}>`);
globalThis.customElements.define(e.elementName, e);
}
if (this.getDebug()) {
// HACK: Re-trigger IPUBElement debugging logic
console.debug("IPUBBody: triggeing debugger");
this.setAttribute("debug", "true");
}
}
}
class IPUBCover extends IPUBElement {
static elementName = `ipub-cover`;
/**
* @type {() => void} callback
*/
onclose = () => {};
connectedCallback() {
super.connectedCallback();
}
cover() {
console.debug("IPUBCover: Setting up cover");
this.setAttribute("aria-busy", "true");
const dialog = this.querySelector("dialog");
// HACK: Test if we can autoplay interactions, soundtracks, etc
/** @type {HTMLMediaElement | null} */
const media =
this.parentElement.querySelector("audio") ??
this.parentElement.querySelector("video");
if (!media) {
console.log("IPUBCover: no media element found, removing cover");
dialog.close();
this.onclose();
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");
dialog.close();
this.onclose();
})
.catch(() => {
console.debug(
"IPUBCover: Cannot autoplay interactions, covering content",
);
dialog.show();
dialog.parentElement.addEventListener("click", () => {
dialog.close();
this.onclose();
});
this.setAttribute("aria-busy", "false");
return;
});
}
}
class IPUBTrack extends IPUBElement {
static elementName = "ipub-track";
}
class IPUBTrackItem extends IPUBElement {
static elementName = `ipub-track-item`;
}
class IPUBTrackItemPosition extends IPUBElement {
static elementName = `ipub-track-item-position`;
}
class IPUBAudio extends IPUBElement {
static elementName = "ipub-audio";
/**
* @param {boolean} [forced=false]
* @throws {Error}
* @returns {Promise<void>}
*/
play(forced = false) {
if (!this.#audioElement.readyState > HTMLMediaElement.HAVE_CURRENT_DATA) {
throw new Error("IPUBAudio: audio is not ready");
}
if (forced) {
this.setAttribute("forced", "true");
}
return this.#audioElement.play();
}
/**
* @param {boolean} [forced=false]
*/
pause(forced = false) {
if (forced) {
this.setAttribute("forced", "true");
}
this.#audioElement.pause();
}
/**
* @param {boolean} state
*/
setLoop(state) {
this.#audioElement.loop = state;
}
/**
* @returns {boolean}
*/
getLoop() {
return this.#audioElement.loop;
}
connectedCallback() {
super.connectedCallback();
const audio = this.querySelector("audio");
if (!audio) {
console.error("IPUBAudio: Missing child <audio> element");
return;
}
this.#audioElement = audio;
}
/**
* @private
* @type {HTMLAudioElement}
*/
#audioElement;
/**
* @param {number} volume
* @param {Object} options
* @param {number} [options.fadetime=0]
* @param {() => void} [options.onfinish=() => {}]
*/
setVolume(volume, { fadetimeMS = 0, onFinishFade = () => {} } = {}) {
if (fadetimeMS === 0) {
this.#audioElement.volume = volume / 100;
return;
}
if (this.#isFading) {
return;
}
this.#onFinishFade = onFinishFade;
const diff = volume - this.#audioElement.volume * 100;
const ticks = diff < 0 ? Math.abs(diff) : diff;
let tick = 0;
const interval = fadetimeMS / ticks;
this.#isFading = true;
this.#fadeTask = setInterval(() => {
tick++;
const cancel = () => {
this.#isFading = false;
if (onFinishFade) {
onFinishFade();
}
clearInterval(this.#fadeTask);
this.#onFinishFade = null;
};
if (!this.#audioElement) {
cancel();
console.error("IPUBAudio: Missing child <audio> element");
return;
}
if (ticks < tick) {
cancel();
return;
}
if (volume === this.getVolume()) {
cancel();
return;
}
if (diff === 0) {
cancel();
return;
}
const nvol =
(diff > 0
? Math.ceil(this.#audioElement.volume * 100 + 1)
: Math.floor(this.#audioElement.volume * 100 - 1)) / 100;
if (nvol > 1 || nvol < 0) {
cancel();
return;
}
this.#audioElement.volume = nvol;
}, interval);
}
/**
* @returns {number}
*/
getVolume() {
return Math.floor((this.#audioElement?.volume ?? 0) * 100);
}
#isFading = false;
#fadeTask = 0;
/** @type {() => void | null} */
#onFinishFade = null;
}
class IPUBBackground extends IPUBElement {
static elementName = "ipub-background";
static observedAttributes = ["nofade"].concat(super.observedAttributes);
/**
* @private
*/
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`,
);
return;
}
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;
}
const set = this.map.get(body);
if (!set) {
return;
}
set.delete(background);
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();
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) {
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 IPUBImage extends IPUBElement {
static elementName = "ipub-image";
}
class IPUBInteraction extends IPUBElement {
static elementName = "ipub-interaction";
}
class IPUBSoundtrack extends IPUBElement {
static elementName = "ipub-soundtrack";
// TODO: Toggle automatic soundtrack playing
/**
* @private
*/
static #player = setInterval(() => {
const last = Array.from(this.#onScreenStack).pop();
if (!last) {
// TODO: Fallback to previous soundtrack if there's no audio
return;
}
// TODO: Get siblings based by group OR parent
/** @type {NodeListOf<IPUBSoundtrack> | undefined} */
const siblings = last.parentElement?.querySelectorAll(
IPUBSoundtrack.elementName,
);
try {
if (siblings) {
siblings.forEach((el) => {
if (el !== last) {
el.fadeOut();
}
});
}
last.fadeIn();
} catch (e) {
// TODO: Fallback to previous soundtrack on error
console.error(
`IPUBSoundtrack: error while trying to play audio, error: ${e}`,
{
error: e,
audio: audio,
},
);
}
}, 1000);
/**
* @private
*/
static #observer = (() => {
return new IntersectionObserver((entries) => {
for (const { intersectionRatio, target, time } of entries) {
/** @type {IPUBSoundtrack} */
const soundtrack = target;
if (intersectionRatio > 0) {
console.debug(`${soundtrack.id} is on screen at ${time}`, soundtrack);
this.#onScreenStack.add(soundtrack);
} else {
console.debug(
`${soundtrack.id} is not on screen ${time}`,
soundtrack,
);
this.#onScreenStack.delete(soundtrack);
}
}
});
})();
/**
* @private
* @type {Set<IPUBSoundtrack>}
*/
static #onScreenStack = new Set();
/**
* @throws {Error}
*/
fadeIn() {
/** @type {IPUBAudio | undefined} */
const audio = this.querySelector(IPUBAudio.elementName);
if (!audio) {
throw new Error("IPUBSoundtrack.fadeIn: missing audio element");
}
// TODO: Global volume settings
audio.play();
audio.setVolume(10, { fadetimeMS: IPUBSoundtrack.FADE_TIME_MS });
}
/**
* @throws {Error}
*/
fadeOut() {
/** @type {IPUBAudio | undefined} */
const audio = this.querySelector(IPUBAudio.elementName);
if (!audio) {
throw new Error("IPUBSoundtrack.fadeIn: missing audio element");
}
audio.setVolume(0, {
fadetimeMS: IPUBSoundtrack.FADE_TIME_MS,
onFinishFade: () => {
audio.pause();
},
});
}
/** @type {Readonly<number>} */
static FADE_TIME_MS = 1000 * 3;
connectedCallback() {
super.connectedCallback();
/** @type {IPUBAudio | undefined} */
const audio = this.querySelector(IPUBAudio.elementName);
if (audio) {
audio.setVolume(0);
} else {
console.error("IPUBSoundtrack: missing audio element");
return;
}
IPUBSoundtrack.#observer.observe(this);
}
// TODO(guz013): Handle if element is moved, it's group should be updated
}
/**
* @param {HTMLElement} el
* @param {string} tagName
* @returns {HTMLElement | undefined}
*/
function getAncestor(el, tagName) {
if (!el.parentElement) {
return undefined;
}
if (el.parentElement.tagName.toLowerCase() === tagName.toLowerCase()) {
return el.parentElement;
}
return getAncestor(el.parentElement, tagName);
}
/**
* @param {Element} el
* @returns {Element[]}
*/
function getAllDescendants(el) {
return Array.from(el.children).reduce(
(acc, current) => {
acc.push(current);
return acc.concat(getAllDescendants(current));
},
/** @type {Element[]} */ [],
);
}
/**
* @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);
}
/**
@@ -96,3 +751,4 @@ function getPercentageInView(element) {
return Math.round((inView / globalThis.innerHeight) * 100);
}

View File

@@ -1,7 +1,9 @@
<?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" />
<meta name="viewport"
content="initial-scale=1,width=device-width,height=device-height,viewport-fit=contain" />
<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 +11,139 @@
</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">
<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.
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>
<ipub-body style="--ipub-padding: 10%;" debug="">
<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-track>
<ipub-track-item>
<ipub-track-item-position style="--ipub-track-item-position: 0%;" />
<ipub-soundtrack style="--ipub-color: cyan">
<figure>
<label>
<input type="checkbox" />
<figcaption>Soundtrack 1</figcaption>
</label>
<ipub-audio>
<audio controls="true" volume="0" controlslist="nofullscreen"
disableremoteplayback="true">
<source src="../audios/track1.webm" />
</audio>
</ipub-audio>
</figure>
</ipub-soundtrack>
</ipub-track-item>
<ipub-track-item>
<ipub-track-item-position style="--ipub-track-item-position: 50%;" />
<ipub-soundtrack style="--ipub-color: green">
<figure>
<label>
<input type="checkbox" />
<figcaption>Soundtrack 2</figcaption>
</label>
<ipub-audio>
<audio controls="true" volume="0" controlslist="nofullscreen"
disableremoteplayback="true">
<source src="../audios/track2.webm" />
</audio>
</ipub-audio>
</figure>
</ipub-soundtrack>
</ipub-track-item>
</ipub-track>
<ipub-background id="background0001">
<img src="../images/background0001.jpg" width="100" height="100" />
</ipub-background>
<!-- <ipub-soundtrack style="==ipub-color:cyan"> -->
<!-- <!== TODO: Search on how to make this more accessible, more semantic as using <details> -->--&gt;
<!-- <figure> -->
<!-- <label> -->
<!-- <input type="checkbox" /> -->
<!-- <figcaption>Soundtrack 1</figcaption> -->
<!-- </label> -->
<!-- <ipub-audio> -->
<!-- <audio controls="true" volume="0" controlslist="nofullscreen" -->
<!-- disableremoteplayback=""> -->
<!-- <source src="../audios/track1.webm" /> -->
<!-- </audio> -->
<!-- </ipub-audio> -->
<!-- </figure> -->
<!-- </ipub-soundtrack> -->
<ipub-image>
<img src="../images/image0001.png" />
<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>
<ipub-image>
<img src="../images/image0002.png" />
</ipub-image>
<!-- <ipub-soundtrack style="==ipub-color:green;"> -->
<!-- <figure> -->
<!-- <label> -->
<!-- <input type="checkbox" /> -->
<!-- <figcaption>Soundtrack 2</figcaption> -->
<!-- </label> -->
<!-- <ipub-audio> -->
<!-- <audio controls="true" volume="0" controlslist="nofullscreen" disableremoteplayback=""> -->
<!-- <source src="../audios/track2.webm" /> -->
<!-- </audio> -->
<!-- </ipub-audio> -->
<!-- </figure> -->
<!-- </ipub-soundtrack> -->
<ipub-background id="background0002">
<picture>
<img src="../images/background0002.jpg" />
</picture>
</ipub-background>
<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>
<ipub-image>
<img src="../images/image0002.png" />
</ipub-image>
<!-- <ipub-soundtrack style="==ipub-color:yellow;"> -->
<!-- <figure> -->
<!-- <label> -->
<!-- <input type="checkbox" /> -->
<!-- <figcaption>Soundtrack 3</figcaption> -->
<!-- </label> -->
<!-- <ipub-audio> -->
<!-- <audio controls="true" volume="0" controlslist="nofullscreen" disableremoteplayback=""> -->
<!-- <source src="../audios/track3.webm" /> -->
<!-- </audio> -->
<!-- </ipub-audio> -->
<!-- </figure> -->
<!-- </ipub-soundtrack> -->
<ipub-image>
<img src="../images/image0003.png" />
</ipub-image>
<ipub-image>
<img src="../images/image0004.png" />
</ipub-image></main>
</ipub-body>
</body>
</html>

View File

@@ -1,50 +1,345 @@
.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"] {
max-height: 100vh;
overflow: clip;
display: flex;
flex-direction: column;
position: relative;
:root {
--z-controls: 10;
--z-cover: 9;
--z-overlays: 6;
}
[data-ipub-element="image"] {
width: var(--ipub-width, unset);
height: var(--ipub-width, unset);
}
[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%)
[debug] {
--ipub-debug-bg-opacity: 30%;
background-color: rgba(
from var(--ipub-debug-color) r g b / var(--ipub-debug-bg-opacity, 30%)
);
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;
outline-color: var(--ipub-debug-color);
outline-width: 1px;
outline-style: solid;
}
a[data-ipub-element="interaction"] {
/* The text inside the interaction anchor are for accessibility purposes */
font-size: 0px;
ipub-track {
z-index: 1000;
position: absolute;
display: inline-block;
top: 0;
left: 0;
width: 100%;
height: 100%;
ipub-track-item {
top: 0;
left: 0;
display: inline-block;
position: absolute;
width: 100%;
height: 100%;
*:not(ipub-offset) {
position: sticky;
top: 0;
left: 0;
}
ipub-track-item-position {
width: 5rem;
display: inline-block;
height: var(--ipub-track-item-position);
}
}
}
img {
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-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%);
--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):not(ipub-soundtrack),
& > ipub-background:first-of-type + *:first-of-type:not(ipub-soundtrack),
& > ipub-soundtrack:first-of-type + *:first-of-type:not(ipub-background) {
margin-top: var(--ipub-padding-t);
margin-bottom: calc(var(--ipub-gap) / 2);
}
& > *:not(ipub-background):not(ipub-soundtrack) {
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),
& > *:last-child:not(ipub-soundtrack),
& > ipub-background:last-of-type + *:last-of-type:not(ipub-soundtrack),
& > ipub-soundtrack:last-of-type + *:last-of-type:not(ipub-background) {
margin-top: calc(var(--ipub-gap) / 2);
margin-bottom: var(--ipub-padding-b);
}
}
}
ipub-soundtrack {
display: inline-block;
z-index: var(--z-overlays);
--ipub-color: red;
top: 0;
left: 0;
width: 100%;
height: 0;
position: sticky;
align-self: start;
border-top: 0.1rem dashed var(--ipub-color);
figure {
margin: 0;
height: 1.5rem;
font-size: small;
width: 100%;
display: flex;
align-items: center;
flex-direction: row;
flex: 1;
label {
background-color: var(--ipub-color);
border-end-end-radius: 0.5rem;
padding: 0.1rem 0.4rem;
width: max-content;
max-width: 50%;
height: 100%;
white-space: nowrap;
display: flex;
align-items: center;
}
&:has(input:checked) label {
border-end-end-radius: 0;
}
figcaption::before {
content: "0 "; /* TODO: change to an icon and better positioning */
}
figcaption::after {
content: " >"; /* TODO: change to an icon and better positioning */
}
&:has(input:checked) figcaption::after {
content: " <"; /* TODO: change to an icon and better positioning */
}
input {
width: 0;
height: 0;
display: none;
}
&:has(input) audio {
width: 0;
height: 0;
}
&:has(input:checked) audio {
width: 100%;
@media (width >= 40rem) {
width: 70%;
}
height: 100%;
background-color: var(--ipub-color);
@media (width >= 40rem) {
border-end-end-radius: 0.5rem;
}
padding: 0.1rem 0.4rem;
}
audio::-webkit-media-controls-enclosure {
background-color: transparent;
padding: 0;
border-radius: 0px;
}
audio::-webkit-media-controls-panel {
margin: 0;
padding: 0;
}
}
&[playing] figure figcaption::before {
content: "P "; /* TODO: change to an icon and better positioning */
}
}
ipub-background {
--ipub-mask: linear-gradient(
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) calc(100% + calc(var(--ipub-fade, 100%) * -1))
);
--ipub-width: 100vw;
--ipub-height: 100vh;
display: inline-block;
top: 0;
left: 0;
width: 0;
height: 0;
position: sticky;
align-self: start;
&:first-of-type,
&[nofade] {
--ipub-mask: unset;
}
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; */
/* background-image: var(--mask); */
mask-image: var(--ipub-mask);
-webkit-mask-image: var(--ipub-mask);
}
& > picture {
position: absolute;
top: 0;
left: 0;
display: block;
width: var(--ipub-width);
height: var(--ipub-height);
& > img {
object-fit: cover;
width: 100%;
height: 100%;
}
}
/* Support standalone img element */
& > img {
position: absolute;
top: 0;
left: 0;
display: block;
object-fit: cover;
width: var(--ipub-width);
height: var(--ipub-height);
}
}
ipub-image {
position: relative;
display: inline-block;
flex-direction: column;
width: var(--ipub-width, unset);
height: var(--ipub-height, unset);
img {
display: block;
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%;
}
}