From 789f4638dc3515c42b50dc4ebcc6087ad4f2148c Mon Sep 17 00:00:00 2001 From: sophie Date: Thu, 19 Sep 2024 12:57:38 +0300 Subject: [PATCH] silly billy cursor --- bun.lockb | Bin 28316 -> 30530 bytes package.json | 2 + website/assets/cursor.ani | Bin 0 -> 81720 bytes website/index.html | 1 + website/scripts/ani_parser.ts | 225 ++++++++++++++++++++++++++++++++++ website/scripts/cursor.ts | 8 ++ website/scripts/util.ts | 2 +- 7 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 website/assets/cursor.ani create mode 100644 website/scripts/ani_parser.ts create mode 100644 website/scripts/cursor.ts diff --git a/bun.lockb b/bun.lockb index 4508eccb62436a116c5f10b25051db73c6ddae34..0b1fbf6c4afbc3a9a7cb53707cd2a785a3a5caee 100755 GIT binary patch delta 4876 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 + diff --git a/website/scripts/ani_parser.ts b/website/scripts/ani_parser.ts new file mode 100644 index 0000000..7fc7baf --- /dev/null +++ b/website/scripts/ani_parser.ts @@ -0,0 +1,225 @@ +// Code by +// https://www.npmjs.com/package/ani-cursor + +// When importing the NPM package it just doesn't work. +// The babel version is so old it doesn't work on my browser. + +import { RIFFFile } from "riff-file"; +import { unpackArray, unpackString } from "byte-data"; + +type Chunk = { + format: string; + chunkId: string; + chunkData: { + start: number; + end: number; + }; + subChunks: Chunk[]; +}; + +// https://www.informit.com/articles/article.aspx?p=1189080&seqNum=3 +type AniMetadata = { + cbSize: number; // Data structure size (in bytes) + nFrames: number; // Number of images (also known as frames) stored in the file + nSteps: number; // Number of frames to be displayed before the animation repeats + iWidth: number; // Width of frame (in pixels) + iHeight: number; // Height of frame (in pixels) + iBitCount: number; // Number of bits per pixel + nPlanes: number; // Number of color planes + iDispRate: number; // Default frame display rate (measured in 1/60th-of-a-second units) + bfAttributes: number; // ANI attribute bit flags +}; + +type ParsedAni = { + rate: number[] | null; + seq: number[] | null; + images: Uint8Array[]; + metadata: AniMetadata; + artist: string | null; + title: string | null; +}; + +const DWORD = { bits: 32, be: false, signed: false, fp: false }; + +export function parseAni(arr: Uint8Array): ParsedAni { + const riff = new RIFFFile(); + + riff.setSignature(arr); + + const signature = riff.signature as Chunk; + if (signature.format !== "ACON") { + throw new Error( + `Expected format. Expected "ACON", got "${signature.format}"` + ); + } + + // Helper function to get a chunk by chunkId and transform it if it's non-null. + function mapChunk(chunkId: string, mapper: (chunk: Chunk) => T): T | null { + const chunk = riff.findChunk(chunkId) as Chunk | null; + return chunk == null ? null : mapper(chunk); + } + + function readImages(chunk: Chunk, frameCount: number): Uint8Array[] { + return chunk.subChunks.slice(0, frameCount).map((c) => { + if (c.chunkId !== "icon") { + throw new Error(`Unexpected chunk type in fram: ${c.chunkId}`); + } + return arr.slice(c.chunkData.start, c.chunkData.end); + }); + } + + const metadata = mapChunk("anih", (c) => { + const words = unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); + return { + cbSize: words[0], + nFrames: words[1], + nSteps: words[2], + iWidth: words[3], + iHeight: words[4], + iBitCount: words[5], + nPlanes: words[6], + iDispRate: words[7], + bfAttributes: words[8], + }; + }); + + if (metadata == null) { + throw new Error("Did not find anih"); + } + + const rate = mapChunk("rate", (c) => { + return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); + }); + // chunkIds are always four chars, hence the trailing space. + const seq = mapChunk("seq ", (c) => { + return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); + }); + + const lists = riff.findChunk("LIST", true) as Chunk[] | null; + const imageChunk = lists?.find((c) => c.format === "fram"); + if (imageChunk == null) { + throw new Error("Did not find fram LIST"); + } + + let images = readImages(imageChunk, metadata.nFrames); + + let title: string | null = null; + let artist: string | null = null; + + const infoChunk = lists?.find((c) => c.format === "INFO"); + if (infoChunk != null) { + infoChunk.subChunks.forEach((c) => { + switch (c.chunkId) { + case "INAM": + title = unpackString(arr, c.chunkData.start, c.chunkData.end); + break; + case "IART": + artist = unpackString(arr, c.chunkData.start, c.chunkData.end); + break; + case "LIST": + // Some cursors with an artist of "Created with Take ONE 3.5 (unregisterred version)" seem to have their frames here for some reason? + if (c.format === "fram") { + images = readImages(c, metadata.nFrames); + } + break; + + default: + // Unexpected subchunk + } + }); + } + + return { images, rate, seq, metadata, artist, title }; +} + +type AniCursorImage = { + frames: { + url: string; + percents: number[]; + }[]; + duration: number; +}; + +const JIFFIES_PER_MS = 1000 / 60; + +// Generate CSS for an animated cursor. +// +// This function returns CSS containing a set of keyframes with embedded Data +// URIs as well as a CSS rule to the given selector. +export function convertAniBinaryToCSS( + selector: string, + aniBinary: Uint8Array +): string { + const ani = readAni(aniBinary); + + const animationName = `ani-cursor-${uniqueId()}`; + + const keyframes = ani.frames.map(({ url, percents }) => { + const percent = percents.map((num) => `${num}%`).join(", "); + return `${percent} { cursor: url(${url}), auto; }`; + }); + + // CSS properties with a animation type of "discrete", like `cursor`, actually + // switch half-way _between_ each keyframe percentage. Luckily this half-way + // measurement is applied _after_ the easing function is applied. So, we can + // force the frames to appear at exactly the % that we specify by using + // `timing-function` of `step-end`. + // + // https://drafts.csswg.org/web-animations-1/#discrete + const timingFunction = "step-end"; + + // Winamp (re)starts the animation cycle when your mouse enters an element. By + // default this approach would cause the animation to run continuously, even + // when the cursor is not visible. To match Winamp's behavior we add a + // `:hover` pseudo selector so that the animation only runs when the cursor is + // visible. + const pseudoSelector = ":hover"; + + // prettier-ignore + return ` + @keyframes ${animationName} { + ${keyframes.join("\n")} + } + ${selector}${pseudoSelector} { + animation: ${animationName} ${ani.duration}ms ${timingFunction} infinite; + } + `; +} + +function readAni(contents: Uint8Array): AniCursorImage { + const ani = parseAni(contents); + const rate = ani.rate ?? ani.images.map(() => ani.metadata.iDispRate); + const duration = sum(rate); + + const frames = ani.images.map((image) => ({ + url: curUrlFromByteArray(image), + percents: [] as number[], + })); + + let elapsed = 0; + rate.forEach((r, i) => { + const frameIdx = ani.seq ? ani.seq[i] : i; + frames[frameIdx].percents.push((elapsed / duration) * 100); + elapsed += r; + }); + + return { duration: duration * JIFFIES_PER_MS, frames }; +} + +/* Utility Functions */ + +let i = 0; +const uniqueId = () => i++; + +function base64FromDataArray(dataArray: Uint8Array): string { + return globalThis.window ? window.btoa(String.fromCharCode(...dataArray)) : Buffer.from(dataArray).toString("base64"); +} + +function curUrlFromByteArray(arr: Uint8Array) { + const base64 = base64FromDataArray(arr); + return `data:image/x-win-bitmap;base64,${base64}`; +} + +function sum(values: number[]): number { + return values.reduce((total, value) => total + value, 0); +} \ No newline at end of file diff --git a/website/scripts/cursor.ts b/website/scripts/cursor.ts new file mode 100644 index 0000000..2e14733 --- /dev/null +++ b/website/scripts/cursor.ts @@ -0,0 +1,8 @@ +import { convertAniBinaryToCSS } from "./ani_parser"; +(async () => { + const req = await fetch("/assets/cursor.ani"); + const buf = new Uint8Array(await req.arrayBuffer()); + const style = document.createElement("style"); + style.innerHTML = convertAniBinaryToCSS("body", buf); + document.body.appendChild(style); +})(); diff --git a/website/scripts/util.ts b/website/scripts/util.ts index 9b541ae..28a7c55 100644 --- a/website/scripts/util.ts +++ b/website/scripts/util.ts @@ -18,4 +18,4 @@ export function timeAgo(input: number | Date) { return formatter.format(Math.round(delta), rangeType); } } -} +} \ No newline at end of file