Compare commits

...

4 Commits

Author SHA1 Message Date
Dustin Brett
492b34e92e Pkg upgrades
Some checks failed
Tests / tests (push) Has been cancelled
2025-09-07 19:55:44 -07:00
Dustin Brett
839c9a9061 Fix ani to gif fps rate 2025-09-07 19:46:25 -07:00
Dustin Brett
8f9eae5027 Another idea 2025-09-07 19:30:39 -07:00
Dustin Brett
dcea214934 Animate cursor support 2025-09-07 19:30:22 -07:00
5 changed files with 517 additions and 457 deletions

View File

@ -319,7 +319,7 @@
- [Xash3D-Emscripten](https://github.com/btarg/Xash3D-Emscripten)
- Minesweeper ([1](https://github.com/ziebelje/minesweeper), [2](https://github.com/ShizukuIchi/minesweeper))
- [S.U.R.F.](https://github.com/jackbuehner/MicrosoftEdge-S.U.R.F.)
- Game of Life ([1](https://github.com/rustwasm/wasm_game_of_life), [2](https://github.com/skeeto/webgl-game-of-life/))
- Game of Life ([1](https://github.com/copy/life), [2](https://github.com/rustwasm/wasm_game_of_life), [3](https://github.com/skeeto/webgl-game-of-life/))
- Solitaire ([1](https://github.com/rjanjic/js-solitaire), [2](https://github.com/1j01/98/tree/master/programs/js-solitaire))
- [Heroes of Might and Magic II](https://github.com/ihhub/fheroes2)
- [Doom 3](https://wasm.continuation-labs.com/d3demo/)

View File

@ -11,7 +11,6 @@ import {
PACKAGE_DATA,
} from "utils/constants";
import {
bufferToUrl,
getDpi,
getExtension,
getMimeType,
@ -53,22 +52,13 @@ const Metadata: FC = () => {
);
const getCursor = useCallback(
async (path: string) => {
const { getFirstAniImage, getLargestIcon } = await import(
"utils/imageDecoder"
);
const imageBuffer = await readFile(path);
const extension = getExtension(path);
let image: Buffer | undefined = imageBuffer;
if (extension === ".ani") {
image = await getFirstAniImage(imageBuffer);
} else {
const largestIcon = await getLargestIcon(imageBuffer, 128);
if (!imageBuffer || imageBuffer.length === 0) return "";
if (largestIcon) return largestIcon;
}
const { cursorToCss } = await import("utils/imageDecoder");
return image ? bufferToUrl(image, getMimeType(path)) : "";
return cursorToCss(imageBuffer, path);
},
[readFile]
);
@ -173,9 +163,7 @@ const Metadata: FC = () => {
/>
);
})}
{customCursor && (
<style>{`*, *::before, *::after { cursor: url(${customCursor}), default !important; }`}</style>
)}
{customCursor && <style>{customCursor}</style>}
</Head>
);
};

View File

@ -39,7 +39,7 @@
"*.{js,ts,tsx}": "eslint --fix"
},
"resolutions": {
"@emotion/is-prop-valid": "^1.3.1"
"@emotion/is-prop-valid": "^1.4.0"
},
"dependencies": {
"@ffmpeg/ffmpeg": "^0.12.15",
@ -63,7 +63,7 @@
"ini": "^5.0.0",
"isomorphic-git": "^1.33.1",
"libheif-js": "^1.19.8",
"mediainfo.js": "0.3.5",
"mediainfo.js": "0.3.6",
"minimist": "^1.2.8",
"motion": "^12.23.12",
"multiformats": "^13.4.0",
@ -98,7 +98,7 @@
"@types/jest": "^30.0.0",
"@types/lunr": "^2.3.7",
"@types/minimist": "^1.2.5",
"@types/node": "^24.3.0",
"@types/node": "^24.3.1",
"@types/offscreencanvas": "^2019.7.3",
"@types/opentype.js": "^1.3.8",
"@types/react": "^19.1.12",
@ -106,8 +106,8 @@
"@types/ua-parser-js": "^0.7.39",
"@types/video.js": "^7.3.58",
"@types/wicg-file-system-access": "^2023.10.6",
"@typescript-eslint/eslint-plugin": "^8.41.0",
"@typescript-eslint/parser": "^8.41.0",
"@typescript-eslint/eslint-plugin": "^8.42.0",
"@typescript-eslint/parser": "^8.42.0",
"emulators": "^8.3.9",
"emulators-ui": "^0.73.9",
"eruda": "^3.4.3",
@ -134,21 +134,21 @@
"html-minifier-terser": "^7.2.0",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"jest": "^30.1.1",
"jest-environment-jsdom": "^30.1.1",
"lint-staged": "^16.1.5",
"jest": "^30.1.3",
"jest-environment-jsdom": "^30.1.2",
"lint-staged": "^16.1.6",
"lunr": "^2.3.9",
"monaco-editor": "^0.52.2",
"pdfjs-dist": "^5.4.54",
"pdfjs-dist": "^5.4.149",
"playwright-core": "^1.55.0",
"postcss": "^8.5.6",
"postcss-styled-syntax": "^0.7.1",
"postcss-syntax": "^0.36.2",
"serve": "^14.2.4",
"stylelint": "^16.23.1",
"serve": "^14.2.5",
"stylelint": "^16.24.0",
"stylelint-config-standard": "^39.0.0",
"stylelint-order": "^7.0.0",
"terser": "^5.43.1",
"terser": "^5.44.0",
"tinymce": "^7.9.1",
"ts-prune": "^0.10.3",
"typescript": "^5.9.2",

View File

@ -8,6 +8,7 @@ import {
blobToBuffer,
bufferToUrl,
cleanUpBufferUrl,
getExtension,
getGifJs,
getMimeType,
imgDataToBuffer,
@ -15,6 +16,9 @@ import {
type JxlDecodeResponse = { data: { imgData: ImageData } };
const JIFFIES_IN_SECOND = 60;
const DEFAULT_JIFFY_RATE = 10;
const supportsImageType = async (type: string): Promise<boolean> => {
const img = document.createElement("img");
@ -71,9 +75,10 @@ const aniToGif = async (aniBuffer: Buffer): Promise<Buffer> => {
const gif = await getGifJs();
const { parseAni } = await import("ani-cursor/dist/parser");
let images: Uint8Array[] = [];
let metadata: { iDispRate?: number } = {};
try {
({ images } = parseAni(aniBuffer));
({ images, metadata } = parseAni(aniBuffer));
} catch {
return aniBuffer;
}
@ -88,7 +93,12 @@ const aniToGif = async (aniBuffer: Buffer): Promise<Buffer> => {
imageIcon.addEventListener(
"load",
() => {
gif.addFrame(imageIcon);
gif.addFrame(imageIcon, {
delay:
((metadata.iDispRate || DEFAULT_JIFFY_RATE) /
JIFFIES_IN_SECOND) *
1000,
});
cleanUpBufferUrl(bufferUrl);
resolve();
},
@ -128,7 +138,46 @@ export const getFirstAniImage = async (
return undefined;
};
export const getLargestIcon = async (
const getGlobalCursorCSS = (cursorUrl: string): string =>
`*, *::before, *::after { cursor: url(${cursorUrl}), default !important; }`;
const aniToCss = async (
imageBuffer: Buffer,
mimeType: string
): Promise<string> => {
const { parseAni } = await import("ani-cursor/dist/parser");
const { metadata, images } = parseAni(imageBuffer);
const toUrl = (image: Uint8Array): string =>
bufferToUrl(Buffer.from(image), mimeType);
if (images.length === 1) return getGlobalCursorCSS(toUrl(images[0]));
if (images.length > 1) {
const animationName = `cursor-ani-${Date.now()}`;
const keyframes = `
@keyframes ${animationName} {
${images
.map(
(image, i) =>
`${((i / images.length) * 100).toFixed(1)}% { cursor: url(${toUrl(image)}), default; }`
)
.join("")}
100% { cursor: url(${toUrl(images[0])}), default; }
}
`;
const duration = Math.ceil(
((metadata.iDispRate || DEFAULT_JIFFY_RATE) / JIFFIES_IN_SECOND) *
images.length *
1000
);
return `${keyframes}* { animation: ${animationName} ${duration}ms infinite steps(1) !important; }`;
}
return "";
};
const getLargestIcon = async (
imageBuffer: Buffer,
maxSize: number
): Promise<string> => {
@ -152,6 +201,23 @@ export const getLargestIcon = async (
}
};
export const cursorToCss = async (
buffer: Buffer,
path: string
): Promise<string> => {
if (getExtension(path) === ".ani") {
const animatedCursorCss = await aniToCss(buffer, getMimeType(path));
if (animatedCursorCss) return animatedCursorCss;
}
const largestIcon = await getLargestIcon(buffer, 128);
return getGlobalCursorCSS(
largestIcon || bufferToUrl(buffer, getMimeType(path))
);
};
const canLoadNative = async (
extension: string,
file: Buffer

856
yarn.lock

File diff suppressed because it is too large Load Diff