mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 10:15:31 +00:00
Refactor to dedupe URIs in CSS
This commit is contained in:
parent
c0c78903dc
commit
afa358e488
5 changed files with 97 additions and 84 deletions
75
packages/webamp/js/aniUtils.ts
Normal file
75
packages/webamp/js/aniUtils.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { parseAni } from "./aniParser";
|
||||
import { AniCursorImage } from "./types";
|
||||
import * as Utils from "./utils";
|
||||
import * as FileUtils from "./fileUtils";
|
||||
|
||||
const JIFFIES_PER_MS = 1000 / 60;
|
||||
|
||||
export function readAni(contents: Uint8Array): AniCursorImage {
|
||||
const ani = parseAni(contents);
|
||||
const rate = ani.rate ?? ani.images.map(() => ani.metadata.iDispRate);
|
||||
const duration = Utils.sum(rate);
|
||||
|
||||
const frames = ani.images.map((image) => ({
|
||||
url: FileUtils.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 };
|
||||
}
|
||||
|
||||
// Generate CSS for an animated cursor.
|
||||
//
|
||||
// Based on https://css-tricks.com/forums/topic/animated-cursor/
|
||||
//
|
||||
// Browsers won't render animated cursor images specified via CSS. For `.ani`
|
||||
// images, we already have the frames as indiviual images, so we create a CSS
|
||||
// animation.
|
||||
//
|
||||
// This function returns CSS containing a set of keyframes with embedded Data
|
||||
// URIs as well as a CSS rule to the given selector.
|
||||
//
|
||||
// **Note:** This does not seem to work on Safari. I've filed an issue here:
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=219564
|
||||
export function aniCss(selector: string, ani: AniCursorImage): string {
|
||||
const animationName = `webamp-ani-cursor-${Utils.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";
|
||||
|
||||
return `
|
||||
@keyframes ${animationName} {
|
||||
${keyframes.join("\n")}
|
||||
}
|
||||
${selector}${pseudoSelector} {
|
||||
animation: ${animationName} ${
|
||||
ani.duration
|
||||
}ms ${timingFunction} infinite;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
|
@ -3,14 +3,13 @@ import { LETTERS } from "../constants";
|
|||
import { imageSelectors, cursorSelectors } from "../skinSelectors";
|
||||
import { useTypedSelector } from "../hooks";
|
||||
import * as Selectors from "../selectors";
|
||||
import { AniFrame, SkinImages } from "../types";
|
||||
import { SkinImages } from "../types";
|
||||
import { createSelector } from "reselect";
|
||||
import * as Utils from "../utils";
|
||||
import Css from "./Css";
|
||||
import ClipPaths from "./ClipPaths";
|
||||
import { aniCss } from "../aniUtils";
|
||||
|
||||
const CSS_PREFIX = "#webamp";
|
||||
const JIFFIES_PER_MS = 1000 / 60;
|
||||
|
||||
const mapRegionNamesToIds: { [key: string]: string } = {
|
||||
normal: "mainWindowClipPath",
|
||||
|
|
@ -34,59 +33,6 @@ const FALLBACKS: { [key: string]: string } = {
|
|||
MAIN_BALANCE_THUMB_ACTIVE: "MAIN_VOLUME_THUMB_SELECTED",
|
||||
};
|
||||
|
||||
// Generate CSS for an animated cursor.
|
||||
//
|
||||
// Based on https://css-tricks.com/forums/topic/animated-cursor/
|
||||
//
|
||||
// Browsers won't render animated cursor images specified via CSS. For `.ani`
|
||||
// images, we already have the frames as indiviual images, so we create a CSS
|
||||
// animation.
|
||||
//
|
||||
// This function returns CSS containing a set of keyframes with embedded Data
|
||||
// URIs as well as a CSS rule to the given selector.
|
||||
//
|
||||
// **Note:** This does not seem to work on Safari. I've filed an issue here:
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=219564
|
||||
function aniCss(selector: string, frames: AniFrame[]): string {
|
||||
const animationName = `webamp-ani-cursor-${Utils.uniqueId()}`;
|
||||
const totalDuration = Utils.sum(frames.map(({ rate }) => rate));
|
||||
|
||||
let elapsed = 0;
|
||||
const keyframes = frames.map(({ url, rate }) => {
|
||||
const percent = (elapsed / totalDuration) * 100;
|
||||
elapsed += rate;
|
||||
|
||||
return `${percent}% { cursor: url(${url}), auto; }`;
|
||||
});
|
||||
|
||||
const durationMs = totalDuration * JIFFIES_PER_MS;
|
||||
|
||||
// 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";
|
||||
|
||||
return `
|
||||
@keyframes ${animationName} {
|
||||
${keyframes.join("\n")}
|
||||
}
|
||||
${selector}${pseudoSelector} {
|
||||
animation: ${animationName} ${durationMs}ms ${timingFunction} infinite;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
// Cursors might appear in context menus which are not nested inside the window layout div.
|
||||
function normalizeCursorSelector(selector: string): string {
|
||||
return `${
|
||||
|
|
@ -146,7 +92,7 @@ const getCssRules = createSelector(
|
|||
case "cur":
|
||||
return `${selector} {cursor: url(${cursor.url}), auto}`;
|
||||
case "ani": {
|
||||
return aniCss(selector, cursor.frames);
|
||||
return aniCss(selector, cursor.ani);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import invariant from "invariant";
|
||||
import { IMusicMetadataBrowserApi } from "./types";
|
||||
import { IAudioMetadata } from "music-metadata-browser"; // Import music-metadata type definitions
|
||||
import * as Utils from "./utils";
|
||||
|
||||
type MediaDataType = string | ArrayBuffer | Blob;
|
||||
|
||||
|
|
@ -123,6 +124,11 @@ function urlIsBlobUrl(url: string): boolean {
|
|||
return /^blob:/.test(url);
|
||||
}
|
||||
|
||||
export function curUrlFromByteArray(arr: Uint8Array) {
|
||||
const base64 = Utils.base64FromDataArray(arr);
|
||||
return `data:image/x-win-bitmap;base64,${base64}`;
|
||||
}
|
||||
|
||||
// This is not perfect, but... meh: https://stackoverflow.com/a/36756650/1263117
|
||||
export function filenameFromUrl(url: string): string | null {
|
||||
if (urlIsBlobUrl(url)) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { PlaylistStyle, SkinGenExColors, CursorImage } from "./types";
|
|||
import SKIN_SPRITES, { Sprite } from "./skinSprites";
|
||||
import { DEFAULT_SKIN } from "./constants";
|
||||
import * as Utils from "./utils";
|
||||
import { parseAni, ParsedAni } from "./aniParser";
|
||||
import * as FileUtils from "./fileUtils";
|
||||
import * as AniUtils from "./aniUtils";
|
||||
|
||||
export const getFileExtension = (fileName: string): string | null => {
|
||||
const matches = /\.([a-z]{3,4})$/i.exec(fileName);
|
||||
|
|
@ -131,20 +132,6 @@ function arrayStartsWith(arr: Uint8Array, matcher: number[]): boolean {
|
|||
return matcher.every((item, i) => arr[i] === item);
|
||||
}
|
||||
|
||||
function curUrlFromByteArray(arr: Uint8Array) {
|
||||
const base64 = Utils.base64FromDataArray(arr);
|
||||
return `data:image/x-win-bitmap;base64,${base64}`;
|
||||
}
|
||||
|
||||
function framesFromAni(ani: ParsedAni) {
|
||||
const rawUrls = ani.images.map(curUrlFromByteArray);
|
||||
const urls = ani.seq == null ? rawUrls : ani.seq.map((i) => rawUrls[i]);
|
||||
return urls.map((url, i) => {
|
||||
const rate = ani.rate == null ? ani.metadata.iDispRate : ani.rate[i];
|
||||
return { url, rate };
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCursorFromFilename(
|
||||
zip: JSZip,
|
||||
fileName: string
|
||||
|
|
@ -155,11 +142,10 @@ export async function getCursorFromFilename(
|
|||
}
|
||||
const contents = file.contents as Uint8Array;
|
||||
if (arrayStartsWith(contents, RIFF_MAGIC)) {
|
||||
const ani = parseAni(contents);
|
||||
return { type: "ani", frames: framesFromAni(ani) };
|
||||
return { type: "ani", ani: AniUtils.readAni(contents) };
|
||||
}
|
||||
|
||||
return { type: "cur", url: curUrlFromByteArray(contents) };
|
||||
return { type: "cur", url: FileUtils.curUrlFromByteArray(contents) };
|
||||
}
|
||||
|
||||
export async function getPlaylistStyle(zip: JSZip): Promise<PlaylistStyle> {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,14 @@ export type Band =
|
|||
|
||||
export type Slider = Band | "preamp";
|
||||
|
||||
export type AniCursorImage = {
|
||||
frames: {
|
||||
url: string;
|
||||
percents: number[];
|
||||
}[];
|
||||
duration: number;
|
||||
};
|
||||
|
||||
export type CursorImage =
|
||||
| {
|
||||
type: "cur";
|
||||
|
|
@ -82,21 +90,13 @@ export type CursorImage =
|
|||
}
|
||||
| {
|
||||
type: "ani";
|
||||
frames: {
|
||||
url: string;
|
||||
rate: number;
|
||||
}[];
|
||||
ani: AniCursorImage;
|
||||
};
|
||||
|
||||
// TODO: Use a type to ensure these keys mirror the CURSORS constant in
|
||||
// skinParser.js
|
||||
export type Cursors = { [cursor: string]: CursorImage };
|
||||
|
||||
export type AniFrame = {
|
||||
url: string;
|
||||
rate: number;
|
||||
};
|
||||
|
||||
export type GenLetterWidths = { [letter: string]: number };
|
||||
|
||||
export interface PlaylistStyle {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue