Sketch out animating .ani cursors

This commit is contained in:
Jordan Eldredge 2020-12-02 23:19:41 -08:00
parent 2d6257712e
commit 3a9d32dfee
3 changed files with 50 additions and 16 deletions

View file

@ -5,6 +5,7 @@ import { useTypedSelector } from "../hooks";
import * as Selectors from "../selectors";
import { SkinImages } from "../types";
import { createSelector } from "reselect";
import * as Utils from "../utils";
import Css from "./Css";
import ClipPaths from "./ClipPaths";
@ -67,19 +68,41 @@ const getCssRules = createSelector(
);
});
}
Object.keys(cursorSelectors).forEach((cursorName) => {
const cursorUrl = skinCursors[cursorName];
if (cursorUrl) {
const cursor = skinCursors[cursorName];
if (cursor) {
cursorSelectors[cursorName].forEach((selector) => {
cssRules.push(
`${
// TODO: Fix this hack
// Maybe our CSS name spacing should be based on some other class/id
// than the one we use for defining the main div.
// That way it could be shared by both the player and the context menu.
selector.startsWith("#webamp-context-menu") ? "" : CSS_PREFIX
} ${selector} {cursor: url(${cursorUrl}), auto}`
);
const normalizedSelector = `${
// TODO: Fix this hack
// Maybe our CSS name spacing should be based on some other class/id
// than the one we use for defining the main div.
// That way it could be shared by both the player and the context menu.
selector.startsWith("#webamp-context-menu") ? "" : CSS_PREFIX
} ${selector}`;
switch (cursor.type) {
case "cur":
cssRules.push(
`${normalizedSelector} {cursor: url(${cursor.url}), auto}`
);
break;
case "ani": {
const animationName = `ani-cursor-${Utils.uniqueId()}`;
const frames = cursor.urls.map((url, i) => {
const percent = (i / (cursor.urls.length - 1)) * 100;
return `${percent}% { cursor: url(${url}), auto; }`;
});
const durationMs = cursor.iDispRate * 16 * frames.length;
const framesCss = frames.join("\n");
const keyframes = `@keyframes ${animationName} { ${framesCss} }`;
cssRules.push(keyframes);
cssRules.push(
`${normalizedSelector} { animation: ${animationName} ${durationMs}ms infinite; }`
);
break;
}
}
});
}
});

View file

@ -1,5 +1,5 @@
import JSZip from "jszip";
import { PlaylistStyle, SkinGenExColors } from "./types";
import { PlaylistStyle, SkinGenExColors, CursorImage } from "./types";
import SKIN_SPRITES, { Sprite } from "./skinSprites";
import { DEFAULT_SKIN } from "./constants";
import * as Utils from "./utils";
@ -129,7 +129,7 @@ const ANI_MAGIC_BASE_64 = "UklGR";
export async function getCursorFromFilename(
zip: JSZip,
fileName: string
): Promise<string | null> {
): Promise<CursorImage | null> {
const file = await getFileFromZip(zip, fileName, "CUR", "base64");
if (file == null) {
return null;
@ -141,10 +141,10 @@ export async function getCursorFromFilename(
throw new Error("We just read this file, how can it be null??");
}
const ani = parseAni(fileArr.contents as Uint8Array);
return ani.urls[0];
return { type: "ani", urls: ani.urls, iDispRate: ani.iDispRate };
}
return `data:image/x-win-bitmap;base64,${contents}`;
return { type: "cur", url: `data:image/x-win-bitmap;base64,${contents}` };
}
export async function getPlaylistStyle(zip: JSZip): Promise<PlaylistStyle> {

View file

@ -75,9 +75,20 @@ export type Band =
export type Slider = Band | "preamp";
export type CursorImage =
| {
type: "cur";
url: string;
}
| {
type: "ani";
iDispRate: number;
urls: string[];
};
// TODO: Use a type to ensure these keys mirror the CURSORS constant in
// skinParser.js
export type Cursors = { [cursor: string]: string };
export type Cursors = { [cursor: string]: CursorImage };
export type GenLetterWidths = { [letter: string]: number };