summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--assets/bweeeeghg.jpgbin0 -> 22174 bytes
-rw-r--r--assets/fagotni.gifbin0 -> 309128 bytes
-rw-r--r--assets/favicon.jpgbin0 -> 36023 bytes
-rw-r--r--assets/hiissssssss.pngbin0 -> 69079 bytes
-rw-r--r--assets/oneko.gifbin0 -> 3316 bytes
-rw-r--r--index.html276
-rw-r--r--justifiedGallery.css167
-rw-r--r--scripts/justifiedgallery.js1167
-rw-r--r--scripts/oneko.js279
9 files changed, 1889 insertions, 0 deletions
diff --git a/assets/bweeeeghg.jpg b/assets/bweeeeghg.jpg
new file mode 100644
index 0000000..7f44257
--- /dev/null
+++ b/assets/bweeeeghg.jpg
Binary files differ
diff --git a/assets/fagotni.gif b/assets/fagotni.gif
new file mode 100644
index 0000000..442ca00
--- /dev/null
+++ b/assets/fagotni.gif
Binary files differ
diff --git a/assets/favicon.jpg b/assets/favicon.jpg
new file mode 100644
index 0000000..e247547
--- /dev/null
+++ b/assets/favicon.jpg
Binary files differ
diff --git a/assets/hiissssssss.png b/assets/hiissssssss.png
new file mode 100644
index 0000000..c4c5067
--- /dev/null
+++ b/assets/hiissssssss.png
Binary files differ
diff --git a/assets/oneko.gif b/assets/oneko.gif
new file mode 100644
index 0000000..a009c2c
--- /dev/null
+++ b/assets/oneko.gif
Binary files differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..e083029
--- /dev/null
+++ b/index.html
@@ -0,0 +1,276 @@
+<!DOCTYPE html>
+<html lang="en"><head>
+<link rel="icon" type="image/jpeg" href="assets/favicon.jpg">
+<meta charset="UTF-8">
+<link rel="stylesheet" href="justifiedGallery.css">
+<title>FAGGOOTTTTT</title>
+<style>
+ :root {
+ --pad: 1.5rem;
+ --gap: 24px; /* space between the page edge and the terminals: same on top and sides */
+ --term-w: 100%;
+ --term-h: 80vh;
+ --term-overlay: rgba(55, 45, 65, 0.6);
+ --term-border: #1a3a4a;
+ }
+
+ html, body {
+ margin: 0;
+ padding: 0;
+ background: rgb(2, 1, 3);
+ }
+
+ body {
+ font-family: ui-monospace, 'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace;
+ font-size: 14px;
+ color: #eee;
+ min-height: 100vh;
+ box-sizing: border-box;
+ padding: var(--gap);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 24px;
+ overflow-x: hidden;
+ }
+
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+
+ a {
+ color: #6cf;
+ text-decoration: none;
+ transition: color 0.15s ease;
+ }
+
+ a:hover {
+ color: #9df;
+ }
+
+ /* both terminals share the exact same size and padding (top == left) */
+ .terminal {
+ position: relative;
+ flex: 0 0 auto;
+ width: var(--term-w);
+ height: var(--term-h);
+ box-sizing: border-box;
+ padding: var(--pad);
+ border: 2px solid var(--term-border);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);
+ overflow: hidden;
+ }
+
+ .faggif {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ max-width: none;
+ object-fit: fill;
+ z-index: 0;
+ pointer-events: none;
+ }
+
+ .background-square {
+ position: absolute;
+ inset: 0;
+ background-color: var(--term-overlay);
+ z-index: 1;
+ pointer-events: none;
+ }
+
+ .terminal-content {
+ position: relative;
+ z-index: 2;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ }
+
+ .prompt {
+ margin-bottom: 0.9rem;
+ white-space: nowrap;
+ }
+ .prompt .user { color: #fff; }
+ .prompt .path { color: #fff; }
+
+ /* fastfetch layout */
+ .fetch {
+ display: flex;
+ gap: 1.25rem;
+ align-items: flex-start;
+ }
+
+ .logo {
+ flex: 0 0 auto;
+ width: min(40%, 60vh);
+ display: block;
+ }
+
+ .info {
+ flex: 1;
+ min-width: 0;
+ line-height: 1.7;
+ }
+
+ .terminal-content > .title {
+ margin-bottom: 0.9rem;
+ }
+
+ .title {
+ font-size: 1.1rem;
+ font-weight: bold;
+ color: #fff;
+ }
+
+ .sep {
+ color: #fff;
+ overflow: hidden;
+ white-space: nowrap;
+ margin-bottom: 0.4rem;
+ }
+
+ .row {
+ display: flex;
+ gap: 0.6em;
+ overflow-wrap: anywhere;
+ }
+
+ .key {
+ color: #fff;
+ font-weight: bold;
+ flex: 0 0 auto;
+ }
+
+ .info a {
+ color: #fff;
+ text-decoration: underline;
+ text-underline-offset: 3px;
+ }
+ .info a:hover {
+ color: #bbb;
+ }
+
+ .note {
+ color: #888;
+ margin-top: 0.6rem;
+ }
+
+ .footer {
+ padding: var(--gap) 0;
+ text-align: center;
+ font-size: 12px;
+ color: #1f1c24; /* barely lighter than the page bg */
+ user-select: none;
+ }
+
+ /* image that sits between the terminals */
+ .middle-img {
+ flex: 0 1 auto;
+ max-width: 220px;
+ align-self: center;
+ }
+</style>
+</head>
+<body>
+ <script src="scripts/oneko.js" data-cat="assets/oneko.gif"></script>
+
+ <div class="terminal">
+ <img class="faggif" src="assets/fagotni.gif" alt="if you are seeing this something went horribly wrong">
+ <div class="background-square"></div>
+ <div class="terminal-content">
+ <div class="prompt"><span class="user">david@faggot-chan</span>:<span class="path">~</span>$ fastfetch</div>
+ <div class="fetch">
+ <img class="logo" src="assets/bweeeeghg.jpg" alt="">
+ <div class="info">
+ <div class="title">FAGGOOTTTTT</div>
+ <div class="sep">---------------</div>
+ <div class="row"><span class="key">gf twitter</span><a href="https://x.com/FlatchestMoe">FlatchestMoe</a></div>
+ <div class="row"><span class="key">twitter</span><a href="https://x.com/david_eldios_8">david_eldios_8</a></div>
+ <div class="row"><span class="key">github</span><a href="https://github.com/davideldios7/">github/davideldios7</a></div>
+ <div class="row"><span class="key">git</span><a href="https://git.faggot-chan.online">git.faggot-chan</a></div>
+ <div class="row"><span class="key">jellyfin</span><a href="https://jelly.faggot-chan.online">jelly.faggot-chan</a></div>
+ <div id="status"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <img class="middle-img" src="assets/hiissssssss.png" alt="">
+
+ <div class="terminal">
+ <div class="background-square"></div>
+ <div class="terminal-content">
+ <div class="title">all my twitter bookmarks!!!!!</div>
+ <div id="gallery-container">
+ <div id="gallery"></div>
+ </div>
+ <div class="note">(some aren't teto, i still have to cleanup and name them)</div>
+ </div>
+ </div>
+ <div class="footer">i vibecoded all of this</div>
+
+ <dialog id="clickfunction">
+ <div id="clickfunction-inner">
+ <button id="clickfunction-close" aria-label="Close">×</button>
+ <img id="clickfunction-img" src="" alt="">
+ <div id="clickfunction-desc"></div>
+ </div>
+ </dialog>
+<script src="scripts/justifiedgallery.js"></script>
+<script>
+(async () => {
+ const box = document.getElementById('status');
+ if (!box) return;
+ try {
+ const r = await fetch('status/status.json', { cache: 'no-store' });
+ if (!r.ok) return;
+ const d = await r.json();
+ const h = d.host;
+
+ const row = (k, v) => {
+ const el = document.createElement('div');
+ el.className = 'row';
+ const a = document.createElement('span');
+ a.className = 'key';
+ a.textContent = k;
+ const b = document.createElement('span');
+ b.textContent = v;
+ el.append(a, b);
+ box.append(el);
+ };
+
+ const sep = document.createElement('div');
+ sep.className = 'sep';
+ sep.textContent = '---------------';
+ box.append(sep);
+
+ const days = Math.floor(h.uptime / 86400);
+ const hrs = Math.floor((h.uptime % 86400) / 3600);
+ const mins = Math.floor((h.uptime % 3600) / 60);
+ row('uptime', days > 0 ? `${days}d ${hrs}h` : `${hrs}h ${mins}m`);
+ row('kernel', h.kernel);
+ row('memory', `${(h.mem_used_mib / 1024).toFixed(1)} / ${(h.mem_total_mib / 1024).toFixed(1)} GiB`);
+ row('disk', `${h.disk_used_gib} / ${h.disk_total_gib} GiB`);
+
+ box.append(sep.cloneNode(true));
+ for (const s of d.services) {
+ row(s.name, s.up ? (s.ms !== null ? `\u25CF ${s.ms}ms` : '\u25CF up') : '\u25CB down');
+ }
+
+ const age = Math.max(0, Math.round((Date.now() / 1000 - d.updated) / 60));
+ const when = age < 60 ? `${age} min ago` : `${Math.floor(age / 60)}h ago`;
+ row('updated', age > 120 ? `${when} (stale)` : when);
+ } catch (e) {
+ /* no status.json yet: just don't show the rows */
+ }
+})();
+</script>
+
+
+
+</body></html>
diff --git a/justifiedGallery.css b/justifiedGallery.css
new file mode 100644
index 0000000..310a8c0
--- /dev/null
+++ b/justifiedGallery.css
@@ -0,0 +1,167 @@
+html, body {
+ margin: 0;
+ height: 100%;
+ background: rgb(2, 1, 3);
+}
+
+#gallery-container {
+ width: 100%;
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ background: rgba(0, 0, 0, 0.35);
+ border: 1px solid #1a3a4a;
+ scrollbar-color: #1a3a4a transparent;
+ scrollbar-width: thin;
+}
+
+#clickfunction {
+ border: none;
+ padding: 0;
+ background: transparent;
+ max-width: 90vw;
+ max-height: 90vh;
+}
+#clickfunction::backdrop {
+ background: rgba(0, 0, 0, 0.85);
+}
+#clickfunction-inner {
+ display: flex;
+ flex-direction: column;
+ background: #1a1a1a;
+ color: #eee;
+ max-width: 90vw;
+ max-height: 90vh;
+}
+#clickfunction-img {
+ max-width: 90vw;
+ max-height: 75vh;
+ object-fit: contain;
+ display: block;
+}
+#clickfunction-desc {
+ padding: 12px 16px;
+ font-size: 14px;
+ line-height: 1.4;
+ white-space: pre-wrap;
+}
+#clickfunction-close {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background: rgba(0, 0, 0, 0.6);
+ color: #fff;
+ border: none;
+ width: 32px;
+ height: 32px;
+ font-size: 18px;
+ cursor: pointer;
+}
+
+.justified-gallery > div {
+ cursor: pointer;
+}
+
+/*!
+ * justifiedGallery - v3.8.1
+ * http://miromannino.github.io/Justified-Gallery/
+ * Copyright (c) 2020 Miro Mannino
+ * Licensed under the MIT license.
+ */
+.justified-gallery {
+ width: 100%;
+ position: relative;
+ overflow: hidden;
+}
+.justified-gallery > a,
+.justified-gallery > div,
+.justified-gallery > figure {
+ position: absolute;
+ display: inline-block;
+ overflow: hidden;
+ border-radius: 0;
+ filter: "alpha(opacity=10)";
+ opacity: 0.1;
+ margin: 0;
+ padding: 0;
+}
+.justified-gallery > a > img,
+.justified-gallery > div > img,
+.justified-gallery > figure > img,
+.justified-gallery > a > a > img,
+.justified-gallery > div > a > img,
+.justified-gallery > figure > a > img,
+.justified-gallery > a > svg,
+.justified-gallery > div > svg,
+.justified-gallery > figure > svg,
+.justified-gallery > a > a > svg,
+.justified-gallery > div > a > svg,
+.justified-gallery > figure > a > svg {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin: 0;
+ padding: 0;
+ border: none;
+ border-radius: 0;
+ filter: "alpha(opacity=0)";
+ opacity: 0;
+}
+.justified-gallery > a > .jg-caption,
+.justified-gallery > div > .jg-caption,
+.justified-gallery > figure > .jg-caption {
+ display: none;
+ position: absolute;
+ bottom: 0;
+ padding: 5px;
+ background-color: #000000;
+ left: 0;
+ right: 0;
+ margin: 0;
+ color: white;
+ font-size: 12px;
+ font-weight: 300;
+}
+.justified-gallery > a > .jg-caption.jg-caption-visible,
+.justified-gallery > div > .jg-caption.jg-caption-visible,
+.justified-gallery > figure > .jg-caption.jg-caption-visible {
+ display: initial;
+ filter: "alpha(opacity=70)";
+ opacity: 0.7;
+ transition: opacity 500ms ease-in;
+}
+.justified-gallery > .jg-entry-visible {
+ filter: "alpha(opacity=100)";
+ opacity: 1;
+ background: none;
+}
+.justified-gallery > .jg-entry-visible > img,
+.justified-gallery > .jg-entry-visible > a > img,
+.justified-gallery > .jg-entry-visible > svg,
+.justified-gallery > .jg-entry-visible > a > svg {
+ filter: "alpha(opacity=100)";
+ opacity: 1;
+ transition: opacity 500ms ease-in;
+}
+.justified-gallery > .jg-filtered {
+ display: none;
+}
+.justified-gallery > .jg-spinner {
+ position: absolute;
+ bottom: 0;
+ margin-left: -24px;
+ padding: 10px 0 10px 0;
+ left: 50%;
+ filter: "alpha(opacity=100)";
+ opacity: 1;
+ overflow: initial;
+}
+.justified-gallery > .jg-spinner > span {
+ display: inline-block;
+ filter: "alpha(opacity=0)";
+ opacity: 0;
+ width: 8px;
+ height: 8px;
+ margin: 0 4px 0 4px;
+ background-color: #000;
+}
diff --git a/scripts/justifiedgallery.js b/scripts/justifiedgallery.js
new file mode 100644
index 0000000..0d8c2e1
--- /dev/null
+++ b/scripts/justifiedgallery.js
@@ -0,0 +1,1167 @@
+(function () {
+
+ const CONFIG = {
+ folders: ['images/', 'images2/'], //many folders
+ imageExtensions: /\.(jpe?g|png|gif|webp|avif|bmp)$/i,
+ thumbFolder: 'thumbs/', //mirrors `folders`, generated by generate-thumbs.sh
+ manifest: 'manifest.json', //built by generate-thumbs.sh: {folder,file,width,height}[]
+ rowHeight: 80, //target row height (px)
+ margins: 2, //gap between images (px)
+ maxRowHeight: '150%', //cap on stretched row height
+ lastRow: 'nojustify' //how to handle the final incomplete row
+ };
+
+ const galleryEl = document.getElementById('gallery');
+ const clickfunction = document.getElementById('clickfunction');
+ const clickfunctionImg = document.getElementById('clickfunction-img');
+ const clickfunctionDesc = document.getElementById('clickfunction-desc');
+ const clickfunctionClose = document.getElementById('clickfunction-close');
+
+ function openClickfunction(src, description) {
+ clickfunctionImg.src = src;
+ clickfunctionDesc.textContent = description || '';
+ clickfunction.showModal();
+ }
+
+ clickfunctionClose.addEventListener('click', () => clickfunction.close());
+ clickfunction.addEventListener('click', (e) => {
+ if (e.target === clickfunction) clickfunction.close();
+ });
+
+
+ //Descriptions are fetched lazily (only when an entry is clicked), not
+ //up front for every image, so the gallery doesn't wait on N extra
+ //requests before it can render.
+ const descriptionCache = new Map();
+ async function fetchDescription(folder, imageFile) {
+ const descFile = folder + imageFile.replace(CONFIG.imageExtensions, '.txt');
+ if (descriptionCache.has(descFile)) return descriptionCache.get(descFile);
+ let description = '';
+ try {
+ const res = await fetch(descFile);
+ if (res.ok) description = (await res.text()).trim();
+ } catch (e) {
+ //ignore all errors
+ }
+ descriptionCache.set(descFile, description);
+ return description;
+ }
+
+ // manifest.json is built by generate-thumbs.sh: real dims mean
+ // justifiedGallery() never has to download full images just to measure
+ // them, and the grid loads actual small thumbnails instead of full-res
+ // originals. Browser handles lazy-loading + caching natively.
+ async function loadGallery() {
+ const res = await fetch(CONFIG.manifest);
+ const items = await res.json();
+
+ items.forEach(({ folder, file, width, height }) => {
+ const src = folder + file;
+ const thumbSrc = CONFIG.thumbFolder + folder + file.replace(CONFIG.imageExtensions, '.webp');
+ const label = file.replace(CONFIG.imageExtensions, '');
+
+ const entry = document.createElement('div');
+ const img = document.createElement('img');
+ img.alt = label; // used by the library for hover captions
+ img.decoding = 'async';
+ img.loading = 'lazy';
+ img.width = width;
+ img.height = height;
+ img.src = thumbSrc;
+
+ entry.appendChild(img);
+ entry.addEventListener('click', async () => {
+ const description = await fetchDescription(folder, file);
+ openClickfunction(src, description || label);
+ });
+
+ galleryEl.appendChild(entry);
+ });
+
+ justifiedGallery('#gallery', {
+ selector: 'div',
+ rowHeight: CONFIG.rowHeight,
+ margins: CONFIG.margins,
+ maxRowHeight: CONFIG.maxRowHeight,
+ lastRow: CONFIG.lastRow
+ });
+ }
+
+ loadGallery();
+
+})();
+
+
+/*!
+ * justifiedGallery - v3.8.1 (Vanilla JS conversion by cfconsultancy, trimmed)
+ * https://miromannino.github.io/Justified-Gallery/
+ * Copyright (c) 2020 Miro Mannino
+ * Licensed under the MIT license.
+ * */
+const justifiedGallery = (function () {
+
+ const utils = {
+ isFunction(obj) {
+ return typeof obj === 'function';
+ },
+ isString(obj) {
+ return typeof obj === 'string';
+ },
+ isObject(obj) {
+ return typeof obj === 'object' && obj !== null;
+ },
+ isBoolean(obj) {
+ return typeof obj === 'boolean';
+ },
+ extend(...args) {
+ const result = {};
+ args.forEach(obj => {
+ if (obj) {
+ Object.keys(obj).forEach(key => {
+ result[key] = obj[key];
+ });
+ }
+ });
+ return result;
+ },
+ addClass(el, className) {
+ el.classList.add(className);
+ },
+ removeClass(el, className) {
+ el.classList.remove(className);
+ },
+ hasClass(el, className) {
+ return el.classList.contains(className);
+ },
+ fadeIn(el, duration, callback) {
+ el.style.opacity = 0;
+ el.style.display = 'block';
+ const start = performance.now();
+ const animate = (time) => {
+ const progress = Math.min((time - start) / duration, 1);
+ el.style.opacity = progress;
+ if (progress < 1) {
+ requestAnimationFrame(animate);
+ } else if (callback) {
+ callback();
+ }
+ };
+ requestAnimationFrame(animate);
+ },
+ fadeTo(el, duration, opacity, callback) {
+ const startOpacity = parseFloat(getComputedStyle(el).opacity) || 0;
+ const start = performance.now();
+ const animate = (time) => {
+ const progress = Math.min((time - start) / duration, 1);
+ el.style.opacity = startOpacity + (opacity - startOpacity) * progress;
+ if (progress < 1) {
+ requestAnimationFrame(animate);
+ } else if (callback) {
+ callback();
+ }
+ };
+ requestAnimationFrame(animate);
+ },
+ trigger(el, eventName, detail) {
+ const event = new CustomEvent(eventName, { detail: detail || {} });
+ el.dispatchEvent(event);
+ }
+ };
+
+ class JustifiedGallery {
+ constructor(gallery, settings) {
+ this.gallery = gallery;
+ this.settings = utils.extend({}, this.defaults, settings);
+ this.checkSettings();
+
+ this.imgAnalyzerTimeout = null;
+ this.entries = null;
+ this.buildingRow = {
+ entriesBuff: [],
+ width: 0,
+ height: 0,
+ aspectRatio: 0
+ };
+ this.lastFetchedEntry = null;
+ this.lastAnalyzedIndex = -1;
+ this.yield = {
+ every: 2,
+ flushed: 0
+ };
+ this.border = this.settings.border >= 0 ? this.settings.border : this.settings.margins;
+ this.maxRowHeight = this.retrieveMaxRowHeight();
+ this.suffixRanges = this.retrieveSuffixRanges();
+ this.offY = this.border;
+ this.rows = 0;
+ this.spinner = {
+ phase: 0,
+ timeSlot: 150,
+ el: this.createSpinnerElement(),
+ intervalId: null
+ };
+ this.scrollBarOn = false;
+ this.resizeObserver = null;
+ this.galleryWidth = this.gallery.offsetWidth;
+ this.galleryPrevStaticHeight = 0;
+
+ this.elementData = new WeakMap();
+ }
+
+ createSpinnerElement() {
+ const spinner = document.createElement('div');
+ spinner.className = 'jg-spinner';
+ for (let i = 0; i < 3; i++) {
+ spinner.appendChild(document.createElement('span'));
+ }
+ return spinner;
+ }
+
+ setData(el, key, value) {
+ let data = this.elementData.get(el);
+ if (!data) {
+ data = {};
+ this.elementData.set(el, data);
+ }
+ data[key] = value;
+ }
+
+ getData(el, key) {
+ const data = this.elementData.get(el);
+ return data ? data[key] : undefined;
+ }
+
+ removeData(el, key) {
+ const data = this.elementData.get(el);
+ if (data && key in data) {
+ delete data[key];
+ }
+ }
+
+ getSuffix(width, height) {
+ const longestSide = Math.max(width, height);
+ for (let i = 0; i < this.suffixRanges.length; i++) {
+ if (longestSide <= this.suffixRanges[i]) {
+ return this.settings.sizeRangeSuffixes[this.suffixRanges[i]];
+ }
+ }
+ return this.settings.sizeRangeSuffixes[this.suffixRanges[this.suffixRanges.length - 1]];
+ }
+
+ removeSuffix(str, suffix) {
+ return str.substring(0, str.length - suffix.length);
+ }
+
+ endsWith(str, suffix) {
+ return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ }
+
+ getUsedSuffix(str) {
+ for (let si in this.settings.sizeRangeSuffixes) {
+ if (this.settings.sizeRangeSuffixes.hasOwnProperty(si)) {
+ if (this.settings.sizeRangeSuffixes[si].length === 0) continue;
+ if (this.endsWith(str, this.settings.sizeRangeSuffixes[si])) {
+ return this.settings.sizeRangeSuffixes[si];
+ }
+ }
+ }
+ return '';
+ }
+
+ newSrc(imageSrc, imgWidth, imgHeight, image) {
+ let newImageSrc;
+ if (this.settings.thumbnailPath) {
+ newImageSrc = this.settings.thumbnailPath(imageSrc, imgWidth, imgHeight, image);
+ } else {
+ const matchRes = imageSrc.match(this.settings.extension);
+ const ext = matchRes !== null ? matchRes[0] : '';
+ newImageSrc = imageSrc.replace(this.settings.extension, '');
+ newImageSrc = this.removeSuffix(newImageSrc, this.getUsedSuffix(newImageSrc));
+ newImageSrc += this.getSuffix(imgWidth, imgHeight) + ext;
+ }
+ return newImageSrc;
+ }
+
+ showImg(entry, callback) {
+ if (this.settings.cssAnimation) {
+ utils.addClass(entry, 'jg-entry-visible');
+ if (callback) callback();
+ } else {
+ utils.fadeTo(entry, this.settings.imagesAnimationDuration, 1.0, callback);
+ const img = this.imgFromEntry(entry);
+ if (img) {
+ utils.fadeTo(img, this.settings.imagesAnimationDuration, 1.0);
+ }
+ }
+ }
+
+ extractImgSrcFromImage(image) {
+ let imageSrc = image.getAttribute('data-safe-src');
+ let imageSrcLoc = 'data-safe-src';
+ if (!imageSrc) {
+ imageSrc = image.getAttribute('src');
+ imageSrcLoc = 'src';
+ }
+ this.setData(image, 'jg.originalSrc', imageSrc);
+ this.setData(image, 'jg.src', imageSrc);
+ this.setData(image, 'jg.originalSrcLoc', imageSrcLoc);
+ return imageSrc;
+ }
+
+ imgFromEntry(entry) {
+ const selectors = this.settings.imgSelector.split(',').map(s => s.trim());
+ for (let selector of selectors) {
+ const cleanSelector = selector.replace(/^>\s*/, '');
+ const elements = Array.from(entry.querySelectorAll(cleanSelector));
+ const img = selector.startsWith('>')
+ ? elements.find(el => el.parentNode === entry)
+ : elements[0];
+ if (img) return img;
+ }
+ return null;
+ }
+
+ captionFromEntry(entry) {
+ const captions = Array.from(entry.querySelectorAll('.jg-caption'));
+ return captions.find(el => el.parentNode === entry) || null;
+ }
+
+ displayEntry(entry, x, y, imgWidth, imgHeight, rowHeight) {
+ entry.style.width = imgWidth + 'px';
+ entry.style.height = rowHeight + 'px';
+ entry.style.top = y + 'px';
+ entry.style.left = x + 'px';
+
+ const image = this.imgFromEntry(entry);
+ if (image !== null) {
+ image.style.width = imgWidth + 'px';
+ image.style.height = imgHeight + 'px';
+ image.style.marginLeft = (-imgWidth / 2) + 'px';
+ image.style.marginTop = (-imgHeight / 2) + 'px';
+
+ let imageSrc = this.getData(image, 'jg.src');
+ if (imageSrc) {
+ imageSrc = this.newSrc(imageSrc, imgWidth, imgHeight, image);
+
+ image.addEventListener('error', () => {
+ this.resetImgSrc(image);
+ }, { once: true });
+
+ const loadNewImage = () => {
+ image.setAttribute('src', imageSrc);
+ };
+
+ if (this.getData(entry, 'jg.loaded') === 'skipped' && imageSrc) {
+ this.onImageEvent(imageSrc, () => {
+ this.showImg(entry, loadNewImage);
+ this.setData(entry, 'jg.loaded', true);
+ });
+ } else {
+ this.showImg(entry, loadNewImage);
+ }
+ }
+ } else {
+ this.showImg(entry);
+ }
+
+ this.displayEntryCaption(entry);
+ }
+
+ displayEntryCaption(entry) {
+ const image = this.imgFromEntry(entry);
+ if (image !== null && this.settings.captions) {
+ let imgCaption = this.captionFromEntry(entry);
+
+ if (imgCaption === null) {
+ let caption = image.getAttribute('alt');
+ if (!this.isValidCaption(caption)) {
+ caption = entry.getAttribute('title');
+ }
+ if (this.isValidCaption(caption)) {
+ imgCaption = document.createElement('div');
+ imgCaption.className = 'jg-caption';
+ imgCaption.textContent = caption;
+ entry.appendChild(imgCaption);
+ this.setData(entry, 'jg.createdCaption', true);
+ }
+ }
+
+ if (imgCaption !== null) {
+ if (!this.settings.cssAnimation) {
+ imgCaption.style.opacity = this.settings.captionSettings.nonVisibleOpacity;
+ }
+ this.addCaptionEventsHandlers(entry);
+ }
+ } else {
+ this.removeCaptionEventsHandlers(entry);
+ }
+ }
+
+ isValidCaption(caption) {
+ return caption && caption.length > 0;
+ }
+
+ onEntryMouseEnterForCaption(event) {
+ const caption = this.captionFromEntry(event.currentTarget);
+ if (this.settings.cssAnimation) {
+ utils.addClass(caption, 'jg-caption-visible');
+ utils.removeClass(caption, 'jg-caption-hidden');
+ } else {
+ utils.fadeTo(caption, this.settings.captionSettings.animationDuration,
+ this.settings.captionSettings.visibleOpacity);
+ }
+ }
+
+ onEntryMouseLeaveForCaption(event) {
+ const caption = this.captionFromEntry(event.currentTarget);
+ if (this.settings.cssAnimation) {
+ utils.removeClass(caption, 'jg-caption-visible');
+ utils.removeClass(caption, 'jg-caption-hidden');
+ } else {
+ utils.fadeTo(caption, this.settings.captionSettings.animationDuration,
+ this.settings.captionSettings.nonVisibleOpacity);
+ }
+ }
+
+ addCaptionEventsHandlers(entry) {
+ let captionMouseEvents = this.getData(entry, 'jg.captionMouseEvents');
+ if (typeof captionMouseEvents === 'undefined') {
+ captionMouseEvents = {
+ mouseenter: this.onEntryMouseEnterForCaption.bind(this),
+ mouseleave: this.onEntryMouseLeaveForCaption.bind(this)
+ };
+ entry.addEventListener('mouseenter', captionMouseEvents.mouseenter);
+ entry.addEventListener('mouseleave', captionMouseEvents.mouseleave);
+ this.setData(entry, 'jg.captionMouseEvents', captionMouseEvents);
+ }
+ }
+
+ removeCaptionEventsHandlers(entry) {
+ const captionMouseEvents = this.getData(entry, 'jg.captionMouseEvents');
+ if (typeof captionMouseEvents !== 'undefined') {
+ entry.removeEventListener('mouseenter', captionMouseEvents.mouseenter);
+ entry.removeEventListener('mouseleave', captionMouseEvents.mouseleave);
+ this.removeData(entry, 'jg.captionMouseEvents');
+ }
+ }
+
+ clearBuildingRow() {
+ this.buildingRow.entriesBuff = [];
+ this.buildingRow.aspectRatio = 0;
+ this.buildingRow.width = 0;
+ }
+
+ prepareBuildingRow(isLastRow, hiddenRow) {
+ let i, entry, imgAspectRatio, newImgW, newImgH, justify = true;
+ let minHeight = 0;
+
+ const availableWidth = this.galleryWidth - 2 * this.border -
+ ((this.buildingRow.entriesBuff.length - 1) * this.settings.margins);
+ const rowHeight = availableWidth / this.buildingRow.aspectRatio;
+ let defaultRowHeight = this.settings.rowHeight;
+ const justifiable = this.buildingRow.width / availableWidth > this.settings.justifyThreshold;
+
+ if (hiddenRow || (isLastRow && this.settings.lastRow === 'hide' && !justifiable)) {
+ for (i = 0; i < this.buildingRow.entriesBuff.length; i++) {
+ entry = this.buildingRow.entriesBuff[i];
+ if (this.settings.cssAnimation) {
+ utils.removeClass(entry, 'jg-entry-visible');
+ } else {
+ utils.fadeTo(entry, 0, 0.1);
+ const img = entry.querySelector('> img, > a > img');
+ if (img) utils.fadeTo(img, 0, 0);
+ }
+ }
+ return -1;
+ }
+
+ if (isLastRow && !justifiable && this.settings.lastRow !== 'justify' && this.settings.lastRow !== 'hide') {
+ justify = false;
+ if (this.rows > 0) {
+ defaultRowHeight = (this.offY - this.border - this.settings.margins * this.rows) / this.rows;
+ justify = defaultRowHeight * this.buildingRow.aspectRatio / availableWidth > this.settings.justifyThreshold;
+ }
+ }
+
+ let availWidth = availableWidth;
+ for (i = 0; i < this.buildingRow.entriesBuff.length; i++) {
+ entry = this.buildingRow.entriesBuff[i];
+ imgAspectRatio = this.getData(entry, 'jg.width') / this.getData(entry, 'jg.height');
+
+ if (justify) {
+ newImgW = (i === this.buildingRow.entriesBuff.length - 1) ? availWidth : rowHeight * imgAspectRatio;
+ newImgH = rowHeight;
+ } else {
+ newImgW = defaultRowHeight * imgAspectRatio;
+ newImgH = defaultRowHeight;
+ }
+
+ availWidth -= Math.round(newImgW);
+ this.setData(entry, 'jg.jwidth', Math.round(newImgW));
+ this.setData(entry, 'jg.jheight', Math.ceil(newImgH));
+ if (i === 0 || minHeight > newImgH) minHeight = newImgH;
+ }
+
+ this.buildingRow.height = minHeight;
+ return justify;
+ }
+
+ flushRow(isLastRow, hiddenRow) {
+ const settings = this.settings;
+ let entry, buildingRowRes, offX = this.border, i;
+
+ buildingRowRes = this.prepareBuildingRow(isLastRow, hiddenRow);
+ if (hiddenRow || (isLastRow && settings.lastRow === 'hide' && buildingRowRes === -1)) {
+ this.clearBuildingRow();
+ return;
+ }
+
+ if (this.maxRowHeight) {
+ if (this.maxRowHeight < this.buildingRow.height) {
+ this.buildingRow.height = this.maxRowHeight;
+ }
+ }
+
+ if (isLastRow && (settings.lastRow === 'center' || settings.lastRow === 'right')) {
+ let availableWidth = this.galleryWidth - 2 * this.border -
+ (this.buildingRow.entriesBuff.length - 1) * settings.margins;
+
+ for (i = 0; i < this.buildingRow.entriesBuff.length; i++) {
+ entry = this.buildingRow.entriesBuff[i];
+ availableWidth -= this.getData(entry, 'jg.jwidth');
+ }
+
+ if (settings.lastRow === 'center') {
+ offX += Math.round(availableWidth / 2);
+ } else if (settings.lastRow === 'right') {
+ offX += availableWidth;
+ }
+ }
+
+ const lastEntryIdx = this.buildingRow.entriesBuff.length - 1;
+ for (i = 0; i <= lastEntryIdx; i++) {
+ entry = this.buildingRow.entriesBuff[this.settings.rtl ? lastEntryIdx - i : i];
+ this.displayEntry(entry, offX, this.offY,
+ this.getData(entry, 'jg.jwidth'),
+ this.getData(entry, 'jg.jheight'),
+ this.buildingRow.height);
+ offX += this.getData(entry, 'jg.jwidth') + settings.margins;
+ }
+
+ this.galleryHeightToSet = this.offY + this.buildingRow.height + this.border;
+ this.setGalleryTempHeight(this.galleryHeightToSet + this.getSpinnerHeight());
+
+ if (!isLastRow || (this.buildingRow.height <= settings.rowHeight && buildingRowRes)) {
+ this.offY += this.buildingRow.height + settings.margins;
+ this.rows += 1;
+ this.clearBuildingRow();
+ this.settings.triggerEvent.call(this, 'jg.rowflush');
+ }
+ }
+
+ rememberGalleryHeight() {
+ this.galleryPrevStaticHeight = this.gallery.offsetHeight;
+ this.gallery.style.height = this.galleryPrevStaticHeight + 'px';
+ }
+
+ setGalleryTempHeight(height) {
+ this.galleryPrevStaticHeight = Math.max(height, this.galleryPrevStaticHeight);
+ this.gallery.style.height = this.galleryPrevStaticHeight + 'px';
+ }
+
+ setGalleryFinalHeight(height) {
+ this.galleryPrevStaticHeight = height;
+ this.gallery.style.height = height + 'px';
+ }
+
+
+ checkWidth() {
+ if (this.resizeObserver) return;
+ this.resizeObserver = new ResizeObserver(() => {
+ const isVisible = this.gallery.offsetParent !== null;
+ if (!isVisible) return;
+
+ const galleryWidth = parseFloat(this.gallery.offsetWidth);
+ if (Math.abs(galleryWidth - this.galleryWidth) > this.settings.refreshSensitivity) {
+ this.galleryWidth = galleryWidth;
+ this.rewind();
+ this.rememberGalleryHeight();
+ this.startImgAnalyzer(true);
+ }
+ });
+ this.resizeObserver.observe(this.gallery);
+ }
+
+ isSpinnerActive() {
+ return this.spinner.intervalId !== null;
+ }
+
+ getSpinnerHeight() {
+ return this.spinner.el.offsetHeight || 0;
+ }
+
+ stopLoadingSpinnerAnimation() {
+ clearInterval(this.spinner.intervalId);
+ this.spinner.intervalId = null;
+ this.setGalleryTempHeight(this.gallery.offsetHeight - this.getSpinnerHeight());
+ if (this.spinner.el.parentNode) {
+ this.spinner.el.parentNode.removeChild(this.spinner.el);
+ }
+ }
+
+ startLoadingSpinnerAnimation() {
+ const spinnerContext = this.spinner;
+ const spinnerPoints = spinnerContext.el.querySelectorAll('span');
+ clearInterval(spinnerContext.intervalId);
+ this.gallery.appendChild(spinnerContext.el);
+ this.setGalleryTempHeight(this.offY + this.buildingRow.height + this.getSpinnerHeight());
+ spinnerContext.intervalId = setInterval(() => {
+ if (spinnerContext.phase < spinnerPoints.length) {
+ utils.fadeTo(spinnerPoints[spinnerContext.phase], spinnerContext.timeSlot, 1);
+ } else {
+ utils.fadeTo(spinnerPoints[spinnerContext.phase - spinnerPoints.length], spinnerContext.timeSlot, 0);
+ }
+ spinnerContext.phase = (spinnerContext.phase + 1) % (spinnerPoints.length * 2);
+ }, spinnerContext.timeSlot);
+ }
+
+ rewind() {
+ this.lastFetchedEntry = null;
+ this.lastAnalyzedIndex = -1;
+ this.offY = this.border;
+ this.rows = 0;
+ this.clearBuildingRow();
+ }
+
+ getSelectorWithoutSpinner() {
+ return this.settings.selector + ', div:not(.jg-spinner)';
+ }
+
+ getAllEntries() {
+ const selectorParts = this.settings.selector.split(',').map(s => s.trim());
+ const allChildren = Array.from(this.gallery.children);
+ return allChildren.filter(child => {
+ if (child.classList && child.classList.contains('jg-spinner')) {
+ return false;
+ }
+ return selectorParts.some(selector => {
+ try {
+ return child.matches(selector);
+ } catch (e) {
+ return false;
+ }
+ });
+ });
+ }
+
+ updateEntries(norewind) {
+ let newEntries;
+
+ if (norewind && this.lastFetchedEntry != null) {
+ const selector = this.getSelectorWithoutSpinner();
+ let sibling = this.lastFetchedEntry.nextElementSibling;
+ newEntries = [];
+ while (sibling) {
+ if (sibling.matches(selector)) {
+ newEntries.push(sibling);
+ }
+ sibling = sibling.nextElementSibling;
+ }
+ } else {
+ this.entries = [];
+ newEntries = this.getAllEntries();
+ }
+
+ if (newEntries.length > 0) {
+ if (utils.isFunction(this.settings.sort)) {
+ newEntries = this.sortArray(newEntries);
+ } else if (this.settings.randomize) {
+ newEntries = this.shuffleArray(newEntries);
+ }
+ this.lastFetchedEntry = newEntries[newEntries.length - 1];
+
+ if (this.settings.filter) {
+ newEntries = this.filterArray(newEntries);
+ } else {
+ this.resetFilters(newEntries);
+ }
+ }
+
+ this.entries = this.entries.concat(newEntries);
+ return true;
+ }
+
+ insertToGallery(entries) {
+ entries.forEach(entry => {
+ this.gallery.appendChild(entry);
+ });
+ }
+
+ shuffleArray(a) {
+ for (let i = a.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [a[i], a[j]] = [a[j], a[i]];
+ }
+ this.insertToGallery(a);
+ return a;
+ }
+
+ sortArray(a) {
+ a.sort(this.settings.sort);
+ this.insertToGallery(a);
+ return a;
+ }
+
+ resetFilters(a) {
+ a.forEach(entry => utils.removeClass(entry, 'jg-filtered'));
+ }
+
+ filterArray(a) {
+ const settings = this.settings;
+ if (utils.isString(settings.filter)) {
+ return a.filter(el => {
+ if (el.matches(settings.filter)) {
+ utils.removeClass(el, 'jg-filtered');
+ return true;
+ } else {
+ utils.addClass(el, 'jg-filtered');
+ utils.removeClass(el, 'jg-visible');
+ return false;
+ }
+ });
+ } else if (utils.isFunction(settings.filter)) {
+ const filteredArr = a.filter(settings.filter);
+ a.forEach(entry => {
+ if (filteredArr.indexOf(entry) === -1) {
+ utils.addClass(entry, 'jg-filtered');
+ utils.removeClass(entry, 'jg-visible');
+ } else {
+ utils.removeClass(entry, 'jg-filtered');
+ }
+ });
+ return filteredArr;
+ }
+ }
+
+ resetImgSrc(img) {
+ if (this.getData(img, 'jg.originalSrcLoc') === 'src') {
+ img.setAttribute('src', this.getData(img, 'jg.originalSrc'));
+ } else {
+ img.setAttribute('src', '');
+ }
+ }
+
+ destroy() {
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ this.resizeObserver = null;
+ }
+ this.stopImgAnalyzerStarter();
+
+ this.getAllEntries().forEach(entry => {
+ entry.style.width = '';
+ entry.style.height = '';
+ entry.style.top = '';
+ entry.style.left = '';
+ this.removeData(entry, 'jg.loaded');
+ utils.removeClass(entry, 'jg-entry');
+ utils.removeClass(entry, 'jg-filtered');
+ utils.removeClass(entry, 'jg-entry-visible');
+
+ const img = this.imgFromEntry(entry);
+ if (img) {
+ img.style.width = '';
+ img.style.height = '';
+ img.style.marginLeft = '';
+ img.style.marginTop = '';
+ this.resetImgSrc(img);
+ this.removeData(img, 'jg.originalSrc');
+ this.removeData(img, 'jg.originalSrcLoc');
+ this.removeData(img, 'jg.src');
+ }
+
+ this.removeCaptionEventsHandlers(entry);
+ const caption = this.captionFromEntry(entry);
+ if (this.getData(entry, 'jg.createdCaption')) {
+ this.removeData(entry, 'jg.createdCaption');
+ if (caption !== null) caption.remove();
+ } else {
+ if (caption !== null) caption.style.opacity = 1;
+ }
+ });
+
+ this.gallery.style.height = '';
+ utils.removeClass(this.gallery, 'justified-gallery');
+ this.elementData.delete(this.gallery);
+ this.settings.triggerEvent.call(this, 'jg.destroy');
+ }
+
+ analyzeImages(isForResize) {
+ for (let i = this.lastAnalyzedIndex + 1; i < this.entries.length; i++) {
+ const entry = this.entries[i];
+ const loaded = this.getData(entry, 'jg.loaded');
+
+ if (loaded === true || loaded === 'skipped') {
+ const availableWidth = this.galleryWidth - 2 * this.border -
+ ((this.buildingRow.entriesBuff.length - 1) * this.settings.margins);
+ const imgAspectRatio = this.getData(entry, 'jg.width') / this.getData(entry, 'jg.height');
+
+ this.buildingRow.entriesBuff.push(entry);
+ this.buildingRow.aspectRatio += imgAspectRatio;
+ this.buildingRow.width += imgAspectRatio * this.settings.rowHeight;
+ this.lastAnalyzedIndex = i;
+
+ if (availableWidth / (this.buildingRow.aspectRatio + imgAspectRatio) < this.settings.rowHeight) {
+ this.flushRow(false, this.settings.maxRowsCount > 0 && this.rows === this.settings.maxRowsCount);
+
+ if (++this.yield.flushed >= this.yield.every) {
+ this.startImgAnalyzer(isForResize);
+ return;
+ }
+ }
+ } else if (loaded !== 'error') {
+ return;
+ }
+ }
+
+ if (this.buildingRow.entriesBuff.length > 0) {
+ this.flushRow(true, this.settings.maxRowsCount > 0 && this.rows === this.settings.maxRowsCount);
+ }
+
+ if (this.isSpinnerActive()) {
+ this.stopLoadingSpinnerAnimation();
+ }
+
+ this.stopImgAnalyzerStarter();
+ this.setGalleryFinalHeight(this.galleryHeightToSet);
+ this.settings.triggerEvent.call(this, isForResize ? 'jg.resize' : 'jg.complete');
+ }
+
+ stopImgAnalyzerStarter() {
+ this.yield.flushed = 0;
+ if (this.imgAnalyzerTimeout !== null) {
+ clearTimeout(this.imgAnalyzerTimeout);
+ this.imgAnalyzerTimeout = null;
+ }
+ }
+
+ startImgAnalyzer(isForResize) {
+ this.stopImgAnalyzerStarter();
+ this.imgAnalyzerTimeout = setTimeout(() => {
+ this.analyzeImages(isForResize);
+ }, 1);
+ }
+
+ onImageEvent(imageSrc, onLoad, onError) {
+ if (!onLoad && !onError) return;
+
+ const memImage = new Image();
+ let loadHandler, errorHandler;
+
+ if (onLoad) {
+ loadHandler = function () {
+ memImage.removeEventListener('load', loadHandler);
+ if (errorHandler) memImage.removeEventListener('error', errorHandler);
+ onLoad(memImage);
+ };
+ memImage.addEventListener('load', loadHandler);
+ }
+
+ if (onError) {
+ errorHandler = function () {
+ if (loadHandler) memImage.removeEventListener('load', loadHandler);
+ memImage.removeEventListener('error', errorHandler);
+ onError(memImage);
+ };
+ memImage.addEventListener('error', errorHandler);
+ }
+
+ memImage.src = imageSrc;
+ }
+
+ init() {
+ let imagesToLoad = false;
+ let skippedImages = false;
+
+ this.entries.forEach((entry) => {
+ const image = this.imgFromEntry(entry);
+
+ utils.addClass(entry, 'jg-entry');
+
+ if (this.getData(entry, 'jg.loaded') !== true && this.getData(entry, 'jg.loaded') !== 'skipped') {
+ if (this.settings.rel !== null) {
+ entry.setAttribute('rel', this.settings.rel);
+ }
+ if (this.settings.target !== null) {
+ entry.setAttribute('target', this.settings.target);
+ }
+
+ if (image !== null) {
+ const imageSrc = this.extractImgSrcFromImage(image);
+
+ if (this.settings.waitThumbnailsLoad === false || !imageSrc) {
+ let width = parseFloat(image.getAttribute('width'));
+ let height = parseFloat(image.getAttribute('height'));
+
+ if (image.tagName.toLowerCase() === 'svg') {
+ const bbox = image.getBBox();
+ width = parseFloat(bbox.width);
+ height = parseFloat(bbox.height);
+ }
+
+ if (!isNaN(width) && !isNaN(height)) {
+ this.setData(entry, 'jg.width', width);
+ this.setData(entry, 'jg.height', height);
+ this.setData(entry, 'jg.loaded', 'skipped');
+ skippedImages = true;
+ this.startImgAnalyzer(false);
+ return;
+ }
+ }
+
+ this.setData(entry, 'jg.loaded', false);
+ imagesToLoad = true;
+
+ if (!this.isSpinnerActive()) {
+ this.startLoadingSpinnerAnimation();
+ }
+
+ this.onImageEvent(imageSrc,
+ (loadImg) => {
+ this.setData(entry, 'jg.width', loadImg.width);
+ this.setData(entry, 'jg.height', loadImg.height);
+ this.setData(entry, 'jg.loaded', true);
+ this.startImgAnalyzer(false);
+ },
+ () => {
+ this.setData(entry, 'jg.loaded', 'error');
+ this.startImgAnalyzer(false);
+ }
+ );
+ } else {
+ this.setData(entry, 'jg.loaded', true);
+ this.setData(entry, 'jg.width',
+ entry.offsetWidth || parseFloat(getComputedStyle(entry).width) || 1);
+ this.setData(entry, 'jg.height',
+ entry.offsetHeight || parseFloat(getComputedStyle(entry).height) || 1);
+ }
+ }
+ });
+
+ if (!imagesToLoad && !skippedImages) {
+ this.startImgAnalyzer(false);
+ }
+ this.checkWidth();
+ }
+
+ checkOrConvertNumber(settingContainer, settingName) {
+ if (typeof settingContainer[settingName] === 'string') {
+ settingContainer[settingName] = parseFloat(settingContainer[settingName]);
+ }
+ if (typeof settingContainer[settingName] === 'number') {
+ if (isNaN(settingContainer[settingName])) {
+ throw 'invalid number for ' + settingName;
+ }
+ } else {
+ throw settingName + ' must be a number';
+ }
+ }
+
+ checkSizeRangesSuffixes() {
+ if (!utils.isObject(this.settings.sizeRangeSuffixes)) {
+ throw 'sizeRangeSuffixes must be defined and must be an object';
+ }
+ const suffixRanges = Object.keys(this.settings.sizeRangeSuffixes);
+ const newSizeRngSuffixes = { 0: '' };
+ suffixRanges.forEach(rangeIdx => {
+ if (typeof rangeIdx === 'string') {
+ try {
+ const numIdx = parseInt(rangeIdx.replace(/^[a-z]+/, ''), 10);
+ newSizeRngSuffixes[numIdx] = this.settings.sizeRangeSuffixes[rangeIdx];
+ } catch (e) {
+ throw 'sizeRangeSuffixes keys must contains correct numbers (' + e + ')';
+ }
+ } else {
+ newSizeRngSuffixes[rangeIdx] = this.settings.sizeRangeSuffixes[rangeIdx];
+ }
+ });
+ this.settings.sizeRangeSuffixes = newSizeRngSuffixes;
+ }
+
+ retrieveMaxRowHeight() {
+ let newMaxRowHeight = null;
+ const rowHeight = this.settings.rowHeight;
+
+ if (typeof this.settings.maxRowHeight === 'string') {
+ if (this.settings.maxRowHeight.match(/^[0-9]+%$/)) {
+ newMaxRowHeight = rowHeight * parseFloat(this.settings.maxRowHeight.match(/^([0-9]+)%$/)[1]) / 100;
+ } else {
+ newMaxRowHeight = parseFloat(this.settings.maxRowHeight);
+ }
+ } else if (typeof this.settings.maxRowHeight === 'number') {
+ newMaxRowHeight = this.settings.maxRowHeight;
+ } else if (this.settings.maxRowHeight === false || this.settings.maxRowHeight == null) {
+ return null;
+ } else {
+ throw 'maxRowHeight must be a number or a percentage';
+ }
+
+ if (isNaN(newMaxRowHeight)) {
+ throw 'invalid number for maxRowHeight';
+ }
+ if (newMaxRowHeight < rowHeight) {
+ newMaxRowHeight = rowHeight;
+ }
+ return newMaxRowHeight;
+ }
+
+ checkSettings() {
+ this.checkSizeRangesSuffixes();
+ this.checkOrConvertNumber(this.settings, 'rowHeight');
+ this.checkOrConvertNumber(this.settings, 'margins');
+ this.checkOrConvertNumber(this.settings, 'border');
+ this.checkOrConvertNumber(this.settings, 'maxRowsCount');
+
+ const lastRowModes = ['justify', 'nojustify', 'left', 'center', 'right', 'hide'];
+ if (lastRowModes.indexOf(this.settings.lastRow) === -1) {
+ throw 'lastRow must be one of: ' + lastRowModes.join(', ');
+ }
+
+ this.checkOrConvertNumber(this.settings, 'justifyThreshold');
+ if (this.settings.justifyThreshold < 0 || this.settings.justifyThreshold > 1) {
+ throw 'justifyThreshold must be in the interval [0,1]';
+ }
+
+ if (!utils.isBoolean(this.settings.cssAnimation)) {
+ throw 'cssAnimation must be a boolean';
+ }
+ if (!utils.isBoolean(this.settings.captions)) {
+ throw 'captions must be a boolean';
+ }
+
+ this.checkOrConvertNumber(this.settings.captionSettings, 'animationDuration');
+ this.checkOrConvertNumber(this.settings.captionSettings, 'visibleOpacity');
+ if (this.settings.captionSettings.visibleOpacity < 0 ||
+ this.settings.captionSettings.visibleOpacity > 1) {
+ throw 'captionSettings.visibleOpacity must be in the interval [0, 1]';
+ }
+
+ this.checkOrConvertNumber(this.settings.captionSettings, 'nonVisibleOpacity');
+ if (this.settings.captionSettings.nonVisibleOpacity < 0 ||
+ this.settings.captionSettings.nonVisibleOpacity > 1) {
+ throw 'captionSettings.nonVisibleOpacity must be in the interval [0, 1]';
+ }
+
+ this.checkOrConvertNumber(this.settings, 'imagesAnimationDuration');
+ this.checkOrConvertNumber(this.settings, 'refreshTime');
+ this.checkOrConvertNumber(this.settings, 'refreshSensitivity');
+
+ if (!utils.isBoolean(this.settings.randomize)) {
+ throw 'randomize must be a boolean';
+ }
+ if (!utils.isString(this.settings.selector)) {
+ throw 'selector must be a string';
+ }
+ if (this.settings.sort !== false && !utils.isFunction(this.settings.sort)) {
+ throw 'sort must be false or a comparison function';
+ }
+ if (this.settings.filter !== false &&
+ !utils.isFunction(this.settings.filter) &&
+ !utils.isString(this.settings.filter)) {
+ throw 'filter must be false, a string or a filter function';
+ }
+ }
+
+ retrieveSuffixRanges() {
+ const suffixRanges = Object.keys(this.settings.sizeRangeSuffixes)
+ .map(key => parseInt(key, 10));
+ suffixRanges.sort((a, b) => a - b);
+ return suffixRanges;
+ }
+
+ updateSettings(newSettings) {
+ this.settings = utils.extend({}, this.settings, newSettings);
+ this.checkSettings();
+ this.border = this.settings.border >= 0 ? this.settings.border : this.settings.margins;
+ this.maxRowHeight = this.retrieveMaxRowHeight();
+ this.suffixRanges = this.retrieveSuffixRanges();
+ }
+
+ get defaults() {
+ return {
+ sizeRangeSuffixes: {},
+ thumbnailPath: undefined,
+ rowHeight: 120,
+ maxRowHeight: false,
+ maxRowsCount: 0,
+ margins: 1,
+ border: -1,
+ lastRow: 'nojustify',
+ justifyThreshold: 0.90,
+ waitThumbnailsLoad: true,
+ captions: true,
+ cssAnimation: true,
+ imagesAnimationDuration: 500,
+ captionSettings: {
+ animationDuration: 500,
+ visibleOpacity: 0.7,
+ nonVisibleOpacity: 0.0
+ },
+ rel: null,
+ target: null,
+ extension: /\.[^.\\/]+$/,
+ refreshTime: 200,
+ refreshSensitivity: 0,
+ randomize: false,
+ rtl: false,
+ sort: false,
+ filter: false,
+ selector: 'a',
+ imgSelector: '> img, > a > img, > svg, > a > svg',
+ triggerEvent: function (event) {
+ utils.trigger(this.gallery, event);
+ }
+ };
+ }
+ }
+
+ function justifiedGallery(selector, arg) {
+ const galleries = typeof selector === 'string'
+ ? document.querySelectorAll(selector)
+ : (selector instanceof NodeList || Array.isArray(selector))
+ ? selector
+ : [selector];
+
+ Array.from(galleries).forEach((gallery) => {
+ utils.addClass(gallery, 'justified-gallery');
+
+ let controller = gallery._jgController;
+
+ if (typeof controller === 'undefined') {
+ if (typeof arg !== 'undefined' && arg !== null && !utils.isObject(arg)) {
+ if (arg === 'destroy') return;
+ throw 'The argument must be an object';
+ }
+ controller = new JustifiedGallery(gallery, arg);
+ gallery._jgController = controller;
+ } else if (arg === 'norewind') {
+ // Don't rewind
+ } else if (arg === 'destroy') {
+ controller.destroy();
+ delete gallery._jgController;
+ return;
+ } else {
+ controller.updateSettings(arg);
+ controller.rewind();
+ }
+
+ if (!controller.updateEntries(arg === 'norewind')) return;
+ controller.init();
+ });
+
+ return galleries;
+ }
+
+ return justifiedGallery;
+})();
+
+window.justifiedGallery = justifiedGallery;
diff --git a/scripts/oneko.js b/scripts/oneko.js
new file mode 100644
index 0000000..671d48c
--- /dev/null
+++ b/scripts/oneko.js
@@ -0,0 +1,279 @@
+// oneko.js: https://github.com/adryd325/oneko.js
+
+(function oneko() {
+ const isReducedMotion =
+ window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
+ window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
+
+ if (isReducedMotion) return;
+
+ const nekoEl = document.createElement("div");
+ let persistPosition = true;
+
+ let nekoPosX = 32;
+ let nekoPosY = 32;
+
+ let mousePosX = 0;
+ let mousePosY = 0;
+
+ let frameCount = 0;
+ let idleTime = 0;
+ let idleAnimation = null;
+ let idleAnimationFrame = 0;
+
+ const nekoSpeed = 10;
+ const spriteSets = {
+ idle: [[-3, -3]],
+ alert: [[-7, -3]],
+ scratchSelf: [
+ [-5, 0],
+ [-6, 0],
+ [-7, 0],
+ ],
+ scratchWallN: [
+ [0, 0],
+ [0, -1],
+ ],
+ scratchWallS: [
+ [-7, -1],
+ [-6, -2],
+ ],
+ scratchWallE: [
+ [-2, -2],
+ [-2, -3],
+ ],
+ scratchWallW: [
+ [-4, 0],
+ [-4, -1],
+ ],
+ tired: [[-3, -2]],
+ sleeping: [
+ [-2, 0],
+ [-2, -1],
+ ],
+ N: [
+ [-1, -2],
+ [-1, -3],
+ ],
+ NE: [
+ [0, -2],
+ [0, -3],
+ ],
+ E: [
+ [-3, 0],
+ [-3, -1],
+ ],
+ SE: [
+ [-5, -1],
+ [-5, -2],
+ ],
+ S: [
+ [-6, -3],
+ [-7, -2],
+ ],
+ SW: [
+ [-5, -3],
+ [-6, -1],
+ ],
+ W: [
+ [-4, -2],
+ [-4, -3],
+ ],
+ NW: [
+ [-1, 0],
+ [-1, -1],
+ ],
+ };
+
+ function init() {
+ let nekoFile = "./oneko.gif"
+ const curScript = document.currentScript
+ if (curScript && curScript.dataset.cat) {
+ nekoFile = curScript.dataset.cat
+ }
+ if (curScript && curScript.dataset.persistPosition) {
+ if (curScript.dataset.persistPosition === "") {
+ persistPosition = true;
+ } else {
+ persistPosition = JSON.parse(curScript.dataset.persistPosition.toLowerCase());
+ }
+ }
+
+ if (persistPosition) {
+ let storedNeko = JSON.parse(window.localStorage.getItem("oneko"));
+ if (storedNeko !== null) {
+ nekoPosX = storedNeko.nekoPosX;
+ nekoPosY = storedNeko.nekoPosY;
+ mousePosX = storedNeko.mousePosX;
+ mousePosY = storedNeko.mousePosY;
+ frameCount = storedNeko.frameCount;
+ idleTime = storedNeko.idleTime;
+ idleAnimation = storedNeko.idleAnimation;
+ idleAnimationFrame = storedNeko.idleAnimationFrame;
+ nekoEl.style.backgroundPosition = storedNeko.bgPos;
+ }
+ }
+
+ nekoEl.id = "oneko";
+ nekoEl.ariaHidden = true;
+ nekoEl.style.width = "32px";
+ nekoEl.style.height = "32px";
+ nekoEl.style.position = "fixed";
+ nekoEl.style.pointerEvents = "none";
+ nekoEl.style.imageRendering = "pixelated";
+ nekoEl.style.left = `${nekoPosX - 16}px`;
+ nekoEl.style.top = `${nekoPosY - 16}px`;
+ nekoEl.style.zIndex = 2147483647;
+
+ nekoEl.style.backgroundImage = `url(${nekoFile})`;
+
+ document.body.appendChild(nekoEl);
+
+ document.addEventListener("mousemove", function (event) {
+ mousePosX = event.clientX;
+ mousePosY = event.clientY;
+ });
+
+ if (persistPosition) {
+ window.addEventListener("beforeunload", function (event) {
+ window.localStorage.setItem("oneko", JSON.stringify({
+ nekoPosX: nekoPosX,
+ nekoPosY: nekoPosY,
+ mousePosX: mousePosX,
+ mousePosY: mousePosY,
+ frameCount: frameCount,
+ idleTime: idleTime,
+ idleAnimation: idleAnimation,
+ idleAnimationFrame: idleAnimationFrame,
+ bgPos: nekoEl.style.backgroundPosition
+ }));
+ });
+ }
+
+ window.requestAnimationFrame(onAnimationFrame);
+ }
+
+ let lastFrameTimestamp;
+
+ function onAnimationFrame(timestamp) {
+ // Stops execution if the neko element is removed from DOM
+ if (!nekoEl.isConnected) {
+ return;
+ }
+ if (!lastFrameTimestamp) {
+ lastFrameTimestamp = timestamp;
+ }
+ if (timestamp - lastFrameTimestamp > 100) {
+ lastFrameTimestamp = timestamp;
+ frame();
+ }
+ window.requestAnimationFrame(onAnimationFrame);
+ }
+
+ function setSprite(name, frame) {
+ const sprite = spriteSets[name][frame % spriteSets[name].length];
+ nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
+ }
+
+ function resetIdleAnimation() {
+ idleAnimation = null;
+ idleAnimationFrame = 0;
+ }
+
+ function idle() {
+ idleTime += 1;
+
+ // every ~ 20 seconds
+ if (
+ idleTime > 10 &&
+ Math.floor(Math.random() * 200) == 0 &&
+ idleAnimation == null
+ ) {
+ let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
+ if (nekoPosX < 32) {
+ avalibleIdleAnimations.push("scratchWallW");
+ }
+ if (nekoPosY < 32) {
+ avalibleIdleAnimations.push("scratchWallN");
+ }
+ if (nekoPosX > window.innerWidth - 32) {
+ avalibleIdleAnimations.push("scratchWallE");
+ }
+ if (nekoPosY > window.innerHeight - 32) {
+ avalibleIdleAnimations.push("scratchWallS");
+ }
+ idleAnimation =
+ avalibleIdleAnimations[
+ Math.floor(Math.random() * avalibleIdleAnimations.length)
+ ];
+ }
+
+ switch (idleAnimation) {
+ case "sleeping":
+ if (idleAnimationFrame < 8) {
+ setSprite("tired", 0);
+ break;
+ }
+ setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
+ if (idleAnimationFrame > 192) {
+ resetIdleAnimation();
+ }
+ break;
+ case "scratchWallN":
+ case "scratchWallS":
+ case "scratchWallE":
+ case "scratchWallW":
+ case "scratchSelf":
+ setSprite(idleAnimation, idleAnimationFrame);
+ if (idleAnimationFrame > 9) {
+ resetIdleAnimation();
+ }
+ break;
+ default:
+ setSprite("idle", 0);
+ return;
+ }
+ idleAnimationFrame += 1;
+ }
+
+ function frame() {
+ frameCount += 1;
+ const diffX = nekoPosX - mousePosX;
+ const diffY = nekoPosY - mousePosY;
+ const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
+
+ if (distance < nekoSpeed || distance < 48) {
+ idle();
+ return;
+ }
+
+ idleAnimation = null;
+ idleAnimationFrame = 0;
+
+ if (idleTime > 1) {
+ setSprite("alert", 0);
+ // count down after being alerted before moving
+ idleTime = Math.min(idleTime, 7);
+ idleTime -= 1;
+ return;
+ }
+
+ let direction;
+ direction = diffY / distance > 0.5 ? "N" : "";
+ direction += diffY / distance < -0.5 ? "S" : "";
+ direction += diffX / distance > 0.5 ? "W" : "";
+ direction += diffX / distance < -0.5 ? "E" : "";
+ setSprite(direction, frameCount);
+
+ nekoPosX -= (diffX / distance) * nekoSpeed;
+ nekoPosY -= (diffY / distance) * nekoSpeed;
+
+ nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
+ nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
+
+ nekoEl.style.left = `${nekoPosX - 16}px`;
+ nekoEl.style.top = `${nekoPosY - 16}px`;
+ }
+
+ init();
+})();