@stanko/ctrls 0.1.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -332,6 +332,40 @@ Example:
332
332
 
333
333
  </div>
334
334
 
335
+ ### Group
336
+
337
+ Collapsible group of controls. All values are going to be nested in an object using the group's name.
338
+
339
+ ```ts
340
+ {
341
+ // Mandatory
342
+ controls: ConfigItem[]
343
+ }
344
+ ```
345
+
346
+ Example:
347
+
348
+ <div class="example">
349
+
350
+ ```json
351
+ {
352
+ "type": "group",
353
+ "name": "color",
354
+ "controls": [
355
+ {
356
+ "type": "boolean",
357
+ "name": "monochrome"
358
+ },
359
+ {
360
+ "type": "easing",
361
+ "name": "distribution"
362
+ }
363
+ ]
364
+ }
365
+ ```
366
+
367
+ </div>
368
+
335
369
  ## Theming
336
370
 
337
371
  Ctrls uses CSS variables for theming. There are many you can adjust, but I recommend starting with these four:
@@ -389,7 +423,7 @@ Thank you for stopping by! If you end up using Ctrls, please let me know, I woul
389
423
  * [x] On input event / handler
390
424
  * [x] Revisit naming `controls` vs `options` vs `values`
391
425
  * [x] Remove lucide as a dependency, swap with local SVGs
392
- * [x] Add Aleas PRNG instead of seedrandom
426
+ * [x] Add Alea PRNG instead of seedrandom
393
427
  * [x] Storage options - none
394
428
  * [x] Fix get random value float point error in dual range (modulo with floats)
395
429
  * [x] Easing - fix handles jumping to previous positions after selecting preset (no storage)
@@ -1,15 +1,17 @@
1
1
  import type { Ctrl, CtrlType, CtrlChangeHandler, CtrlConfig } from ".";
