Compare commits
8 Commits
671a4d9b90
...
test/ipub-
| Author | SHA1 | Date | |
|---|---|---|---|
|
e1734a5310
|
|||
|
f76be67247
|
|||
|
c90bff53a3
|
|||
|
7d1a21430c
|
|||
|
a3c2efd5b0
|
|||
|
77631f2a6c
|
|||
|
185001308d
|
|||
|
bbb9ad0e35
|
@@ -1,14 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
class IPUBElement extends HTMLElement {
|
||||
static observedAttributes = ["id"];
|
||||
/**
|
||||
* @protected
|
||||
* @type {Readonly<string[]>}
|
||||
*/
|
||||
static observedAttributes = ["id", "debug"];
|
||||
|
||||
connectedCallback() {
|
||||
this.#ensureID();
|
||||
}
|
||||
|
||||
attributeChangedCallback(_name, _oldValue, _newValue) {
|
||||
this.#ensureID();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,6 +34,339 @@ class IPUBElement extends HTMLElement {
|
||||
this.id = hashFromHTML(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
});
|
||||
|
||||
class IPUBBody extends IPUBElement {
|
||||
static elementName = `ipub-body`;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -28,49 +376,84 @@ class IPUBBackground extends IPUBElement {
|
||||
/**
|
||||
* @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,
|
||||
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`,
|
||||
);
|
||||
instance.fade(perc);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return new IntersectionObserver((entries) =>
|
||||
entries.forEach((e) => {
|
||||
let instance = e.target.parentElement;
|
||||
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;
|
||||
}
|
||||
|
||||
if (
|
||||
instance.tagName.toLowerCase() !==
|
||||
IPUBBackground.elementName.toLowerCase()
|
||||
) {
|
||||
instance = instance.parentElement;
|
||||
}
|
||||
const set = this.map.get(body);
|
||||
if (!set) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
instance.tagName.toLowerCase() !==
|
||||
IPUBBackground.elementName.toLowerCase()
|
||||
) {
|
||||
console.error(
|
||||
"IPUBBackground: malformed <ipub-background> element",
|
||||
e.target,
|
||||
);
|
||||
return;
|
||||
}
|
||||
set.delete(background);
|
||||
|
||||
if (e.intersectionRatio > 0 && instance.id) {
|
||||
instancesOnScreen.set(instance.id, instance);
|
||||
} else if (instance.id) {
|
||||
instancesOnScreen.delete(instance.id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
})();
|
||||
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();
|
||||
@@ -126,8 +509,6 @@ class IPUBBackground extends IPUBElement {
|
||||
* @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;
|
||||
@@ -143,11 +524,6 @@ class IPUBBackground extends IPUBElement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IPUBContent extends IPUBElement {
|
||||
static elementName = "ipub-content";
|
||||
}
|
||||
|
||||
class IPUBImage extends IPUBElement {
|
||||
static elementName = "ipub-image";
|
||||
}
|
||||
@@ -156,42 +532,160 @@ class IPUBInteraction extends IPUBElement {
|
||||
static elementName = "ipub-interaction";
|
||||
}
|
||||
|
||||
globalThis.addEventListener("load", () => {
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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
|
||||
@@ -257,3 +751,4 @@ function getPercentageInView(element) {
|
||||
|
||||
return Math.round((inView / globalThis.innerHeight) * 100);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<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,51 +11,139 @@
|
||||
</script>
|
||||
</head>
|
||||
<body xmlns:epub="http://www.idpf.org/2007/ops" class="body">
|
||||
<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" />
|
||||
<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-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-image>
|
||||
<img src="../images/image0003.png" />
|
||||
</ipub-image>
|
||||
<ipub-image>
|
||||
<img src="../images/image0004.png" />
|
||||
</ipub-image>
|
||||
</main>
|
||||
</ipub-content>
|
||||
<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> -->-->
|
||||
|
||||
<!-- <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>
|
||||
|
||||
@@ -8,9 +8,92 @@
|
||||
margin: 0;
|
||||
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
overflow: clip;
|
||||
display: flex;
|
||||
|
||||
:root {
|
||||
--z-controls: 10;
|
||||
--z-cover: 9;
|
||||
--z-overlays: 6;
|
||||
}
|
||||
|
||||
ipub-content {
|
||||
[debug] {
|
||||
--ipub-debug-bg-opacity: 30%;
|
||||
background-color: rgba(
|
||||
from var(--ipub-debug-color) r g b / var(--ipub-debug-bg-opacity, 30%)
|
||||
);
|
||||
outline-color: var(--ipub-debug-color);
|
||||
outline-width: 1px;
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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%);
|
||||
@@ -28,12 +111,14 @@ ipub-content {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
& > *:first-child:not(ipub-background),
|
||||
& > ipub-background:first-child + *:first-of-type {
|
||||
& > *: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-background):not(ipub-soundtrack) {
|
||||
margin-top: calc(var(--ipub-gap) / 2);
|
||||
margin-right: var(--ipub-padding-r);
|
||||
margin-left: var(--ipub-padding-l);
|
||||
@@ -41,43 +126,151 @@ ipub-content {
|
||||
}
|
||||
|
||||
& > *:last-child:not(ipub-background),
|
||||
& > ipub-background:last-child + *:last-of-type {
|
||||
& > *: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;
|
||||
|
||||
&[sticky] {
|
||||
display: inline-block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: sticky;
|
||||
align-self: start;
|
||||
display: inline-block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: sticky;
|
||||
align-self: start;
|
||||
|
||||
&:first-of-type,
|
||||
&[nofade] {
|
||||
--ipub-mask: unset;
|
||||
}
|
||||
|
||||
&[fade] img {
|
||||
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);
|
||||
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);
|
||||
@@ -89,6 +282,9 @@ ipub-background {
|
||||
}
|
||||
/* Support standalone img element */
|
||||
& > img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: var(--ipub-width);
|
||||
|
||||
Reference in New Issue
Block a user