tilekit / src /url-state.ts
osolmaz's picture
osolmaz HF Staff
feat: add shareable configuration URLs
7614202 verified
Raw
History Blame Contribute Delete
4.03 kB
export interface ShareState {
width: number;
height: number;
symbol: string;
density: number;
size: number;
variation: number;
spacing: number;
rotation: number;
seed: number;
}
export const DEFAULT_SHARE_STATE: ShareState = {
width: 3840,
height: 2160,
symbol: "hf",
density: 88,
size: 1,
variation: 1,
spacing: 1,
rotation: 52,
seed: 84237,
};
const CANVAS_ALIASES = new Map<string, readonly [number, number]>([
["1080p", [1920, 1080]],
["1440p", [2560, 1440]],
["4k", [3840, 2160]],
["5k", [5120, 2880]],
["8k", [7680, 4320]],
]);
const CANVAS_NAMES = new Map([...CANVAS_ALIASES].map(([name, dimensions]) => [`${dimensions[0]}x${dimensions[1]}`, name]));
function compactNumber(value: number): string {
return String(Number(value.toFixed(2)));
}
function firstGrapheme(value: string): string | undefined {
const trimmed = value.trim();
if (!trimmed) return undefined;
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
return segmenter.segment(trimmed)[Symbol.iterator]().next().value?.segment;
}
function readNumber(params: URLSearchParams, name: string, minimum: number, maximum: number, integer = false): number | undefined {
const raw = params.get(name);
if (raw === null || raw.trim() === "") return undefined;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed < minimum || parsed > maximum) return undefined;
return integer ? Math.round(parsed) : Math.round(parsed * 100) / 100;
}
function readCanvas(value: string | null): readonly [number, number] | undefined {
if (!value) return undefined;
const alias = CANVAS_ALIASES.get(value.toLowerCase());
if (alias) return alias;
const match = /^(\d{3,4})x(\d{3,4})$/i.exec(value);
if (!match) return undefined;
const width = Number(match[1]);
const height = Number(match[2]);
if (width < 640 || width > 7680 || height < 360 || height > 4320) return undefined;
return [width, height];
}
export function encodeShareState(state: ShareState): string {
const params = new URLSearchParams();
if (state.width !== DEFAULT_SHARE_STATE.width || state.height !== DEFAULT_SHARE_STATE.height) {
const dimensions = `${state.width}x${state.height}`;
params.set("canvas", CANVAS_NAMES.get(dimensions) ?? dimensions);
}
if (state.symbol !== DEFAULT_SHARE_STATE.symbol) params.set("symbol", state.symbol);
if (state.density !== DEFAULT_SHARE_STATE.density) params.set("density", String(Math.round(state.density)));
if (state.size !== DEFAULT_SHARE_STATE.size) params.set("size", compactNumber(state.size));
if (state.variation !== DEFAULT_SHARE_STATE.variation) params.set("variation", compactNumber(state.variation));
if (state.spacing !== DEFAULT_SHARE_STATE.spacing) params.set("spacing", compactNumber(state.spacing));
if (state.rotation !== DEFAULT_SHARE_STATE.rotation) params.set("rotation", String(Math.round(state.rotation)));
if (state.seed !== DEFAULT_SHARE_STATE.seed) params.set("seed", String(Math.round(state.seed) >>> 0));
return params.toString();
}
export function decodeShareState(search: string): Partial<ShareState> {
const params = new URLSearchParams(search);
const state: Partial<ShareState> = {};
const canvas = readCanvas(params.get("canvas"));
if (canvas) [state.width, state.height] = canvas;
const symbol = params.get("symbol");
if (symbol === "hf") state.symbol = "hf";
else if (symbol) {
const emoji = firstGrapheme(symbol);
if (emoji) state.symbol = emoji;
}
state.density = readNumber(params, "density", 24, 512, true);
state.size = readNumber(params, "size", 0.5, 1.6);
state.variation = readNumber(params, "variation", 0, 1.5);
state.spacing = readNumber(params, "spacing", 0.25, 2);
state.rotation = readNumber(params, "rotation", 0, 70, true);
state.seed = readNumber(params, "seed", 0, 4294967295, true);
for (const key of Object.keys(state) as (keyof ShareState)[]) {
if (state[key] === undefined) delete state[key];
}
return state;
}