@oxyhq/bloom 0.35.0 → 0.35.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.
@@ -7,8 +7,9 @@ exports.computeClusterLayout = computeClusterLayout;
7
7
  /**
8
8
  * Deterministic organic circle-packing for the {@link AvatarGroup} `cluster`
9
9
  * layout — the iMessage-style "magnetic bubble cluster" where several avatars of
10
- * varying sizes nestle together inside a round bounding box (a large primary in
11
- * the middle/front, smaller members packed around it with a uniform gap).
10
+ * NEAR-EQUAL size nestle tightly together and fill a round bounding box (a
11
+ * modestly larger primary with the rest only slightly smaller, packed against
12
+ * each other with a small uniform gap — not a big primary ringed by tiny dots).
12
13
  *
13
14
  * For 4+ members the layout is produced by a small, FULLY DETERMINISTIC
14
15
  * force-directed relaxation (no `Math.random`): the primary is pinned at the
@@ -23,9 +24,11 @@ exports.computeClusterLayout = computeClusterLayout;
23
24
  *
24
25
  * Output is resolution-independent: each bubble is expressed as a fraction of
25
26
  * the group's bounding box (`cx`/`cy` centre, `d` diameter, all 0..1), so the
26
- * consumer just multiplies by the pixel `size`. Every bubble is guaranteed to
27
- * sit fully inside the `[0, 1]` box (the packed result is scaled + translated to
28
- * fit), so the cluster drops in exactly where a single round Avatar would.
27
+ * consumer just multiplies by the pixel `size`. The relaxed blob is recentred on
28
+ * its own centre and scaled so the outermost bubble edge lands on the box edge,
29
+ * so it fills the round box densely with minimal empty margin while every bubble
30
+ * is guaranteed to sit fully inside the `[0, 1]` box — the cluster drops in
31
+ * exactly where a single round Avatar would.
29
32
  */
30
33
 
31
34
  /** A single packed bubble, expressed as fractions of the bounding box (0..1). */
@@ -45,17 +48,20 @@ const CENTERING = 0.05;
45
48
  // the pinned primary and many passes this stays stable and converges.
46
49
  const SEPARATION_STRENGTH = 1;
47
50
  // Uniform gap kept between every touching pair, in primary-radius units
48
- // (primary radius = 1). Reads as consistent spacing everywhere in the cluster.
49
- const LAYOUT_GAP = 0.16;
51
+ // (primary radius = 1). Small, so near-equal bubbles nestle tightly like
52
+ // magnets rather than floating apart with visible margins.
53
+ const LAYOUT_GAP = 0.12;
50
54
  // Initial spiral spacing. Only affects convergence (the result is re-fitted to
51
55
  // the box afterwards), not the final scale.
52
56
  const SEED_SPACING = 1.7;
53
- // Relative radii: the primary is the largest; the remaining members taper from
54
- // SECONDARY_MAX (nearest the primary) down to SECONDARY_MIN (outermost) so size
55
- // decreases outward and later/overflow members are the smallest.
57
+ // Relative radii: the primary is only MODESTLY the largest; every other member
58
+ // tapers gently from SECONDARY_MAX (nearest the primary) down to SECONDARY_MIN
59
+ // (outermost). The spread is deliberately narrow so the cluster reads as a pack
60
+ // of near-equal magnetic bubbles — a slightly larger primary, not a big primary
61
+ // ringed by tiny dots.
56
62
  const PRIMARY_RADIUS = 1;
57
- const SECONDARY_MAX = 0.72;
58
- const SECONDARY_MIN = 0.5;
63
+ const SECONDARY_MAX = 0.9;
64
+ const SECONDARY_MIN = 0.8;
59
65
  const EPSILON = 1e-6;
60
66
 
61
67
  /** Relative radius for member `index` of a `count`-member cluster. */
@@ -145,20 +151,41 @@ function packCluster(count) {
145
151
  ys[0] = 0;
146
152
  }
147
153
 
148
- // Fit to the unit box: the primary is at the origin, so scale so the
149
- // outermost circle edge lands on the box radius (0.5) and place the primary at
150
- // the box centre.
154
+ // Fit to the unit box so the pack FILLS the round box edge-to-edge with
155
+ // minimal empty margin. The primary was pinned at the origin during
156
+ // relaxation, but the relaxed blob is not centred on it, so we recentre on the
157
+ // cluster's OWN centre (its bounding-box midpoint) rather than the primary:
158
+ // enclose every bubble in a circle around that centre, then scale so the
159
+ // outermost bubble edge lands exactly on the box radius (0.5). Centring the
160
+ // enclosing circle at the box centre keeps every bubble inside the [0, 1] box
161
+ // (the enclosing circle is inscribed in the square) while packing the blob
162
+ // tightly against the edge instead of floating with a gap.
163
+ let minX = Infinity;
164
+ let maxX = -Infinity;
165
+ let minY = Infinity;
166
+ let maxY = -Infinity;
167
+ for (let i = 0; i < count; i++) {
168
+ const x = xs[i] ?? 0;
169
+ const y = ys[i] ?? 0;
170
+ const r = radii[i] ?? 0;
171
+ if (x - r < minX) minX = x - r;
172
+ if (x + r > maxX) maxX = x + r;
173
+ if (y - r < minY) minY = y - r;
174
+ if (y + r > maxY) maxY = y + r;
175
+ }
176
+ const centerX = (minX + maxX) / 2;
177
+ const centerY = (minY + maxY) / 2;
151
178
  let bound = 0;
