diff options
| author | david <[email protected]> | 2026-10-09 02:48:17 -0300 |
|---|---|---|
| committer | david <[email protected]> | 2026-10-09 02:48:17 -0300 |
| commit | b7b8e0ea6227309240b1f181f6d7568375fc8fa9 (patch) | |
| tree | e52bbeb323c56e0997dc89d7ee686d02aaf36a48 /scripts | |
| download | webpage-b7b8e0ea6227309240b1f181f6d7568375fc8fa9.tar.gz webpage-b7b8e0ea6227309240b1f181f6d7568375fc8fa9.zip | |
hello
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/justifiedgallery.js | 1167 | ||||
| -rw-r--r-- | scripts/oneko.js | 279 |
2 files changed, 1446 insertions, 0 deletions
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(); +})(); |
