Compare commits
16 Commits
f465564546
...
test/ipub-
| Author | SHA1 | Date | |
|---|---|---|---|
|
e1734a5310
|
|||
|
f76be67247
|
|||
|
c90bff53a3
|
|||
|
7d1a21430c
|
|||
|
a3c2efd5b0
|
|||
|
77631f2a6c
|
|||
|
185001308d
|
|||
|
bbb9ad0e35
|
|||
|
6a7abdea6f
|
|||
|
b52e7f165f
|
|||
|
d775301567
|
|||
|
ba7ca52ed2
|
|||
|
185ca863fe
|
|||
|
11456db9c4
|
|||
|
d556b0eefe
|
|||
|
007de6b9f1
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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> -->-->
|
||||
|
||||
<!-- <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>
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user