summaryrefslogtreecommitdiff
path: root/src/js
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-12-14 16:14:29 -0800
committerElizabeth Hunt <me@liz.coffee>2025-12-14 16:17:29 -0800
commit8ec712c8c884110600954860c21f58107455cfdc (patch)
tree5e5b16ec8b0a1d15d58beae5bc8a7fd5285c6d0e /src/js
parentdb0d9b80b4412a46cae0e58997f4baa7213948e3 (diff)
downloadadelie-8ec712c8c884110600954860c21f58107455cfdc.tar.gz
adelie-8ec712c8c884110600954860c21f58107455cfdc.zip
Move to typescript
Diffstat (limited to 'src/js')
-rw-r--r--src/js/oneko.js284
-rw-r--r--src/js/oneko.ts277
-rw-r--r--src/js/script.js87
-rw-r--r--src/js/script.ts78
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();
+});