| 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; |
| } |
|
|