@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.
- package/lib/commonjs/avatar-group/cluster-layout.js +63 -28
- package/lib/commonjs/avatar-group/cluster-layout.js.map +1 -1
- package/lib/module/avatar-group/cluster-layout.js +63 -28
- package/lib/module/avatar-group/cluster-layout.js.map +1 -1
- package/lib/typescript/commonjs/avatar-group/cluster-layout.d.ts +16 -9
- package/lib/typescript/commonjs/avatar-group/cluster-layout.d.ts.map +1 -1
- package/lib/typescript/module/avatar-group/cluster-layout.d.ts +16 -9
- package/lib/typescript/module/avatar-group/cluster-layout.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/AvatarGroupCluster.test.tsx +49 -4
- package/src/avatar-group/AvatarGroup.stories.tsx +6 -5
- package/src/avatar-group/cluster-layout.ts +60 -24
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
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`.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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).
|
|
49
|
-
|
|
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;
|
|
54
|
-
// SECONDARY_MAX (nearest the primary) down to SECONDARY_MIN
|
|
55
|
-
//
|
|
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.
|
|
58
|
-
const SECONDARY_MIN = 0.
|
|
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
|
|
149
|
-
//
|
|
150
|
-
// the
|
|
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:
|
|
176
|
-
*
|
|
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
|
|
180
|
-
*
|
|
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.
|
|
192
|
-
cy: 0.
|
|
193
|
-
d: 0.
|
|
226
|
+
cx: 0.35,
|
|
227
|
+
cy: 0.65,
|
|
228
|
+
d: 0.64
|
|
194
229
|
}, {
|
|
195
|
-
cx: 0.
|
|
196
|
-
cy: 0.
|
|
197
|
-
d: 0.
|
|
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":";;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAUA;AACA;AACA,MAAMA,YAAY,GAAGC,IAAI,CAACC,EAAE,IAAI,CAAC,GAAGD,IAAI,CAACE,IAAI,CAAC,CAAC,CAAC,CAAC;AACjD;AACA;AACA,MAAMC,UAAU,GAAG,GAAG;AACtB,MAAMC,kBAAkB,GAAG,EAAE;AAC7B;AACA;AACA;AACA,MAAMC,SAAS,GAAG,IAAI;AACtB;AACA;AACA,MAAMC,mBAAmB,GAAG,CAAC;AAC7B;AACA;AACA,MAAMC,UAAU,GAAG,IAAI;AACvB;AACA;AACA,MAAMC,YAAY,GAAG,GAAG;AACxB;AACA;AACA;AACA,MAAMC,cAAc,GAAG,CAAC;AACxB,MAAMC,aAAa,GAAG,
|
|
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":";;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAUA;AACA;AACA,MAAMA,YAAY,GAAGC,IAAI,CAACC,EAAE,IAAI,CAAC,GAAGD,IAAI,CAACE,IAAI,CAAC,CAAC,CAAC,CAAC;AACjD;AACA;AACA,MAAMC,UAAU,GAAG,GAAG;AACtB,MAAMC,kBAAkB,GAAG,EAAE;AAC7B;AACA;AACA;AACA,MAAMC,SAAS,GAAG,IAAI;AACtB;AACA;AACA,MAAMC,mBAAmB,GAAG,CAAC;AAC7B;AACA;AACA;AACA,MAAMC,UAAU,GAAG,IAAI;AACvB;AACA;AACA,MAAMC,YAAY,GAAG,GAAG;AACxB;AACA;AACA;AACA;AACA;AACA,MAAMC,cAAc,GAAG,CAAC;AACxB,MAAMC,aAAa,GAAG,GAAG;AACzB,MAAMC,aAAa,GAAG,GAAG;AACzB,MAAMC,OAAO,GAAG,IAAI;;AAEpB;AACA,SAASC,cAAcA,CAACC,KAAa,EAAEC,KAAa,EAAU;EAC5D,IAAID,KAAK,KAAK,CAAC,EAAE,OAAOL,cAAc;EACtC,IAAIM,KAAK,IAAI,CAAC,EAAE,OAAOL,aAAa;EACpC,MAAMM,CAAC,GAAG,CAACF,KAAK,GAAG,CAAC,KAAKC,KAAK,GAAG,CAAC,CAAC;EACnC,OAAOL,aAAa,GAAG,CAACC,aAAa,GAAGD,aAAa,IAAIM,CAAC;AAC5D;;AAEA;AACA;AACA;AACA;AACA;AACA,SAASC,QAAQA,CAACC,EAAY,EAAEC,EAAY,EAAEC,KAAe,EAAEL,KAAa,EAAQ;EAClF,KAAK,IAAIM,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;IAC9B,KAAK,IAAIC,CAAC,GAAGD,CAAC,GAAG,CAAC,EAAEC,CAAC,GAAGP,KAAK,EAAEO,CAAC,EAAE,EAAE;MAClC,IAAIC,EAAE,GAAG,CAACL,EAAE,CAACI,CAAC,CAAC,IAAI,CAAC,KAAKJ,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,CAAC;MACpC,IAAIG,EAAE,GAAG,CAACL,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,KAAKH,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,CAAC;MACpC,IAAII,IAAI,GAAGzB,IAAI,CAAC0B,KAAK,CAACH,EAAE,EAAEC,EAAE,CAAC;MAC7B,MAAMG,OAAO,GAAG,CAACP,KAAK,CAACC,CAAC,CAAC,IAAI,CAAC,KAAKD,KAAK,CAACE,CAAC,CAAC,IAAI,CAAC,CAAC,GAAGf,UAAU;MAC9D,IAAIkB,IAAI,IAAIE,OAAO,EAAE;MACrB,IAAIF,IAAI,GAAGb,OAAO,EAAE;QAClB;QACA;QACA,MAAMgB,CAAC,GAAG,CAACP,CAAC,GAAG,CAAC,IAAItB,YAAY,GAAGuB,CAAC;QACpCC,EAAE,GAAGvB,IAAI,CAAC6B,GAAG,CAACD,CAAC,CAAC;QAChBJ,EAAE,GAAGxB,IAAI,CAAC8B,GAAG,CAACF,CAAC,CAAC;QAChBH,IAAI,GAAG,CAAC;MACV;MACA,MAAMM,OAAO,GAAG,CAACJ,OAAO,GAAGF,IAAI,IAAInB,mBAAmB;MACtD,MAAM0B,EAAE,GAAGT,EAAE,GAAGE,IAAI;MACpB,MAAMQ,EAAE,GAAGT,EAAE,GAAGC,IAAI;MACpB,IAAIJ,CAAC,KAAK,CAAC,EAAE;QACX;QACAH,EAAE,CAACI,CAAC,CAAC,GAAG,CAACJ,EAAE,CAACI,CAAC,CAAC,IAAI,CAAC,IAAIU,EAAE,GAAGD,OAAO;QACnCZ,EAAE,CAACG,CAAC,CAAC,GAAG,CAACH,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,IAAIW,EAAE,GAAGF,OAAO;MACrC,CAAC,MAAM;QACLb,EAAE,CAACG,CAAC,CAAC,GAAG,CAACH,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,IAAKW,EAAE,GAAGD,OAAO,GAAI,CAAC;QACzCZ,EAAE,CAACE,CAAC,CAAC,GAAG,CAACF,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,IAAKY,EAAE,GAAGF,OAAO,GAAI,CAAC;QACzCb,EAAE,CAACI,CAAC,CAAC,GAAG,CAACJ,EAAE,CAACI,CAAC,CAAC,IAAI,CAAC,IAAKU,EAAE,GAAGD,OAAO,GAAI,CAAC;QACzCZ,EAAE,CAACG,CAAC,CAAC,GAAG,CAACH,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,IAAKW,EAAE,GAAGF,OAAO,GAAI,CAAC;MAC3C;IACF;EACF;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,SAASG,WAAWA,CAACnB,KAAa,EAAmB;EACnD,MAAMK,KAAK,GAAG,IAAIe,KAAK,CAASpB,KAAK,CAAC;EACtC,MAAMG,EAAE,GAAG,IAAIiB,KAAK,CAASpB,KAAK,CAAC;EACnC,MAAMI,EAAE,GAAG,IAAIgB,KAAK,CAASpB,KAAK,CAAC;EAEnC,KAAK,IAAIM,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;IAC9BD,KAAK,CAACC,CAAC,CAAC,GAAGR,cAAc,CAACQ,CAAC,EAAEN,KAAK,CAAC;IACnC,IAAIM,CAAC,KAAK,CAAC,EAAE;MACXH,EAAE,CAACG,CAAC,CAAC,GAAG,CAAC;MACTF,EAAE,CAACE,CAAC,CAAC,GAAG,CAAC;IACX,CAAC,MAAM;MACL;MACA,MAAMe,KAAK,GAAG5B,YAAY,GAAGR,IAAI,CAACE,IAAI,CAACmB,CAAC,CAAC;MACzC,MAAMgB,KAAK,GAAGhB,CAAC,GAAGtB,YAAY;MAC9BmB,EAAE,CAACG,CAAC,CAAC,GAAGe,KAAK,GAAGpC,IAAI,CAAC6B,GAAG,CAACQ,KAAK,CAAC;MAC/BlB,EAAE,CAACE,CAAC,CAAC,GAAGe,KAAK,GAAGpC,IAAI,CAAC8B,GAAG,CAACO,KAAK,CAAC;IACjC;EACF;EAEA,KAAK,IAAIC,IAAI,GAAG,CAAC,EAAEA,IAAI,GAAGnC,UAAU,EAAEmC,IAAI,EAAE,EAAE;IAC5C;IACA,KAAK,IAAIjB,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;MAC9BH,EAAE,CAACG,CAAC,CAAC,GAAG,CAACH,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAGhB,SAAS,CAAC;MACtCc,EAAE,CAACE,CAAC,CAAC,GAAG,CAACF,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAGhB,SAAS,CAAC;IACxC;IACA;IACAY,QAAQ,CAACC,EAAE,EAAEC,EAAE,EAAEC,KAAK,EAAEL,KAAK,CAAC;IAC9BG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;IACTC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;EACX;EACA;EACA,KAAK,IAAImB,IAAI,GAAG,CAAC,EAAEA,IAAI,GAAGlC,kBAAkB,EAAEkC,IAAI,EAAE,EAAE;IACpDrB,QAAQ,CAACC,EAAE,EAAEC,EAAE,EAAEC,KAAK,EAAEL,KAAK,CAAC;IAC9BG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;IACTC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;EACX;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,IAAIoB,IAAI,GAAGC,QAAQ;EACnB,IAAIC,IAAI,GAAG,CAACD,QAAQ;EACpB,IAAIE,IAAI,GAAGF,QAAQ;EACnB,IAAIG,IAAI,GAAG,CAACH,QAAQ;EACpB,KAAK,IAAInB,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;IAC9B,MAAMuB,CAAC,GAAG1B,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC;IACpB,MAAMwB,CAAC,GAAG1B,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC;IACpB,MAAMyB,CAAC,GAAG1B,KAAK,CAACC,CAAC,CAAC,IAAI,CAAC;IACvB,IAAIuB,CAAC,GAAGE,CAAC,GAAGP,IAAI,EAAEA,IAAI,GAAGK,CAAC,GAAGE,CAAC;IAC9B,IAAIF,CAAC,GAAGE,CAAC,GAAGL,IAAI,EAAEA,IAAI,GAAGG,CAAC,GAAGE,CAAC;IAC9B,IAAID,CAAC,GAAGC,CAAC,GAAGJ,IAAI,EAAEA,IAAI,GAAGG,CAAC,GAAGC,CAAC;IAC9B,IAAID,CAAC,GAAGC,CAAC,GAAGH,IAAI,EAAEA,IAAI,GAAGE,CAAC,GAAGC,CAAC;EAChC;EACA,MAAMC,OAAO,GAAG,CAACR,IAAI,GAAGE,IAAI,IAAI,CAAC;EACjC,MAAMO,OAAO,GAAG,CAACN,IAAI,GAAGC,IAAI,IAAI,CAAC;EACjC,IAAIM,KAAK,GAAG,CAAC;EACb,KAAK,IAAI5B,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;IAC9B,MAAM6B,KAAK,GACTlD,IAAI,CAAC0B,KAAK,CAAC,CAACR,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,IAAI0B,OAAO,EAAE,CAAC5B,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,IAAI2B,OAAO,CAAC,IAAI5B,KAAK,CAACC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC9E,IAAI6B,KAAK,GAAGD,KAAK,EAAEA,KAAK,GAAGC,KAAK;EAClC;EACA,MAAMC,KAAK,GAAGF,KAAK,GAAGrC,OAAO,GAAG,GAAG,GAAGqC,KAAK,GAAG,GAAG;EAEjD,MAAMG,OAAO,GAAG,IAAIjB,KAAK,CAAgBpB,KAAK,CAAC;EAC/C,KAAK,IAAIM,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;IAC9B+B,OAAO,CAAC/B,CAAC,CAAC,GAAG;MACXgC,EAAE,EAAE,GAAG,GAAG,CAAC,CAACnC,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,IAAI0B,OAAO,IAAII,KAAK;MAC1CG,EAAE,EAAE,GAAG,GAAG,CAAC,CAACnC,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,IAAI2B,OAAO,IAAIG,KAAK;MAC1CI,CAAC,EAAE,CAAC,IAAInC,KAAK,CAACC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG8B;IAC3B,CAAC;EACH;EACA,OAAOC,OAAO;AAChB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASI,oBAAoBA,CAACzC,KAAa,EAAmB;EACnE,IAAIA,KAAK,IAAI,CAAC,EAAE,OAAO,EAAE;EACzB,IAAIA,KAAK,KAAK,CAAC,EAAE,OAAO,CAAC;IAAEsC,EAAE,EAAE,GAAG;IAAEC,EAAE,EAAE,GAAG;IAAEC,CAAC,EAAE;EAAE,CAAC,CAAC;EACpD,IAAIxC,KAAK,KAAK,CAAC,EAAE;IACf;IACA;IACA;IACA;IACA,OAAO,CACL;MAAEsC,EAAE,EAAE,IAAI;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAK,CAAC,EAC/B;MAAEF,EAAE,EAAE,IAAI;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAK,CAAC,CAChC;EACH;EACA,IAAIxC,KAAK,KAAK,CAAC,EAAE;IACf,OAAO,CACL;MAAEsC,EAAE,EAAE,GAAG;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAK,CAAC,EAC9B;MAAEF,EAAE,EAAE,KAAK;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAI,CAAC,EAC/B;MAAEF,EAAE,EAAE,KAAK;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAI,CAAC,CAChC;EACH;EACA,OAAOrB,WAAW,CAACnB,KAAK,CAAC;AAC3B","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
|
-
*
|
|
7
|
-
*
|
|
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`.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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).
|
|
45
|
-
|
|
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;
|
|
50
|
-
// SECONDARY_MAX (nearest the primary) down to SECONDARY_MIN
|
|
51
|
-
//
|
|
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.
|
|
54
|
-
const SECONDARY_MIN = 0.
|
|
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
|
|
145
|
-
//
|
|
146
|
-
// the
|
|
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:
|
|
172
|
-
*
|
|
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
|
|
176
|
-
*
|
|
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.
|
|
188
|
-
cy: 0.
|
|
189
|
-
d: 0.
|
|
222
|
+
cx: 0.35,
|
|
223
|
+
cy: 0.65,
|
|
224
|
+
d: 0.64
|
|
190
225
|
}, {
|
|
191
|
-
cx: 0.
|
|
192
|
-
cy: 0.
|
|
193
|
-
d: 0.
|
|
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":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAUA;AACA;AACA,MAAMA,YAAY,GAAGC,IAAI,CAACC,EAAE,IAAI,CAAC,GAAGD,IAAI,CAACE,IAAI,CAAC,CAAC,CAAC,CAAC;AACjD;AACA;AACA,MAAMC,UAAU,GAAG,GAAG;AACtB,MAAMC,kBAAkB,GAAG,EAAE;AAC7B;AACA;AACA;AACA,MAAMC,SAAS,GAAG,IAAI;AACtB;AACA;AACA,MAAMC,mBAAmB,GAAG,CAAC;AAC7B;AACA;AACA,MAAMC,UAAU,GAAG,IAAI;AACvB;AACA;AACA,MAAMC,YAAY,GAAG,GAAG;AACxB;AACA;AACA;AACA,MAAMC,cAAc,GAAG,CAAC;AACxB,MAAMC,aAAa,GAAG,
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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`.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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:
|
|
41
|
-
*
|
|
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
|
|
45
|
-
*
|
|
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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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`.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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:
|
|
41
|
-
*
|
|
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
|
|
45
|
-
*
|
|
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
|
|
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
|
@@ -74,11 +74,35 @@ describe('computeClusterLayout', () => {
|
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
it('
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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`.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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).
|
|
51
|
-
|
|
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;
|
|
56
|
-
// SECONDARY_MAX (nearest the primary) down to SECONDARY_MIN
|
|
57
|
-
//
|
|
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.
|
|
60
|
-
const SECONDARY_MIN = 0.
|
|
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
|
|
153
|
-
//
|
|
154
|
-
// the
|
|
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 =
|
|
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:
|
|
181
|
-
*
|
|
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
|
|
185
|
-
*
|
|
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.
|
|
193
|
-
{ cx: 0.
|
|
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) {
|