2
2
  export declare class BooleanCtrl implements Ctrl<boolean> {
3
3
  type: CtrlType;
4
+ id: string;
5
+ group?: string;
4
6
  name: string;
5
7
  label: string;
6
8
  value: boolean;
7
9
  isRandomizationDisabled: boolean;
8
- onChange: CtrlChangeHandler<boolean>;
9
- onInput: CtrlChangeHandler<boolean>;
10
+ onChange: CtrlChangeHandler;
11
+ onInput: CtrlChangeHandler;
10
12
  element: HTMLElement;
11
13
  input: HTMLInputElement;
12
- constructor(config: CtrlConfig<boolean>, onChange: CtrlChangeHandler<boolean>, onInput: CtrlChangeHandler<boolean>);
14
+ constructor(config: CtrlConfig<boolean>, onChange: CtrlChangeHandler, onInput: CtrlChangeHandler);
13
15
  parse: (string: string) => string is "true";
14
16
  getRandomValue: () => boolean;
15
17
  getDefaultValue: () => boolean;
@@ -25,11 +25,11 @@ export class BooleanCtrl {
25
25
  input.checked = this.value;
26
26
  input.addEventListener("change", () => {
27
27
  this.value = input.checked;
28
- this.onChange(this.name, this.value);
28
+ this.onChange(this);
29
29
  });
30
30
  input.addEventListener("input", () => {
31
31
  this.value = input.checked;
32
- this.onInput(this.name, this.value);
32
+ this.onInput(this);
33
33
  });
34
34
  const checkmark = document.createElement("span");
35
35
  checkmark.classList.add("ctrls__boolean-checkmark");
@@ -56,6 +56,8 @@ export class BooleanCtrl {
56
56
  };
57
57
  this.type = "boolean";
58
58
  this.name = config.name;
59
+ this.id = config.id || config.name;
60
+ this.group = config.group || "";
59
61
  this.label = config.label || config.name;
60
62
  this.value =
61
63
  config.defaultValue === undefined
@@ -11,12 +11,14 @@ export type DualRangeValue = {
11
11
  };
12
12
  export declare class DualRangeCtrl implements Ctrl<DualRangeValue> {
13
13
  type: CtrlType;
14
+ id: string;
15
+ group?: string;
14
16
  name: string;
15
17
  label: string;
16
18
  value: DualRangeValue;
17
19
  isRandomizationDisabled: boolean;
18
- onChange: CtrlChangeHandler<DualRangeValue>;
19
- onInput: CtrlChangeHandler<DualRangeValue>;
20
+ onChange: CtrlChangeHandler;
21
+ onInput: CtrlChangeHandler;
20
22
  min: number;
21
23
  max: number;
22
24
  step: number;
@@ -25,7 +27,7 @@ export declare class DualRangeCtrl implements Ctrl<DualRangeValue> {
25
27
  maxInput: HTMLInputElement;
26
28
  dualRange: DualRangeInput;
27
29
  valueSpan: HTMLSpanElement;
28
- constructor(config: ConfigFor<"dual-range">, onChange: CtrlChangeHandler<DualRangeValue>, onInput: CtrlChangeHandler<DualRangeValue>);
30
+ constructor(config: ConfigFor<"dual-range">, onChange: CtrlChangeHandler, onInput: CtrlChangeHandler);
29
31
  parse: (string: string) => {
30
32
  min: number;
31
33
  max: number;
@@ -36,7 +36,7 @@ export class DualRangeCtrl {
36
36
  max: parseFloat(maxInput.value),
37
37
  };
38
38
  this.update(this.value);
39
- this.onChange(this.name, this.value);
39
+ this.onChange(this);
40
40
  };
41
41
  const inputHandler = () => {
42
42
  this.value = {
@@ -44,7 +44,7 @@ export class DualRangeCtrl {
44
44
  max: parseFloat(maxInput.value),
45
45
  };
46
46
  this.update(this.value);
47
- this.onInput(this.name, this.value);
47
+ this.onInput(this);
48
48
  };
49
49
  const minInput = document.createElement("input");
50
50
  minInput.setAttribute("type", "range");
@@ -101,6 +101,8 @@ export class DualRangeCtrl {
101
101
  this.dualRange.update();
102
102
  };
103
103
  this.name = config.name;
104
+ this.id = config.id || config.name;
105
+ this.group = config.group || "";
104
106
  this.label = config.label || config.name;
105
107
  this.min = config.min;
106
108
  this.max = config.max;
@@ -2,12 +2,14 @@ import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
2
2
  export type Easing = [number, number, number, number];
3
3
  export declare class EasingCtrl implements Ctrl<Easing> {
4
4
  type: CtrlType;
5
+ id: string;
6
+ group?: string;
5
7
  name: string;
6
8
  label: string;
7
9
  value: Easing;
8
10
  isRandomizationDisabled: boolean;
9
- onChange: CtrlChangeHandler<Easing>;
10
- onInput: CtrlChangeHandler<Easing>;
11
+ onChange: CtrlChangeHandler;
12
+ onInput: CtrlChangeHandler;
11
13
  element: HTMLElement;
12
14
  ticks: SVGLineElement[];
13
15
  control: HTMLDivElement;
@@ -15,7 +17,7 @@ export declare class EasingCtrl implements Ctrl<Easing> {
15
17
  lines: SVGLineElement[];
16
18
  path: SVGPathElement;
17
19
  presets: Record<string, Easing>;
18
- constructor(config: ConfigFor<"easing">, onChange: CtrlChangeHandler<Easing>, onInput: CtrlChangeHandler<Easing>);
20
+ constructor(config: ConfigFor<"easing">, onChange: CtrlChangeHandler, onInput: CtrlChangeHandler);
19
21
  parse: (string: string) => Easing;
20
22
  getRandomValue: () => Easing;
21
23
  getDefaultValue: () => Easing;
@@ -117,14 +117,14 @@ export class EasingCtrl {
117
117
  dragging = false;
118
118
  const newValue = getNewValue(e.clientX, e.clientY);
119
119
  this.value = newValue;
120
- this.onChange(this.name, this.value);
120
+ this.onChange(this);
121
121
  this.update();
122
122
  }
123
123
  });
124
124
  document.addEventListener("mousemove", (e) => {
125
125
  if (dragging) {
126
126
  const newValue = getNewValue(e.clientX, e.clientY);
127
- this.onInput(this.name, this.value);
127
+ this.onInput(this);
128
128
  this.updateUI(newValue);
129
129
  }
130
130
  });
@@ -145,7 +145,7 @@ export class EasingCtrl {
145
145
  if (dragging) {
146
146
  e.preventDefault();
147
147
  const newValue = getNewValue(e.touches[0].clientX, e.touches[0].clientY);
148
- this.onInput(this.name, this.value);
148
+ this.onInput(this);
149
149
  this.updateUI(newValue);
150
150
  }
151
151
  });
@@ -155,7 +155,7 @@ export class EasingCtrl {
155
155
  dragging = false;
156
156
  const newValue = getNewValue(e.changedTouches[0].clientX, e.changedTouches[0].clientY);
157
157
  this.value = newValue;
158
- this.onChange(this.name, this.value);
158
+ this.onChange(this);
159
159
  this.update();
160
160
  }
161
161
  });
@@ -175,7 +175,7 @@ export class EasingCtrl {
175
175
  // Cap x values
176
176
  newValue[index] = Math.max(Math.min(newValue[index], 1), 0);
177
177
  this.value = newValue;
178
- this.onChange(this.name, this.value);
178
+ this.onChange(this);
179
179
  this.update();
180
180
  }
181
181
  });