152
179
  for (let i = 0; i < count; i++) {
153
- const reach = Math.hypot(xs[i] ?? 0, ys[i] ?? 0) + (radii[i] ?? 0);
180
+ const reach = Math.hypot((xs[i] ?? 0) - centerX, (ys[i] ?? 0) - centerY) + (radii[i] ?? 0);
154
181
  if (reach > bound) bound = reach;
155
182
  }
156
183
  const scale = bound > EPSILON ? 0.5 / bound : 0.5;
157
184
  const bubbles = new Array(count);
158
185
  for (let i = 0; i < count; i++) {
159
186
  bubbles[i] = {
160
- cx: 0.5 + (xs[i] ?? 0) * scale,
161
- cy: 0.5 + (ys[i] ?? 0) * scale,
187
+ cx: 0.5 + ((xs[i] ?? 0) - centerX) * scale,
188
+ cy: 0.5 + ((ys[i] ?? 0) - centerY) * scale,
162
189
  d: 2 * (radii[i] ?? 0) * scale
163
190
  };
164
191
  }
@@ -172,12 +199,16 @@ function packCluster(count) {
172
199
  *
173
200
  * - `<= 0` → empty.
174
201
  * - `1` → a single bubble filling the box.
175
- * - `2` → the iMessage "one in front, one behind" pair: a larger primary set
176
- * low-left with a smaller member tucked behind it to the upper-right.
202
+ * - `2` → the iMessage "one in front, one behind" pair: two EQUAL-diameter
203
+ * bubbles offset on a diagonal the front set low-left, the other tucked
204
+ * behind it to the upper-right. Same radius, so neither member reads as
205
+ * secondary; the overlap + separator ring alone convey the stacking.
177
206
  * - `3` → an iMessage-style triangle: the larger primary along the bottom with
178
207
  * two smaller members above it.
179
- * - `4+` → the deterministic force-directed pack (primary centred, the rest
180
- * packed magnetically around it, denser as the count grows).
208
+ * - `4+` → the deterministic force-directed pack of near-equal bubbles, recentred
209
+ * and scaled to fill the round box edge-to-edge (a modestly larger primary near
210
+ * the centre, the rest packed magnetically around it, denser as the count
211
+ * grows).
181
212
  */
182
213
  function computeClusterLayout(count) {
183
214
  if (count <= 0) return [];
@@ -187,14 +218,18 @@ function computeClusterLayout(count) {
187
218
  d: 1
188
219
  }];
189
220
  if (count === 2) {
221
+ // Two EQUAL-diameter bubbles, offset symmetrically on the diagonal: the
222
+ // front (index 0, highest zIndex) sits low-left, the other tucks behind it
223
+ // to the upper-right. They overlap (the intentional "front + behind" pair);
224
+ // the separator ring — not a size difference — reads the stacking.
190
225
  return [{
191
- cx: 0.42,
192
- cy: 0.56,
193
- d: 0.66
226
+ cx: 0.35,
227
+ cy: 0.65,
228
+ d: 0.64
194
229
  }, {
195
- cx: 0.68,
196
- cy: 0.36,
197
- d: 0.5
230
+ cx: 0.65,
231
+ cy: 0.35,
232
+ d: 0.64
198
233
  }];
199
234
  }
200
235
  if (count === 3) {
@@ -1 +1 @@
1
- {"version":3,"names":["GOLDEN_ANGLE","Math","PI","sqrt","ITERATIONS","CLEANUP_ITERATIONS","CENTERING","SEPARATION_STRENGTH","LAYOUT_GAP","SEED_SPACING","PRIMARY_RADIUS","SECONDARY_MAX","SECONDARY_MIN","EPSILON","relativeRadius","index","count","t","separate","xs","ys","radii","i","j","dx","dy","dist","hypot","minDist","a","cos","sin","overlap","nx","ny","packCluster","Array","seedR","angle","iter","bound","reach","scale","bubbles","cx","cy","d","computeClusterLayout"],"sourceRoot":"../../../src","sources":["avatar-group/cluster-layout.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["GOLDEN_ANGLE","Math","PI","sqrt","ITERATIONS","CLEANUP_ITERATIONS","CENTERING","SEPARATION_STRENGTH","LAYOUT_GAP","SEED_SPACING","PRIMARY_RADIUS","SECONDARY_MAX","SECONDARY_MIN","EPSILON","relativeRadius","index","count","t","separate","xs","ys","radii","i","j","dx","dy","dist","hypot","minDist","a","cos","sin","overlap","nx","ny","packCluster","Array","seedR","angle","iter","minX","Infinity","maxX","minY","maxY","x","y","r","centerX","centerY","bound","reach","scale","bubbles","cx","cy","d","computeClusterLayout"],"sourceRoot":"../../../src","sources":["avatar-group/cluster-layout.ts"],"mappings":"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","ignoreList":[]}
@@ -3,8 +3,9 @@
3
3
  /**
4
4
  * Deterministic organic circle-packing for the {@link AvatarGroup} `cluster`
5
5
  * layout — the iMessage-style "magnetic bubble cluster" where several avatars of
6
- * varying sizes nestle together inside a round bounding box (a large primary in
7
- * the middle/front, smaller members packed around it with a uniform gap).
6
+ * NEAR-EQUAL size nestle tightly together and fill a round bounding box (a
7
+ * modestly larger primary with the rest only slightly smaller, packed against
8
+ * each other with a small uniform gap — not a big primary ringed by tiny dots).
8
9
  *
9
10
  * For 4+ members the layout is produced by a small, FULLY DETERMINISTIC
10
11
  * force-directed relaxation (no `Math.random`): the primary is pinned at the
@@ -19,9 +20,11 @@
19
20
  *
20
21
  * Output is resolution-independent: each bubble is expressed as a fraction of
21
22
  * the group's bounding box (`cx`/`cy` centre, `d` diameter, all 0..1), so the
22
- * consumer just multiplies by the pixel `size`. Every bubble is guaranteed to
23
- * sit fully inside the `[0, 1]` box (the packed result is scaled + translated to
24
- * fit), so the cluster drops in exactly where a single round Avatar would.
23
+ * consumer just multiplies by the pixel `size`. The relaxed blob is recentred on
24
+ * its own centre and scaled so the outermost bubble edge lands on the box edge,
25
+ * so it fills the round box densely with minimal empty margin while every bubble
26
+ * is guaranteed to sit fully inside the `[0, 1]` box — the cluster drops in
27
+ * exactly where a single round Avatar would.
25
28
  */
26
29
 
27
30
  /** A single packed bubble, expressed as fractions of the bounding box (0..1). */
@@ -41,17 +44,20 @@ const CENTERING = 0.05;
41
44
  // the pinned primary and many passes this stays stable and converges.
42
45
  const SEPARATION_STRENGTH = 1;
43
46
  // Uniform gap kept between every touching pair, in primary-radius units
44
- // (primary radius = 1). Reads as consistent spacing everywhere in the cluster.
45
- const LAYOUT_GAP = 0.16;
47
+ // (primary radius = 1). Small, so near-equal bubbles nestle tightly like
48
+ // magnets rather than floating apart with visible margins.
49
+ const LAYOUT_GAP = 0.12;
46
50
  // Initial spiral spacing. Only affects convergence (the result is re-fitted to
47
51
  // the box afterwards), not the final scale.
48
52
  const SEED_SPACING = 1.7;
49
- // Relative radii: the primary is the largest; the remaining members taper from
50
- // SECONDARY_MAX (nearest the primary) down to SECONDARY_MIN (outermost) so size
51
- // decreases outward and later/overflow members are the smallest.
53
+ // Relative radii: the primary is only MODESTLY the largest; every other member
54
+ // tapers gently from SECONDARY_MAX (nearest the primary) down to SECONDARY_MIN
55
+ // (outermost). The spread is deliberately narrow so the cluster reads as a pack
56
+ // of near-equal magnetic bubbles — a slightly larger primary, not a big primary
57
+ // ringed by tiny dots.
52
58
  const PRIMARY_RADIUS = 1;
53
- const SECONDARY_MAX = 0.72;
54
- const SECONDARY_MIN = 0.5;
59
+ const SECONDARY_MAX = 0.9;
60
+ const SECONDARY_MIN = 0.8;
55
61
  const EPSILON = 1e-6;
56
62
 
57
63
  /** Relative radius for member `index` of a `count`-member cluster. */
@@ -141,20 +147,41 @@ function packCluster(count) {
141
147
  ys[0] = 0;
142
148
  }
143
149
 
144
- // Fit to the unit box: the primary is at the origin, so scale so the
145
- // outermost circle edge lands on the box radius (0.5) and place the primary at
146
- // the box centre.
150
+ // Fit to the unit box so the pack FILLS the round box edge-to-edge with
151
+ // minimal empty margin. The primary was pinned at the origin during
152
+ // relaxation, but the relaxed blob is not centred on it, so we recentre on the
153
+ // cluster's OWN centre (its bounding-box midpoint) rather than the primary:
154
+ // enclose every bubble in a circle around that centre, then scale so the
155
+ // outermost bubble edge lands exactly on the box radius (0.5). Centring the
156
+ // enclosing circle at the box centre keeps every bubble inside the [0, 1] box
157
+ // (the enclosing circle is inscribed in the square) while packing the blob
158
+ // tightly against the edge instead of floating with a gap.
159
+ let minX = Infinity;
160
+ let maxX = -Infinity;
161
+ let minY = Infinity;
162
+ let maxY = -Infinity;
163
+ for (let i = 0; i < count; i++) {
164
+ const x = xs[i] ?? 0;
165
+ const y = ys[i] ?? 0;
166
+ const r = radii[i] ?? 0;
167
+ if (x - r < minX) minX = x - r;
168
+ if (x + r > maxX) maxX = x + r;
169
+ if (y - r < minY) minY = y - r;
170
+ if (y + r > maxY) maxY = y + r;
171
+ }
172
+ const centerX = (minX + maxX) / 2;
173
+ const centerY = (minY + maxY) / 2;
147
174
  let bound = 0;
148
175
  for (let i = 0; i < count; i++) {
149
- const reach = Math.hypot(xs[i] ?? 0, ys[i] ?? 0) + (radii[i] ?? 0);
176
+ const reach = Math.hypot((xs[i] ?? 0) - centerX, (ys[i] ?? 0) - centerY) + (radii[i] ?? 0);
150
177
  if (reach > bound) bound = reach;
151
178
  }
152
179
  const scale = bound > EPSILON ? 0.5 / bound : 0.5;
153
180
  const bubbles = new Array(count);
154
181
  for (let i = 0; i < count; i++) {
155
182
  bubbles[i] = {
156
- cx: 0.5 + (xs[i] ?? 0) * scale,
157
- cy: 0.5 + (ys[i] ?? 0) * scale,
183
+ cx: 0.5 + ((xs[i] ?? 0) - centerX) * scale,
184
+ cy: 0.5 + ((ys[i] ?? 0) - centerY) * scale,
158
185
  d: 2 * (radii[i] ?? 0) * scale
159
186
  };
160
187
  }
@@ -168,12 +195,16 @@ function packCluster(count) {
168
195
  *
169
196
  * - `<= 0` → empty.
170
197
  * - `1` → a single bubble filling the box.
171
- * - `2` → the iMessage "one in front, one behind" pair: a larger primary set
172
- * low-left with a smaller member tucked behind it to the upper-right.
198
+ * - `2` → the iMessage "one in front, one behind" pair: two EQUAL-diameter
199
+ * bubbles offset on a diagonal the front set low-left, the other tucked
200
+ * behind it to the upper-right. Same radius, so neither member reads as
201
+ * secondary; the overlap + separator ring alone convey the stacking.
173
202
  * - `3` → an iMessage-style triangle: the larger primary along the bottom with
174
203
  * two smaller members above it.
175
- * - `4+` → the deterministic force-directed pack (primary centred, the rest
176
- * packed magnetically around it, denser as the count grows).
204
+ * - `4+` → the deterministic force-directed pack of near-equal bubbles, recentred
205
+ * and scaled to fill the round box edge-to-edge (a modestly larger primary near
206
+ * the centre, the rest packed magnetically around it, denser as the count
207
+ * grows).
177
208
  */
178
209
  export function computeClusterLayout(count) {
179
210
  if (count <= 0) return [];
@@ -183,14 +214,18 @@ export function computeClusterLayout(count) {
183
214
  d: 1
184
215
  }];
185
216
  if (count === 2) {
217
+ // Two EQUAL-diameter bubbles, offset symmetrically on the diagonal: the
218
+ // front (index 0, highest zIndex) sits low-left, the other tucks behind it
219
+ // to the upper-right. They overlap (the intentional "front + behind" pair);
220
+ // the separator ring — not a size difference — reads the stacking.
186
221
  return [{
187
- cx: 0.42,
188
- cy: 0.56,
189
- d: 0.66
222
+ cx: 0.35,
223
+ cy: 0.65,
224
+ d: 0.64
190
225
  }, {
191
- cx: 0.68,
192
- cy: 0.36,
193
- d: 0.5
226
+ cx: 0.65,
227
+ cy: 0.35,
228
+ d: 0.64
194
229
  }];
195
230
  }
196
231
  if (count === 3) {
@@ -1 +1 @@
1
- {"version":3,"names":["GOLDEN_ANGLE","Math","PI","sqrt","ITERATIONS","CLEANUP_ITERATIONS","CENTERING","SEPARATION_STRENGTH","LAYOUT_GAP","SEED_SPACING","PRIMARY_RADIUS","SECONDARY_MAX","SECONDARY_MIN","EPSILON","relativeRadius","index","count","t","separate","xs","ys","radii","i","j","dx","dy","dist","hypot","minDist","a","cos","sin","overlap","nx","ny","packCluster","Array","seedR","angle","iter","bound","reach","scale","bubbles","cx","cy","d","computeClusterLayout"],"sourceRoot":"../../../src","sources":["avatar-group/cluster-layout.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["GOLDEN_ANGLE","Math","PI","sqrt","ITERATIONS","CLEANUP_ITERATIONS","CENTERING","SEPARATION_STRENGTH","LAYOUT_GAP","SEED_SPACING","PRIMARY_RADIUS","SECONDARY_MAX","SECONDARY_MIN","EPSILON","relativeRadius","index","count","t","separate","xs","ys","radii","i","j","dx","dy","dist","hypot","minDist","a","cos","sin","overlap","nx","ny","packCluster","Array","seedR","angle","iter","minX","Infinity","maxX","minY","maxY","x","y","r","centerX","centerY","bound","reach","scale","bubbles","cx","cy","d","computeClusterLayout"],"sourceRoot":"../../../src","sources":["avatar-group/cluster-layout.ts"],"mappings":"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","ignoreList":[]}
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Deterministic organic circle-packing for the {@link AvatarGroup} `cluster`
3
3
  * layout — the iMessage-style "magnetic bubble cluster" where several avatars of
4
- * varying sizes nestle together inside a round bounding box (a large primary in
5
- * the middle/front, smaller members packed around it with a uniform gap).
4
+ * NEAR-EQUAL size nestle tightly together and fill a round bounding box (a
5
+ * modestly larger primary with the rest only slightly smaller, packed against
6
+ * each other with a small uniform gap — not a big primary ringed by tiny dots).
6
7
  *
7
8
  * For 4+ members the layout is produced by a small, FULLY DETERMINISTIC
8
9
  * force-directed relaxation (no `Math.random`): the primary is pinned at the
@@ -17,9 +18,11 @@
17
18
  *
18
19
  * Output is resolution-independent: each bubble is expressed as a fraction of
19
20
  * the group's bounding box (`cx`/`cy` centre, `d` diameter, all 0..1), so the
20
- * consumer just multiplies by the pixel `size`. Every bubble is guaranteed to
21
- * sit fully inside the `[0, 1]` box (the packed result is scaled + translated to
22
- * fit), so the cluster drops in exactly where a single round Avatar would.
21
+ * consumer just multiplies by the pixel `size`. The relaxed blob is recentred on
22
+ * its own centre and scaled so the outermost bubble edge lands on the box edge,
23
+ * so it fills the round box densely with minimal empty margin while every bubble
24
+ * is guaranteed to sit fully inside the `[0, 1]` box — the cluster drops in
25
+ * exactly where a single round Avatar would.
23
26
  */
24
27
  /** A single packed bubble, expressed as fractions of the bounding box (0..1). */
25
28
  export interface ClusterBubble {
@@ -37,12 +40,16 @@ export interface ClusterBubble {
37
40
  *
38
41
  * - `<= 0` → empty.
39
42
  * - `1` → a single bubble filling the box.
40
- * - `2` → the iMessage "one in front, one behind" pair: a larger primary set
41
- * low-left with a smaller member tucked behind it to the upper-right.
43
+ * - `2` → the iMessage "one in front, one behind" pair: two EQUAL-diameter
44
+ * bubbles offset on a diagonal the front set low-left, the other tucked
45
+ * behind it to the upper-right. Same radius, so neither member reads as
46
+ * secondary; the overlap + separator ring alone convey the stacking.
42
47
  * - `3` → an iMessage-style triangle: the larger primary along the bottom with
43
48
  * two smaller members above it.
44
- * - `4+` → the deterministic force-directed pack (primary centred, the rest
45
- * packed magnetically around it, denser as the count grows).
49
+ * - `4+` → the deterministic force-directed pack of near-equal bubbles, recentred
50
+ * and scaled to fill the round box edge-to-edge (a modestly larger primary near
51
+ * the centre, the rest packed magnetically around it, denser as the count
52
+ * grows).
46
53
  */
47
54
  export declare function computeClusterLayout(count: number): ClusterBubble[];
48
55
  //# sourceMappingURL=cluster-layout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cluster-layout.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/cluster-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;CACX;AA4ID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAiBnE"}
1
+ {"version":3,"file":"cluster-layout.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/cluster-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;CACX;AAqKD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAqBnE"}
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Deterministic organic circle-packing for the {@link AvatarGroup} `cluster`
3
3
  * layout — the iMessage-style "magnetic bubble cluster" where several avatars of
4
- * varying sizes nestle together inside a round bounding box (a large primary in
5
- * the middle/front, smaller members packed around it with a uniform gap).
4
+ * NEAR-EQUAL size nestle tightly together and fill a round bounding box (a
5
+ * modestly larger primary with the rest only slightly smaller, packed against
6
+ * each other with a small uniform gap — not a big primary ringed by tiny dots).
6
7
  *
7
8
  * For 4+ members the layout is produced by a small, FULLY DETERMINISTIC
8
9
  * force-directed relaxation (no `Math.random`): the primary is pinned at the
@@ -17,9 +18,11 @@
17
18
  *
18
19
  * Output is resolution-independent: each bubble is expressed as a fraction of
19
20
  * the group's bounding box (`cx`/`cy` centre, `d` diameter, all 0..1), so the
20
- * consumer just multiplies by the pixel `size`. Every bubble is guaranteed to
21
- * sit fully inside the `[0, 1]` box (the packed result is scaled + translated to
22
- * fit), so the cluster drops in exactly where a single round Avatar would.
21
+ * consumer just multiplies by the pixel `size`. The relaxed blob is recentred on
22
+ * its own centre and scaled so the outermost bubble edge lands on the box edge,
23
+ * so it fills the round box densely with minimal empty margin while every bubble
24
+ * is guaranteed to sit fully inside the `[0, 1]` box — the cluster drops in
25
+ * exactly where a single round Avatar would.
23
26
  */
24
27
  /** A single packed bubble, expressed as fractions of the bounding box (0..1). */
25
28
  export interface ClusterBubble {
@@ -37,12 +40,16 @@ export interface ClusterBubble {
37
40
  *
38
41
  * - `<= 0` → empty.
39
42
  * - `1` → a single bubble filling the box.
40
- * - `2` → the iMessage "one in front, one behind" pair: a larger primary set
41
- * low-left with a smaller member tucked behind it to the upper-right.
43
+ * - `2` → the iMessage "one in front, one behind" pair: two EQUAL-diameter
44
+ * bubbles offset on a diagonal the front set low-left, the other tucked
45
+ * behind it to the upper-right. Same radius, so neither member reads as
46
+ * secondary; the overlap + separator ring alone convey the stacking.
42
47
  * - `3` → an iMessage-style triangle: the larger primary along the bottom with
43
48
  * two smaller members above it.
44
- * - `4+` → the deterministic force-directed pack (primary centred, the rest
45
- * packed magnetically around it, denser as the count grows).
49
+ * - `4+` → the deterministic force-directed pack of near-equal bubbles, recentred
50
+ * and scaled to fill the round box edge-to-edge (a modestly larger primary near
51
+ * the centre, the rest packed magnetically around it, denser as the count
52
+ * grows).
46
53
  */
47
54
  export declare function computeClusterLayout(count: number): ClusterBubble[];
48
55
  //# sourceMappingURL=cluster-layout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cluster-layout.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/cluster-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;CACX;AA4ID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAiBnE"}
1
+ {"version":3,"file":"cluster-layout.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/cluster-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;CACX;AAqKD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAqBnE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -74,11 +74,35 @@ describe('computeClusterLayout', () => {
74
74
  }
75
75
  });
76
76
 
77
- it('centres the primary for the force-packed counts (4+)', () => {
77
+ it('fills the round box edge-to-edge for the force-packed counts (4+)', () => {
78
+ // After the recentre + scale, the outermost bubble edge lands on the box
79
+ // radius (0.5 from the box centre) — the blob fills the round box densely
80
+ // with no big empty margin — and the whole cluster stays roughly centred.
78
81
  for (const count of [4, 5, 8, 12, 20]) {
79
- const primary = bubbleAt(computeClusterLayout(count), 0);
80
- expect(primary.cx).toBeCloseTo(0.5, 5);
81
- expect(primary.cy).toBeCloseTo(0.5, 5);
82
+ const bubbles = computeClusterLayout(count);
83
+ let reach = 0;
84
+ for (const bubble of bubbles) {
85
+ const r = Math.hypot(bubble.cx - 0.5, bubble.cy - 0.5) + bubble.d / 2;
86
+ if (r > reach) reach = r;
87
+ }
88
+ expect(reach).toBeCloseTo(0.5, 6);
89
+ const primary = bubbleAt(bubbles, 0);
90
+ expect(primary.cx).toBeGreaterThan(0.4);
91
+ expect(primary.cx).toBeLessThan(0.6);
92
+ expect(primary.cy).toBeGreaterThan(0.4);
93
+ expect(primary.cy).toBeLessThan(0.6);
94
+ }
95
+ });
96
+
97
+ it('keeps the force-packed bubbles near-equal in size (mild taper)', () => {
98
+ // The pack should read as near-equal magnetic bubbles: a modestly larger
99
+ // primary, everyone else only slightly smaller — not a big primary ringed by
100
+ // tiny dots. The smallest bubble stays at least ~75% of the largest.
101
+ for (const count of [4, 5, 6, 8, 12, 20]) {
102
+ const diameters = computeClusterLayout(count).map((bubble) => bubble.d);
103
+ const maxD = Math.max(...diameters);
104
+ const minD = Math.min(...diameters);
105
+ expect(minD / maxD).toBeGreaterThan(0.75);
82
106
  }
83
107
  });
84
108
 
@@ -88,6 +112,27 @@ describe('computeClusterLayout', () => {
88
112
  expect(minEdgeGap(computeClusterLayout(count))).toBeGreaterThan(-1e-3);
89
113
  }
90
114
  });
115
+
116
+ it('gives the two-member cluster equal-diameter, overlapping bubbles', () => {
117
+ const bubbles = computeClusterLayout(2);
118
+ expect(bubbles).toHaveLength(2);
119
+ const front = bubbleAt(bubbles, 0);
120
+ const behind = bubbleAt(bubbles, 1);
121
+ // The fix: both bubbles share the SAME diameter — the one drawn behind is no
122
+ // longer rendered too small.
123
+ expect(front.d).toBeCloseTo(behind.d, 10);
124
+ // Still the intentional iMessage "front + behind" overlap (negative gap).
125
+ expect(minEdgeGap(bubbles)).toBeLessThan(0);
126
+ // Deterministic (no Math.random).
127
+ expect(computeClusterLayout(2)).toEqual(bubbles);
128
+ // Both bubbles sit fully inside the unit box.
129
+ for (const bubble of bubbles) {
130
+ expect(bubble.cx - bubble.d / 2).toBeGreaterThanOrEqual(-1e-6);
131
+ expect(bubble.cx + bubble.d / 2).toBeLessThanOrEqual(1 + 1e-6);
132
+ expect(bubble.cy - bubble.d / 2).toBeGreaterThanOrEqual(-1e-6);
133
+ expect(bubble.cy + bubble.d / 2).toBeLessThanOrEqual(1 + 1e-6);
134
+ }
135
+ });
91
136
  });
92
137
 
93
138
  const CLUSTER_ITEMS: AvatarGroupItem[] = Array.from({ length: 25 }, (_, i) => ({
@@ -157,15 +157,16 @@ const MANY: AvatarGroupItem[] = Array.from({ length: 25 }, (_, i) => ({
157
157
 
158
158
  /**
159
159
  * Cluster layout: a compact iMessage-style "magnetic bubble cluster" of
160
- * varying-size avatars packed into a round box (`size` = box diameter). The
161
- * primary is largest and centred/in front; the rest nestle around it with a
162
- * uniform gap. `showInitials` renders a colored letter for avatar-less members
163
- * so the packing reads clearly. Shown at counts 3, 5, 8, 12, and 20.
160
+ * NEAR-EQUAL-size avatars packed densely into a round box (`size` = box
161
+ * diameter). A modestly larger primary sits in front; the rest are only slightly
162
+ * smaller and nestle against it with a small uniform gap, filling the round box
163
+ * edge-to-edge. `showInitials` renders a colored letter for avatar-less members
164
+ * so the packing reads clearly. Shown at counts 3, 4, 5, 6, 8, 12, and 20.
164
165
  */
165
166
  export const ClusterLayout: Story = {
166
167
  render: () => (
167
168
  <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 24, alignItems: 'center' }}>
168
- {[3, 5, 8, 12, 20].map((count) => (
169
+ {[3, 4, 5, 6, 8, 12, 20].map((count) => (
169
170
  <View key={count} style={{ alignItems: 'center', gap: 8 }}>
170
171
  <AvatarGroup
171
172
  layout="cluster"
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Deterministic organic circle-packing for the {@link AvatarGroup} `cluster`
3
3
  * layout — the iMessage-style "magnetic bubble cluster" where several avatars of
4
- * varying sizes nestle together inside a round bounding box (a large primary in
5
- * the middle/front, smaller members packed around it with a uniform gap).
4
+ * NEAR-EQUAL size nestle tightly together and fill a round bounding box (a
5
+ * modestly larger primary with the rest only slightly smaller, packed against
6
+ * each other with a small uniform gap — not a big primary ringed by tiny dots).
6
7
  *
7
8
  * For 4+ members the layout is produced by a small, FULLY DETERMINISTIC
8
9
  * force-directed relaxation (no `Math.random`): the primary is pinned at the
@@ -17,9 +18,11 @@
17
18
  *
18
19
  * Output is resolution-independent: each bubble is expressed as a fraction of
19
20
  * the group's bounding box (`cx`/`cy` centre, `d` diameter, all 0..1), so the
20
- * consumer just multiplies by the pixel `size`. Every bubble is guaranteed to
21
- * sit fully inside the `[0, 1]` box (the packed result is scaled + translated to
22
- * fit), so the cluster drops in exactly where a single round Avatar would.
21
+ * consumer just multiplies by the pixel `size`. The relaxed blob is recentred on
22
+ * its own centre and scaled so the outermost bubble edge lands on the box edge,
23
+ * so it fills the round box densely with minimal empty margin while every bubble
24
+ * is guaranteed to sit fully inside the `[0, 1]` box — the cluster drops in
25
+ * exactly where a single round Avatar would.
23
26
  */
24
27
 
25
28
  /** A single packed bubble, expressed as fractions of the bounding box (0..1). */
@@ -47,17 +50,20 @@ const CENTERING = 0.05;
47
50
  // the pinned primary and many passes this stays stable and converges.
48
51
  const SEPARATION_STRENGTH = 1;
49
52
  // Uniform gap kept between every touching pair, in primary-radius units
50
- // (primary radius = 1). Reads as consistent spacing everywhere in the cluster.
51
- const LAYOUT_GAP = 0.16;
53
+ // (primary radius = 1). Small, so near-equal bubbles nestle tightly like
54
+ // magnets rather than floating apart with visible margins.
55
+ const LAYOUT_GAP = 0.12;
52
56
  // Initial spiral spacing. Only affects convergence (the result is re-fitted to
53
57
  // the box afterwards), not the final scale.
54
58
  const SEED_SPACING = 1.7;
55
- // Relative radii: the primary is the largest; the remaining members taper from
56
- // SECONDARY_MAX (nearest the primary) down to SECONDARY_MIN (outermost) so size
57
- // decreases outward and later/overflow members are the smallest.
59
+ // Relative radii: the primary is only MODESTLY the largest; every other member
60
+ // tapers gently from SECONDARY_MAX (nearest the primary) down to SECONDARY_MIN
61
+ // (outermost). The spread is deliberately narrow so the cluster reads as a pack
62
+ // of near-equal magnetic bubbles — a slightly larger primary, not a big primary
63
+ // ringed by tiny dots.
58
64
  const PRIMARY_RADIUS = 1;
59
- const SECONDARY_MAX = 0.72;
60
- const SECONDARY_MIN = 0.5;
65
+ const SECONDARY_MAX = 0.9;
66
+ const SECONDARY_MIN = 0.8;
61
67
  const EPSILON = 1e-6;
62
68
 
63
69
  /** Relative radius for member `index` of a `count`-member cluster. */
@@ -149,12 +155,34 @@ function packCluster(count: number): ClusterBubble[] {
149
155
  ys[0] = 0;
150
156
  }
151
157
 
152
- // Fit to the unit box: the primary is at the origin, so scale so the
153
- // outermost circle edge lands on the box radius (0.5) and place the primary at
154
- // the box centre.
158
+ // Fit to the unit box so the pack FILLS the round box edge-to-edge with
159
+ // minimal empty margin. The primary was pinned at the origin during
160
+ // relaxation, but the relaxed blob is not centred on it, so we recentre on the
161
+ // cluster's OWN centre (its bounding-box midpoint) rather than the primary:
162
+ // enclose every bubble in a circle around that centre, then scale so the
163
+ // outermost bubble edge lands exactly on the box radius (0.5). Centring the
164
+ // enclosing circle at the box centre keeps every bubble inside the [0, 1] box
165
+ // (the enclosing circle is inscribed in the square) while packing the blob
166
+ // tightly against the edge instead of floating with a gap.
167
+ let minX = Infinity;
168
+ let maxX = -Infinity;
169
+ let minY = Infinity;
170
+ let maxY = -Infinity;
171
+ for (let i = 0; i < count; i++) {
172
+ const x = xs[i] ?? 0;
173
+ const y = ys[i] ?? 0;
174
+ const r = radii[i] ?? 0;
175
+ if (x - r < minX) minX = x - r;
176
+ if (x + r > maxX) maxX = x + r;
177
+ if (y - r < minY) minY = y - r;
178
+ if (y + r > maxY) maxY = y + r;
179
+ }
180
+ const centerX = (minX + maxX) / 2;
181
+ const centerY = (minY + maxY) / 2;
155
182
  let bound = 0;
156
183
  for (let i = 0; i < count; i++) {
157
- const reach = Math.hypot(xs[i] ?? 0, ys[i] ?? 0) + (radii[i] ?? 0);
184
+ const reach =
185
+ Math.hypot((xs[i] ?? 0) - centerX, (ys[i] ?? 0) - centerY) + (radii[i] ?? 0);
158
186
  if (reach > bound) bound = reach;
159
187
  }
160
188
  const scale = bound > EPSILON ? 0.5 / bound : 0.5;
@@ -162,8 +190,8 @@ function packCluster(count: number): ClusterBubble[] {
162
190
  const bubbles = new Array<ClusterBubble>(count);
163
191
  for (let i = 0; i < count; i++) {
164
192
  bubbles[i] = {
165
- cx: 0.5 + (xs[i] ?? 0) * scale,
166
- cy: 0.5 + (ys[i] ?? 0) * scale,
193
+ cx: 0.5 + ((xs[i] ?? 0) - centerX) * scale,
194
+ cy: 0.5 + ((ys[i] ?? 0) - centerY) * scale,
167
195
  d: 2 * (radii[i] ?? 0) * scale,
168
196
  };
169
197
  }
@@ -177,20 +205,28 @@ function packCluster(count: number): ClusterBubble[] {
177
205
  *
178
206
  * - `<= 0` → empty.
179
207
  * - `1` → a single bubble filling the box.
180
- * - `2` → the iMessage "one in front, one behind" pair: a larger primary set
181
- * low-left with a smaller member tucked behind it to the upper-right.
208
+ * - `2` → the iMessage "one in front, one behind" pair: two EQUAL-diameter
209
+ * bubbles offset on a diagonal the front set low-left, the other tucked
210
+ * behind it to the upper-right. Same radius, so neither member reads as
211
+ * secondary; the overlap + separator ring alone convey the stacking.
182
212
  * - `3` → an iMessage-style triangle: the larger primary along the bottom with
183
213
  * two smaller members above it.
184
- * - `4+` → the deterministic force-directed pack (primary centred, the rest
185
- * packed magnetically around it, denser as the count grows).
214
+ * - `4+` → the deterministic force-directed pack of near-equal bubbles, recentred
215
+ * and scaled to fill the round box edge-to-edge (a modestly larger primary near
216
+ * the centre, the rest packed magnetically around it, denser as the count
217
+ * grows).
186
218
  */
187
219
  export function computeClusterLayout(count: number): ClusterBubble[] {
188
220
  if (count <= 0) return [];
189
221
  if (count === 1) return [{ cx: 0.5, cy: 0.5, d: 1 }];
190
222
  if (count === 2) {
223
+ // Two EQUAL-diameter bubbles, offset symmetrically on the diagonal: the
224
+ // front (index 0, highest zIndex) sits low-left, the other tucks behind it
225
+ // to the upper-right. They overlap (the intentional "front + behind" pair);
226
+ // the separator ring — not a size difference — reads the stacking.
191
227
  return [
192
- { cx: 0.42, cy: 0.56, d: 0.66 },
193
- { cx: 0.68, cy: 0.36, d: 0.5 },
228
+ { cx: 0.35, cy: 0.65, d: 0.64 },
229
+ { cx: 0.65, cy: 0.35, d: 0.64 },
194
230
  ];
195
231
  }
196
232
  if (count === 3) {