This commit is contained in:
parent
789f4638dc
commit
10239c13b7
BIN
website/assets/binkies/lel.gif
Normal file
BIN
website/assets/binkies/lel.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 479 KiB |
Binary file not shown.
|
@ -7,7 +7,6 @@ __TEMPLATE_HEAD__
|
|||
<script type="module" src="scripts/lastfm.js"></script>
|
||||
<script type="module" src="scripts/binkies.js"></script>
|
||||
<script type="module" src="scripts/commit.js"></script>
|
||||
<script type="module" src="scripts/cursor.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
|
|
|
@ -1,225 +0,0 @@
|
|||
// 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<T>(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);
|
||||
}
|
|
@ -25,7 +25,8 @@ const binky = [
|
|||
["https://bee.nekoweb.org/", "bee.png"],
|
||||
["https://ultrakill.nekoweb.org/", "ultrakill.png"],
|
||||
["https://immjs.dev", "immjs.gif"],
|
||||
|
||||
["https://lel.nekoweb.org", "lel.gif"],
|
||||
|
||||
["insane.gif", "piracy.gif", "dsb.gif", "universe.gif", "css.png", "vscbutton.gif", "ezgif.gif"],
|
||||
];
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
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);
|
||||
})();
|
Loading…
Reference in a new issue