osolmaz HF Staff commited on
Commit
3a2fcd3
·
verified ·
1 Parent(s): c0515fc

feat: add size variation control

Browse files
Files changed (4) hide show
  1. index.html +4 -0
  2. src/layout.test.ts +17 -0
  3. src/layout.ts +10 -3
  4. src/main.ts +6 -2
index.html CHANGED
@@ -57,6 +57,10 @@
57
  <span>Logo size <output id="size-value">1.00×</output></span>
58
  <input id="logo-size" type="range" min="0.5" max="1.6" step="0.01" value="1" />
59
  </label>
 
 
 
 
60
  <label class="range-label">
61
  <span>Spacing <output id="spacing-value">1.00×</output></span>
62
  <input id="spacing" type="range" min="0.25" max="2" step="0.01" value="1" />
 
57
  <span>Logo size <output id="size-value">1.00×</output></span>
58
  <input id="logo-size" type="range" min="0.5" max="1.6" step="0.01" value="1" />
59
  </label>
60
+ <label class="range-label">
61
+ <span>Size variation <output id="size-variation-value">100%</output></span>
62
+ <input id="size-variation" type="range" min="0" max="1.5" step="0.01" value="1" />
63
+ </label>
64
  <label class="range-label">
65
  <span>Spacing <output id="spacing-value">1.00×</output></span>
66
  <input id="spacing" type="range" min="0.25" max="2" step="0.01" value="1" />
