Spaces:
Running
Running
Download apps/web/src/customize.ts from osolmaz/microquack: direct link, hf CLI and curl.
- Browser
- Download file 3.67 kB
-
https://huggingface.co/spaces/osolmaz/microquack/resolve/main/apps/web/src/customize.ts
- Command line
-
hf download hf://spaces/osolmaz/microquack/apps/web/src/customize.ts
-
curl -L -o customize.ts https://huggingface.co/spaces/osolmaz/microquack/resolve/main/apps/web/src/customize.ts
3.67 kB
| /** | |
| * Duck customization profiles: seven colors and a name, packed into a | |
| * compact base64url string | |
| * ([version=1][slot count=7][7 x rgb][name utf-8, <=24 bytes]). | |
| * The encoded form lives in localStorage and, for signed-in users, in | |
| * their private Hugging Face storage β never in URLs. | |
| */ | |
| export type DuckProfile = { | |
| name: string; | |
| /** sRGB 0..255 per channel; one entry per SLOT_LABELS item. */ | |
| colors: [number, number, number][]; | |
| }; | |
| const VERSION = 1; | |
| const SLOT_COUNT = 7; | |
| const HEADER_BYTES = 2; | |
| const NAME_MAX_BYTES = 24; | |
| const STORAGE_KEY = "mq-duck"; | |
| export const SLOT_LABELS = ["Head", "Shell", "Face", "Beak", "Eye", "Feet", "Soles"]; | |
| function toBase64Url(bytes: Uint8Array): string { | |
| let bin = ""; | |
| for (const b of bytes) bin += String.fromCharCode(b); | |
| return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, ""); | |
| } | |
| function fromBase64Url(text: string): Uint8Array | null { | |
| try { | |
| const bin = atob(text.replaceAll("-", "+").replaceAll("_", "/")); | |
| return Uint8Array.from(bin, (c) => c.charCodeAt(0)); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export function encodeProfile(profile: DuckProfile): string { | |
| const name = new TextEncoder().encode(profile.name).slice(0, NAME_MAX_BYTES); | |
| const bytes = new Uint8Array(HEADER_BYTES + SLOT_COUNT * 3 + name.length); | |
| bytes[0] = VERSION; | |
| bytes[1] = SLOT_COUNT; | |
| for (let i = 0; i < SLOT_COUNT; i++) { | |
| const c = profile.colors[i] ?? [255, 255, 255]; | |
| bytes[HEADER_BYTES + i * 3] = c[0]; | |
| bytes[HEADER_BYTES + i * 3 + 1] = c[1]; | |
| bytes[HEADER_BYTES + i * 3 + 2] = c[2]; | |
| } | |
| bytes.set(name, HEADER_BYTES + SLOT_COUNT * 3); | |
| return toBase64Url(bytes); | |
| } | |
| function isCurrentProfile(bytes: Uint8Array | null): bytes is Uint8Array { | |
| return ( | |
| bytes !== null && | |
| bytes.length >= HEADER_BYTES + SLOT_COUNT * 3 && | |
| bytes[0] === VERSION && | |
| bytes[1] === SLOT_COUNT | |
| ); | |
| } | |
| export function decodeProfile(encoded: string): DuckProfile | null { | |
| const bytes = fromBase64Url(encoded); | |
| if (!isCurrentProfile(bytes)) return null; | |
| const colors: [number, number, number][] = []; | |
| for (let i = 0; i < SLOT_COUNT; i++) { | |
| const offset = HEADER_BYTES + i * 3; | |
| colors.push([bytes[offset] ?? 0, bytes[offset + 1] ?? 0, bytes[offset + 2] ?? 0]); | |
| } | |
| const name = new TextDecoder().decode(bytes.slice(HEADER_BYTES + SLOT_COUNT * 3)); | |
| return { name, colors }; | |
| } | |
| export function profileFromStorage(): DuckProfile | null { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY); | |
| return raw ? decodeProfile(raw) : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| /** Persist locally; the duck lives in this browser (or the HF account). */ | |
| export function saveProfile(profile: DuckProfile): void { | |
| try { | |
| localStorage.setItem(STORAGE_KEY, encodeProfile(profile)); | |
| } catch { | |
| // private mode: the duck lasts for this visit only | |
| } | |
| } | |
| // ββ color space: pickers speak sRGB hex, materials speak linear βββββββββ | |
| export function srgbToLinear(c: [number, number, number]): [number, number, number] { | |
| return [(c[0] / 255) ** 2.2, (c[1] / 255) ** 2.2, (c[2] / 255) ** 2.2]; | |
| } | |
| export function linearToSrgb(c: [number, number, number]): [number, number, number] { | |
| const conv = (v: number): number => Math.round(255 * Math.min(1, Math.max(0, v)) ** (1 / 2.2)); | |
| return [conv(c[0]), conv(c[1]), conv(c[2])]; | |
| } | |
| export function srgbToHex(c: [number, number, number]): string { | |
| return `#${c.map((v) => v.toString(16).padStart(2, "0")).join("")}`; | |
| } | |
| export function hexToSrgb(hex: string): [number, number, number] { | |
| const n = parseInt(hex.replace("#", ""), 16); | |
| return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; | |
| } | |