From b7b8e0ea6227309240b1f181f6d7568375fc8fa9 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 9 Oct 2026 02:48:17 -0300 Subject: hello --- assets/bweeeeghg.jpg | Bin 0 -> 22174 bytes assets/fagotni.gif | Bin 0 -> 309128 bytes assets/favicon.jpg | Bin 0 -> 36023 bytes assets/hiissssssss.png | Bin 0 -> 69079 bytes assets/oneko.gif | Bin 0 -> 3316 bytes index.html | 276 ++++++++++ justifiedGallery.css | 167 +++++++ scripts/justifiedgallery.js | 1167 +++++++++++++++++++++++++++++++++++++++++++ scripts/oneko.js | 279 +++++++++++ 9 files changed, 1889 insertions(+) create mode 100644 assets/bweeeeghg.jpg create mode 100644 assets/fagotni.gif create mode 100644 assets/favicon.jpg create mode 100644 assets/hiissssssss.png create mode 100644 assets/oneko.gif create mode 100644 index.html create mode 100644 justifiedGallery.css create mode 100644 scripts/justifiedgallery.js create mode 100644 scripts/oneko.js diff --git a/assets/bweeeeghg.jpg b/assets/bweeeeghg.jpg new file mode 100644 index 0000000..7f44257 Binary files /dev/null and b/assets/bweeeeghg.jpg differ diff --git a/assets/fagotni.gif b/assets/fagotni.gif new file mode 100644 index 0000000..442ca00 Binary files /dev/null and b/assets/fagotni.gif differ diff --git a/assets/favicon.jpg b/assets/favicon.jpg new file mode 100644 index 0000000..e247547 Binary files /dev/null and b/assets/favicon.jpg differ diff --git a/assets/hiissssssss.png b/assets/hiissssssss.png new file mode 100644 index 0000000..c4c5067 Binary files /dev/null and b/assets/hiissssssss.png differ diff --git a/assets/oneko.gif b/assets/oneko.gif new file mode 100644 index 0000000..a009c2c Binary files /dev/null and b/assets/oneko.gif 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 @@ + + + + + +FAGGOOTTTTT + + + + + +
+ if you are seeing this something went horribly wrong +
+
+
david@faggot-chan:~$ fastfetch
+
+ +
+
FAGGOOTTTTT
+
---------------
+
gf twitterFlatchestMoe
+ + + + +
+
+
+
+
+ + + +
+
+
+
all my twitter bookmarks!!!!!
+ +
(some aren't teto, i still have to cleanup and name them)
+
+
+ + + +
+ + +
+
+
+ + + + + + 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(); +})(); -- cgit v1.3-2-g0d8e