@@ -192,7 +192,7 @@ export class EasingCtrl {
192
192
  button.textContent = key.toLowerCase().replace("ease_", "");
193
193
  button.addEventListener("click", () => {
194
194
  this.value = this.presets[key];
195
- this.onChange(this.name, this.value);
195
+ this.onChange(this);
196
196
  this.update();
197
197
  });
198
198
  presetButtons.appendChild(button);
@@ -270,6 +270,8 @@ export class EasingCtrl {
270
270
  this.element.setAttribute("data-value", value.join(","));
271
271
  };
272
272
  this.name = config.name;
273
+ this.id = config.id || config.name;
274
+ this.group = config.group || "";
273
275
  this.label = config.label || config.name;
274
276
  this.value =
275
277
  config.defaultValue === undefined
@@ -8,17 +8,19 @@ export type RadioControlOptions = {
8
8
  };
9
9
  export declare class RadioCtrl implements Ctrl<string> {
10
10
  type: CtrlType;
11
+ htmlId: string;
12
+ id: string;
13
+ group?: string;
11
14
  name: string;
12
15
  label: string;
13
16
  value: string;
14
17
  isRandomizationDisabled: boolean;
15
- onChange: CtrlChangeHandler<string>;
16
- onInput: CtrlChangeHandler<string>;
18
+ onChange: CtrlChangeHandler;
19
+ onInput: CtrlChangeHandler;
17
20
  items: Option[];
18
21
  element: HTMLElement;
19
22
  columns: 1 | 2 | 3 | 4 | 5;
20
- id: string;
21
- constructor(config: ConfigFor<"radio">, onChange: CtrlChangeHandler<string>, onInput: CtrlChangeHandler<string>);
23
+ constructor(config: ConfigFor<"radio">, onChange: CtrlChangeHandler, onInput: CtrlChangeHandler);
22
24
  parse: (string: string) => string;
23
25
  getRandomValue: () => string;
24
26
  getDefaultValue: () => string;
@@ -23,17 +23,17 @@ export class RadioCtrl {
23
23
  const inputs = items.map((item) => {
24
24
  const input = document.createElement("input");
25
25
  input.setAttribute("type", "radio");
26
- input.setAttribute("name", this.id);
27
- input.setAttribute("id", `${this.id}-${toKebabCase(item.value)}`);
26
+ input.setAttribute("name", this.htmlId);
27
+ input.setAttribute("id", `${this.htmlId}-${toKebabCase(item.value)}`);
28
28
  input.setAttribute("value", item.value);
29
29
  input.checked = item.value === value;
30
30
  input.addEventListener("change", () => {
31
31
  this.value = this.parse(input.value);
32
- this.onChange(this.name, this.value);
32
+ this.onChange(this);
33
33
  });
34
34
  input.addEventListener("input", () => {
35
35
  this.value = this.parse(input.value);
36
- this.onInput(this.name, this.value);
36
+ this.onInput(this);
37
37
  });
38
38
  const label = document.createElement("span");
39
39
  label.textContent = item.label;
@@ -77,8 +77,10 @@ export class RadioCtrl {
77
77
  });
78
78
  this.columns = config.columns || 3;
79
79
  this.name = config.name;
80
+ this.id = config.id || config.name;
81
+ this.group = config.group || "";
80
82
  this.label = config.label || config.name;
81
- this.id = `ctrls__${toKebabCase(config.name)}-${getRandomString()}`;
83
+ this.htmlId = `ctrls__${toKebabCase(config.name)}-${getRandomString()}`;
82
84
  const defaultValue = this.items.find((item) => item.value === config.defaultValue);
83
85
  this.value = defaultValue?.value || this.getDefaultValue();
84
86
  this.isRandomizationDisabled = config.isRandomizationDisabled || false;
@@ -1,19 +1,21 @@
1
1
  import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
2
2
  export declare class RangeCtrl implements Ctrl<number> {
3
3
  type: CtrlType;
4
+ id: string;
5
+ group?: string;
4
6
  name: string;
5
7
  label: string;
6
8
  value: number;
7
9
  isRandomizationDisabled: boolean;
8
- onChange: CtrlChangeHandler<number>;
9
- onInput: CtrlChangeHandler<number>;
10
+ onChange: CtrlChangeHandler;
11
+ onInput: CtrlChangeHandler;
10
12
  min: number;
11
13
  max: number;
12
14
  step: number;
13
15
  element: HTMLElement;
14
16
  input: HTMLInputElement;
15
17
  valueSpan: HTMLSpanElement;
16
- constructor(config: ConfigFor<"range">, onChange: CtrlChangeHandler<number>, onInput: CtrlChangeHandler<number>);
18
+ constructor(config: ConfigFor<"range">, onChange: CtrlChangeHandler, onInput: CtrlChangeHandler);
17
19
  parse: (string: string) => number;
18
20
  getRandomValue: () => number;
19
21
  getDefaultValue: () => number;
@@ -33,12 +33,12 @@ export class RangeCtrl {
33
33
  input.addEventListener("input", () => {
34
34
  this.value = this.parse(input.value);
35
35
  this.update(this.value);
36
- this.onInput(this.name, this.value);
36
+ this.onInput(this);
37
37
  });
38
38
  input.addEventListener("change", () => {
39
39
  this.value = this.parse(input.value);
40
40
  this.update(this.value);
41
- this.onChange(this.name, this.value);
41
+ this.onChange(this);
42
42
  });
43
43
  input.addEventListener("input", () => {
44
44
  const value = this.parse(input.value);
@@ -73,6 +73,8 @@ export class RangeCtrl {
73
73
  this.element.style.setProperty("--gradient-position", `${percentage.toFixed(2)}%`);
74
74
  };
75
75
  this.name = config.name;
76
+ this.id = config.id || config.name;
77
+ this.group = config.group || "";
76
78
  this.label = config.label || config.name;
77
79
  this.isRandomizationDisabled = config.isRandomizationDisabled || false;
78
80
  this.onChange = onChange;
@@ -1,15 +1,17 @@
1
1
  import type { Ctrl, CtrlChangeHandler, CtrlConfig, CtrlType } from ".";
2
2
  export declare class SeedCtrl implements Ctrl<string> {
3
3
  type: CtrlType;
4
+ id: string;
5
+ group?: string;
4
6
  name: string;
5
7
  label: string;
6
8
  value: string;
7
9
  isRandomizationDisabled: boolean;
8
- onChange: CtrlChangeHandler<string>;
9
- onInput: CtrlChangeHandler<string>;
10
+ onChange: CtrlChangeHandler;
11
+ onInput: CtrlChangeHandler;
10
12
  element: HTMLElement;
11
13
  input: HTMLInputElement;
12
- constructor(config: CtrlConfig<string>, onChange: CtrlChangeHandler<string>, onInput: CtrlChangeHandler<string>);
14
+ constructor(config: CtrlConfig<string>, onChange: CtrlChangeHandler, onInput: CtrlChangeHandler);
13
15
  parse: (string: string) => string;
14
16
  getRandomValue: () => string;
15
17
  getDefaultValue: () => string;
@@ -30,11 +30,11 @@ export class SeedCtrl {
30
30
  input.setAttribute("name", id);
31
31
  input.addEventListener("change", () => {
32
32
  this.value = this.parse(input.value);
33
- this.onChange(this.name, this.value);
33
+ this.onChange(this);
34
34
  });
35
35
  input.addEventListener("input", () => {
36
36
  this.value = this.parse(input.value);
37
- this.onInput(this.name, this.value);
37
+ this.onInput(this);
38
38
  });
39
39
  const reload = document.createElement("button");
40
40
  reload.innerHTML = refreshIcon;
@@ -42,7 +42,7 @@ export class SeedCtrl {
42
42
  reload.addEventListener("click", () => {
43
43
  this.value = this.getRandomValue();
44
44
  this.update();
45
- this.onChange(this.name, this.getRandomValue());
45
+ this.onChange(this);
46
46
  });
47
47
  const right = document.createElement("div");
48
48
  right.classList.add("ctrls__control-right");
@@ -66,6 +66,8 @@ export class SeedCtrl {
66
66
  this.input.value = value;
67
67
  };
68
68
  this.name = config.name;
69
+ this.id = config.id || config.name;
70
+ this.group = config.group || "";
69
71
  this.label = config.label || config.name;
70
72
  this.value =
71
73
  config.defaultValue === undefined
@@ -8,21 +8,25 @@ import { SeedCtrl } from "./ctrl-seed";
8
8
  export interface PRNG {
9
9
  (): number;
10
10
  }
11
- export type CtrlType = "boolean" | "range" | "radio" | "seed" | "easing" | "dual-range";
12
- export type CtrlChangeHandler<T> = (name: string, value: T) => void;
11
+ export type CtrlType = "boolean" | "range" | "radio" | "seed" | "easing" | "dual-range" | "group";
12
+ export type CtrlChangeHandler = (control: CtrlComponent) => void;
13
13
  export type CtrlConfig<T = unknown> = {
14
14
  type: CtrlType;
15
+ id?: string;
15
16
  name: string;
17
+ group?: string;
16
18
  label?: string;
17
19
  defaultValue?: T;
18
20
  isRandomizationDisabled?: boolean;
19
21
  };
20
22
  export interface Ctrl<T> {
23
+ id: string;
24
+ group?: string;
21
25
  name: string;
22
26
  label: string;
23
27
  type: CtrlType;
24
28
  isRandomizationDisabled: boolean;
25
- onChange: CtrlChangeHandler<T>;
29
+ onChange: CtrlChangeHandler;
26
30
  parse: (value: string) => T;
27
31
  getRandomValue: () => T;
28
32
  getDefaultValue: () => T;
@@ -59,23 +63,44 @@ export interface CtrlTypeMap {
59
63
  max: number;
60
64
  step?: number;
61
65
  };
66
+ group: {
67
+ value: Record<string, unknown>;
68
+ controls: readonly ConfigItem[];
69
+ isRandomizationDisabled?: boolean;
70
+ };
62
71
  }
63
72
  export type TypedControlConfig = {
64
73
  [K in CtrlType]: {
65
74
  type: K;
75
+ id?: string;
66
76
  name: string;
77
+ group?: string;
67
78
  label?: string;
68
79
  defaultValue?: CtrlTypeMap[K]["value"];
69
80
  isRandomizationDisabled?: boolean;
70
81
  } & Omit<CtrlTypeMap[K], "value">;
71
82
  }[CtrlType];
83
+ export type GroupConfig = {
84
+ type: "group";
85
+ name: string;
86
+ label?: string;
87
+ controls: readonly TypedControlConfig[];
88
+ isRandomizationDisabled?: boolean;
89
+ };
90
+ export type ConfigItem = TypedControlConfig | GroupConfig;
72
91
  export type ConfigFor<T extends CtrlType> = Extract<TypedControlConfig, {
73
92
  type: T;
74
93
  }>;
75
- type ExtractValues<Configs extends readonly TypedControlConfig[]> = {
76
- [C in Configs[number] as C["name"]]: CtrlTypeMap[C["type"]]["value"];
94
+ type ExtractValues<Configs extends readonly ConfigItem[]> = {
95
+ [C in Extract<Configs[number], {
96
+ type: Exclude<CtrlType, "group">;
97
+ }> as C["name"]]: CtrlTypeMap[C["type"]]["value"];
98
+ } & {
99
+ [C in Extract<Configs[number], {
100
+ type: "group";
101
+ }> as C["name"]]: OptionsMap<C["controls"]>;
77
102
  };
78
- type DerivedProps<Configs extends readonly TypedControlConfig[]> = {
103
+ type DerivedProps<Configs extends readonly ConfigItem[]> = {
79
104
  [C in Extract<Configs[number], {
80
105
  type: "easing";
81
106
  }> as `${C["name"]}Easing`]: ReturnType<typeof BezierEasing>;
@@ -83,8 +108,12 @@ type DerivedProps<Configs extends readonly TypedControlConfig[]> = {
83
108
  [C in Extract<Configs[number], {
84
109
  type: "seed";
85
110
  }> as `${C["name"]}Rng`]: PRNG;
111
+ } & {
112
+ [C in Extract<Configs[number], {
113
+ type: "group";
114
+ }> as C["name"]]: DerivedProps<C["controls"]>;
86
115
  };
87
- type OptionsMap<Configs extends readonly TypedControlConfig[]> = ExtractValues<Configs> & DerivedProps<Configs>;
116
+ type OptionsMap<Configs extends readonly ConfigItem[]> = ExtractValues<Configs> & DerivedProps<Configs>;
88
117
  type ControlsOptions = {
89
118
  showRandomizeButton?: boolean;
90
119
  storage?: "hash" | "none";
@@ -93,20 +122,22 @@ type ControlsOptions = {
93
122
  title?: string;
94
123
  };
95
124
  type CtrlComponent = BooleanCtrl | RangeCtrl | RadioCtrl | SeedCtrl | EasingCtrl | DualRangeCtrl;
96
- export declare class Ctrls<Configs extends readonly TypedControlConfig[]> {
125
+ export declare class Ctrls<Configs extends readonly ConfigItem[]> {
97
126
  options: ControlsOptions;
98
127
  controls: CtrlComponent[];
99
128
  controlsMap: Record<string, CtrlComponent>;
100
129
  element: HTMLDivElement;
101
130
  onChange?: (updatedValues: Partial<ReturnType<typeof this.getValues>>) => void;
102
131
  onInput?: (updatedValues: Partial<ReturnType<typeof this.getValues>>) => void;
103
- constructor(controls: Configs, options?: ControlsOptions);
132
+ constructor(configs: Configs, options?: ControlsOptions);
133
+ registerControl: (config: TypedControlConfig, onChangeControlHandler: (control: CtrlComponent) => void, onInputControlHandler: (control: CtrlComponent) => void, group?: string) => void;
104
134
  buildUI: () => HTMLDivElement;
105
135
  toggleVisibility: () => void;
106
136
  addHashListeners: () => void;
107
137
  getHash: () => string;
108
138
  setHash: () => void;
109
139
  updateFromHash: () => void;
140
+ updateValuesObject(values: any, control: CtrlComponent): void;
110
141
  getValues(): OptionsMap<Configs>;
111
142
  randomize: () => void;
112
143
  }
@@ -17,16 +17,65 @@ const controlMap = {
17
17
  "dual-range": DualRangeCtrl,
18
18
  };
19
19
  export class Ctrls {
20
- constructor(controls, options) {
20
+ constructor(configs, options) {
21
+ this.controls = [];
21
22
  this.controlsMap = {};
23
+ this.registerControl = (config, onChangeControlHandler, onInputControlHandler, group = "") => {
24
+ // To make typescript happy
25
+ if (config.type === "group") {
26
+ return;
27
+ }
28
+ // TODO
29
+ // Again, document as it is my personal preference
30
+ if (!config.label) {
31
+ config.label = toSpaceCase(config.name);
32
+ }
33
+ // TODO
34
+ // Document this behaviour
35
+ // This might counter-intuitive for some people,
36
+ // but it is my personal preference to have properties named in camel case
37
+ // when using them in code
38
+ //
39
+ // However, they are going to be converted to kebab case when used in the hash,
40
+ // because it is nicer that URL be all lowercase
41
+ config.name = toCamelCase(config.name);
42
+ if (group) {
43
+ config.group = group;
44
+ config.id = toCamelCase(`${group}-${config.name}`);
45
+ }
46
+ const ControlComponent = controlMap[config.type];
47
+ const control = new ControlComponent(config, onChangeControlHandler, onInputControlHandler);
48
+ this.controlsMap[control.id] = control;
49
+ this.controls.push(control);
50
+ };
22
51
  this.buildUI = () => {
23
52
  const element = document.createElement("div");
24
53
  element.classList.add("ctrls");
25
54
  element.classList.add(`ctrls--${this.options.theme}-theme`);
26
55
  const controlsContainer = document.createElement("div");
27
56
  controlsContainer.classList.add("ctrls__controls");
57
+ let group = "";
58
+ let groupElement;
28
59
  this.controls.forEach((control) => {
29
- controlsContainer.appendChild(control.element);
60
+ if (control.group) {
61
+ if (control.group !== group) {
62
+ group = control.group;
63
+ groupElement = document.createElement("div");
64
+ groupElement.classList.add("ctrls__group");
65
+ const groupTitle = document.createElement("button");
66
+ groupTitle.classList.add("ctrls__group-title");
67
+ groupTitle.innerText = control.group;
68
+ groupTitle.addEventListener("click", () => {
69
+ groupTitle.parentElement?.classList.toggle("ctrls__group--hidden");
70
+ });
71
+ groupElement.append(groupTitle);
72
+ controlsContainer.appendChild(groupElement);
73
+ }
74
+ groupElement.append(control.element);
75
+ }
76
+ else {
77
+ controlsContainer.appendChild(control.element);
78
+ }
30
79
  });
31
80
  if (this.options.showRandomizeButton) {
32
81
  const randomizeButton = document.createElement("button");
@@ -59,7 +108,7 @@ export class Ctrls {
59
108
  this.getHash = () => {
60
109
  const values = this.controls
61
110
  .map((control) => {
62
- return `${toKebabCase(control.name)}:${control.valueToString()}`;
111
+ return `${toKebabCase(control.id)}:${control.valueToString()}`;
63
112
  })
64
113
  .join("/");
65
114
  return `#/${values}`;
@@ -73,22 +122,22 @@ export class Ctrls {
73
122
  const items = [];
74
123
  pairs.forEach((pair) => {
75
124
  const [kebabCaseName, value] = pair.split(":");
76
- const name = toCamelCase(kebabCaseName);
77
- const control = this.controlsMap[name];
125
+ const id = toCamelCase(kebabCaseName);
126
+ const control = this.controlsMap[id];
78
127
  if (control) {
79
128
  const parsed = control.parse(value);
80
129
  items.push({
81
- name,
130
+ id,
82
131
  value: parsed,
83
132
  });
84
133
  }
85
134
  });
86
135
  const updatedValues = {};
87
136
  items.forEach((item) => {
88
- const { name, value } = item;
89
- const control = this.controlsMap[name];
137
+ const { id, value } = item;
138
+ const control = this.controlsMap[id];
90
139
  if (control && JSON.stringify(value) !== JSON.stringify(control.value)) {
91
- updatedValues[name] = value;
140
+ this.updateValuesObject(updatedValues, control);
92
141
  control.update(value);
93
142
  }
94
143
  });
@@ -105,7 +154,7 @@ export class Ctrls {
105
154
  }
106
155
  control.value = control.getRandomValue();
107
156
  control.update(control.value);
108
- updatedValues[control.name] = control.value;
157
+ this.updateValuesObject(updatedValues, control);
109
158
  });
110
159
  if (Object.keys(updatedValues).length > 0) {
111
160
  this.onChange?.(updatedValues);
@@ -121,34 +170,28 @@ export class Ctrls {
121
170
  theme: "system",
122
171
  ...options,
123
172
  };
124
- const onChangeControlHandler = (name, value) => {
125
- this.onChange?.({ [name]: value });
173
+ const onChangeControlHandler = (control) => {
174
+ const updatedValues = {};
175
+ this.updateValuesObject(updatedValues, control);
176
+ this.onChange?.(updatedValues);
126
177
  if (this.options.storage === "hash") {
127
178
  this.setHash();
128
179
  }
129
180
  };
130
- const onInputControlHandler = (name, value) => {
131
- this.onInput?.({ [name]: value });
181
+ const onInputControlHandler = (control) => {
182
+ const updatedValues = {};
183
+ this.updateValuesObject(updatedValues, control);
184
+ this.onInput?.(updatedValues);
132
185
  };
133
- this.controls = controls.map((config) => {
134
- // TODO
135
- // Document this behaviour
136
- // This might counter-intuitive for some people,
137
- // but it is my personal preference to have properties named in camel case
138
- // when using them in code
139
- //
140
- // However, they are going to be converted to kebab case when used in the hash,
141
- // because it is nicer that URL be all lowercase
142
- config.name = toCamelCase(config.name);
143
- // TODO
144
- // Again, document as it is my personal preference
145
- if (!config.label) {
146
- config.label = toSpaceCase(config.name);
186
+ configs.map((config) => {
187
+ if (config.type === "group") {
188
+ config.controls.forEach((groupConfig) => {
189
+ this.registerControl(groupConfig, onChangeControlHandler, onInputControlHandler, toCamelCase(config.name));
190
+ });
191
+ }
192
+ else {
193
+ this.registerControl(config, onChangeControlHandler, onInputControlHandler);
147
194
  }
148
- const ControlComponent = controlMap[config.type];
149
- const control = new ControlComponent(config, onChangeControlHandler, onInputControlHandler);
150
- this.controlsMap[control.name] = control;
151
- return control;
152
195
  });
153
196
  this.element = this.buildUI();
154
197
  if (this.options.storage === "hash") {
@@ -158,17 +201,27 @@ export class Ctrls {
158
201
  this.options.parent.appendChild(this.element);
159
202
  }
160
203
  }
204
+ updateValuesObject(values, control) {
205
+ let objectToUpdate = values;
206
+ if (control.group) {
207
+ if (!values[control.group]) {
208
+ values[control.group] = {};
209
+ }
210
+ objectToUpdate = values[control.group];
211
+ }
212
+ objectToUpdate[control.name] = control.value;
213
+ if (control.type === "easing") {
214
+ objectToUpdate[control.name + "Easing"] = BezierEasing(...control.value);
215
+ }
216
+ else if (control.type === "seed") {
217
+ objectToUpdate[control.name + "Rng"] = Alea(...control.value.split("-"));
218
+ }
219
+ }
161
220
  getValues() {
162
- const options = {};
221
+ const values = {};
163
222
  this.controls.forEach((control) => {
164
- options[control.name] = control.value;
165
- if (control.type === "easing") {
166
- options[control.name + "Easing"] = BezierEasing(...control.value);
167
- }
168
- else if (control.type === "seed") {
169
- options[control.name + "Rng"] = Alea(control.value);
170
- }
223
+ this.updateValuesObject(values, control);
171
224
  });
172
- return options;
225
+ return values;
173
226
  }
174
227
  }
package/dist/ctrls.css CHANGED
@@ -170,8 +170,8 @@
170
170
  ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
171
171
  "DejaVu Sans Mono", monospace;
172
172
  --ctrls-radius: 4px;
173
- --ctrls-label-width: 5rem;
174
- --ctrls-width: 22rem;
173
+ --ctrls-label-width: 7rem;
174
+ --ctrls-width: 24rem;
175
175
  --ctrls-font-size: 0.75rem;
176
176
  --ctrls-value-font-size: 0.6875rem;
177
177
  --ctrls-c: 0.25;
@@ -301,6 +301,7 @@
301
301
  overflow: visible;
302
302
  }
303
303
 
304
+ .ctrls__group-title,
304
305
  .ctrls__title {
305
306
  background: none;
306
307
  border: none;
@@ -312,15 +313,17 @@
312
313
  cursor: pointer;
313
314
  transition: color 300ms, background-color 300ms;
314
315
  }
315
- .ctrls__title:focus-visible, .ctrls__title:hover {
316
+ .ctrls__group-title:focus-visible, .ctrls__group-title:hover,
317
+ .ctrls__title:focus-visible,
318
+ .ctrls__title:hover {
316
319
  color: var(--ctrls-theme);
317
320
  background: var(--ctrls-btn-hover-bg);
318
321
  }
319
322
 
320
323
  .ctrls__controls {
321
324
  display: grid;
325
+ padding-block: 0.5rem;
322
326
  gap: 0.5rem;
323
- padding: 0.5rem;
324
327
  overflow: auto;
325
328
  scrollbar-width: thin;
326
329
  scrollbar-color: var(--ctrls-scrollbar-thumb-bg) transparent;
@@ -330,6 +333,10 @@
330
333
  display: none;
331
334
  }
332
335
 
336
+ .ctrls--hidden .ctrls__title {
337
+ border-block-end: none;
338
+ }
339
+
333
340
  /* ----- Buttons ----- */
334
341
  .ctrls__btn {
335
342
  background: none;
@@ -355,7 +362,8 @@
355
362
  }
356
363
 
357
364
  .ctrls__btn--lg {
358
- margin-left: var(--ctrls-label-width);
365
+ margin-inline-start: calc(var(--ctrls-label-width) + 0.5rem);
366
+ margin-inline-end: 0.5rem;
359
367
  padding: 0.5rem 1rem;
360
368
  background: var(--ctrls-btn-bg);
361
369
  border: 1px solid var(--ctrls-input-border);
@@ -375,8 +383,40 @@
375
383
  opacity: 0;
376
384
  }
377
385
 
378
- /* ----- General ----- */
386
+ /* ----- Groups ----- */
387
+ .ctrls__group {
388
+ margin: 0 0.5rem;
389
+ display: grid;
390
+ gap: 0.5rem;
391
+ padding-block-end: 0.5rem;
392
+ border: 1px solid var(--ctrls-border);
393
+ border-radius: var(--ctrls-radius);
394
+ }
395
+
396
+ .ctrls__group .ctrls__control {
397
+ grid-template-columns: calc(var(--ctrls-label-width) - 0.5rem) minmax(0, 1fr);
398
+ }
399
+
400
+ .ctrls__group:has(+ .ctrls__group) {
401
+ border-block-end: none;
402
+ margin-block-end: 0;
403
+ }
404
+
405
+ .ctrls__group-title {
406
+ text-align: left;
407
+ }
408
+
409
+ .ctrls__group--hidden {
410
+ padding-block-end: 0;
411
+ }
412
+
413
+ .ctrls__group--hidden .ctrls__control {
414
+ display: none;
415
+ }
416
+
417
+ /* ----- Controls ----- */
379
418
  .ctrls__control {
419
+ padding-inline: 0.5rem;
380
420
  display: grid;
381
421
  grid-template-columns: var(--ctrls-label-width) minmax(0, 1fr);
382
422
  align-items: center;
@@ -1 +1 @@
1
- {"version":3,"sourceRoot":"","sources":["../node_modules/@stanko/dual-range-input/dist/index.css","../src/scss/_ctrls.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;;AC3IF;EACE;AAAA;AAAA;EAGA;EACA;EACA;EACA;EACA;EAGA;EACA;EAEA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EAGA;EAGA;EACA;EACA;EAGA;EACA;EAGA;EAGA;EAEA;EACA;EAEA;EACA;EAGA;EAGA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EAGA;EACA;EAEA;EAEA;EACA;EAEA;EAGA;EAEA;EACA;;;AA+CF;EACE;IA3CA;IAEA;IACA;IAGA;IACA;IACA;IACA;IACA;IACA;IAGA;IAGA;IACA;IACA;IAGA;IACA;IAGA;IAGA;IAEA;IAGA;IAGA;AAAA;AAAA;;;AAWF;EAhDE;EAEA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EAGA;EAGA;EACA;EACA;EAGA;EACA;EAGA;EAGA;EAEA;EAGA;EAGA;AAAA;AAAA;;;AAiBF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;EAGE;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;;AAGF;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAGA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;;AAGF;EACE;EACA;EACA,YACE;;AAIJ;EACE;;;AAKJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EAEE;EACA;;;AAOA;EACE;;AAGF;AAAA;AAAA;EAEE;;;AAKN;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKA;AAAA;EACE;EACA;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAKJ;EACE;EACA;EACA;;;AAIF;EACE;;;AAIF;EACE;;;AAIF;EACE;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAmCJ;EA5BE;EACA;EACA;EACA;EACA;EAEA;;;AA0BF;EAhCE;EACA;EACA;EACA;EACA;EAEA;;;AA8BF;EAtBE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAkBA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKF;EAvDE;EACA;EACA;EACA;EACA;EAEA;;;AAqDF;EA7CE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAwCA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EAGE;;AAEA;EACE;;AANJ;EASE;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;;AAEA;EACE;;;AAIJ;AAEA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;EAEF;EACA;EACA;;AAEA;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;;AAIE;EACE;;;AAKN;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;AAAA;EAEE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EAEE;;AAEA;EACE;EACA","file":"ctrls.css"}
1
+ {"version":3,"sourceRoot":"","sources":["../node_modules/@stanko/dual-range-input/dist/index.css","../src/scss/_ctrls.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;;AC3IF;EACE;AAAA;AAAA;EAGA;EACA;EACA;EACA;EACA;EAGA;EACA;EAEA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EAGA;EAGA;EACA;EACA;EAGA;EACA;EAGA;EAGA;EAEA;EACA;EAEA;EACA;EAGA;EAGA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EAGA;EACA;EAEA;EAEA;EACA;EAEA;EAGA;EAEA;EACA;;;AA+CF;EACE;IA3CA;IAEA;IACA;IAGA;IACA;IACA;IACA;IACA;IACA;IAGA;IAGA;IACA;IACA;IAGA;IACA;IAGA;IAGA;IAEA;IAGA;IAGA;AAAA;AAAA;;;AAWF;EAhDE;EAEA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EAGA;EAGA;EACA;EACA;EAGA;EACA;EAGA;EAGA;EAEA;EAGA;EAGA;AAAA;AAAA;;;AAiBF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;EAGE;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;;AAGF;EACE;EACA;;;AAIJ;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;AAAA;AAAA;EAEE;EACA;;;AAOJ;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAGA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;;AAGF;EACE;EACA;EACA,YACE;;AAIJ;EACE;;;AAKJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EAEE;EACA;;;AAOA;EACE;;AAGF;AAAA;AAAA;EAEE;;;AAKN;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKA;AAAA;EACE;EACA;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAKJ;EACE;EACA;EACA;;;AAIF;EACE;;;AAIF;EACE;;;AAIF;EACE;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAmCJ;EA5BE;EACA;EACA;EACA;EACA;EAEA;;;AA0BF;EAhCE;EACA;EACA;EACA;EACA;EAEA;;;AA8BF;EAtBE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAkBA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKF;EAvDE;EACA;EACA;EACA;EACA;EAEA;;;AAqDF;EA7CE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAwCA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EAGE;;AAEA;EACE;;AANJ;EASE;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;;AAEA;EACE;;;AAIJ;AAEA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;EAEF;EACA;EACA;;AAEA;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;;AAIE;EACE;;;AAKN;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;AAAA;EAEE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EAEE;;AAEA;EACE;EACA","file":"ctrls.css"}
@@ -1,2 +1,2 @@
1
- declare const Alea: (seed?: string) => (() => number);
1
+ declare const Alea: (...seeds: string[]) => (() => number);
2
2
  export default Alea;
@@ -1,36 +1,45 @@
1
1
  // Algorithm by Johannes Baagøe
2
2
  // Slim TypeScript version of this implementation:
3
3
  // https://github.com/coverslide/node-alea
4
- const getMash = () => {
5
- let n = 0xefc8249d;
6
- const mash = (seed) => {
7
- for (let i = 0; i < seed.length; i++) {
8
- n += seed.charCodeAt(i);
9
- let h = 0.02519603282416938 * n;
10
- n = h >>> 0;
11
- h -= n;
12
- h *= n;
13
- n = h >>> 0;
14
- h -= n;
15
- n += h * 0x100000000; // 2^32
16
- }
17
- return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
4
+ const Alea = (...seeds) => {
5
+ const getMash = () => {
6
+ let n = 0xefc8249d;
7
+ const mash = (seed) => {
8
+ for (let i = 0; i < seed.length; i++) {
9
+ n += seed.charCodeAt(i);
10
+ let h = 0.02519603282416938 * n;
11
+ n = h >>> 0;
12
+ h -= n;
13
+ h *= n;
14
+ n = h >>> 0;
15
+ h -= n;
16
+ n += h * 0x100000000; // 2^32
17
+ }
18
+ return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
19
+ };
20
+ return mash;
18
21
  };
19
- return mash;
20
- };
21
- const Alea = (seed = Date.now().toString()) => {
22
- let s = [0, 0, 0];
22
+ const getRandomSeeds = () => {
23
+ return [
24
+ Math.random().toString(36).slice(2),
25
+ Math.random().toString(36).slice(2),
26
+ Math.random().toString(36).slice(2),
27
+ ];
28
+ };
29
+ const mash = getMash();
30
+ const s = [mash(" "), mash(" "), mash(" ")];
23
31
  let c = 1;
24
- let mash = getMash();
25
- s.forEach((_, i) => {
26
- s[i] = mash(" ") - mash(seed);
27
- if (s[i] < 0) {
28
- s[i] += 1;
29
- }
32
+ (seeds || getRandomSeeds()).forEach((seed) => {
33
+ s.forEach((_, i) => {
34
+ s[i] -= mash(seed);
35
+ if (s[i] < 0) {
36
+ s[i] += 1;
37
+ }
38
+ });
30
39
  });
31
40
  const random = () => {
32
41
  const t = 2091639 * s[0] + c * 2.3283064365386963e-10; // 2^-32
33
- c = t | 0;
42
+ c = t | 0; // quicker floor
34
43
  s[0] = s[1];
35
44
  s[1] = s[2];
36
45
  s[2] = t - c;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@stanko/ctrls",
3
- "version": "0.1.9",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
+ "test": "vitest",
6
7
  "start": "npm run parse-markdown && vite",
7
8
  "build": "npm run parse-markdown && tsc && rm -rf ./docs && vite build && touch ./docs/.nojekyll",
8
9
  "build-css": "sass ./src/scss/index.scss ./dist/ctrls.css",
@@ -19,12 +20,13 @@
19
20
  ],
20
21
  "devDependencies": {
21
22
  "highlight.js": "^11.11.1",
22
- "marked": "^16.2.1",
23
- "marked-highlight": "^2.2.2",
24
- "sass": "^1.92.1",
23
+ "marked": "^17.0.0",
24
+ "marked-highlight": "^2.2.3",
25
+ "sass": "^1.94.0",
25
26
  "simplex-noise": "^4.0.3",
26
- "typescript": "~5.9.2",
27
- "vite": "^7.1.5"
27
+ "typescript": "~5.9.3",
28
+ "vite": "^7.2.2",
29
+ "vitest": "^4.0.10"
28
30
  },
29
31
  "dependencies": {
30
32
  "@stanko/dual-range-input": "^1.0.1",