diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2025-12-14 16:14:29 -0800 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2025-12-14 16:17:29 -0800 |
| commit | 8ec712c8c884110600954860c21f58107455cfdc (patch) | |
| tree | 5e5b16ec8b0a1d15d58beae5bc8a7fd5285c6d0e /src/js | |
| parent | db0d9b80b4412a46cae0e58997f4baa7213948e3 (diff) | |
| download | adelie-8ec712c8c884110600954860c21f58107455cfdc.tar.gz adelie-8ec712c8c884110600954860c21f58107455cfdc.zip | |
Move to typescript
Diffstat (limited to 'src/js')
| -rw-r--r-- | src/js/oneko.js | 284 | ||||
| -rw-r--r-- | src/js/oneko.ts | 277 | ||||
| -rw-r--r-- | src/js/script.js | 87 | ||||
| -rw-r--r-- | src/js/script.ts | 78 |
4 files changed, 355 insertions, 371 deletions
diff --git a/src/js/oneko.js b/src/js/oneko.js deleted file mode 100644 index dcf927f..0000000 --- a/src/js/oneko.js +++ /dev/null @@ -1,284 +0,0 @@ -// oneko.js: https://github.com/adryd325/oneko.js - -export function initOneko() { - 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/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(); -} diff --git a/src/js/oneko.ts b/src/js/oneko.ts new file mode 100644 index 0000000..236c7cb --- /dev/null +++ b/src/js/oneko.ts @@ -0,0 +1,277 @@ +// oneko.js: https://github.com/adryd325/oneko.js + +export function initOneko(): void { + 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: string | null = null; + let idleAnimationFrame = 0; + + const nekoSpeed = 10; + const spriteSets: Record<string, [number, number][]> = { + 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(): void { + const assetBase = window.ASSET_BASE || ''; + let nekoFile = `${assetBase}/oneko/oneko.gif`; + const curScript = document.currentScript as HTMLScriptElement; + if (curScript?.dataset.cat) { + nekoFile = curScript.dataset.cat; + } + if (curScript?.dataset.persistPosition) { + if (curScript.dataset.persistPosition === '') { + persistPosition = true; + } else { + persistPosition = JSON.parse(curScript.dataset.persistPosition.toLowerCase()); + } + } + + if (persistPosition) { + const storedNekoStr = window.localStorage.getItem('oneko'); + const storedNeko = storedNekoStr ? JSON.parse(storedNekoStr) : null; + 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', (event: MouseEvent) => { + mousePosX = event.clientX; + mousePosY = event.clientY; + }); + + if (persistPosition) { + window.addEventListener('beforeunload', () => { + 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: number | undefined; + + function onAnimationFrame(timestamp: number): void { + if (!nekoEl.isConnected) { + return; + } + if (!lastFrameTimestamp) { + lastFrameTimestamp = timestamp; + } + if (lastFrameTimestamp && timestamp - lastFrameTimestamp > 100) { + lastFrameTimestamp = timestamp; + frame(); + } + window.requestAnimationFrame(onAnimationFrame); + } + + function setSprite(name: string, frame: number): void { + const sprite = spriteSets[name][frame % spriteSets[name].length]; + nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`; + } + + function resetIdleAnimation(): void { + idleAnimation = null; + idleAnimationFrame = 0; + } + + function idle(): void { + idleTime += 1; + + // every ~ 20 seconds + if (idleTime > 10 && Math.floor(Math.random() * 200) === 0 && idleAnimation == null) { + const avalibleIdleAnimations: string[] = ['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(): void { + 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(); +} diff --git a/src/js/script.js b/src/js/script.js deleted file mode 100644 index 8ba3d82..0000000 --- a/src/js/script.js +++ /dev/null @@ -1,87 +0,0 @@ -import Prism from 'prismjs'; -import 'prismjs/components/prism-javascript'; -import 'prismjs/components/prism-css'; -import 'prismjs/components/prism-markup'; -import { initOneko } from './oneko.js'; - -(() => { - const toggleButton = document.getElementById("theme-toggle"); - const html = document.documentElement; - - const sessionTheme = sessionStorage.getItem("theme"); - const systemPrefersDark = window.matchMedia( - "(prefers-color-scheme: dark)", - ).matches; - - const initialTheme = sessionTheme || (systemPrefersDark ? "dark" : "light"); - - if (initialTheme === "dark") { - html.setAttribute("data-theme", "dark"); - toggleButton.checked = true; - } - - toggleButton.addEventListener("change", () => { - const theme = html.getAttribute("data-theme"); - - if (theme === "dark") { - html.removeAttribute("data-theme"); - sessionStorage.setItem("theme", "light"); - toggleButton.checked = false; - } else { - html.setAttribute("data-theme", "dark"); - sessionStorage.setItem("theme", "dark"); - toggleButton.checked = true; - } - }); -})(); - -(() => { - const colors = [ - "#ff69b4", - "#b19cd9", - "#8b6f47", - "#ff85c0", - "#c4b5fd", - "#d4a574", - ]; - const shapes = ["❀", "✿", "✽", "✾", "✻", "❊", "❋", "✼"]; - - document.addEventListener("mousemove", (e) => { - createParticle(e.clientX, e.clientY); - }); - - const createParticle = (x, y) => { - const particle = document.createElement("div"); - particle.className = "fairy-dust"; - - const shape = shapes[Math.floor(Math.random() * shapes.length)]; - const size = Math.random() * 8 + 6; - const color = colors[Math.floor(Math.random() * colors.length)]; - const offsetX = (Math.random() - 0.5) * 20; - const offsetY = (Math.random() - 0.5) * 20; - const rotation = Math.random() * 360; - - particle.textContent = shape; - particle.style.cssText = ` - position: fixed; - left: ${x + offsetX}px; - top: ${y + offsetY}px; - font-size: ${size}px; - color: ${color}; - opacity: 0.4; - pointer-events: none; - z-index: 9001; /* it's over 9000 */ - line-height: 1; - transform: rotate(${rotation}deg); - animation: fairy-float 0.8s ease-out forwards; - `; - - document.body.appendChild(particle); - setTimeout(() => particle.remove(), 800); - }; -})(); - -document.addEventListener('DOMContentLoaded', () => { - Prism.highlightAll(); - initOneko(); -}); diff --git a/src/js/script.ts b/src/js/script.ts new file mode 100644 index 0000000..e0b0b85 --- /dev/null +++ b/src/js/script.ts @@ -0,0 +1,78 @@ +import Prism from 'prismjs'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-css'; +import 'prismjs/components/prism-markup'; +import { initOneko } from './oneko'; + +(() => { + const toggleButton = document.getElementById('theme-toggle') as HTMLInputElement; + const html = document.documentElement; + + const sessionTheme = sessionStorage.getItem('theme'); + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + + const initialTheme = sessionTheme || (systemPrefersDark ? 'dark' : 'light'); + + if (initialTheme === 'dark') { + html.setAttribute('data-theme', 'dark'); + toggleButton.checked = true; + } + + toggleButton.addEventListener('change', () => { + const theme = html.getAttribute('data-theme'); + + if (theme === 'dark') { + html.removeAttribute('data-theme'); + sessionStorage.setItem('theme', 'light'); + toggleButton.checked = false; + } else { + html.setAttribute('data-theme', 'dark'); + sessionStorage.setItem('theme', 'dark'); + toggleButton.checked = true; + } + }); +})(); + +(() => { + const colors = ['#ff69b4', '#b19cd9', '#8b6f47', '#ff85c0', '#c4b5fd', '#d4a574']; + const shapes = ['❀', '✿', '✽', '✾', '✻', '❊', '❋', '✼']; + + document.addEventListener('mousemove', (e: MouseEvent) => { + createParticle(e.clientX, e.clientY); + }); + + const createParticle = (x: number, y: number) => { + const particle = document.createElement('div'); + particle.className = 'fairy-dust'; + + const shape = shapes[Math.floor(Math.random() * shapes.length)]; + const size = Math.random() * 8 + 6; + const color = colors[Math.floor(Math.random() * colors.length)]; + const offsetX = (Math.random() - 0.5) * 20; + const offsetY = (Math.random() - 0.5) * 20; + const rotation = Math.random() * 360; + + particle.textContent = shape; + particle.style.cssText = ` + position: fixed; + left: ${x + offsetX}px; + top: ${y + offsetY}px; + font-size: ${size}px; + color: ${color}; + opacity: 0.4; + pointer-events: none; + z-index: 9001; /* it's over 9000 */ + line-height: 1; + transform: rotate(${rotation}deg); + animation: fairy-float 0.8s ease-out forwards; + `; + + document.body.appendChild(particle); + setTimeout(() => particle.remove(), 800); + }; +})(); + +document.addEventListener('DOMContentLoaded', () => { + Prism.highlightAll(); + initOneko(); +}); |