src/layout.test.ts CHANGED
@@ -5,6 +5,7 @@ const options = {
5
  aspect: 16 / 9,
6
  count: 88,
7
  logoScale: 1,
 
8
  spacing: 1,
9
  rotation: 52,
10
  seed: 84237,
@@ -43,6 +44,22 @@ describe("generateLayout", () => {
43
  expect([...layout].every(Number.isFinite)).toBe(true);
44
  });
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  it("changes spacing continuously and visibly", () => {
47
  const compact = generateLayout({ ...options, spacing: 0.5 });
48
  const neutral = generateLayout({ ...options, spacing: 1 });
 
5
  aspect: 16 / 9,
6
  count: 88,
7
  logoScale: 1,
8
+ sizeVariation: 1,
9
  spacing: 1,
10
  rotation: 52,
11
  seed: 84237,
 
44
  expect([...layout].every(Number.isFinite)).toBe(true);
45
  });
46
 
47
+ it("controls size variation continuously", () => {
48
+ const uniform = generateLayout({ ...options, sizeVariation: 0 });
49
+ const varied = generateLayout({ ...options, sizeVariation: 1 });
50
+ const exaggerated = generateLayout({ ...options, sizeVariation: 1.5 });
51
+ const widths = (layout: Float32Array): number[] => {
52
+ const values: number[] = [];
53
+ for (let offset = 2; offset < layout.length; offset += PLACEMENT_STRIDE) values.push(layout[offset] ?? 0);
54
+ return values;
55
+ };
56
+ const range = (values: number[]): number => Math.max(...values) - Math.min(...values);
57
+
58
+ expect(range(widths(uniform))).toBeLessThan(0.000001);
59
+ expect(range(widths(varied))).toBeGreaterThan(0.15);
60
+ expect(range(widths(exaggerated))).toBeGreaterThan(range(widths(varied)));
61
+ });
62
+
63
  it("changes spacing continuously and visibly", () => {
64
  const compact = generateLayout({ ...options, spacing: 0.5 });
65
  const neutral = generateLayout({ ...options, spacing: 1 });
src/layout.ts CHANGED
@@ -2,6 +2,7 @@ export interface LayoutOptions {
2
  aspect: number;
3
  count: number;
4
  logoScale: number;
 
5
  spacing: number;
6
  rotation: number;
7
  seed: number;
@@ -31,10 +32,14 @@ class Random {
31
  }
32
  }
33
 
34
- function pickWidth(random: Random, scale: number): number {
35
  const roll = random.next();
36
  for (const [threshold, low, high] of SIZE_BANDS) {
37
- if (roll <= threshold) return (low + random.next() * (high - low)) * scale;
 
 
 
 
38
  }
39
  return 0.07 * scale;
40
  }
@@ -49,7 +54,9 @@ export function generateLayout(options: LayoutOptions): Float32Array {
49
  const width = Math.max(0.5, options.aspect);
50
  const height = 1;
51
  const widths = new Float32Array(count);
52
- for (let index = 0; index < count; index += 1) widths[index] = pickWidth(random, options.logoScale);
 
 
53
  widths.sort();
54
  widths.reverse();
55
 
 
2
  aspect: number;
3
  count: number;
4
  logoScale: number;
5
+ sizeVariation: number;
6
  spacing: number;
7
  rotation: number;
8
  seed: number;
 
32
  }
33
  }
34
 
35
+ function pickWidth(random: Random, scale: number, variation: number): number {
36
  const roll = random.next();
37
  for (const [threshold, low, high] of SIZE_BANDS) {
38
+ if (roll <= threshold) {
39
+ const sampledWidth = low + random.next() * (high - low);
40
+ const uniformWidth = 0.115;
41
+ return Math.max(0.025, uniformWidth + (sampledWidth - uniformWidth) * variation) * scale;
42
+ }
43
  }
44
  return 0.07 * scale;
45
  }
 
54
  const width = Math.max(0.5, options.aspect);
55
  const height = 1;
56
  const widths = new Float32Array(count);
57
+ for (let index = 0; index < count; index += 1) {
58
+ widths[index] = pickWidth(random, options.logoScale, Math.max(0, options.sizeVariation));
59
+ }
60
  widths.sort();
61
  widths.reverse();
62
 
src/main.ts CHANGED
@@ -21,6 +21,7 @@ const widthInput = element<HTMLInputElement>("width");
21
  const heightInput = element<HTMLInputElement>("height");
22
  const densityInput = element<HTMLInputElement>("density");
23
  const sizeInput = element<HTMLInputElement>("logo-size");
 
24
  const spacingInput = element<HTMLInputElement>("spacing");
25
  const rotationInput = element<HTMLInputElement>("rotation");
26
  const seedInput = element<HTMLInputElement>("seed");
@@ -46,6 +47,7 @@ function readConfig(): Config {
46
  aspect: width / height,
47
  count: Math.round(numberValue(densityInput, 88)),
48
  logoScale: numberValue(sizeInput, 1),
 
49
  spacing: numberValue(spacingInput, 1),
50
  rotation: numberValue(rotationInput, 52),
51
  seed: Math.round(numberValue(seedInput, 84237)) >>> 0,
@@ -53,12 +55,13 @@ function readConfig(): Config {
53
  }
54
 
55
  function configKey(config: Config): string {
56
- return [config.aspect.toFixed(6), config.count, config.logoScale, config.spacing, config.rotation, config.seed].join(":");
57
  }
58
 
59
  function updateLabels(config: Config): void {
60
  element<HTMLOutputElement>("density-value").value = String(config.count);
61
  element<HTMLOutputElement>("size-value").value = `${config.logoScale.toFixed(2)}×`;
 
62
  element<HTMLOutputElement>("spacing-value").value = `${config.spacing.toFixed(2)}×`;
63
  element<HTMLOutputElement>("rotation-value").value = `${Math.round(config.rotation)}°`;
64
  element<HTMLSpanElement>("pixel-count").textContent = `${((config.width * config.height) / 1_000_000).toFixed(1)} MP`;
@@ -151,6 +154,7 @@ function setDefaults(): void {
151
  heightInput.value = "2160";
152
  densityInput.value = "88";
153
  sizeInput.value = "1";
 
154
  spacingInput.value = "1";
155
  rotationInput.value = "52";
156
  seedInput.value = "84237";
@@ -168,7 +172,7 @@ resolution.addEventListener("change", () => {
168
  schedulePreview();
169
  });
170
 
171
- for (const input of [widthInput, heightInput, densityInput, sizeInput, spacingInput, rotationInput, seedInput]) {
172
  input.addEventListener("input", () => {
173
  if (input === widthInput || input === heightInput) resolution.value = "custom";
174
  schedulePreview();
 
21
  const heightInput = element<HTMLInputElement>("height");
22
  const densityInput = element<HTMLInputElement>("density");
23
  const sizeInput = element<HTMLInputElement>("logo-size");
24
+ const sizeVariationInput = element<HTMLInputElement>("size-variation");
25
  const spacingInput = element<HTMLInputElement>("spacing");
26
  const rotationInput = element<HTMLInputElement>("rotation");
27
  const seedInput = element<HTMLInputElement>("seed");
 
47
  aspect: width / height,
48
  count: Math.round(numberValue(densityInput, 88)),
49
  logoScale: numberValue(sizeInput, 1),
50
+ sizeVariation: numberValue(sizeVariationInput, 1),
51
  spacing: numberValue(spacingInput, 1),
52
  rotation: numberValue(rotationInput, 52),
53
  seed: Math.round(numberValue(seedInput, 84237)) >>> 0,
 
55
  }
56
 
57
  function configKey(config: Config): string {
58
+ return [config.aspect.toFixed(6), config.count, config.logoScale, config.sizeVariation, config.spacing, config.rotation, config.seed].join(":");
59
  }
60
 
61
  function updateLabels(config: Config): void {
62
  element<HTMLOutputElement>("density-value").value = String(config.count);
63
  element<HTMLOutputElement>("size-value").value = `${config.logoScale.toFixed(2)}×`;
64
+ element<HTMLOutputElement>("size-variation-value").value = `${Math.round(config.sizeVariation * 100)}%`;
65
  element<HTMLOutputElement>("spacing-value").value = `${config.spacing.toFixed(2)}×`;
66
  element<HTMLOutputElement>("rotation-value").value = `${Math.round(config.rotation)}°`;
67
  element<HTMLSpanElement>("pixel-count").textContent = `${((config.width * config.height) / 1_000_000).toFixed(1)} MP`;
 
154
  heightInput.value = "2160";
155
  densityInput.value = "88";
156
  sizeInput.value = "1";
157
+ sizeVariationInput.value = "1";
158
  spacingInput.value = "1";
159
  rotationInput.value = "52";
160
  seedInput.value = "84237";
 
172
  schedulePreview();
173
  });
174
 
175
+ for (const input of [widthInput, heightInput, densityInput, sizeInput, sizeVariationInput, spacingInput, rotationInput, seedInput]) {
176
  input.addEventListener("input", () => {
177
  if (input === widthInput || input === heightInput) resolution.value = "custom";
178
  schedulePreview();