File size: 4,026 Bytes
7614202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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;
}