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([ ["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 { const params = new URLSearchParams(search); const state: Partial = {}; 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; }