@hadoku/prefs-client 1.1.16 → 1.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/avatar.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AvatarCosmetics, C as CosmeticsView, L as LoadoutUpdate } from './cosmetics-contract-
|
|
2
|
-
export { a as CosmeticSlot, b as Loadout, P as Placement } from './cosmetics-contract-
|
|
1
|
+
import { A as AvatarCosmetics, C as CosmeticsView, L as LoadoutUpdate } from './cosmetics-contract-CWnMmEjr.js';
|
|
2
|
+
export { a as CosmeticSlot, b as Loadout, P as Placement } from './cosmetics-contract-CWnMmEjr.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Avatars — the wire contract, shared by prefs-api and the client.
|
|
@@ -170,6 +170,14 @@ interface CosmeticGrantRequest {
|
|
|
170
170
|
note?: string;
|
|
171
171
|
}
|
|
172
172
|
/** Admin: GET /api/v1/admin/cosmetics?name= */
|
|
173
|
+
/** GET /api/v1/admin/cosmetics/people: every live person who can wear cosmetics, members included. */
|
|
174
|
+
interface CosmeticPeople {
|
|
175
|
+
people: {
|
|
176
|
+
userId: string;
|
|
177
|
+
name: string;
|
|
178
|
+
tier: string;
|
|
179
|
+
}[];
|
|
180
|
+
}
|
|
173
181
|
interface CosmeticGrantList {
|
|
174
182
|
grants: {
|
|
175
183
|
item: string;
|
|
@@ -179,4 +187,4 @@ interface CosmeticGrantList {
|
|
|
179
187
|
}[];
|
|
180
188
|
}
|
|
181
189
|
|
|
182
|
-
export { type AvatarCosmetics as A, type Box as B, type CosmeticsView as C, EARNED_COSMETICS as E, type LoadoutUpdate as L, type OverlayArt as O, type Placement as P, type CosmeticSlot as a, type Loadout as b, COSMETICS as c, COSMETICS_BY_ID as d, COSMETIC_ID_PATTERN as e, COSMETIC_SLOTS as f, type CosmeticGrantList as g, type CosmeticGrantRequest as h, type CosmeticItem as i,
|
|
190
|
+
export { type AvatarCosmetics as A, type Box as B, type CosmeticsView as C, EARNED_COSMETICS as E, type LoadoutUpdate as L, type OverlayArt as O, type Placement as P, type CosmeticSlot as a, type Loadout as b, COSMETICS as c, COSMETICS_BY_ID as d, COSMETIC_ID_PATTERN as e, COSMETIC_SLOTS as f, type CosmeticGrantList as g, type CosmeticGrantRequest as h, type CosmeticItem as i, type CosmeticPeople as j, PLACEMENT_DEFAULT as k, PLACEMENT_LIMITS as l, PLACEMENT_ZONE as m, cleanLoadout as n, cleanWorn as o, emptyLoadout as p, fitPlacement as q, overlayBounds as r, ownedFrom as s, wornCosmetics as w };
|
package/dist/cosmetics.d.ts
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
|
-
import { A as AvatarCosmetics, P as Placement } from './cosmetics-contract-
|
|
2
|
-
export { B as Box, c as COSMETICS, d as COSMETICS_BY_ID, e as COSMETIC_ID_PATTERN, f as COSMETIC_SLOTS, g as CosmeticGrantList, h as CosmeticGrantRequest, i as CosmeticItem, a as CosmeticSlot, C as CosmeticsView, E as EARNED_COSMETICS, b as Loadout, L as LoadoutUpdate, O as OverlayArt,
|
|
1
|
+
import { A as AvatarCosmetics, P as Placement } from './cosmetics-contract-CWnMmEjr.js';
|
|
2
|
+
export { B as Box, c as COSMETICS, d as COSMETICS_BY_ID, e as COSMETIC_ID_PATTERN, f as COSMETIC_SLOTS, g as CosmeticGrantList, h as CosmeticGrantRequest, i as CosmeticItem, j as CosmeticPeople, a as CosmeticSlot, C as CosmeticsView, E as EARNED_COSMETICS, b as Loadout, L as LoadoutUpdate, O as OverlayArt, k as PLACEMENT_DEFAULT, l as PLACEMENT_LIMITS, m as PLACEMENT_ZONE, n as cleanLoadout, o as cleanWorn, p as emptyLoadout, q as fitPlacement, r as overlayBounds, s as ownedFrom, w as wornCosmetics } from './cosmetics-contract-CWnMmEjr.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A person's colour from their avatar picture: the picture's alpha-weighted
|
|
6
|
+
* average, lightness clamped so a black or white picture still reads as a name
|
|
7
|
+
* on a card. Their name cosmetics derive from it (`--hc-base`), so every app
|
|
8
|
+
* that draws a person (watchparty's chat, the avatar dialog's preview) reads it
|
|
9
|
+
* here and they agree. With no picture, or one that cannot be read, the app
|
|
10
|
+
* uses the person's character colour.
|
|
11
|
+
*
|
|
12
|
+
* The picture is read through a canvas, so it is loaded once more with
|
|
13
|
+
* `crossOrigin="anonymous"`. Discord and Steam both answer
|
|
14
|
+
* `Access-Control-Allow-Origin: *`; a host that does not taints the canvas and
|
|
15
|
+
* reads as null. One read per URL per page, cached, failures included.
|
|
16
|
+
*
|
|
17
|
+
* Browser only; outside one every read is null.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The alpha-weighted mean of RGBA pixels as `#rrggbb`, lightness clamped to
|
|
21
|
+
* the readable band. Null when no pixel is opaque enough to count.
|
|
22
|
+
*/
|
|
23
|
+
declare function averagePictureColour(pixels: ArrayLike<number>): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* The picture's colour if it has been read: a colour, null (unreadable), or
|
|
26
|
+
* undefined (not read yet; {@link readPictureColour} starts the read).
|
|
27
|
+
*/
|
|
28
|
+
declare function pictureColour(url: string): string | null | undefined;
|
|
29
|
+
/** Be told whenever a read settles. Returns the unsubscribe. */
|
|
30
|
+
declare function onPictureColour(listener: () => void): () => void;
|
|
31
|
+
/** Read the picture's colour (once per URL); null when it cannot be read. */
|
|
32
|
+
declare function readPictureColour(url: string): Promise<string | null>;
|
|
3
33
|
|
|
4
34
|
/**
|
|
5
35
|
* Avatar cosmetics — the renderer every app uses, so a name, an avatar and a
|
|
@@ -67,4 +97,4 @@ declare function cosmeticsCss(assetBase?: string): string;
|
|
|
67
97
|
/** Add the stylesheet and the reveal clean-up to a document, once. */
|
|
68
98
|
declare function ensureCosmeticsStyles(doc?: Document, assetBase?: string): void;
|
|
69
99
|
|
|
70
|
-
export { AvatarCosmetics, COSMETICS_ASSET_BASE, Placement, avatarHtml, colourStyle, cosmeticsCss, decorHtml, ensureCosmeticsStyles, finishReveal, messageHtml, nameHtml, placementTransform };
|
|
100
|
+
export { AvatarCosmetics, COSMETICS_ASSET_BASE, Placement, avatarHtml, averagePictureColour, colourStyle, cosmeticsCss, decorHtml, ensureCosmeticsStyles, finishReveal, messageHtml, nameHtml, onPictureColour, pictureColour, placementTransform, readPictureColour };
|
package/dist/cosmetics.js
CHANGED
|
@@ -226,6 +226,116 @@ function wornCosmetics(loadout) {
|
|
|
226
226
|
return Object.keys(worn).length ? worn : void 0;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// src/picture-colour.ts
|
|
230
|
+
var MIN_LIGHTNESS = 0.3;
|
|
231
|
+
var MAX_LIGHTNESS = 0.82;
|
|
232
|
+
var SAMPLE_SIZE = 24;
|
|
233
|
+
var MIN_ALPHA = 32;
|
|
234
|
+
function averagePictureColour(pixels) {
|
|
235
|
+
let r = 0;
|
|
236
|
+
let g = 0;
|
|
237
|
+
let b = 0;
|
|
238
|
+
let weight = 0;
|
|
239
|
+
for (let i = 0; i + 3 < pixels.length; i += 4) {
|
|
240
|
+
const a = pixels[i + 3];
|
|
241
|
+
if (a < MIN_ALPHA) continue;
|
|
242
|
+
r += pixels[i] * a;
|
|
243
|
+
g += pixels[i + 1] * a;
|
|
244
|
+
b += pixels[i + 2] * a;
|
|
245
|
+
weight += a;
|
|
246
|
+
}
|
|
247
|
+
if (weight === 0) return null;
|
|
248
|
+
return clampLightness(r / weight, g / weight, b / weight);
|
|
249
|
+
}
|
|
250
|
+
function clampLightness(r, g, b) {
|
|
251
|
+
const [h, s, l] = rgbToHsl(r / 255, g / 255, b / 255);
|
|
252
|
+
const clamped = Math.min(MAX_LIGHTNESS, Math.max(MIN_LIGHTNESS, l));
|
|
253
|
+
const [r2, g2, b2] = hslToRgb(h, s, clamped);
|
|
254
|
+
const hex = (v) => Math.round(v * 255).toString(16).padStart(2, "0");
|
|
255
|
+
return `#${hex(r2)}${hex(g2)}${hex(b2)}`;
|
|
256
|
+
}
|
|
257
|
+
function rgbToHsl(r, g, b) {
|
|
258
|
+
const max = Math.max(r, g, b);
|
|
259
|
+
const min = Math.min(r, g, b);
|
|
260
|
+
const l = (max + min) / 2;
|
|
261
|
+
if (max === min) return [0, 0, l];
|
|
262
|
+
const d = max - min;
|
|
263
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
264
|
+
let h;
|
|
265
|
+
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
266
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
267
|
+
else h = (r - g) / d + 4;
|
|
268
|
+
return [h / 6, s, l];
|
|
269
|
+
}
|
|
270
|
+
function hslToRgb(h, s, l) {
|
|
271
|
+
if (s === 0) return [l, l, l];
|
|
272
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
273
|
+
const p = 2 * l - q;
|
|
274
|
+
const channel = (t) => {
|
|
275
|
+
if (t < 0) t += 1;
|
|
276
|
+
if (t > 1) t -= 1;
|
|
277
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
278
|
+
if (t < 1 / 2) return q;
|
|
279
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
280
|
+
return p;
|
|
281
|
+
};
|
|
282
|
+
return [channel(h + 1 / 3), channel(h), channel(h - 1 / 3)];
|
|
283
|
+
}
|
|
284
|
+
var colours = /* @__PURE__ */ new Map();
|
|
285
|
+
var pending = /* @__PURE__ */ new Map();
|
|
286
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
287
|
+
function pictureColour(url) {
|
|
288
|
+
return colours.get(url);
|
|
289
|
+
}
|
|
290
|
+
function onPictureColour(listener) {
|
|
291
|
+
listeners.add(listener);
|
|
292
|
+
return () => {
|
|
293
|
+
listeners.delete(listener);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function readPictureColour(url) {
|
|
297
|
+
if (colours.has(url)) return Promise.resolve(colours.get(url) ?? null);
|
|
298
|
+
const inFlight = pending.get(url);
|
|
299
|
+
if (inFlight) return inFlight;
|
|
300
|
+
const read = sample(url).then((colour) => {
|
|
301
|
+
pending.delete(url);
|
|
302
|
+
colours.set(url, colour);
|
|
303
|
+
for (const l of listeners) l();
|
|
304
|
+
return colour;
|
|
305
|
+
});
|
|
306
|
+
pending.set(url, read);
|
|
307
|
+
return read;
|
|
308
|
+
}
|
|
309
|
+
function sample(url) {
|
|
310
|
+
if (typeof Image === "undefined" || typeof document === "undefined") {
|
|
311
|
+
return Promise.resolve(null);
|
|
312
|
+
}
|
|
313
|
+
return new Promise((resolve) => {
|
|
314
|
+
const img = new Image();
|
|
315
|
+
img.crossOrigin = "anonymous";
|
|
316
|
+
img.referrerPolicy = "no-referrer";
|
|
317
|
+
img.onload = () => {
|
|
318
|
+
const canvas = document.createElement("canvas");
|
|
319
|
+
canvas.width = SAMPLE_SIZE;
|
|
320
|
+
canvas.height = SAMPLE_SIZE;
|
|
321
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
322
|
+
if (!ctx) {
|
|
323
|
+
resolve(null);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
ctx.drawImage(img, 0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
|
327
|
+
try {
|
|
328
|
+
resolve(averagePictureColour(ctx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE).data));
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.debug("[prefs-client] picture colour unreadable", url, err);
|
|
331
|
+
resolve(null);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
img.onerror = () => resolve(null);
|
|
335
|
+
img.src = url;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
229
339
|
// src/cosmetics.ts
|
|
230
340
|
var COSMETICS_ASSET_BASE = "/cosmetics";
|
|
231
341
|
var STYLE_ID = "hadoku-cosmetics";
|
|
@@ -524,6 +634,7 @@ export {
|
|
|
524
634
|
PLACEMENT_LIMITS,
|
|
525
635
|
PLACEMENT_ZONE,
|
|
526
636
|
avatarHtml,
|
|
637
|
+
averagePictureColour,
|
|
527
638
|
cleanLoadout,
|
|
528
639
|
cleanWorn,
|
|
529
640
|
colourStyle,
|
|
@@ -535,8 +646,11 @@ export {
|
|
|
535
646
|
fitPlacement,
|
|
536
647
|
messageHtml,
|
|
537
648
|
nameHtml,
|
|
649
|
+
onPictureColour,
|
|
538
650
|
overlayBounds,
|
|
539
651
|
ownedFrom,
|
|
652
|
+
pictureColour,
|
|
540
653
|
placementTransform,
|
|
654
|
+
readPictureColour,
|
|
541
655
|
wornCosmetics
|
|
542
656
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hadoku/prefs-client",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "1.1.18",
|
|
4
|
+
"gitHead": "8c7dcb837abaaca515f53c166d0a8cd740af2430",
|
|
5
5
|
"description": "Client SDK for the hadoku unified preferences store. Wraps /prefs/api/v1/* with localStorage caching, debounced writes, Zod-validated reads, and a usePrefs() React hook, plus match history, ratings and leaderboards for every game (./games), the one resolved avatar per user (./avatar), and the avatar cosmetics renderer (./cosmetics).",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|