@frybynite/image-cloud 0.4.2 → 0.5.0

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/dist/react.js ADDED
@@ -0,0 +1,3256 @@
1
+ import { jsx as Ot } from "react/jsx-runtime";
2
+ import { forwardRef as $t, useRef as wt, useImperativeHandle as Ut, useEffect as Pt } from "react";
3
+ const ft = Object.freeze({
4
+ none: "none",
5
+ sm: "0 2px 4px rgba(0,0,0,0.1)",
6
+ md: "0 4px 16px rgba(0,0,0,0.4)",
7
+ lg: "0 8px 32px rgba(0,0,0,0.5)",
8
+ glow: "0 0 30px rgba(255,255,255,0.6)"
9
+ }), xt = Object.freeze({
10
+ energetic: Object.freeze({ overshoot: 0.25, bounces: 2, decayRatio: 0.5 }),
11
+ playful: Object.freeze({ overshoot: 0.15, bounces: 1, decayRatio: 0.5 }),
12
+ subtle: Object.freeze({ overshoot: 0.08, bounces: 1, decayRatio: 0.5 })
13
+ }), Et = Object.freeze({
14
+ gentle: Object.freeze({ stiffness: 150, damping: 30, mass: 1, oscillations: 2 }),
15
+ bouncy: Object.freeze({ stiffness: 300, damping: 15, mass: 1, oscillations: 4 }),
16
+ wobbly: Object.freeze({ stiffness: 180, damping: 12, mass: 1.5, oscillations: 5 }),
17
+ snappy: Object.freeze({ stiffness: 400, damping: 25, mass: 0.8, oscillations: 2 })
18
+ }), St = Object.freeze({
19
+ gentle: Object.freeze({ amplitude: 30, frequency: 1.5, decay: !0, decayRate: 0.9, phase: 0 }),
20
+ playful: Object.freeze({ amplitude: 50, frequency: 2.5, decay: !0, decayRate: 0.7, phase: 0 }),
21
+ serpentine: Object.freeze({ amplitude: 60, frequency: 3, decay: !1, decayRate: 1, phase: 0 }),
22
+ flutter: Object.freeze({ amplitude: 20, frequency: 4, decay: !0, decayRate: 0.5, phase: 0 })
23
+ }), pt = Object.freeze({
24
+ type: "linear"
25
+ }), bt = Object.freeze({
26
+ mode: "none"
27
+ }), yt = Object.freeze({
28
+ mode: "none"
29
+ }), Rt = Object.freeze({
30
+ default: Object.freeze({
31
+ border: Object.freeze({
32
+ width: 0,
33
+ color: "#000000",
34
+ radius: 0,
35
+ style: "solid"
36
+ }),
37
+ shadow: "none",
38
+ filter: Object.freeze({}),
39
+ opacity: 1,
40
+ cursor: "pointer",
41
+ outline: Object.freeze({
42
+ width: 0,
43
+ color: "#000000",
44
+ style: "solid",
45
+ offset: 0
46
+ })
47
+ }),
48
+ hover: Object.freeze({
49
+ shadow: "none"
50
+ }),
51
+ focused: Object.freeze({
52
+ shadow: "none"
53
+ })
54
+ }), _t = Object.freeze({
55
+ rows: 1,
56
+ amplitude: 100,
57
+ frequency: 2,
58
+ phaseShift: 0,
59
+ synchronization: "offset"
60
+ // Note: Image rotation along wave is now controlled via image.rotation.mode = 'tangent'
61
+ }), Ht = Object.freeze({
62
+ mobile: Object.freeze({ maxWidth: 767 }),
63
+ tablet: Object.freeze({ maxWidth: 1199 })
64
+ }), Nt = Object.freeze({
65
+ mode: "adaptive",
66
+ // Default to adaptive sizing
67
+ minSize: 50,
68
+ // Adaptive mode minimum
69
+ maxSize: 400,
70
+ // Adaptive mode maximum
71
+ variance: Object.freeze({
72
+ min: 1,
73
+ // No variance by default
74
+ max: 1
75
+ })
76
+ }), jt = Object.freeze({
77
+ mode: "none",
78
+ range: Object.freeze({
79
+ min: -15,
80
+ max: 15
81
+ })
82
+ }), Ct = Object.freeze({
83
+ sizing: Nt,
84
+ rotation: jt
85
+ }), At = Object.freeze({
86
+ validateUrls: !0,
87
+ validationTimeout: 5e3,
88
+ validationMethod: "head",
89
+ allowedExtensions: ["jpg", "jpeg", "png", "gif", "webp", "bmp"]
90
+ }), Lt = Object.freeze({
91
+ enabled: !1,
92
+ centers: !1,
93
+ loaders: !1
94
+ }), y = Object.freeze({
95
+ // Loader configuration (always an array, composite behavior is implicit)
96
+ loaders: [],
97
+ // Shared loader settings and debug config
98
+ config: Object.freeze({
99
+ loaders: At,
100
+ debug: Lt
101
+ }),
102
+ // Image sizing and rotation configuration
103
+ image: Ct,
104
+ // Pattern-based layout configuration
105
+ layout: Object.freeze({
106
+ algorithm: "radial",
107
+ scaleDecay: 0,
108
+ // No decay by default (0-1 for radial/spiral)
109
+ responsive: Ht,
110
+ targetCoverage: 0.6,
111
+ // Target 60% of container area
112
+ densityFactor: 1,
113
+ // Default density
114
+ spacing: Object.freeze({
115
+ padding: 50,
116
+ // padding from viewport edges
117
+ minGap: 20
118
+ // minimum spacing between images
119
+ })
120
+ }),
121
+ // Pattern-based animation configuration
122
+ animation: Object.freeze({
123
+ duration: 600,
124
+ // milliseconds
125
+ easing: Object.freeze({
126
+ default: "cubic-bezier(0.4, 0.0, 0.2, 1)",
127
+ // smooth easing
128
+ bounce: "cubic-bezier(0.68, -0.55, 0.265, 1.55)",
129
+ // bounce effect
130
+ focus: "cubic-bezier(0.4, 0.0, 0.2, 1)"
131
+ // focus/zoom easing
132
+ }),
133
+ queue: Object.freeze({
134
+ enabled: !0,
135
+ // When false, all images display simultaneously
136
+ interval: 150,
137
+ // ms between processing queue items (when enabled)
138
+ maxConcurrent: void 0
139
+ // STUB: Not implemented yet
140
+ }),
141
+ performance: Object.freeze({
142
+ useGPU: void 0,
143
+ // STUB: Not implemented yet
144
+ reduceMotion: void 0
145
+ // STUB: Not implemented yet
146
+ }),
147
+ entry: Object.freeze({
148
+ start: Object.freeze({
149
+ position: "nearest-edge",
150
+ // Default to nearest edge (current behavior)
151
+ offset: 100,
152
+ // pixels beyond edge
153
+ circular: Object.freeze({
154
+ radius: "120%",
155
+ // 120% of container diagonal
156
+ distribution: "even"
157
+ })
158
+ }),
159
+ timing: Object.freeze({
160
+ duration: 600
161
+ // ms
162
+ }),
163
+ easing: "cubic-bezier(0.25, 1, 0.5, 1)",
164
+ // smooth deceleration
165
+ path: pt,
166
+ rotation: bt,
167
+ scale: yt
168
+ })
169
+ }),
170
+ // Pattern-based interaction configuration
171
+ interaction: Object.freeze({
172
+ focus: Object.freeze({
173
+ scalePercent: 0.8,
174
+ // 80% of container height
175
+ zIndex: 1e3,
176
+ animationDuration: void 0
177
+ // Use default animation duration
178
+ }),
179
+ navigation: Object.freeze({
180
+ keyboard: void 0,
181
+ // STUB: Not implemented yet
182
+ swipe: void 0,
183
+ // STUB: Not implemented yet
184
+ mouseWheel: void 0
185
+ // STUB: Not implemented yet
186
+ }),
187
+ gestures: Object.freeze({
188
+ pinchToZoom: void 0,
189
+ // STUB: Not implemented yet
190
+ doubleTapToFocus: void 0
191
+ // STUB: Not implemented yet
192
+ })
193
+ }),
194
+ // Pattern-based rendering configuration
195
+ rendering: Object.freeze({
196
+ responsive: Object.freeze({
197
+ breakpoints: Object.freeze({
198
+ mobile: 768,
199
+ tablet: void 0,
200
+ // STUB: Not implemented yet
201
+ desktop: void 0
202
+ // STUB: Not implemented yet
203
+ }),
204
+ mobileDetection: () => typeof window > "u" ? !1 : window.innerWidth <= 768
205
+ }),
206
+ ui: Object.freeze({
207
+ showLoadingSpinner: !1,
208
+ showImageCounter: !1,
209
+ showThumbnails: void 0,
210
+ // STUB: Not implemented yet
211
+ theme: void 0
212
+ // STUB: Not implemented yet
213
+ }),
214
+ performance: Object.freeze({
215
+ lazyLoad: void 0,
216
+ // STUB: Not implemented yet
217
+ preloadCount: void 0,
218
+ // STUB: Not implemented yet
219
+ imageQuality: void 0
220
+ // STUB: Not implemented yet
221
+ })
222
+ }),
223
+ // Image styling
224
+ styling: Rt
225
+ });
226
+ function Z(n, t) {
227
+ if (!n) return t || {};
228
+ if (!t) return { ...n };
229
+ const e = { ...n };
230
+ return t.border !== void 0 && (e.border = { ...n.border, ...t.border }), t.borderTop !== void 0 && (e.borderTop = { ...n.borderTop, ...t.borderTop }), t.borderRight !== void 0 && (e.borderRight = { ...n.borderRight, ...t.borderRight }), t.borderBottom !== void 0 && (e.borderBottom = { ...n.borderBottom, ...t.borderBottom }), t.borderLeft !== void 0 && (e.borderLeft = { ...n.borderLeft, ...t.borderLeft }), t.filter !== void 0 && (e.filter = { ...n.filter, ...t.filter }), t.outline !== void 0 && (e.outline = { ...n.outline, ...t.outline }), t.shadow !== void 0 && (e.shadow = t.shadow), t.opacity !== void 0 && (e.opacity = t.opacity), t.cursor !== void 0 && (e.cursor = t.cursor), t.className !== void 0 && (e.className = t.className), t.objectFit !== void 0 && (e.objectFit = t.objectFit), t.aspectRatio !== void 0 && (e.aspectRatio = t.aspectRatio), t.borderRadiusTopLeft !== void 0 && (e.borderRadiusTopLeft = t.borderRadiusTopLeft), t.borderRadiusTopRight !== void 0 && (e.borderRadiusTopRight = t.borderRadiusTopRight), t.borderRadiusBottomRight !== void 0 && (e.borderRadiusBottomRight = t.borderRadiusBottomRight), t.borderRadiusBottomLeft !== void 0 && (e.borderRadiusBottomLeft = t.borderRadiusBottomLeft), e;
231
+ }
232
+ function kt(n, t) {
233
+ if (!t) return { ...n };
234
+ const e = Z(n.default, t.default), i = Z(
235
+ Z(e, n.hover),
236
+ t.hover
237
+ ), o = Z(
238
+ Z(e, n.focused),
239
+ t.focused
240
+ );
241
+ return {
242
+ default: e,
243
+ hover: i,
244
+ focused: o
245
+ };
246
+ }
247
+ function Wt(n, t) {
248
+ if (!t) return { ...n };
249
+ const e = { ...n };
250
+ if (t.sizing !== void 0 && (e.sizing = {
251
+ ...n.sizing,
252
+ ...t.sizing
253
+ }, t.sizing.variance)) {
254
+ const i = t.sizing.variance, o = i.min !== void 0 && i.min >= 0.25 && i.min <= 1 ? i.min : n.sizing?.variance?.min ?? 1, s = i.max !== void 0 && i.max >= 1 && i.max <= 1.75 ? i.max : n.sizing?.variance?.max ?? 1;
255
+ e.sizing.variance = { min: o, max: s };
256
+ }
257
+ if (t.rotation !== void 0 && (e.rotation = {
258
+ ...n.rotation,
259
+ ...t.rotation
260
+ }, t.rotation.range)) {
261
+ const i = t.rotation.range, o = i.min !== void 0 && i.min >= -180 && i.min <= 0 ? i.min : n.rotation?.range?.min ?? -15, s = i.max !== void 0 && i.max >= 0 && i.max <= 180 ? i.max : n.rotation?.range?.max ?? 15;
262
+ e.rotation.range = { min: o, max: s };
263
+ }
264
+ return e;
265
+ }
266
+ function Gt(n) {
267
+ const t = n.layout?.rotation;
268
+ if (t && "enabled" in t)
269
+ return {
270
+ rotation: {
271
+ mode: t.enabled ? "random" : "none",
272
+ range: t.range
273
+ }
274
+ };
275
+ }
276
+ function qt(n) {
277
+ const t = n.layout?.sizing?.variance;
278
+ if (t)
279
+ return {
280
+ sizing: {
281
+ mode: "adaptive",
282
+ // Legacy variance config implies adaptive mode
283
+ variance: t
284
+ }
285
+ };
286
+ }
287
+ function Yt(n = {}) {
288
+ const t = Gt(n), e = qt(n);
289
+ let i = n.image;
290
+ (t || e) && (i = {
291
+ ...e || {},
292
+ ...t || {},
293
+ ...i
294
+ }, i.rotation && t?.rotation && n.image?.rotation && (i.rotation = {
295
+ ...t.rotation,
296
+ ...n.image.rotation
297
+ }));
298
+ const o = [...n.loaders ?? []];
299
+ n.images && n.images.length > 0 && o.unshift({
300
+ static: {
301
+ sources: [{ urls: n.images }]
302
+ }
303
+ });
304
+ const r = {
305
+ loaders: {
306
+ ...At,
307
+ ...n.config?.loaders ?? {}
308
+ }
309
+ }, a = {
310
+ loaders: o,
311
+ config: r,
312
+ image: Wt(Ct, i),
313
+ layout: { ...y.layout },
314
+ animation: { ...y.animation },
315
+ interaction: { ...y.interaction },
316
+ rendering: { ...y.rendering },
317
+ styling: kt(Rt, n.styling)
318
+ };
319
+ return n.layout && (a.layout = {
320
+ ...y.layout,
321
+ ...n.layout
322
+ }, n.layout.responsive && (a.layout.responsive = {
323
+ ...y.layout.responsive,
324
+ mobile: n.layout.responsive.mobile ? { ...y.layout.responsive.mobile, ...n.layout.responsive.mobile } : y.layout.responsive.mobile,
325
+ tablet: n.layout.responsive.tablet ? { ...y.layout.responsive.tablet, ...n.layout.responsive.tablet } : y.layout.responsive.tablet
326
+ }), n.layout.spacing && (a.layout.spacing = {
327
+ ...y.layout.spacing,
328
+ ...n.layout.spacing
329
+ })), n.animation && (a.animation = {
330
+ ...y.animation,
331
+ ...n.animation
332
+ }, n.animation.easing && (a.animation.easing = {
333
+ ...y.animation.easing,
334
+ ...n.animation.easing
335
+ }), n.animation.queue && (a.animation.queue = {
336
+ ...y.animation.queue,
337
+ ...n.animation.queue
338
+ }), n.animation.performance && (a.animation.performance = {
339
+ ...y.animation.performance,
340
+ ...n.animation.performance
341
+ }), n.animation.entry && (a.animation.entry = {
342
+ ...y.animation.entry,
343
+ ...n.animation.entry,
344
+ start: n.animation.entry.start ? {
345
+ ...y.animation.entry.start,
346
+ ...n.animation.entry.start,
347
+ circular: n.animation.entry.start.circular ? { ...y.animation.entry.start.circular, ...n.animation.entry.start.circular } : y.animation.entry.start.circular
348
+ } : y.animation.entry.start,
349
+ timing: n.animation.entry.timing ? { ...y.animation.entry.timing, ...n.animation.entry.timing } : y.animation.entry.timing,
350
+ path: n.animation.entry.path ? { ...pt, ...n.animation.entry.path } : y.animation.entry.path,
351
+ rotation: n.animation.entry.rotation ? { ...bt, ...n.animation.entry.rotation } : y.animation.entry.rotation,
352
+ scale: n.animation.entry.scale ? { ...yt, ...n.animation.entry.scale } : y.animation.entry.scale
353
+ })), n.interaction && (a.interaction = {
354
+ ...y.interaction,
355
+ ...n.interaction
356
+ }, n.interaction.focus && (a.interaction.focus = {
357
+ ...y.interaction.focus,
358
+ ...n.interaction.focus
359
+ }), n.interaction.navigation && (a.interaction.navigation = {
360
+ ...y.interaction.navigation,
361
+ ...n.interaction.navigation
362
+ }), n.interaction.gestures && (a.interaction.gestures = {
363
+ ...y.interaction.gestures,
364
+ ...n.interaction.gestures
365
+ })), n.rendering && (a.rendering = {
366
+ ...y.rendering,
367
+ ...n.rendering
368
+ }, n.rendering.responsive && (a.rendering.responsive = {
369
+ ...y.rendering.responsive,
370
+ ...n.rendering.responsive,
371
+ breakpoints: n.rendering.responsive.breakpoints ? { ...y.rendering.responsive.breakpoints, ...n.rendering.responsive.breakpoints } : y.rendering.responsive.breakpoints,
372
+ mobileDetection: n.rendering.responsive.mobileDetection ? n.rendering.responsive.mobileDetection : y.rendering.responsive.mobileDetection
373
+ }), n.rendering.ui && (a.rendering.ui = {
374
+ ...y.rendering.ui,
375
+ ...n.rendering.ui
376
+ }), n.rendering.performance && (a.rendering.performance = {
377
+ ...y.rendering.performance,
378
+ ...n.rendering.performance
379
+ })), a.config.debug = {
380
+ ...Lt,
381
+ ...n.config?.debug ?? {}
382
+ }, a;
383
+ }
384
+ function Xt(n, t) {
385
+ return { ...n ? xt[n] : xt.playful, ...t };
386
+ }
387
+ function Bt(n, t) {
388
+ return { ...n ? Et[n] : Et.gentle, ...t };
389
+ }
390
+ function Jt(n, t) {
391
+ return { ...n ? St[n] : St.gentle, ...t };
392
+ }
393
+ class Vt {
394
+ constructor(t) {
395
+ this.activeAnimations = /* @__PURE__ */ new Map(), this.animationIdCounter = 0, this.config = t;
396
+ }
397
+ /**
398
+ * Build transform string from transform params
399
+ * Always starts with centering transform to match image positioning system
400
+ */
401
+ buildTransformString(t) {
402
+ const e = ["translate(-50%, -50%)"];
403
+ if (t.x !== void 0 || t.y !== void 0) {
404
+ const i = t.x ?? 0, o = t.y ?? 0;
405
+ e.push(`translate(${i}px, ${o}px)`);
406
+ }
407
+ return t.rotation !== void 0 && e.push(`rotate(${t.rotation}deg)`), t.scale !== void 0 && e.push(`scale(${t.scale})`), e.join(" ");
408
+ }
409
+ /**
410
+ * Start a cancellable transform animation using Web Animations API
411
+ * @param element - The element to animate
412
+ * @param from - Starting transform state
413
+ * @param to - Ending transform state
414
+ * @param duration - Animation duration in ms (optional)
415
+ * @param easing - CSS easing function (optional)
416
+ * @returns AnimationHandle that can be used to cancel or query the animation
417
+ */
418
+ animateTransformCancellable(t, e, i, o = null, s = null) {
419
+ this.cancelAllAnimations(t);
420
+ const r = o ?? this.config.duration, a = s ?? this.config.easing.default, l = this.buildTransformString(e), c = this.buildTransformString(i);
421
+ t.style.transition = "none";
422
+ const u = t.animate(
423
+ [
424
+ { transform: l },
425
+ { transform: c }
426
+ ],
427
+ {
428
+ duration: r,
429
+ easing: a,
430
+ fill: "forwards"
431
+ // Keep final state after animation
432
+ }
433
+ ), h = {
434
+ id: `anim-${++this.animationIdCounter}`,
435
+ element: t,
436
+ animation: u,
437
+ fromState: e,
438
+ toState: i,
439
+ startTime: performance.now(),
440
+ duration: r
441
+ };
442
+ return this.activeAnimations.set(t, h), u.finished.then(() => {
443
+ t.style.transform = c, this.activeAnimations.delete(t);
444
+ }).catch(() => {
445
+ this.activeAnimations.delete(t);
446
+ }), h;
447
+ }
448
+ /**
449
+ * Cancel an active animation
450
+ * @param handle - The animation handle to cancel
451
+ * @param commitStyle - If true, keeps current position; if false, no style change
452
+ * @returns Snapshot of where the animation was when cancelled
453
+ */
454
+ cancelAnimation(t, e = !0) {
455
+ const i = this.getCurrentTransform(t.element);
456
+ if (t.animation.cancel(), e) {
457
+ const o = this.buildTransformString({
458
+ x: i.x,
459
+ y: i.y,
460
+ rotation: i.rotation,
461
+ scale: i.scale
462
+ });
463
+ t.element.style.transform = o;
464
+ }
465
+ return this.activeAnimations.delete(t.element), i;
466
+ }
467
+ /**
468
+ * Cancel all animations on an element
469
+ * Uses Web Animations API to find and cancel ALL animations, not just tracked ones
470
+ * @param element - The element to cancel animations for
471
+ */
472
+ cancelAllAnimations(t) {
473
+ const e = this.activeAnimations.get(t);
474
+ e && this.cancelAnimation(e, !1);
475
+ const i = t.getAnimations();
476
+ for (const o of i)
477
+ o.cancel();
478
+ }
479
+ /**
480
+ * Get current transform state of an element (works mid-animation)
481
+ * Uses DOMMatrix to parse the computed transform
482
+ * @param element - The element to query
483
+ * @returns Current transform snapshot
484
+ */
485
+ getCurrentTransform(t) {
486
+ const i = getComputedStyle(t).transform;
487
+ if (i === "none" || !i)
488
+ return { x: 0, y: 0, rotation: 0, scale: 1 };
489
+ const o = new DOMMatrix(i), s = Math.sqrt(o.a * o.a + o.b * o.b), r = Math.atan2(o.b, o.a) * (180 / Math.PI), a = o.e, l = o.f;
490
+ return { x: a, y: l, rotation: r, scale: s };
491
+ }
492
+ /**
493
+ * Check if an element has an active animation
494
+ * @param element - The element to check
495
+ * @returns True if animation is in progress
496
+ */
497
+ hasActiveAnimation(t) {
498
+ return this.activeAnimations.has(t);
499
+ }
500
+ /**
501
+ * Get animation handle for an element if it exists
502
+ * @param element - The element to query
503
+ * @returns AnimationHandle or undefined
504
+ */
505
+ getAnimationHandle(t) {
506
+ return this.activeAnimations.get(t);
507
+ }
508
+ /**
509
+ * Animate element transform with smooth easing (CSS transitions - legacy method)
510
+ * @param element - The element to animate
511
+ * @param properties - Transform properties {x, y, rotation, scale}
512
+ * @param duration - Animation duration in ms (optional)
513
+ * @param easing - CSS easing function (optional)
514
+ * @returns Promise that resolves when animation completes
515
+ */
516
+ animateTransform(t, e, i = null, o = null) {
517
+ return new Promise((s) => {
518
+ const r = i ?? this.config.duration, a = o ?? this.config.easing.default;
519
+ t.style.transition = `transform ${r}ms ${a}, box-shadow ${r}ms ${a}`, t.style.transform = this.buildTransformString(e), setTimeout(() => {
520
+ s();
521
+ }, r);
522
+ });
523
+ }
524
+ /**
525
+ * Reset element to its original transform
526
+ * @param element - The element to reset
527
+ * @param originalState - Original transform state {x, y, rotation, scale}
528
+ * @returns Promise that resolves when animation completes
529
+ */
530
+ resetTransform(t, e) {
531
+ return this.animateTransform(t, e);
532
+ }
533
+ /**
534
+ * Remove transition styles from element
535
+ * @param element - The element to clear
536
+ */
537
+ clearTransition(t) {
538
+ t.style.transition = "";
539
+ }
540
+ /**
541
+ * Utility: Wait for a specified duration
542
+ * @param ms - Milliseconds to wait
543
+ * @returns Promise that resolves after the specified duration
544
+ */
545
+ wait(t) {
546
+ return new Promise((e) => setTimeout(e, t));
547
+ }
548
+ }
549
+ function V(n, t, e) {
550
+ return n + (t - n) * e;
551
+ }
552
+ function Kt(n, t, e, i) {
553
+ const { overshoot: o, bounces: s, decayRatio: r } = i, a = e.x - t.x, l = e.y - t.y, c = Zt(s, r);
554
+ let u = 0, h = 0, d = 1, f = o, b = !1;
555
+ for (let g = 0; g < c.length; g++)
556
+ if (n <= c[g].time) {
557
+ h = g === 0 ? 0 : c[g - 1].time, d = c[g].time, f = c[g].overshoot, b = c[g].isOvershoot;
558
+ break;
559
+ }
560
+ const p = (n - h) / (d - h);
561
+ if (b)
562
+ u = 1 + f * ot(p);
563
+ else if (h === 0)
564
+ u = ot(p);
565
+ else {
566
+ const m = 1 + (c.find(
567
+ (E, v) => E.time > h && v > 0 && c[v - 1].isOvershoot
568
+ )?.overshoot || f);
569
+ u = V(m, 1, ot(p));
570
+ }
571
+ return {
572
+ x: t.x + a * u,
573
+ y: t.y + l * u
574
+ };
575
+ }
576
+ function Zt(n, t) {
577
+ const e = [];
578
+ let i = 0.6;
579
+ e.push({ time: i, overshoot: 0, isOvershoot: !1 });
580
+ let o = 0.15;
581
+ const r = 0.4 / (n * 2);
582
+ for (let a = 0; a < n; a++)
583
+ i += r, e.push({ time: i, overshoot: o, isOvershoot: !0 }), i += r, e.push({ time: i, overshoot: o * t, isOvershoot: !1 }), o *= t;
584
+ return e.push({ time: 1, overshoot: 0, isOvershoot: !1 }), e;
585
+ }
586
+ function Qt(n, t, e, i) {
587
+ const { stiffness: o, damping: s, mass: r, oscillations: a } = i, l = e.x - t.x, c = e.y - t.y, u = Math.sqrt(o / r), h = s / (2 * Math.sqrt(o * r));
588
+ let d;
589
+ if (h < 1) {
590
+ const f = u * Math.sqrt(1 - h * h), b = Math.exp(-h * u * n * 3), p = Math.cos(f * n * a * Math.PI);
591
+ d = 1 - b * p;
592
+ } else
593
+ d = 1 - Math.exp(-u * n * 3);
594
+ return d = Math.max(0, Math.min(d, 1.3)), {
595
+ x: t.x + l * d,
596
+ y: t.y + c * d
597
+ };
598
+ }
599
+ function te(n, t, e, i) {
600
+ const { amplitude: o, frequency: s, decay: r, decayRate: a, phase: l } = i, c = e.x - t.x, u = e.y - t.y, h = Math.sqrt(c * c + u * u), d = h > 0 ? -u / h : 0, f = h > 0 ? c / h : 1, b = s * Math.PI * 2 * n + l, p = r ? Math.pow(1 - n, a) : 1, g = o * Math.sin(b) * p, m = ee(n);
601
+ return {
602
+ x: V(t.x, e.x, m) + g * d,
603
+ y: V(t.y, e.y, m) + g * f
604
+ };
605
+ }
606
+ function ot(n) {
607
+ return 1 - (1 - n) * (1 - n);
608
+ }
609
+ function ee(n) {
610
+ return 1 - Math.pow(1 - n, 3);
611
+ }
612
+ function ie(n, t, e) {
613
+ const { amplitude: i, frequency: o, decay: s } = e, r = Math.sin(n * o * Math.PI * 2), a = s ? Math.pow(1 - n, 2) : 1, l = i * r * a;
614
+ return t + l;
615
+ }
616
+ function ne(n, t, e) {
617
+ const { overshoot: i, bounces: o } = e, s = [];
618
+ s.push({ time: 0.5, scale: i });
619
+ let r = i;
620
+ const a = 0.5, c = 0.5 / (o * 2);
621
+ let u = 0.5;
622
+ for (let d = 0; d < o; d++) {
623
+ const f = 1 - (r - 1) * a;
624
+ u += c, s.push({ time: u, scale: f }), r = 1 + (r - 1) * a * a, u += c, d < o - 1 && s.push({ time: u, scale: r });
625
+ }
626
+ s.push({ time: 1, scale: 1 });
627
+ let h = 1;
628
+ for (let d = 0; d < s.length; d++)
629
+ if (n <= s[d].time) {
630
+ const f = d === 0 ? 0 : s[d - 1].time, b = d === 0 ? 1 : s[d - 1].scale, p = (n - f) / (s[d].time - f), g = ot(p);
631
+ h = b + (s[d].scale - b) * g;
632
+ break;
633
+ }
634
+ return h * t;
635
+ }
636
+ function oe(n) {
637
+ const {
638
+ element: t,
639
+ startPosition: e,
640
+ endPosition: i,
641
+ pathConfig: o,
642
+ duration: s,
643
+ imageWidth: r,
644
+ imageHeight: a,
645
+ rotation: l,
646
+ scale: c,
647
+ onComplete: u,
648
+ rotationConfig: h,
649
+ startRotation: d,
650
+ scaleConfig: f,
651
+ startScale: b
652
+ } = n, p = o.type, g = d !== void 0 && d !== l, m = h?.mode === "wobble", E = h?.wobble || { amplitude: 15, frequency: 3, decay: !0 }, v = g || m, w = b !== void 0 && b !== c, S = f?.mode === "pop", x = f?.pop || { overshoot: 1.2, bounces: 1 };
653
+ if ((p === "linear" || p === "arc") && !v && !(w || S)) {
654
+ u && u();
655
+ return;
656
+ }
657
+ const M = performance.now(), z = -r / 2, P = -a / 2;
658
+ function _(Y) {
659
+ const N = Y - M, R = Math.min(N / s, 1);
660
+ let D;
661
+ switch (p) {
662
+ case "bounce": {
663
+ const O = Xt(
664
+ o.bouncePreset,
665
+ o.bounce
666
+ );
667
+ D = Kt(R, e, i, O);
668
+ break;
669
+ }
670
+ case "elastic": {
671
+ const O = Bt(
672
+ o.elasticPreset,
673
+ o.elastic
674
+ );
675
+ D = Qt(R, e, i, O);
676
+ break;
677
+ }
678
+ case "wave": {
679
+ const O = Jt(
680
+ o.wavePreset,
681
+ o.wave
682
+ );
683
+ D = te(R, e, i, O);
684
+ break;
685
+ }
686
+ default:
687
+ D = {
688
+ x: V(e.x, i.x, R),
689
+ y: V(e.y, i.y, R)
690
+ };
691
+ }
692
+ const k = D.x - i.x, H = D.y - i.y;
693
+ let A;
694
+ m ? A = ie(R, l, E) : g ? A = V(d, l, R) : A = l;
695
+ let C;
696
+ S ? C = ne(R, c, x) : w ? C = V(b, c, R) : C = c, t.style.transform = `translate(${z}px, ${P}px) translate(${k}px, ${H}px) rotate(${A}deg) scale(${C})`, R < 1 ? requestAnimationFrame(_) : (t.style.transform = `translate(${z}px, ${P}px) rotate(${l}deg) scale(${c})`, u && u());
697
+ }
698
+ requestAnimationFrame(_);
699
+ }
700
+ function se(n) {
701
+ return n === "bounce" || n === "elastic" || n === "wave";
702
+ }
703
+ const ae = {
704
+ radial: "center",
705
+ spiral: "center",
706
+ grid: "top",
707
+ cluster: "nearest-edge",
708
+ random: "nearest-edge",
709
+ wave: "left"
710
+ };
711
+ class re {
712
+ constructor(t, e) {
713
+ this.config = t, this.layoutAlgorithm = e, this.resolvedStartPosition = this.resolveStartPosition(), this.pathConfig = t.path || pt, this.rotationConfig = t.rotation || bt, this.scaleConfig = t.scale || yt;
714
+ }
715
+ /**
716
+ * Get the effective start position, considering layout-aware defaults
717
+ */
718
+ resolveStartPosition() {
719
+ return this.config.start.position ? this.config.start.position : ae[this.layoutAlgorithm] || "nearest-edge";
720
+ }
721
+ /**
722
+ * Calculate the starting position for an image's entry animation
723
+ */
724
+ calculateStartPosition(t, e, i, o, s) {
725
+ const r = this.resolvedStartPosition, a = this.config.start.offset ?? 100;
726
+ switch (r) {
727
+ case "nearest-edge":
728
+ return this.calculateNearestEdge(t, e, i, a);
729
+ case "top":
730
+ return this.calculateEdgePosition("top", t, e, i, a);
731
+ case "bottom":
732
+ return this.calculateEdgePosition("bottom", t, e, i, a);
733
+ case "left":
734
+ return this.calculateEdgePosition("left", t, e, i, a);
735
+ case "right":
736
+ return this.calculateEdgePosition("right", t, e, i, a);
737
+ case "center":
738
+ return this.calculateCenterPosition(i, t, e);
739
+ case "random-edge":
740
+ return this.calculateRandomEdge(t, e, i, a);
741
+ case "circular":
742
+ return this.calculateCircularPosition(
743
+ t,
744
+ e,
745
+ i,
746
+ o,
747
+ s
748
+ );
749
+ default:
750
+ return this.calculateNearestEdge(t, e, i, a);
751
+ }
752
+ }
753
+ /**
754
+ * Calculate start position from the nearest edge (current default behavior)
755
+ */
756
+ calculateNearestEdge(t, e, i, o) {
757
+ const s = t.x, r = t.y, a = s, l = i.width - s, c = r, u = i.height - r, h = Math.min(a, l, c, u);
758
+ let d = t.x, f = t.y;
759
+ return h === a ? d = -(e.width + o) : h === l ? d = i.width + o : h === c ? f = -(e.height + o) : f = i.height + o, { x: d, y: f };
760
+ }
761
+ /**
762
+ * Calculate start position from a specific edge
763
+ */
764
+ calculateEdgePosition(t, e, i, o, s) {
765
+ let r = e.x, a = e.y;
766
+ switch (t) {
767
+ case "top":
768
+ a = -(i.height + s);
769
+ break;
770
+ case "bottom":
771
+ a = o.height + s;
772
+ break;
773
+ case "left":
774
+ r = -(i.width + s);
775
+ break;
776
+ case "right":
777
+ r = o.width + s;
778
+ break;
779
+ }
780
+ return { x: r, y: a };
781
+ }
782
+ /**
783
+ * Calculate start position from center with scale animation
784
+ */
785
+ calculateCenterPosition(t, e, i) {
786
+ const o = t.width / 2, s = t.height / 2;
787
+ return {
788
+ x: o,
789
+ y: s,
790
+ useScale: !0
791
+ // Signal to use scale animation from 0
792
+ };
793
+ }
794
+ /**
795
+ * Calculate start position from a random edge
796
+ */
797
+ calculateRandomEdge(t, e, i, o) {
798
+ const s = ["top", "bottom", "left", "right"], r = s[Math.floor(Math.random() * s.length)];
799
+ return this.calculateEdgePosition(r, t, e, i, o);
800
+ }
801
+ /**
802
+ * Calculate start position on a circle around the container
803
+ */
804
+ calculateCircularPosition(t, e, i, o, s) {
805
+ const r = this.config.start.circular || {}, a = r.distribution || "even";
806
+ let l;
807
+ const c = r.radius || "120%";
808
+ if (typeof c == "string" && c.endsWith("%")) {
809
+ const p = parseFloat(c) / 100;
810
+ l = Math.sqrt(
811
+ i.width ** 2 + i.height ** 2
812
+ ) * p / 2;
813
+ } else
814
+ l = typeof c == "number" ? c : 500;
815
+ let u;
816
+ a === "even" ? u = o / s * 2 * Math.PI : u = Math.random() * 2 * Math.PI;
817
+ const h = i.width / 2, d = i.height / 2, f = h + Math.cos(u) * l, b = d + Math.sin(u) * l;
818
+ return { x: f, y: b };
819
+ }
820
+ /**
821
+ * Get animation parameters for an image
822
+ */
823
+ getAnimationParams(t) {
824
+ const e = this.config.timing.duration, i = this.config.easing;
825
+ return {
826
+ startTransform: "",
827
+ // Will be computed by caller based on start position
828
+ duration: e,
829
+ delay: 0,
830
+ easing: i
831
+ };
832
+ }
833
+ /**
834
+ * Build a CSS transform string for the start position
835
+ * Uses pixel-based centering offset for reliable cross-browser behavior
836
+ */
837
+ buildStartTransform(t, e, i, o, s, r, a, l) {
838
+ const c = t.x - e.x, u = t.y - e.y, h = a !== void 0 ? a : i, d = l !== void 0 ? l : o, f = s !== void 0 ? -s / 2 : 0, b = r !== void 0 ? -r / 2 : 0, p = s !== void 0 ? `translate(${f}px, ${b}px)` : "translate(-50%, -50%)";
839
+ return t.useScale ? `${p} translate(${c}px, ${u}px) rotate(${h}deg) scale(0)` : `${p} translate(${c}px, ${u}px) rotate(${h}deg) scale(${d})`;
840
+ }
841
+ /**
842
+ * Build the final CSS transform string
843
+ * Uses pixel-based centering offset for reliable cross-browser behavior
844
+ */
845
+ buildFinalTransform(t, e, i, o) {
846
+ if (i !== void 0 && o !== void 0) {
847
+ const s = -i / 2, r = -o / 2;
848
+ return `translate(${s}px, ${r}px) rotate(${t}deg) scale(${e})`;
849
+ }
850
+ return `translate(-50%, -50%) rotate(${t}deg) scale(${e})`;
851
+ }
852
+ /**
853
+ * Get the transition CSS for entry animation
854
+ * For JS-animated paths, only animate opacity (transform handled by JS)
855
+ */
856
+ getTransitionCSS() {
857
+ const t = this.config.timing.duration, e = this.config.easing;
858
+ return this.requiresJSAnimation() ? `opacity ${t}ms ease-out` : `opacity ${t}ms ease-out, transform ${t}ms ${e}`;
859
+ }
860
+ /**
861
+ * Check if the current path type requires JavaScript animation
862
+ */
863
+ requiresJSAnimation() {
864
+ return se(this.pathConfig.type);
865
+ }
866
+ /**
867
+ * Get the path configuration
868
+ */
869
+ getPathConfig() {
870
+ return this.pathConfig;
871
+ }
872
+ /**
873
+ * Get the path type
874
+ */
875
+ getPathType() {
876
+ return this.pathConfig.type;
877
+ }
878
+ /**
879
+ * Get animation timing configuration
880
+ */
881
+ getTiming() {
882
+ return {
883
+ duration: this.config.timing.duration
884
+ };
885
+ }
886
+ /**
887
+ * Get the rotation configuration
888
+ */
889
+ getRotationConfig() {
890
+ return this.rotationConfig;
891
+ }
892
+ /**
893
+ * Get the rotation mode
894
+ */
895
+ getRotationMode() {
896
+ return this.rotationConfig.mode;
897
+ }
898
+ /**
899
+ * Calculate the starting rotation for an entry animation
900
+ * @param finalRotation - The final rotation from the layout
901
+ * @returns The starting rotation in degrees
902
+ */
903
+ calculateStartRotation(t) {
904
+ switch (this.rotationConfig.mode) {
905
+ case "none":
906
+ return t;
907
+ case "settle": {
908
+ const i = this.rotationConfig.startRotation;
909
+ if (i === void 0)
910
+ return t + (Math.random() - 0.5) * 60;
911
+ if (typeof i == "number")
912
+ return i;
913
+ const o = i.max - i.min;
914
+ return i.min + Math.random() * o;
915
+ }
916
+ case "spin": {
917
+ const i = this.rotationConfig.spinCount ?? 1, o = this.resolveSpinDirection(t);
918
+ return t + i * 360 * o;
919
+ }
920
+ case "random":
921
+ return t + (Math.random() - 0.5) * 60;
922
+ case "wobble":
923
+ return t;
924
+ default:
925
+ return t;
926
+ }
927
+ }
928
+ /**
929
+ * Resolve spin direction based on config
930
+ * @returns 1 for clockwise, -1 for counterclockwise
931
+ */
932
+ resolveSpinDirection(t) {
933
+ switch (this.rotationConfig.direction ?? "auto") {
934
+ case "clockwise":
935
+ return -1;
936
+ // Negative rotation = clockwise spin to final
937
+ case "counterclockwise":
938
+ return 1;
939
+ // Positive rotation = counterclockwise spin to final
940
+ case "random":
941
+ return Math.random() < 0.5 ? 1 : -1;
942
+ default:
943
+ return t >= 0 ? 1 : -1;
944
+ }
945
+ }
946
+ /**
947
+ * Check if the current rotation mode requires JavaScript animation
948
+ * (as opposed to CSS transitions)
949
+ */
950
+ requiresJSRotation() {
951
+ return this.rotationConfig.mode === "wobble";
952
+ }
953
+ /**
954
+ * Calculate wobble rotation for a given animation progress
955
+ * @param progress - Animation progress from 0 to 1
956
+ * @param finalRotation - The final rotation in degrees
957
+ * @returns The current rotation in degrees
958
+ */
959
+ calculateWobbleRotation(t, e) {
960
+ if (this.rotationConfig.mode !== "wobble")
961
+ return e;
962
+ const i = this.rotationConfig.wobble || {
963
+ amplitude: 15,
964
+ frequency: 3,
965
+ decay: !0
966
+ }, { amplitude: o, frequency: s, decay: r } = i, a = Math.sin(t * s * Math.PI * 2), l = r ? Math.pow(1 - t, 2) : 1, c = o * a * l;
967
+ return e + c;
968
+ }
969
+ /**
970
+ * Get the scale configuration
971
+ */
972
+ getScaleConfig() {
973
+ return this.scaleConfig;
974
+ }
975
+ /**
976
+ * Get the scale mode
977
+ */
978
+ getScaleMode() {
979
+ return this.scaleConfig.mode;
980
+ }
981
+ /**
982
+ * Calculate the starting scale for an entry animation
983
+ * @param finalScale - The final scale from the layout
984
+ * @returns The starting scale
985
+ */
986
+ calculateStartScale(t) {
987
+ switch (this.scaleConfig.mode) {
988
+ case "none":
989
+ return t;
990
+ case "grow":
991
+ return (this.scaleConfig.startScale ?? 0.3) * t;
992
+ case "shrink":
993
+ return (this.scaleConfig.startScale ?? 1.5) * t;
994
+ case "pop":
995
+ return t;
996
+ case "random": {
997
+ const i = this.scaleConfig.range ?? { min: 0.5, max: 1 };
998
+ return (i.min + Math.random() * (i.max - i.min)) * t;
999
+ }
1000
+ default:
1001
+ return t;
1002
+ }
1003
+ }
1004
+ /**
1005
+ * Check if the current scale mode requires JavaScript animation
1006
+ * (as opposed to CSS transitions)
1007
+ */
1008
+ requiresJSScale() {
1009
+ return this.scaleConfig.mode === "pop";
1010
+ }
1011
+ /**
1012
+ * Calculate pop scale for a given animation progress
1013
+ * @param progress - Animation progress from 0 to 1
1014
+ * @param finalScale - The final scale value
1015
+ * @returns The current scale value with bounce effect
1016
+ */
1017
+ calculatePopScale(t, e) {
1018
+ if (this.scaleConfig.mode !== "pop")
1019
+ return e;
1020
+ const i = this.scaleConfig.pop || {
1021
+ overshoot: 1.2,
1022
+ bounces: 1
1023
+ }, { overshoot: o, bounces: s } = i, r = this.generateScaleBounceKeyframes(s, o);
1024
+ let a = e;
1025
+ for (let l = 0; l < r.length; l++)
1026
+ if (t <= r[l].time) {
1027
+ const c = l === 0 ? 0 : r[l - 1].time, u = l === 0 ? e : r[l - 1].scale, h = (t - c) / (r[l].time - c), d = this.easeOutQuad(h);
1028
+ a = u + (r[l].scale - u) * d;
1029
+ break;
1030
+ }
1031
+ return a * e;
1032
+ }
1033
+ /**
1034
+ * Generate keyframes for scale bounce animation
1035
+ */
1036
+ generateScaleBounceKeyframes(t, e) {
1037
+ const i = [];
1038
+ i.push({ time: 0.5, scale: e });
1039
+ let o = e;
1040
+ const s = 0.5, a = 0.5 / (t * 2);
1041
+ let l = 0.5;
1042
+ for (let c = 0; c < t; c++) {
1043
+ const u = 1 - (o - 1) * s;
1044
+ l += a, i.push({ time: l, scale: u }), o = 1 + (o - 1) * s * s, l += a, c < t - 1 && i.push({ time: l, scale: o });
1045
+ }
1046
+ return i.push({ time: 1, scale: 1 }), i;
1047
+ }
1048
+ /**
1049
+ * Easing function for smooth transitions
1050
+ */
1051
+ easeOutQuad(t) {
1052
+ return 1 - (1 - t) * (1 - t);
1053
+ }
1054
+ }
1055
+ class ce {
1056
+ constructor(t, e = {}) {
1057
+ this.config = t, this.imageConfig = e;
1058
+ }
1059
+ /**
1060
+ * Generate random layout positions for images
1061
+ * @param imageCount - Number of images to layout
1062
+ * @param containerBounds - Container dimensions {width, height}
1063
+ * @param options - Optional overrides (includes fixedHeight)
1064
+ * @returns Array of layout objects with position, rotation, scale
1065
+ */
1066
+ generate(t, e, i = {}) {
1067
+ const o = [], { width: s, height: r } = e, a = this.config.spacing.padding, l = i.fixedHeight ?? 200, c = this.imageConfig.rotation?.mode ?? "none", u = this.imageConfig.rotation?.range?.min ?? -15, h = this.imageConfig.rotation?.range?.max ?? 15, d = this.imageConfig.sizing?.variance?.min ?? 1, f = this.imageConfig.sizing?.variance?.max ?? 1, b = d !== 1 || f !== 1, g = l * 1.5 / 2, m = l / 2, E = s - a - g, v = r - a - m, w = a + g, S = a + m;
1068
+ for (let x = 0; x < t; x++) {
1069
+ const T = this.random(w, E), M = this.random(S, v), z = c === "random" ? this.random(u, h) : 0, P = b ? this.random(d, f) : 1, _ = l * P, Y = {
1070
+ id: x,
1071
+ x: T,
1072
+ y: M,
1073
+ rotation: z,
1074
+ scale: P,
1075
+ baseSize: _
1076
+ };
1077
+ o.push(Y);
1078
+ }
1079
+ return o;
1080
+ }
1081
+ /**
1082
+ * Utility: Generate random number between min and max
1083
+ * @param min - Minimum value
1084
+ * @param max - Maximum value
1085
+ * @returns Random number in range
1086
+ */
1087
+ random(t, e) {
1088
+ return Math.random() * (e - t) + t;
1089
+ }
1090
+ }
1091
+ class le {
1092
+ constructor(t, e = {}) {
1093
+ this.config = t, this.imageConfig = e;
1094
+ }
1095
+ /**
1096
+ * Generate radial layout positions for images
1097
+ * @param imageCount - Number of images to layout
1098
+ * @param containerBounds - Container dimensions {width, height}
1099
+ * @param options - Optional overrides
1100
+ * @returns Array of layout objects with position, rotation, scale
1101
+ */
1102
+ generate(t, e, i = {}) {
1103
+ const o = [], { width: s, height: r } = e, a = i.fixedHeight ?? 200, l = this.imageConfig.rotation?.mode ?? "none", c = this.imageConfig.rotation?.range?.min ?? -15, u = this.imageConfig.rotation?.range?.max ?? 15, h = this.imageConfig.sizing?.variance?.min ?? 1, d = this.imageConfig.sizing?.variance?.max ?? 1, f = h !== 1 || d !== 1, b = this.config.scaleDecay ?? 0, p = i.fixedHeight ?? a, g = s / 2, m = r / 2, E = Math.ceil(Math.sqrt(t));
1104
+ if (t > 0) {
1105
+ const S = f ? this.random(h, d) : 1, x = p * S;
1106
+ o.push({
1107
+ id: 0,
1108
+ x: g,
1109
+ y: m,
1110
+ rotation: l === "random" ? this.random(c * 0.33, u * 0.33) : 0,
1111
+ // Less rotation for center
1112
+ scale: S,
1113
+ baseSize: x,
1114
+ zIndex: 100
1115
+ // Center image is highest
1116
+ });
1117
+ }
1118
+ let v = 1, w = 1;
1119
+ for (; v < t; ) {
1120
+ const S = w / E, x = b > 0 ? 1 - S * b * 0.5 : 1, T = w * (p * 0.8), M = T * 1.5, z = Math.PI * (3 * (M + T) - Math.sqrt((3 * M + T) * (M + 3 * T))), P = this.estimateWidth(p), _ = Math.floor(z / (P * 0.7));
1121
+ if (_ === 0) {
1122
+ w++;
1123
+ continue;
1124
+ }
1125
+ const Y = 2 * Math.PI / _, N = w * (20 * Math.PI / 180);
1126
+ for (let R = 0; R < _ && v < t; R++) {
1127
+ const D = R * Y + N, k = f ? this.random(h, d) : 1, H = x * k, A = p * H;
1128
+ let C = g + Math.cos(D) * M, O = m + Math.sin(D) * T;
1129
+ const $ = this.config.spacing.padding ?? 50, U = A * 1.5 / 2, L = A / 2;
1130
+ C - U < $ ? C = $ + U : C + U > s - $ && (C = s - $ - U), O - L < $ ? O = $ + L : O + L > r - $ && (O = r - $ - L);
1131
+ const W = l === "random" ? this.random(c, u) : 0;
1132
+ o.push({
1133
+ id: v,
1134
+ x: C,
1135
+ y: O,
1136
+ rotation: W,
1137
+ scale: H,
1138
+ baseSize: A,
1139
+ zIndex: Math.max(1, 100 - w)
1140
+ // Outer rings have lower z-index
1141
+ }), v++;
1142
+ }
1143
+ w++;
1144
+ }
1145
+ return o;
1146
+ }
1147
+ /**
1148
+ * Estimate image width based on height
1149
+ * Assumes landscape aspect ratio (approximately 1.4:1)
1150
+ * @param height - Image height
1151
+ * @returns Estimated width
1152
+ */
1153
+ estimateWidth(t) {
1154
+ return t * 1.4;
1155
+ }
1156
+ /**
1157
+ * Utility: Generate random number between min and max
1158
+ * @param min - Minimum value
1159
+ * @param max - Maximum value
1160
+ * @returns Random number in range
1161
+ */
1162
+ random(t, e) {
1163
+ return Math.random() * (e - t) + t;
1164
+ }
1165
+ }
1166
+ const he = {
1167
+ columns: "auto",
1168
+ rows: "auto",
1169
+ stagger: "none",
1170
+ jitter: 0,
1171
+ overlap: 0,
1172
+ fillDirection: "row",
1173
+ alignment: "center",
1174
+ gap: 10,
1175
+ overflowOffset: 0.25
1176
+ }, It = [
1177
+ { x: 1, y: 1 },
1178
+ // bottom-right
1179
+ { x: -1, y: -1 },
1180
+ // upper-left
1181
+ { x: 1, y: -1 },
1182
+ // upper-right
1183
+ { x: -1, y: 1 },
1184
+ // bottom-left
1185
+ { x: -1, y: 0 },
1186
+ // left
1187
+ { x: 1, y: 0 },
1188
+ // right
1189
+ { x: 0, y: -1 },
1190
+ // up
1191
+ { x: 0, y: 1 }
1192
+ // down
1193
+ ];
1194
+ class de {
1195
+ constructor(t, e = {}) {
1196
+ this.config = t, this.imageConfig = e;
1197
+ }
1198
+ /**
1199
+ * Generate grid layout positions for images
1200
+ * @param imageCount - Number of images to layout
1201
+ * @param containerBounds - Container dimensions {width, height}
1202
+ * @param options - Optional overrides (includes fixedHeight)
1203
+ * @returns Array of layout objects with position, rotation, scale
1204
+ */
1205
+ generate(t, e, i = {}) {
1206
+ const o = [], { width: s, height: r } = e, a = { ...he, ...this.config.grid }, l = this.config.spacing.padding, c = i.fixedHeight ?? 200, u = this.imageConfig.rotation?.mode ?? "none", h = this.imageConfig.sizing?.variance?.min ?? 1, d = this.imageConfig.sizing?.variance?.max ?? 1, f = h !== 1 || d !== 1, b = s - 2 * l, p = r - 2 * l, { columns: g, rows: m } = this.calculateGridDimensions(
1207
+ t,
1208
+ b,
1209
+ p,
1210
+ c,
1211
+ a
1212
+ ), E = a.stagger === "row", v = a.stagger === "column", w = E ? g + 0.5 : g, S = v ? m + 0.5 : m, x = (b - a.gap * (g - 1)) / w, T = (p - a.gap * (m - 1)) / S, M = E ? x / 2 : 0, z = v ? T / 2 : 0, P = 1 + a.overlap, _ = Math.min(x, T) * P, Y = i.fixedHeight ? Math.min(i.fixedHeight, _) : _, N = g * x + (g - 1) * a.gap + M, R = m * T + (m - 1) * a.gap + z, D = l + (b - N) / 2, k = l + (p - R) / 2, H = g * m, A = a.columns !== "auto" && a.rows !== "auto", C = A && t > H;
1213
+ typeof window < "u" && (window.__gridOverflowDebug = {
1214
+ gridConfigColumns: a.columns,
1215
+ gridConfigRows: a.rows,
1216
+ columns: g,
1217
+ rows: m,
1218
+ cellCount: H,
1219
+ hasFixedGrid: A,
1220
+ imageCount: t,
1221
+ isOverflowMode: C
1222
+ });
1223
+ const O = C ? new Array(H).fill(0) : [], $ = Math.min(x, T) * a.overflowOffset;
1224
+ for (let F = 0; F < t; F++) {
1225
+ let U, L, W = 0;
1226
+ if (C && F >= H) {
1227
+ const q = F - H, j = q % H;
1228
+ W = Math.floor(q / H) + 1, O[j]++, a.fillDirection === "row" ? (U = j % g, L = Math.floor(j / g)) : (L = j % m, U = Math.floor(j / m));
1229
+ } else
1230
+ a.fillDirection === "row" ? (U = F % g, L = Math.floor(F / g)) : (L = F % m, U = Math.floor(F / m));
1231
+ let G = D + U * (x + a.gap) + x / 2, X = k + L * (T + a.gap) + T / 2;
1232
+ if (a.stagger === "row" && L % 2 === 1 ? G += x / 2 : a.stagger === "column" && U % 2 === 1 && (X += T / 2), W > 0) {
1233
+ const q = (W - 1) % It.length, j = It[q];
1234
+ G += j.x * $, X += j.y * $;
1235
+ }
1236
+ if (a.jitter > 0) {
1237
+ const q = x / 2 * a.jitter, j = T / 2 * a.jitter;
1238
+ G += this.random(-q, q), X += this.random(-j, j);
1239
+ }
1240
+ let B = G, J = X;
1241
+ if (!C && a.fillDirection === "row") {
1242
+ const q = t % g || g;
1243
+ if (L === Math.floor((t - 1) / g) && q < g) {
1244
+ const vt = q * x + (q - 1) * a.gap;
1245
+ let gt = 0;
1246
+ a.alignment === "center" ? gt = (N - vt) / 2 : a.alignment === "end" && (gt = N - vt), B += gt;
1247
+ }
1248
+ }
1249
+ const rt = f ? this.random(h, d) : 1, K = Y * rt, it = K * 1.5 / 2, nt = K / 2, lt = l + it, ht = s - l - it, Ft = l + nt, Dt = r - l - nt;
1250
+ B = Math.max(lt, Math.min(B, ht)), J = Math.max(Ft, Math.min(J, Dt));
1251
+ let dt = 0;
1252
+ if (u === "random") {
1253
+ const q = this.imageConfig.rotation?.range?.min ?? -15, j = this.imageConfig.rotation?.range?.max ?? 15;
1254
+ a.jitter > 0 ? dt = this.random(q * a.jitter, j * a.jitter) : dt = this.random(q, j);
1255
+ }
1256
+ let ut;
1257
+ C && W > 0 ? ut = 50 - W : ut = C ? 100 + F : F + 1, o.push({
1258
+ id: F,
1259
+ x: B,
1260
+ y: J,
1261
+ rotation: dt,
1262
+ scale: rt,
1263
+ baseSize: K,
1264
+ zIndex: ut
1265
+ });
1266
+ }
1267
+ return o;
1268
+ }
1269
+ /**
1270
+ * Calculate optimal grid dimensions based on image count and container
1271
+ */
1272
+ calculateGridDimensions(t, e, i, o, s) {
1273
+ let r, a;
1274
+ if (s.columns !== "auto" && s.rows !== "auto")
1275
+ r = s.columns, a = s.rows;
1276
+ else if (s.columns !== "auto")
1277
+ r = s.columns, a = Math.ceil(t / r);
1278
+ else if (s.rows !== "auto")
1279
+ a = s.rows, r = Math.ceil(t / a);
1280
+ else {
1281
+ const l = e / i;
1282
+ for (r = Math.max(1, Math.round(Math.sqrt(t * l / 1.4))), a = Math.ceil(t / r); r > 1 && (r - 1) * a >= t; )
1283
+ r--;
1284
+ }
1285
+ return { columns: Math.max(1, r), rows: Math.max(1, a) };
1286
+ }
1287
+ /**
1288
+ * Utility: Generate random number between min and max
1289
+ */
1290
+ random(t, e) {
1291
+ return Math.random() * (e - t) + t;
1292
+ }
1293
+ }
1294
+ const ue = Math.PI * (3 - Math.sqrt(5)), ge = {
1295
+ spiralType: "golden",
1296
+ direction: "counterclockwise",
1297
+ tightness: 1,
1298
+ scaleDecay: 0,
1299
+ startAngle: 0
1300
+ };
1301
+ class fe {
1302
+ constructor(t, e = {}) {
1303
+ this.config = t, this.imageConfig = e;
1304
+ }
1305
+ /**
1306
+ * Generate spiral layout positions for images
1307
+ * @param imageCount - Number of images to layout
1308
+ * @param containerBounds - Container dimensions {width, height}
1309
+ * @param options - Optional overrides (includes fixedHeight)
1310
+ * @returns Array of layout objects with position, rotation, scale
1311
+ */
1312
+ generate(t, e, i = {}) {
1313
+ const o = [], { width: s, height: r } = e, a = { ...ge, ...this.config.spiral }, l = this.config.spacing.padding, c = i.fixedHeight ?? 200, u = this.imageConfig.rotation?.mode ?? "none", h = this.imageConfig.rotation?.range?.min ?? -15, d = this.imageConfig.rotation?.range?.max ?? 15, f = this.imageConfig.sizing?.variance?.min ?? 1, b = this.imageConfig.sizing?.variance?.max ?? 1, p = f !== 1 || b !== 1, g = this.config.scaleDecay ?? a.scaleDecay, m = s / 2, E = r / 2, v = Math.min(
1314
+ m - l - c / 2,
1315
+ E - l - c / 2
1316
+ ), w = a.direction === "clockwise" ? -1 : 1;
1317
+ for (let S = 0; S < t; S++) {
1318
+ let x, T;
1319
+ if (a.spiralType === "golden")
1320
+ x = S * ue * w + a.startAngle, T = this.calculateGoldenRadius(S, t, v, a.tightness);
1321
+ else if (a.spiralType === "archimedean") {
1322
+ const G = S * 0.5 * a.tightness;
1323
+ x = G * w + a.startAngle, T = this.calculateArchimedeanRadius(G, t, v, a.tightness);
1324
+ } else {
1325
+ const G = S * 0.3 * a.tightness;
1326
+ x = G * w + a.startAngle, T = this.calculateLogarithmicRadius(G, t, v, a.tightness);
1327
+ }
1328
+ const M = m + Math.cos(x) * T, z = E + Math.sin(x) * T, P = T / v, _ = g > 0 ? 1 - P * g * 0.5 : 1, Y = p ? this.random(f, b) : 1, N = _ * Y, R = c * N, k = R * 1.5 / 2, H = R / 2, A = l + k, C = s - l - k, O = l + H, $ = r - l - H, F = Math.max(A, Math.min(M, C)), U = Math.max(O, Math.min(z, $));
1329
+ let L = 0;
1330
+ if (u === "random") {
1331
+ const G = x * 180 / Math.PI % 360, X = this.random(h, d);
1332
+ L = a.spiralType === "golden" ? X : G * 0.1 + X * 0.9;
1333
+ } else u === "tangent" && (L = this.calculateSpiralTangent(x, T, a));
1334
+ const W = t - S;
1335
+ o.push({
1336
+ id: S,
1337
+ x: F,
1338
+ y: U,
1339
+ rotation: L,
1340
+ scale: N,
1341
+ baseSize: R,
1342
+ zIndex: W
1343
+ });
1344
+ }
1345
+ return o;
1346
+ }
1347
+ /**
1348
+ * Calculate tangent angle for spiral curve at given position
1349
+ * This aligns the image along the spiral's direction of travel
1350
+ */
1351
+ calculateSpiralTangent(t, e, i) {
1352
+ let o;
1353
+ if (i.spiralType === "golden")
1354
+ o = t + Math.PI / 2;
1355
+ else if (i.spiralType === "archimedean") {
1356
+ const r = 1 / i.tightness, a = Math.atan(e / r);
1357
+ o = t + a;
1358
+ } else {
1359
+ const r = 0.15 / i.tightness, a = Math.atan(1 / r);
1360
+ o = t + a;
1361
+ }
1362
+ return o * 180 / Math.PI % 360 - 90;
1363
+ }
1364
+ /**
1365
+ * Calculate radius for golden spiral (Vogel's model)
1366
+ * Creates even distribution like sunflower seeds
1367
+ */
1368
+ calculateGoldenRadius(t, e, i, o) {
1369
+ const r = i / Math.sqrt(e) * Math.sqrt(t) / o;
1370
+ return Math.min(r, i);
1371
+ }
1372
+ /**
1373
+ * Calculate radius for Archimedean spiral
1374
+ * r = a + b*θ (constant spacing between arms)
1375
+ */
1376
+ calculateArchimedeanRadius(t, e, i, o) {
1377
+ const s = e * 0.5 * o;
1378
+ return t / s * i;
1379
+ }
1380
+ /**
1381
+ * Calculate radius for logarithmic (equiangular) spiral
1382
+ * r = a * e^(b*θ)
1383
+ */
1384
+ calculateLogarithmicRadius(t, e, i, o) {
1385
+ const s = i * 0.05, r = 0.15 / o, a = s * Math.exp(r * t), l = e * 0.3 * o, c = s * Math.exp(r * l);
1386
+ return a / c * i;
1387
+ }
1388
+ /**
1389
+ * Utility: Generate random number between min and max
1390
+ */
1391
+ random(t, e) {
1392
+ return Math.random() * (e - t) + t;
1393
+ }
1394
+ }
1395
+ const me = {
1396
+ clusterCount: "auto",
1397
+ clusterSpread: 150,
1398
+ clusterSpacing: 200,
1399
+ density: "uniform",
1400
+ overlap: 0.3,
1401
+ distribution: "gaussian"
1402
+ };
1403
+ class pe {
1404
+ constructor(t, e = {}) {
1405
+ this.config = t, this.imageConfig = e;
1406
+ }
1407
+ /**
1408
+ * Generate cluster layout positions for images
1409
+ * @param imageCount - Number of images to layout
1410
+ * @param containerBounds - Container dimensions {width, height}
1411
+ * @param options - Optional overrides (includes fixedHeight)
1412
+ * @returns Array of layout objects with position, rotation, scale
1413
+ */
1414
+ generate(t, e, i = {}) {
1415
+ const o = [], { width: s, height: r } = e, a = { ...me, ...this.config.cluster }, l = this.config.spacing.padding, c = i.fixedHeight ?? 200, u = this.imageConfig.rotation?.mode ?? "none", h = this.imageConfig.rotation?.range?.min ?? -15, d = this.imageConfig.rotation?.range?.max ?? 15, f = this.imageConfig.sizing?.variance?.min ?? 1, b = this.imageConfig.sizing?.variance?.max ?? 1, p = f !== 1 || b !== 1, g = this.calculateClusterCount(
1416
+ t,
1417
+ a.clusterCount,
1418
+ s,
1419
+ r,
1420
+ a.clusterSpacing
1421
+ ), m = this.generateClusterCenters(
1422
+ g,
1423
+ s,
1424
+ r,
1425
+ l,
1426
+ a
1427
+ ), E = new Array(g).fill(0);
1428
+ for (let w = 0; w < t; w++)
1429
+ E[w % g]++;
1430
+ let v = 0;
1431
+ for (let w = 0; w < g; w++) {
1432
+ const S = m[w], x = E[w];
1433
+ for (let T = 0; T < x; T++) {
1434
+ let M, z;
1435
+ if (a.distribution === "gaussian")
1436
+ M = this.gaussianRandom() * S.spread, z = this.gaussianRandom() * S.spread;
1437
+ else {
1438
+ const L = this.random(0, Math.PI * 2), W = this.random(0, S.spread);
1439
+ M = Math.cos(L) * W, z = Math.sin(L) * W;
1440
+ }
1441
+ const P = 1 + a.overlap * 0.5, _ = 1 + a.overlap * 0.3;
1442
+ M /= P, z /= P;
1443
+ const Y = p ? this.random(f, b) : 1, N = _ * Y, R = c * N;
1444
+ let D = S.x + M, k = S.y + z;
1445
+ const A = R * 1.5 / 2, C = R / 2;
1446
+ D = Math.max(l + A, Math.min(D, s - l - A)), k = Math.max(l + C, Math.min(k, r - l - C));
1447
+ const O = u === "random" ? this.random(h, d) : 0, F = Math.sqrt(M * M + z * z) / S.spread, U = Math.round((1 - F) * 50) + 1;
1448
+ o.push({
1449
+ id: v,
1450
+ x: D,
1451
+ y: k,
1452
+ rotation: O,
1453
+ scale: N,
1454
+ baseSize: R,
1455
+ zIndex: U
1456
+ }), v++;
1457
+ }
1458
+ }
1459
+ return o;
1460
+ }
1461
+ /**
1462
+ * Calculate optimal number of clusters based on image count and container
1463
+ */
1464
+ calculateClusterCount(t, e, i, o, s) {
1465
+ if (e !== "auto")
1466
+ return Math.max(1, Math.min(e, t));
1467
+ const a = Math.max(1, Math.ceil(t / 8)), l = Math.floor(
1468
+ i / s * (o / s) * 0.6
1469
+ );
1470
+ return Math.max(1, Math.min(a, l, 10));
1471
+ }
1472
+ /**
1473
+ * Generate cluster center positions with spacing constraints
1474
+ */
1475
+ generateClusterCenters(t, e, i, o, s) {
1476
+ const r = [], l = o + s.clusterSpread, c = e - o - s.clusterSpread, u = o + s.clusterSpread, h = i - o - s.clusterSpread;
1477
+ for (let d = 0; d < t; d++) {
1478
+ let f = null, b = -1;
1479
+ for (let p = 0; p < 100; p++) {
1480
+ const g = {
1481
+ x: this.random(l, c),
1482
+ y: this.random(u, h),
1483
+ spread: this.calculateClusterSpread(s)
1484
+ };
1485
+ let m = 1 / 0;
1486
+ for (const E of r) {
1487
+ const v = g.x - E.x, w = g.y - E.y, S = Math.sqrt(v * v + w * w);
1488
+ m = Math.min(m, S);
1489
+ }
1490
+ if ((r.length === 0 || m > b) && (f = g, b = m), m >= s.clusterSpacing)
1491
+ break;
1492
+ }
1493
+ f && r.push(f);
1494
+ }
1495
+ return r;
1496
+ }
1497
+ /**
1498
+ * Calculate spread for a cluster (may vary if density='varied')
1499
+ */
1500
+ calculateClusterSpread(t) {
1501
+ return t.density === "uniform" ? t.clusterSpread : t.clusterSpread * this.random(0.5, 1.5);
1502
+ }
1503
+ /**
1504
+ * Generate a random number with approximately Gaussian distribution
1505
+ * Using Box-Muller transform
1506
+ */
1507
+ gaussianRandom() {
1508
+ let t = 0, e = 0;
1509
+ for (; t === 0; ) t = Math.random();
1510
+ for (; e === 0; ) e = Math.random();
1511
+ const i = Math.sqrt(-2 * Math.log(t)) * Math.cos(2 * Math.PI * e);
1512
+ return Math.max(-3, Math.min(3, i)) / 3;
1513
+ }
1514
+ /**
1515
+ * Utility: Generate random number between min and max
1516
+ */
1517
+ random(t, e) {
1518
+ return Math.random() * (e - t) + t;
1519
+ }
1520
+ }
1521
+ class be {
1522
+ constructor(t, e = {}) {
1523
+ this.config = t, this.imageConfig = e;
1524
+ }
1525
+ /**
1526
+ * Generate wave layout positions for images
1527
+ * @param imageCount - Number of images to layout
1528
+ * @param containerBounds - Container dimensions {width, height}
1529
+ * @param options - Optional overrides
1530
+ * @returns Array of layout objects with position, rotation, scale
1531
+ */
1532
+ generate(t, e, i = {}) {
1533
+ const o = [], { width: s, height: r } = e, a = i.fixedHeight ?? 200, l = this.config.spacing.padding ?? 50, c = this.imageConfig.rotation?.mode ?? "none", u = this.imageConfig.rotation?.range?.min ?? -15, h = this.imageConfig.rotation?.range?.max ?? 15, d = this.imageConfig.sizing?.variance?.min ?? 1, f = this.imageConfig.sizing?.variance?.max ?? 1, b = d !== 1 || f !== 1, p = i.fixedHeight ?? a, g = {
1534
+ ..._t,
1535
+ ...this.config.wave
1536
+ }, { rows: m, amplitude: E, frequency: v, phaseShift: w, synchronization: S } = g, x = Math.ceil(t / m), z = p * 1.5 / 2, P = l + z, _ = s - l - z, Y = _ - P, N = x > 1 ? Y / (x - 1) : 0, R = l + E + p / 2, D = r - l - E - p / 2, k = D - R, H = m > 1 ? k / (m - 1) : 0;
1537
+ let A = 0;
1538
+ for (let C = 0; C < m && A < t; C++) {
1539
+ const O = m === 1 ? (R + D) / 2 : R + C * H;
1540
+ let $ = 0;
1541
+ S === "offset" ? $ = C * w : S === "alternating" && ($ = C * Math.PI);
1542
+ for (let F = 0; F < x && A < t; F++) {
1543
+ const U = x === 1 ? (P + _) / 2 : P + F * N, L = this.calculateWaveY(U, s, E, v, $), W = U, G = O + L, X = b ? this.random(d, f) : 1, B = p * X;
1544
+ let J = 0;
1545
+ c === "tangent" ? J = this.calculateRotation(U, s, E, v, $) : c === "random" && (J = this.random(u, h));
1546
+ const K = B * 1.5 / 2, ct = B / 2, it = l + K, nt = s - l - K, lt = l + ct, ht = r - l - ct;
1547
+ o.push({
1548
+ id: A,
1549
+ x: Math.max(it, Math.min(W, nt)),
1550
+ y: Math.max(lt, Math.min(G, ht)),
1551
+ rotation: J,
1552
+ scale: X,
1553
+ baseSize: B,
1554
+ zIndex: A + 1
1555
+ }), A++;
1556
+ }
1557
+ }
1558
+ return o;
1559
+ }
1560
+ /**
1561
+ * Calculate Y position displacement on wave curve
1562
+ * @param x - Horizontal position
1563
+ * @param containerWidth - Container width
1564
+ * @param amplitude - Wave amplitude
1565
+ * @param frequency - Wave frequency
1566
+ * @param phase - Phase offset
1567
+ * @returns Y displacement from baseline
1568
+ */
1569
+ calculateWaveY(t, e, i, o, s) {
1570
+ const r = t / e;
1571
+ return i * Math.sin(o * r * 2 * Math.PI + s);
1572
+ }
1573
+ /**
1574
+ * Calculate rotation based on wave tangent
1575
+ * @param x - Horizontal position
1576
+ * @param containerWidth - Container width
1577
+ * @param amplitude - Wave amplitude
1578
+ * @param frequency - Wave frequency
1579
+ * @param phase - Phase offset
1580
+ * @returns Rotation angle in degrees
1581
+ */
1582
+ calculateRotation(t, e, i, o, s) {
1583
+ const r = t / e, a = i * o * 2 * Math.PI * Math.cos(o * r * 2 * Math.PI + s) / e;
1584
+ return Math.atan(a) * (180 / Math.PI);
1585
+ }
1586
+ /**
1587
+ * Estimate image width based on height
1588
+ /**
1589
+ * Utility: Generate random number between min and max
1590
+ * @param min - Minimum value
1591
+ * @param max - Maximum value
1592
+ * @returns Random number in range
1593
+ */
1594
+ random(t, e) {
1595
+ return Math.random() * (e - t) + t;
1596
+ }
1597
+ }
1598
+ class ye {
1599
+ constructor(t) {
1600
+ this.config = t.layout, this.imageConfig = t.image, this.layouts = /* @__PURE__ */ new Map(), this.placementLayout = this.initLayout();
1601
+ }
1602
+ /**
1603
+ * Initialize the appropriate placement layout based on config type
1604
+ * @returns Initialized placement layout
1605
+ */
1606
+ initLayout() {
1607
+ switch (this.config.algorithm) {
1608
+ case "radial":
1609
+ return new le(this.config, this.imageConfig);
1610
+ case "grid":
1611
+ return new de(this.config, this.imageConfig);
1612
+ case "spiral":
1613
+ return new fe(this.config, this.imageConfig);
1614
+ case "cluster":
1615
+ return new pe(this.config, this.imageConfig);
1616
+ case "wave":
1617
+ return new be(this.config, this.imageConfig);
1618
+ default:
1619
+ return new ce(this.config, this.imageConfig);
1620
+ }
1621
+ }
1622
+ /**
1623
+ * Generate layout positions for images
1624
+ * @param imageCount - Number of images to layout
1625
+ * @param containerBounds - Container dimensions {width, height}
1626
+ * @param options - Optional overrides for configuration (e.g. fixedHeight)
1627
+ * @returns Array of layout objects with position, rotation, scale
1628
+ */
1629
+ generateLayout(t, e, i = {}) {
1630
+ const o = this.placementLayout.generate(t, e, i);
1631
+ return o.forEach((s) => {
1632
+ this.layouts.set(s.id, s);
1633
+ }), o;
1634
+ }
1635
+ /**
1636
+ * Get the original layout state for an image
1637
+ * @param imageId - The image ID (number or string)
1638
+ * @returns Original layout state or undefined if not found
1639
+ */
1640
+ getOriginalState(t) {
1641
+ return this.layouts.get(Number(t));
1642
+ }
1643
+ /**
1644
+ * Reset all stored layouts
1645
+ */
1646
+ reset() {
1647
+ this.layouts.clear();
1648
+ }
1649
+ /**
1650
+ * Update config dynamically (useful for responsive changes)
1651
+ * @param newConfig - Updated configuration
1652
+ */
1653
+ updateConfig(t) {
1654
+ t.layout && (Object.assign(this.config, t.layout), t.layout.algorithm && t.layout.algorithm !== this.config.algorithm && (this.placementLayout = this.initLayout())), t.image && Object.assign(this.imageConfig, t.image);
1655
+ }
1656
+ /**
1657
+ * Get responsive breakpoints from layout config
1658
+ */
1659
+ getBreakpoints() {
1660
+ return this.config.responsive ?? {
1661
+ mobile: { maxWidth: 767 },
1662
+ tablet: { maxWidth: 1199 }
1663
+ };
1664
+ }
1665
+ /**
1666
+ * Resolve breakpoint name based on viewport width
1667
+ */
1668
+ resolveBreakpoint(t) {
1669
+ const e = this.getBreakpoints();
1670
+ return t <= e.mobile.maxWidth ? "mobile" : t <= e.tablet.maxWidth ? "tablet" : "screen";
1671
+ }
1672
+ /**
1673
+ * Resolve the effective base height based on image config and current viewport
1674
+ * @param viewportWidth - Current viewport width
1675
+ * @returns Resolved base height or undefined if should auto-calculate (adaptive mode)
1676
+ */
1677
+ resolveBaseHeight(t) {
1678
+ const e = this.imageConfig.sizing;
1679
+ if (!e || e.mode === "adaptive")
1680
+ return;
1681
+ const i = e.height;
1682
+ if (i === void 0)
1683
+ return;
1684
+ if (typeof i == "number")
1685
+ return i;
1686
+ const o = i, s = this.resolveBreakpoint(t);
1687
+ return s === "mobile" ? o.mobile ?? o.tablet ?? o.screen : s === "tablet" ? o.tablet ?? o.screen ?? o.mobile : o.screen ?? o.tablet ?? o.mobile;
1688
+ }
1689
+ /**
1690
+ * Calculate adaptive image size based on container dimensions and image count
1691
+ * @param containerBounds - Container dimensions {width, height}
1692
+ * @param imageCount - Number of images to display
1693
+ * @param maxHeight - Maximum height constraint (upper bound)
1694
+ * @param viewportWidth - Current viewport width for baseHeight resolution
1695
+ * @returns Calculated sizing result with height
1696
+ */
1697
+ calculateAdaptiveSize(t, e, i, o) {
1698
+ const s = this.imageConfig.sizing, r = this.resolveBaseHeight(o);
1699
+ if (r !== void 0)
1700
+ return { height: r };
1701
+ const a = s?.minSize ?? 50, l = s?.maxSize ?? 400, c = this.config.targetCoverage ?? 0.6, u = this.config.densityFactor ?? 1, { width: h, height: d } = t, p = h * d * c / e;
1702
+ let m = Math.sqrt(p / 1.4);
1703
+ m *= u, m = Math.min(m, i);
1704
+ let E = this.clamp(m, a, l);
1705
+ if (E === a && m < a) {
1706
+ const v = Math.max(a * 0.05, 20);
1707
+ E = Math.max(v, m);
1708
+ }
1709
+ return { height: E };
1710
+ }
1711
+ /**
1712
+ * Utility: Clamp a value between min and max
1713
+ */
1714
+ clamp(t, e, i) {
1715
+ return Math.max(e, Math.min(i, t));
1716
+ }
1717
+ }
1718
+ var I = /* @__PURE__ */ ((n) => (n.IDLE = "idle", n.FOCUSING = "focusing", n.FOCUSED = "focused", n.UNFOCUSING = "unfocusing", n.CROSS_ANIMATING = "cross_animating", n))(I || {});
1719
+ function ve(n) {
1720
+ return n in ft;
1721
+ }
1722
+ function we(n) {
1723
+ return n ? ve(n) ? ft[n] : n : ft.md;
1724
+ }
1725
+ function xe(n) {
1726
+ if (!n) return "";
1727
+ const t = [];
1728
+ if (n.grayscale !== void 0 && t.push(`grayscale(${n.grayscale})`), n.blur !== void 0 && t.push(`blur(${n.blur}px)`), n.brightness !== void 0 && t.push(`brightness(${n.brightness})`), n.contrast !== void 0 && t.push(`contrast(${n.contrast})`), n.saturate !== void 0 && t.push(`saturate(${n.saturate})`), n.opacity !== void 0 && t.push(`opacity(${n.opacity})`), n.sepia !== void 0 && t.push(`sepia(${n.sepia})`), n.hueRotate !== void 0 && t.push(`hue-rotate(${n.hueRotate}deg)`), n.invert !== void 0 && t.push(`invert(${n.invert})`), n.dropShadow !== void 0)
1729
+ if (typeof n.dropShadow == "string")
1730
+ t.push(`drop-shadow(${n.dropShadow})`);
1731
+ else {
1732
+ const e = n.dropShadow;
1733
+ t.push(`drop-shadow(${e.x}px ${e.y}px ${e.blur}px ${e.color})`);
1734
+ }
1735
+ return t.join(" ");
1736
+ }
1737
+ function Q(n) {
1738
+ if (!n || n.style === "none" || n.width === 0)
1739
+ return "none";
1740
+ const t = n.width ?? 0, e = n.style ?? "solid", i = n.color ?? "#000000";
1741
+ return `${t}px ${e} ${i}`;
1742
+ }
1743
+ function st(n) {
1744
+ if (!n) return {};
1745
+ const t = {};
1746
+ if (n.borderRadiusTopLeft !== void 0 || n.borderRadiusTopRight !== void 0 || n.borderRadiusBottomRight !== void 0 || n.borderRadiusBottomLeft !== void 0) {
1747
+ const s = n.border?.radius ?? 0;
1748
+ n.borderRadiusTopLeft !== void 0 ? t.borderTopLeftRadius = `${n.borderRadiusTopLeft}px` : s && (t.borderTopLeftRadius = `${s}px`), n.borderRadiusTopRight !== void 0 ? t.borderTopRightRadius = `${n.borderRadiusTopRight}px` : s && (t.borderTopRightRadius = `${s}px`), n.borderRadiusBottomRight !== void 0 ? t.borderBottomRightRadius = `${n.borderRadiusBottomRight}px` : s && (t.borderBottomRightRadius = `${s}px`), n.borderRadiusBottomLeft !== void 0 ? t.borderBottomLeftRadius = `${n.borderRadiusBottomLeft}px` : s && (t.borderBottomLeftRadius = `${s}px`);
1749
+ } else n.border?.radius !== void 0 && (t.borderRadius = `${n.border.radius}px`);
1750
+ if (n.borderTop || n.borderRight || n.borderBottom || n.borderLeft) {
1751
+ const s = n.border || {}, r = { ...s, ...n.borderTop }, a = { ...s, ...n.borderRight }, l = { ...s, ...n.borderBottom }, c = { ...s, ...n.borderLeft };
1752
+ t.borderTop = Q(r), t.borderRight = Q(a), t.borderBottom = Q(l), t.borderLeft = Q(c);
1753
+ } else n.border && (t.border = Q(n.border));
1754
+ n.shadow !== void 0 && (t.boxShadow = we(n.shadow));
1755
+ const o = xe(n.filter);
1756
+ if (t.filter = o || "none", n.opacity !== void 0 && (t.opacity = String(n.opacity)), n.cursor !== void 0 && (t.cursor = n.cursor), n.outline && n.outline.style !== "none" && (n.outline.width ?? 0) > 0) {
1757
+ const s = n.outline.width ?? 0, r = n.outline.style ?? "solid", a = n.outline.color ?? "#000000";
1758
+ t.outline = `${s}px ${r} ${a}`, n.outline.offset !== void 0 && (t.outlineOffset = `${n.outline.offset}px`);
1759
+ }
1760
+ return n.objectFit !== void 0 && (t.objectFit = n.objectFit), n.aspectRatio !== void 0 && (t.aspectRatio = n.aspectRatio), t;
1761
+ }
1762
+ function tt(n, t) {
1763
+ t.borderRadius !== void 0 && (n.style.borderRadius = t.borderRadius), t.borderTopLeftRadius !== void 0 && (n.style.borderTopLeftRadius = t.borderTopLeftRadius), t.borderTopRightRadius !== void 0 && (n.style.borderTopRightRadius = t.borderTopRightRadius), t.borderBottomRightRadius !== void 0 && (n.style.borderBottomRightRadius = t.borderBottomRightRadius), t.borderBottomLeftRadius !== void 0 && (n.style.borderBottomLeftRadius = t.borderBottomLeftRadius), t.border !== void 0 && (n.style.border = t.border), t.borderTop !== void 0 && (n.style.borderTop = t.borderTop), t.borderRight !== void 0 && (n.style.borderRight = t.borderRight), t.borderBottom !== void 0 && (n.style.borderBottom = t.borderBottom), t.borderLeft !== void 0 && (n.style.borderLeft = t.borderLeft), t.boxShadow !== void 0 && (n.style.boxShadow = t.boxShadow), t.filter !== void 0 && (n.style.filter = t.filter), t.opacity !== void 0 && (n.style.opacity = t.opacity), t.cursor !== void 0 && (n.style.cursor = t.cursor), t.outline !== void 0 && (n.style.outline = t.outline), t.outlineOffset !== void 0 && (n.style.outlineOffset = t.outlineOffset), t.objectFit !== void 0 && (n.style.objectFit = t.objectFit), t.aspectRatio !== void 0 && (n.style.aspectRatio = t.aspectRatio);
1764
+ }
1765
+ function Mt(n) {
1766
+ return n ? Array.isArray(n) ? n.join(" ") : n : "";
1767
+ }
1768
+ function et(n, t) {
1769
+ const e = Mt(t);
1770
+ e && e.split(" ").forEach((i) => {
1771
+ i.trim() && n.classList.add(i.trim());
1772
+ });
1773
+ }
1774
+ function zt(n, t) {
1775
+ const e = Mt(t);
1776
+ e && e.split(" ").forEach((i) => {
1777
+ i.trim() && n.classList.remove(i.trim());
1778
+ });
1779
+ }
1780
+ const Tt = {
1781
+ UNFOCUSING: 999,
1782
+ FOCUSING: 1e3
1783
+ };
1784
+ class Ee {
1785
+ constructor(t, e, i) {
1786
+ this.state = I.IDLE, this.currentFocus = null, this.focusData = null, this.outgoing = null, this.incoming = null, this.focusGeneration = 0, this.config = t, this.animationEngine = e, this.defaultStyles = st(i?.default), this.focusedStyles = st(i?.focused), this.defaultClassName = i?.default?.className, this.focusedClassName = i?.focused?.className;
1787
+ }
1788
+ /**
1789
+ * Get current state machine state
1790
+ */
1791
+ getState() {
1792
+ return this.state;
1793
+ }
1794
+ /**
1795
+ * Check if any animation is in progress
1796
+ */
1797
+ isAnimating() {
1798
+ return this.state !== I.IDLE && this.state !== I.FOCUSED;
1799
+ }
1800
+ /**
1801
+ * Normalize scalePercent value
1802
+ */
1803
+ normalizeScalePercent(t) {
1804
+ return t > 1 ? t / 100 : t;
1805
+ }
1806
+ /**
1807
+ * Calculate target dimensions for focused image
1808
+ * Returns actual pixel dimensions instead of scale factor for sharper rendering
1809
+ */
1810
+ calculateFocusDimensions(t, e, i) {
1811
+ const o = this.normalizeScalePercent(this.config.scalePercent), s = i.height * o, r = t / e;
1812
+ let a = s, l = a * r;
1813
+ const c = i.width * o;
1814
+ return l > c && (l = c, a = l / r), { width: l, height: a };
1815
+ }
1816
+ /**
1817
+ * Calculate the transform needed to center an image (position only, no scale)
1818
+ * Scale is handled by animating actual dimensions for sharper rendering
1819
+ */
1820
+ calculateFocusTransform(t, e) {
1821
+ const i = t.width / 2, o = t.height / 2, s = i - e.x, r = o - e.y;
1822
+ return {
1823
+ x: s,
1824
+ y: r,
1825
+ rotation: 0,
1826
+ scale: 1
1827
+ // No scale transform - dimensions are animated instead
1828
+ };
1829
+ }
1830
+ /**
1831
+ * Build transform string for dimension-based zoom (no scale in transform)
1832
+ */
1833
+ buildDimensionZoomTransform(t) {
1834
+ const e = ["translate(-50%, -50%)"];
1835
+ if (t.x !== void 0 || t.y !== void 0) {
1836
+ const i = t.x ?? 0, o = t.y ?? 0;
1837
+ e.push(`translate(${i}px, ${o}px)`);
1838
+ }
1839
+ return t.rotation !== void 0 && e.push(`rotate(${t.rotation}deg)`), e.join(" ");
1840
+ }
1841
+ /**
1842
+ * Create a Web Animation that animates both transform (position) and dimensions
1843
+ * This provides sharper zoom by re-rendering at target size instead of scaling pixels
1844
+ */
1845
+ animateWithDimensions(t, e, i, o, s, r, a, l) {
1846
+ const c = this.buildDimensionZoomTransform(e), u = this.buildDimensionZoomTransform(i);
1847
+ return t.style.transition = "none", t.animate(
1848
+ [
1849
+ {
1850
+ transform: c,
1851
+ width: `${o}px`,
1852
+ height: `${s}px`
1853
+ },
1854
+ {
1855
+ transform: u,
1856
+ width: `${r}px`,
1857
+ height: `${a}px`
1858
+ }
1859
+ ],
1860
+ {
1861
+ duration: l,
1862
+ easing: "cubic-bezier(0.4, 0, 0.2, 1)",
1863
+ fill: "forwards"
1864
+ }
1865
+ );
1866
+ }
1867
+ /**
1868
+ * Apply focused styling to an element
1869
+ */
1870
+ applyFocusedStyling(t, e) {
1871
+ t.style.zIndex = String(e), t.classList.add("fbn-ic-focused"), tt(t, this.focusedStyles), et(t, this.focusedClassName);
1872
+ }
1873
+ /**
1874
+ * Remove focused styling from an element
1875
+ */
1876
+ removeFocusedStyling(t, e) {
1877
+ t.style.zIndex = e, t.classList.remove("fbn-ic-focused"), zt(t, this.focusedClassName), tt(t, this.defaultStyles), et(t, this.defaultClassName);
1878
+ }
1879
+ /**
1880
+ * Start focus animation for an image using dimension-based zoom
1881
+ * Animates actual width/height for sharper rendering instead of transform scale
1882
+ * @param fromTransform - Optional starting transform (for mid-animation reversals)
1883
+ * @param fromDimensions - Optional starting dimensions (for mid-animation reversals)
1884
+ */
1885
+ startFocusAnimation(t, e, i, o, s) {
1886
+ const r = t.style.zIndex || "", a = t.offsetWidth, l = t.offsetHeight, c = this.calculateFocusDimensions(a, l, e), u = this.calculateFocusTransform(e, i);
1887
+ this.applyFocusedStyling(t, Tt.FOCUSING), this.animationEngine.cancelAllAnimations(t);
1888
+ const h = o ?? {
1889
+ x: 0,
1890
+ y: 0,
1891
+ rotation: i.rotation,
1892
+ scale: 1
1893
+ // No scale - using dimensions
1894
+ }, d = s?.width ?? a, f = s?.height ?? l, b = this.config.animationDuration ?? 600, p = this.animateWithDimensions(
1895
+ t,
1896
+ h,
1897
+ u,
1898
+ d,
1899
+ f,
1900
+ c.width,
1901
+ c.height,
1902
+ b
1903
+ ), g = {
1904
+ id: `focus-${Date.now()}`,
1905
+ element: t,
1906
+ animation: p,
1907
+ fromState: h,
1908
+ toState: u,
1909
+ startTime: performance.now(),
1910
+ duration: b
1911
+ };
1912
+ return this.focusData = {
1913
+ element: t,
1914
+ originalState: i,
1915
+ focusTransform: u,
1916
+ originalZIndex: r,
1917
+ originalWidth: a,
1918
+ originalHeight: l,
1919
+ focusWidth: c.width,
1920
+ focusHeight: c.height
1921
+ }, {
1922
+ element: t,
1923
+ originalState: i,
1924
+ animationHandle: g,
1925
+ direction: "in",
1926
+ originalWidth: a,
1927
+ originalHeight: l
1928
+ };
1929
+ }
1930
+ /**
1931
+ * Start unfocus animation for an image using dimension-based zoom
1932
+ * Animates back to original dimensions for consistent behavior
1933
+ * @param fromDimensions - Optional starting dimensions (for mid-animation reversals)
1934
+ */
1935
+ startUnfocusAnimation(t, e, i, o) {
1936
+ t.style.zIndex = String(Tt.UNFOCUSING), this.animationEngine.cancelAllAnimations(t);
1937
+ const s = i ?? this.focusData?.focusTransform ?? { x: 0, y: 0, rotation: 0, scale: 1 }, r = o?.width ?? this.focusData?.focusWidth ?? t.offsetWidth, a = o?.height ?? this.focusData?.focusHeight ?? t.offsetHeight, l = {
1938
+ x: 0,
1939
+ y: 0,
1940
+ rotation: e.rotation,
1941
+ scale: 1
1942
+ // No scale - using dimensions
1943
+ }, c = this.focusData?.originalWidth ?? t.offsetWidth, u = this.focusData?.originalHeight ?? t.offsetHeight, h = this.config.animationDuration ?? 600, d = this.animateWithDimensions(
1944
+ t,
1945
+ s,
1946
+ l,
1947
+ r,
1948
+ a,
1949
+ c,
1950
+ u,
1951
+ h
1952
+ ), f = {
1953
+ id: `unfocus-${Date.now()}`,
1954
+ element: t,
1955
+ animation: d,
1956
+ fromState: s,
1957
+ toState: l,
1958
+ startTime: performance.now(),
1959
+ duration: h
1960
+ };
1961
+ return {
1962
+ element: t,
1963
+ originalState: e,
1964
+ animationHandle: f,
1965
+ direction: "out",
1966
+ originalWidth: c,
1967
+ originalHeight: u
1968
+ };
1969
+ }
1970
+ /**
1971
+ * Handle animation completion
1972
+ */
1973
+ async waitForAnimation(t) {
1974
+ try {
1975
+ await t.animation.finished;
1976
+ } catch {
1977
+ }
1978
+ }
1979
+ /**
1980
+ * Reset an element instantly to its original position and dimensions (no animation)
1981
+ */
1982
+ resetElementInstantly(t, e, i, o, s) {
1983
+ this.animationEngine.cancelAllAnimations(t);
1984
+ const r = ["translate(-50%, -50%)"];
1985
+ r.push("translate(0px, 0px)"), r.push(`rotate(${e.rotation}deg)`), t.style.transition = "none", t.style.transform = r.join(" "), o !== void 0 && s !== void 0 && (t.style.width = `${o}px`, t.style.height = `${s}px`), this.removeFocusedStyling(t, i);
1986
+ }
1987
+ /**
1988
+ * Focus (zoom) an image to center of container
1989
+ * Implements cross-animation when swapping focus
1990
+ */
1991
+ async focusImage(t, e, i) {
1992
+ if (this.currentFocus === t && this.state === I.FOCUSED)
1993
+ return this.unfocusImage();
1994
+ if (this.incoming?.element === t && this.state === I.FOCUSING) {
1995
+ const s = this.animationEngine.cancelAnimation(this.incoming.animationHandle, !0), r = {
1996
+ x: s.x,
1997
+ y: s.y,
1998
+ rotation: s.rotation,
1999
+ scale: 1
2000
+ // No scale transform - using dimensions
2001
+ }, a = {
2002
+ width: t.offsetWidth,
2003
+ height: t.offsetHeight
2004
+ };
2005
+ this.outgoing = this.startUnfocusAnimation(
2006
+ t,
2007
+ this.incoming.originalState,
2008
+ r,
2009
+ a
2010
+ ), this.incoming = null, this.state = I.UNFOCUSING, await this.waitForAnimation(this.outgoing.animationHandle), this.removeFocusedStyling(this.outgoing.element, this.focusData?.originalZIndex || ""), this.outgoing = null, this.currentFocus = null, this.focusData = null, this.state = I.IDLE;
2011
+ return;
2012
+ }
2013
+ const o = ++this.focusGeneration;
2014
+ switch (this.state) {
2015
+ case I.IDLE:
2016
+ if (this.state = I.FOCUSING, this.incoming = this.startFocusAnimation(t, e, i), await this.waitForAnimation(this.incoming.animationHandle), this.focusGeneration !== o) return;
2017
+ this.currentFocus = t, this.incoming = null, this.state = I.FOCUSED;
2018
+ break;
2019
+ case I.FOCUSED:
2020
+ if (this.state = I.CROSS_ANIMATING, this.currentFocus && this.focusData && (this.outgoing = this.startUnfocusAnimation(
2021
+ this.currentFocus,
2022
+ this.focusData.originalState
2023
+ )), this.incoming = this.startFocusAnimation(t, e, i), await Promise.all([
2024
+ this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),
2025
+ this.waitForAnimation(this.incoming.animationHandle)
2026
+ ]), this.focusGeneration !== o)
2027
+ return;
2028
+ this.outgoing && (this.removeFocusedStyling(this.outgoing.element, this.outgoing.originalState.zIndex?.toString() || ""), this.outgoing = null), this.currentFocus = t, this.incoming = null, this.state = I.FOCUSED;
2029
+ break;
2030
+ case I.FOCUSING:
2031
+ if (this.incoming && (this.animationEngine.cancelAnimation(this.incoming.animationHandle, !1), this.resetElementInstantly(
2032
+ this.incoming.element,
2033
+ this.incoming.originalState,
2034
+ this.focusData?.originalZIndex || "",
2035
+ this.focusData?.originalWidth,
2036
+ this.focusData?.originalHeight
2037
+ ), this.incoming = null), this.incoming = this.startFocusAnimation(t, e, i), await this.waitForAnimation(this.incoming.animationHandle), this.focusGeneration !== o) return;
2038
+ this.currentFocus = t, this.incoming = null, this.state = I.FOCUSED;
2039
+ break;
2040
+ case I.UNFOCUSING:
2041
+ if (this.state = I.CROSS_ANIMATING, this.incoming = this.startFocusAnimation(t, e, i), await Promise.all([
2042
+ this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),
2043
+ this.waitForAnimation(this.incoming.animationHandle)
2044
+ ]), this.focusGeneration !== o) return;
2045
+ this.outgoing && (this.removeFocusedStyling(this.outgoing.element, this.outgoing.originalState.zIndex?.toString() || ""), this.outgoing = null), this.currentFocus = t, this.incoming = null, this.state = I.FOCUSED;
2046
+ break;
2047
+ case I.CROSS_ANIMATING:
2048
+ if (this.incoming?.element === t)
2049
+ return;
2050
+ if (this.outgoing?.element === t) {
2051
+ const s = this.animationEngine.cancelAnimation(this.outgoing.animationHandle, !0), r = {
2052
+ x: s.x,
2053
+ y: s.y,
2054
+ rotation: s.rotation,
2055
+ scale: 1
2056
+ // No scale - using dimensions
2057
+ }, a = {
2058
+ width: t.offsetWidth,
2059
+ height: t.offsetHeight
2060
+ };
2061
+ if (this.incoming) {
2062
+ const l = this.animationEngine.cancelAnimation(this.incoming.animationHandle, !0), c = {
2063
+ x: l.x,
2064
+ y: l.y,
2065
+ rotation: l.rotation,
2066
+ scale: 1
2067
+ // No scale - using dimensions
2068
+ }, u = {
2069
+ width: this.incoming.element.offsetWidth,
2070
+ height: this.incoming.element.offsetHeight
2071
+ };
2072
+ this.outgoing = this.startUnfocusAnimation(
2073
+ this.incoming.element,
2074
+ this.incoming.originalState,
2075
+ c,
2076
+ u
2077
+ );
2078
+ } else
2079
+ this.outgoing = null;
2080
+ if (this.incoming = this.startFocusAnimation(t, e, i, r, a), await Promise.all([
2081
+ this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),
2082
+ this.waitForAnimation(this.incoming.animationHandle)
2083
+ ]), this.focusGeneration !== o) return;
2084
+ this.outgoing && (this.removeFocusedStyling(this.outgoing.element, this.outgoing.originalState.zIndex?.toString() || ""), this.outgoing = null), this.currentFocus = t, this.incoming = null, this.state = I.FOCUSED;
2085
+ return;
2086
+ }
2087
+ if (this.outgoing && (this.animationEngine.cancelAnimation(this.outgoing.animationHandle, !1), this.resetElementInstantly(
2088
+ this.outgoing.element,
2089
+ this.outgoing.originalState,
2090
+ this.outgoing.originalState.zIndex?.toString() || "",
2091
+ this.outgoing.originalWidth,
2092
+ this.outgoing.originalHeight
2093
+ ), this.outgoing = null), this.incoming) {
2094
+ const s = this.animationEngine.cancelAnimation(this.incoming.animationHandle, !0), r = {
2095
+ x: s.x,
2096
+ y: s.y,
2097
+ rotation: s.rotation,
2098
+ scale: 1
2099
+ // No scale - using dimensions
2100
+ }, a = {
2101
+ width: this.incoming.element.offsetWidth,
2102
+ height: this.incoming.element.offsetHeight
2103
+ };
2104
+ this.outgoing = this.startUnfocusAnimation(
2105
+ this.incoming.element,
2106
+ this.incoming.originalState,
2107
+ r,
2108
+ a
2109
+ );
2110
+ }
2111
+ if (this.incoming = this.startFocusAnimation(t, e, i), await Promise.all([
2112
+ this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),
2113
+ this.waitForAnimation(this.incoming.animationHandle)
2114
+ ]), this.focusGeneration !== o) return;
2115
+ this.outgoing && (this.removeFocusedStyling(this.outgoing.element, this.outgoing.originalState.zIndex?.toString() || ""), this.outgoing = null), this.currentFocus = t, this.incoming = null, this.state = I.FOCUSED;
2116
+ break;
2117
+ }
2118
+ }
2119
+ /**
2120
+ * Unfocus current image, returning it to original position
2121
+ */
2122
+ async unfocusImage() {
2123
+ const t = ++this.focusGeneration;
2124
+ if (!this.currentFocus || !this.focusData) {
2125
+ if (this.incoming && this.state === I.FOCUSING) {
2126
+ const s = this.animationEngine.cancelAnimation(this.incoming.animationHandle, !0), r = {
2127
+ x: s.x,
2128
+ y: s.y,
2129
+ rotation: s.rotation,
2130
+ scale: 1
2131
+ // No scale - using dimensions
2132
+ }, a = {
2133
+ width: this.incoming.element.offsetWidth,
2134
+ height: this.incoming.element.offsetHeight
2135
+ };
2136
+ if (this.outgoing = this.startUnfocusAnimation(
2137
+ this.incoming.element,
2138
+ this.incoming.originalState,
2139
+ r,
2140
+ a
2141
+ ), this.incoming = null, this.state = I.UNFOCUSING, await this.waitForAnimation(this.outgoing.animationHandle), this.focusGeneration !== t) return;
2142
+ this.removeFocusedStyling(this.outgoing.element, this.focusData?.originalZIndex || ""), this.outgoing = null, this.focusData = null, this.state = I.IDLE;
2143
+ }
2144
+ return;
2145
+ }
2146
+ if (this.state === I.CROSS_ANIMATING && this.incoming) {
2147
+ const s = this.animationEngine.cancelAnimation(this.incoming.animationHandle, !0), r = {
2148
+ x: s.x,
2149
+ y: s.y,
2150
+ rotation: s.rotation,
2151
+ scale: 1
2152
+ // No scale - using dimensions
2153
+ }, a = {
2154
+ width: this.incoming.element.offsetWidth,
2155
+ height: this.incoming.element.offsetHeight
2156
+ }, l = this.startUnfocusAnimation(
2157
+ this.incoming.element,
2158
+ this.incoming.originalState,
2159
+ r,
2160
+ a
2161
+ );
2162
+ if (await Promise.all([
2163
+ this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),
2164
+ this.waitForAnimation(l.animationHandle)
2165
+ ]), this.focusGeneration !== t) return;
2166
+ this.outgoing && this.removeFocusedStyling(this.outgoing.element, this.outgoing.originalState.zIndex?.toString() || ""), this.removeFocusedStyling(l.element, this.incoming.originalState.zIndex?.toString() || ""), this.outgoing = null, this.incoming = null, this.currentFocus = null, this.focusData = null, this.state = I.IDLE;
2167
+ return;
2168
+ }
2169
+ this.state = I.UNFOCUSING;
2170
+ const e = this.currentFocus, i = this.focusData.originalState, o = this.focusData.originalZIndex;
2171
+ this.outgoing = this.startUnfocusAnimation(e, i), await this.waitForAnimation(this.outgoing.animationHandle), this.focusGeneration === t && (this.removeFocusedStyling(e, o), this.outgoing = null, this.currentFocus = null, this.focusData = null, this.state = I.IDLE);
2172
+ }
2173
+ /**
2174
+ * Swap focus from current image to a new one (alias for focusImage with cross-animation)
2175
+ */
2176
+ async swapFocus(t, e, i) {
2177
+ return this.focusImage(t, e, i);
2178
+ }
2179
+ /**
2180
+ * Get currently focused image element
2181
+ */
2182
+ getCurrentFocus() {
2183
+ return this.currentFocus;
2184
+ }
2185
+ /**
2186
+ * Check if an image is currently focused (stable state)
2187
+ */
2188
+ isFocused(t) {
2189
+ return this.currentFocus === t && this.state === I.FOCUSED;
2190
+ }
2191
+ /**
2192
+ * Check if an image is the target of current focus animation
2193
+ */
2194
+ isTargetingFocus(t) {
2195
+ return this.incoming?.element === t;
2196
+ }
2197
+ /**
2198
+ * Check if an image is involved in any focus/animation state
2199
+ * Returns true if the image is focused, animating in, or animating out
2200
+ * Useful for hover state management - don't apply hover to animating images
2201
+ */
2202
+ isInvolved(t) {
2203
+ return this.currentFocus === t || this.incoming?.element === t || this.outgoing?.element === t;
2204
+ }
2205
+ /**
2206
+ * Apply a temporary horizontal drag offset to the focused image
2207
+ * Used during swipe gestures for visual feedback
2208
+ */
2209
+ setDragOffset(t) {
2210
+ if (!this.currentFocus || !this.focusData || this.state !== I.FOCUSED) return;
2211
+ const e = this.currentFocus, i = this.focusData.focusTransform, o = ["translate(-50%, -50%)"], s = (i.x ?? 0) + t, r = i.y ?? 0;
2212
+ o.push(`translate(${s}px, ${r}px)`), i.rotation !== void 0 && o.push(`rotate(${i.rotation}deg)`), e.style.transition = "none", e.style.transform = o.join(" ");
2213
+ }
2214
+ /**
2215
+ * Clear the drag offset, optionally animating back to center
2216
+ * @param animate - If true, animate back to center; if false, snap instantly
2217
+ * @param duration - Animation duration in ms (default 150)
2218
+ */
2219
+ clearDragOffset(t, e = 150) {
2220
+ if (!this.currentFocus || !this.focusData || this.state !== I.FOCUSED) return;
2221
+ const i = this.currentFocus, o = this.focusData.focusTransform, s = ["translate(-50%, -50%)"], r = o.x ?? 0, a = o.y ?? 0;
2222
+ s.push(`translate(${r}px, ${a}px)`), o.rotation !== void 0 && s.push(`rotate(${o.rotation}deg)`);
2223
+ const l = s.join(" ");
2224
+ t ? (i.style.transition = `transform ${e}ms ease-out`, i.style.transform = l, setTimeout(() => {
2225
+ this.currentFocus === i && (i.style.transition = "none");
2226
+ }, e)) : (i.style.transition = "none", i.style.transform = l);
2227
+ }
2228
+ /**
2229
+ * Reset zoom state (cancels all animations)
2230
+ */
2231
+ reset() {
2232
+ this.outgoing && (this.animationEngine.cancelAnimation(this.outgoing.animationHandle, !1), this.resetElementInstantly(
2233
+ this.outgoing.element,
2234
+ this.outgoing.originalState,
2235
+ this.outgoing.originalState.zIndex?.toString() || "",
2236
+ this.outgoing.originalWidth,
2237
+ this.outgoing.originalHeight
2238
+ )), this.incoming && (this.animationEngine.cancelAnimation(this.incoming.animationHandle, !1), this.resetElementInstantly(
2239
+ this.incoming.element,
2240
+ this.incoming.originalState,
2241
+ this.focusData?.originalZIndex || "",
2242
+ this.focusData?.originalWidth,
2243
+ this.focusData?.originalHeight
2244
+ )), this.currentFocus && this.focusData && this.resetElementInstantly(
2245
+ this.currentFocus,
2246
+ this.focusData.originalState,
2247
+ this.focusData.originalZIndex,
2248
+ this.focusData.originalWidth,
2249
+ this.focusData.originalHeight
2250
+ ), this.state = I.IDLE, this.currentFocus = null, this.focusData = null, this.outgoing = null, this.incoming = null;
2251
+ }
2252
+ }
2253
+ const Se = 50, Ie = 0.5, Te = 20, Re = 0.3, Ce = 150, Ae = 30, at = class at {
2254
+ constructor(t, e) {
2255
+ this.enabled = !1, this.touchState = null, this.recentTouchTimestamp = 0, this.container = t, this.callbacks = e, this.boundTouchStart = this.handleTouchStart.bind(this), this.boundTouchMove = this.handleTouchMove.bind(this), this.boundTouchEnd = this.handleTouchEnd.bind(this), this.boundTouchCancel = this.handleTouchCancel.bind(this);
2256
+ }
2257
+ /**
2258
+ * Start listening for touch events
2259
+ */
2260
+ enable() {
2261
+ this.enabled || (this.enabled = !0, this.container.style.touchAction = "pan-y", this.container.addEventListener("touchstart", this.boundTouchStart, { passive: !1 }), this.container.addEventListener("touchmove", this.boundTouchMove, { passive: !1 }), this.container.addEventListener("touchend", this.boundTouchEnd, { passive: !0 }), this.container.addEventListener("touchcancel", this.boundTouchCancel, { passive: !0 }));
2262
+ }
2263
+ /**
2264
+ * Stop listening for touch events
2265
+ */
2266
+ disable() {
2267
+ this.enabled && (this.enabled = !1, this.container.style.touchAction = "", this.container.removeEventListener("touchstart", this.boundTouchStart), this.container.removeEventListener("touchmove", this.boundTouchMove), this.container.removeEventListener("touchend", this.boundTouchEnd), this.container.removeEventListener("touchcancel", this.boundTouchCancel), this.touchState?.isDragging && this.callbacks.onDragEnd(!1), this.touchState = null);
2268
+ }
2269
+ /**
2270
+ * Clean up all event listeners
2271
+ */
2272
+ destroy() {
2273
+ this.disable();
2274
+ }
2275
+ /**
2276
+ * Check if a touch interaction happened recently
2277
+ * Used to prevent click-outside from unfocusing immediately after touch
2278
+ */
2279
+ hadRecentTouch() {
2280
+ return Date.now() - this.recentTouchTimestamp < at.TOUCH_CLICK_DELAY;
2281
+ }
2282
+ handleTouchStart(t) {
2283
+ if (t.touches.length !== 1) return;
2284
+ this.recentTouchTimestamp = Date.now();
2285
+ const e = t.touches[0];
2286
+ this.touchState = {
2287
+ startX: e.clientX,
2288
+ startY: e.clientY,
2289
+ startTime: performance.now(),
2290
+ currentX: e.clientX,
2291
+ isDragging: !1,
2292
+ isHorizontalSwipe: null
2293
+ };
2294
+ }
2295
+ handleTouchMove(t) {
2296
+ if (!this.touchState || t.touches.length !== 1) return;
2297
+ const e = t.touches[0], i = e.clientX - this.touchState.startX, o = e.clientY - this.touchState.startY;
2298
+ if (this.touchState.isHorizontalSwipe === null && Math.sqrt(i * i + o * o) > 10) {
2299
+ const a = Math.atan2(Math.abs(o), Math.abs(i)) * (180 / Math.PI);
2300
+ this.touchState.isHorizontalSwipe = a <= Ae;
2301
+ }
2302
+ if (this.touchState.isHorizontalSwipe !== !1 && this.touchState.isHorizontalSwipe === !0) {
2303
+ t.preventDefault(), this.touchState.isDragging = !0, this.touchState.currentX = e.clientX;
2304
+ const s = i * Re;
2305
+ this.callbacks.onDragOffset(s);
2306
+ }
2307
+ }
2308
+ handleTouchEnd(t) {
2309
+ if (!this.touchState) return;
2310
+ this.recentTouchTimestamp = Date.now();
2311
+ const e = this.touchState.currentX - this.touchState.startX, i = performance.now() - this.touchState.startTime, o = Math.abs(e) / i, s = Math.abs(e);
2312
+ let r = !1;
2313
+ this.touchState.isHorizontalSwipe === !0 && this.touchState.isDragging && (s >= Se || o >= Ie && s >= Te) && (r = !0, e < 0 ? this.callbacks.onNext() : this.callbacks.onPrev()), this.touchState.isDragging && this.callbacks.onDragEnd(r), this.touchState = null;
2314
+ }
2315
+ handleTouchCancel(t) {
2316
+ this.touchState?.isDragging && this.callbacks.onDragEnd(!1), this.touchState = null;
2317
+ }
2318
+ };
2319
+ at.TOUCH_CLICK_DELAY = 300;
2320
+ let mt = at;
2321
+ class Le {
2322
+ constructor(t) {
2323
+ if (this._prepared = !1, this._discoveredUrls = [], this.apiKey = t.apiKey ?? "", this.apiEndpoint = t.apiEndpoint ?? "https://www.googleapis.com/drive/v3/files", this.debugLogging = t.debugLogging ?? !1, this.sources = t.sources ?? [], !this.sources || this.sources.length === 0)
2324
+ throw new Error("GoogleDriveLoader requires at least one source to be configured");
2325
+ }
2326
+ /**
2327
+ * Prepare the loader by discovering all images from configured sources
2328
+ * @param filter - Filter to apply to discovered images
2329
+ */
2330
+ async prepare(t) {
2331
+ this._discoveredUrls = [];
2332
+ for (const e of this.sources)
2333
+ if ("folders" in e)
2334
+ for (const i of e.folders) {
2335
+ const o = e.recursive !== void 0 ? e.recursive : !0, s = await this.loadFromFolder(i, t, o);
2336
+ this._discoveredUrls.push(...s);
2337
+ }
2338
+ else if ("files" in e) {
2339
+ const i = await this.loadFiles(e.files, t);
2340
+ this._discoveredUrls.push(...i);
2341
+ }
2342
+ this._prepared = !0;
2343
+ }
2344
+ /**
2345
+ * Get the number of discovered images
2346
+ * @throws Error if called before prepare()
2347
+ */
2348
+ imagesLength() {
2349
+ if (!this._prepared)
2350
+ throw new Error("GoogleDriveLoader.imagesLength() called before prepare()");
2351
+ return this._discoveredUrls.length;
2352
+ }
2353
+ /**
2354
+ * Get the ordered list of image URLs
2355
+ * @throws Error if called before prepare()
2356
+ */
2357
+ imageURLs() {
2358
+ if (!this._prepared)
2359
+ throw new Error("GoogleDriveLoader.imageURLs() called before prepare()");
2360
+ return [...this._discoveredUrls];
2361
+ }
2362
+ /**
2363
+ * Check if the loader has been prepared
2364
+ */
2365
+ isPrepared() {
2366
+ return this._prepared;
2367
+ }
2368
+ /**
2369
+ * Extract folder ID from various Google Drive URL formats
2370
+ * @param folderUrl - Google Drive folder URL
2371
+ * @returns Folder ID or null if invalid
2372
+ */
2373
+ extractFolderId(t) {
2374
+ const e = [
2375
+ /\/folders\/([a-zA-Z0-9_-]+)/,
2376
+ // Standard format
2377
+ /id=([a-zA-Z0-9_-]+)/
2378
+ // Alternative format
2379
+ ];
2380
+ for (const i of e) {
2381
+ const o = t.match(i);
2382
+ if (o && o[1])
2383
+ return o[1];
2384
+ }
2385
+ return null;
2386
+ }
2387
+ /**
2388
+ * Load images from a Google Drive folder
2389
+ * @param folderUrl - Google Drive folder URL
2390
+ * @param filter - Filter to apply to discovered images
2391
+ * @param recursive - Whether to include images from subfolders
2392
+ * @returns Promise resolving to array of image URLs
2393
+ */
2394
+ async loadFromFolder(t, e, i = !0) {
2395
+ const o = this.extractFolderId(t);
2396
+ if (!o)
2397
+ throw new Error("Invalid Google Drive folder URL. Please check the URL format.");
2398
+ if (!this.apiKey || this.apiKey === "YOUR_API_KEY_HERE")
2399
+ return this.loadImagesDirectly(o, e);
2400
+ try {
2401
+ return i ? await this.loadImagesRecursively(o, e) : await this.loadImagesFromSingleFolder(o, e);
2402
+ } catch (s) {
2403
+ return console.error("Error loading from Google Drive API:", s), this.loadImagesDirectly(o, e);
2404
+ }
2405
+ }
2406
+ /**
2407
+ * Load images from a single folder (non-recursive)
2408
+ * @param folderId - Google Drive folder ID
2409
+ * @param filter - Filter to apply to discovered images
2410
+ * @returns Promise resolving to array of image URLs
2411
+ */
2412
+ async loadImagesFromSingleFolder(t, e) {
2413
+ const i = [], o = `'${t}' in parents and trashed=false`, r = `${this.apiEndpoint}?q=${encodeURIComponent(o)}&fields=files(id,name,mimeType,thumbnailLink)&key=${this.apiKey}`, a = await fetch(r);
2414
+ if (!a.ok)
2415
+ throw new Error(`API request failed: ${a.status} ${a.statusText}`);
2416
+ const c = (await a.json()).files.filter(
2417
+ (u) => u.mimeType.startsWith("image/") && e.isAllowed(u.name)
2418
+ );
2419
+ return this.log(`Found ${c.length} images in folder ${t} (non-recursive)`), c.forEach((u) => {
2420
+ i.push(`https://lh3.googleusercontent.com/d/${u.id}=s1600`), this.log(`Added file: ${u.name}`);
2421
+ }), i;
2422
+ }
2423
+ /**
2424
+ * Load specific files by their URLs or IDs
2425
+ * @param fileUrls - Array of Google Drive file URLs or IDs
2426
+ * @param filter - Filter to apply to discovered images
2427
+ * @returns Promise resolving to array of image URLs
2428
+ */
2429
+ async loadFiles(t, e) {
2430
+ const i = [];
2431
+ for (const o of t) {
2432
+ const s = this.extractFileId(o);
2433
+ if (!s) {
2434
+ this.log(`Skipping invalid file URL: ${o}`);
2435
+ continue;
2436
+ }
2437
+ if (this.apiKey && this.apiKey !== "YOUR_API_KEY_HERE")
2438
+ try {
2439
+ const r = `${this.apiEndpoint}/${s}?fields=name,mimeType&key=${this.apiKey}`, a = await fetch(r);
2440
+ if (a.ok) {
2441
+ const l = await a.json();
2442
+ l.mimeType.startsWith("image/") && e.isAllowed(l.name) ? (i.push(`https://lh3.googleusercontent.com/d/${s}=s1600`), this.log(`Added file: ${l.name}`)) : this.log(`Skipping non-image file: ${l.name} (${l.mimeType})`);
2443
+ } else
2444
+ this.log(`Failed to fetch metadata for file ${s}: ${a.status}`);
2445
+ } catch (r) {
2446
+ this.log(`Error fetching metadata for file ${s}:`, r);
2447
+ }
2448
+ else
2449
+ i.push(`https://lh3.googleusercontent.com/d/${s}=s1600`);
2450
+ }
2451
+ return i;
2452
+ }
2453
+ /**
2454
+ * Extract file ID from Google Drive file URL
2455
+ * @param fileUrl - Google Drive file URL or file ID
2456
+ * @returns File ID or null if invalid
2457
+ */
2458
+ extractFileId(t) {
2459
+ if (!/[/:.]/.test(t))
2460
+ return t;
2461
+ const e = [
2462
+ /\/file\/d\/([a-zA-Z0-9_-]+)/,
2463
+ // Standard file format
2464
+ /\/open\?id=([a-zA-Z0-9_-]+)/,
2465
+ // Alternative format
2466
+ /id=([a-zA-Z0-9_-]+)/
2467
+ // Generic id parameter
2468
+ ];
2469
+ for (const i of e) {
2470
+ const o = t.match(i);
2471
+ if (o && o[1])
2472
+ return o[1];
2473
+ }
2474
+ return null;
2475
+ }
2476
+ /**
2477
+ * Recursively load images from a folder and all its subfolders
2478
+ * @param folderId - Google Drive folder ID
2479
+ * @param filter - Filter to apply to discovered images
2480
+ * @returns Promise resolving to array of image URLs
2481
+ */
2482
+ async loadImagesRecursively(t, e) {
2483
+ const i = [], o = `'${t}' in parents and trashed=false`, r = `${this.apiEndpoint}?q=${encodeURIComponent(o)}&fields=files(id,name,mimeType,thumbnailLink)&key=${this.apiKey}`, a = await fetch(r);
2484
+ if (!a.ok)
2485
+ throw new Error(`API request failed: ${a.status} ${a.statusText}`);
2486
+ const l = await a.json(), c = l.files.filter(
2487
+ (h) => h.mimeType.startsWith("image/") && e.isAllowed(h.name)
2488
+ ), u = l.files.filter(
2489
+ (h) => h.mimeType === "application/vnd.google-apps.folder"
2490
+ );
2491
+ this.log(`Found ${l.files.length} total items in folder ${t}`), l.files.forEach((h) => this.log(` - File: ${h.name} (${h.mimeType})`)), this.log(`- ${c.length} valid files (images only)`), this.log(`- ${u.length} subfolders`), c.forEach((h) => {
2492
+ i.push(`https://lh3.googleusercontent.com/d/${h.id}=s1600`), this.log(`Added file: ${h.name}`);
2493
+ });
2494
+ for (const h of u) {
2495
+ this.log(`Loading images from subfolder: ${h.name}`);
2496
+ const d = await this.loadImagesRecursively(h.id, e);
2497
+ i.push(...d);
2498
+ }
2499
+ return i;
2500
+ }
2501
+ /**
2502
+ * Direct loading method (no API key required, but less reliable)
2503
+ * Uses embedded folder view to scrape image IDs
2504
+ * @param folderId - Google Drive folder ID
2505
+ * @param filter - Filter to apply (not used in fallback mode)
2506
+ * @returns Promise resolving to array of image URLs
2507
+ */
2508
+ async loadImagesDirectly(t, e) {
2509
+ try {
2510
+ const i = `https://drive.google.com/embeddedfolderview?id=${t}`, o = await fetch(i, { mode: "cors" });
2511
+ if (!o.ok)
2512
+ throw new Error("Cannot access folder directly (CORS or permissions issue)");
2513
+ const s = await o.text(), r = /\/file\/d\/([a-zA-Z0-9_-]+)/g, a = [...s.matchAll(r)];
2514
+ return [...new Set(a.map((u) => u[1]))].map(
2515
+ (u) => `https://drive.google.com/uc?export=view&id=${u}`
2516
+ );
2517
+ } catch (i) {
2518
+ throw console.error("Direct loading failed:", i), new Error(
2519
+ `Unable to load images. Please ensure:
2520
+ 1. The folder is shared publicly (Anyone with the link can view)
2521
+ 2. The folder contains image files
2522
+ 3. Consider adding a Google Drive API key in config.js for better reliability`
2523
+ );
2524
+ }
2525
+ }
2526
+ /**
2527
+ * Manually add image URLs (for testing or when auto-loading fails)
2528
+ * @param imageIds - Array of Google Drive file IDs
2529
+ * @returns Array of direct image URLs
2530
+ */
2531
+ manualImageUrls(t) {
2532
+ return t.map((e) => `https://drive.google.com/uc?export=view&id=${e}`);
2533
+ }
2534
+ /**
2535
+ * Debug logging helper
2536
+ * @param args - Arguments to log
2537
+ */
2538
+ log(...t) {
2539
+ this.debugLogging && typeof console < "u" && console.log(...t);
2540
+ }
2541
+ }
2542
+ class Me {
2543
+ constructor(t) {
2544
+ if (this._prepared = !1, this._discoveredUrls = [], this.validateUrls = t.validateUrls !== !1, this.validationTimeout = t.validationTimeout ?? 5e3, this.validationMethod = t.validationMethod ?? "head", this.debugLogging = t.debugLogging ?? !1, this.sources = t.sources ?? [], !this.sources || this.sources.length === 0)
2545
+ throw new Error("StaticImageLoader requires at least one source to be configured");
2546
+ this.log("StaticImageLoader initialized with config:", t);
2547
+ }
2548
+ /**
2549
+ * Prepare the loader by discovering all images from configured sources
2550
+ * @param filter - Filter to apply to discovered images
2551
+ */
2552
+ async prepare(t) {
2553
+ this._discoveredUrls = [], this.log(`Processing ${this.sources.length} source(s)`);
2554
+ for (const e of this.sources)
2555
+ try {
2556
+ const i = await this.processSource(e, t);
2557
+ this._discoveredUrls.push(...i);
2558
+ } catch (i) {
2559
+ console.warn("Failed to process source:", e, i);
2560
+ }
2561
+ this._prepared = !0, this.log(`Successfully loaded ${this._discoveredUrls.length} image(s)`);
2562
+ }
2563
+ /**
2564
+ * Get the number of discovered images
2565
+ * @throws Error if called before prepare()
2566
+ */
2567
+ imagesLength() {
2568
+ if (!this._prepared)
2569
+ throw new Error("StaticImageLoader.imagesLength() called before prepare()");
2570
+ return this._discoveredUrls.length;
2571
+ }
2572
+ /**
2573
+ * Get the ordered list of image URLs
2574
+ * @throws Error if called before prepare()
2575
+ */
2576
+ imageURLs() {
2577
+ if (!this._prepared)
2578
+ throw new Error("StaticImageLoader.imageURLs() called before prepare()");
2579
+ return [...this._discoveredUrls];
2580
+ }
2581
+ /**
2582
+ * Check if the loader has been prepared
2583
+ */
2584
+ isPrepared() {
2585
+ return this._prepared;
2586
+ }
2587
+ /**
2588
+ * Process a single source object using shape-based detection
2589
+ * @param source - Source configuration detected by key presence
2590
+ * @param filter - Filter to apply to discovered images
2591
+ * @returns Promise resolving to array of valid URLs from this source
2592
+ */
2593
+ async processSource(t, e) {
2594
+ return t ? "urls" in t ? await this.processUrls(t.urls, e) : "path" in t ? await this.processPath(t.path, t.files, e) : "json" in t ? await this.processJson(t.json, e) : (console.warn("Unknown source shape:", t), []) : (console.warn("Invalid source object:", t), []);
2595
+ }
2596
+ /**
2597
+ * Process a list of direct URLs
2598
+ * @param urls - Array of image URLs
2599
+ * @param filter - Filter to apply to discovered images
2600
+ * @returns Promise resolving to array of validated URLs
2601
+ */
2602
+ async processUrls(t, e) {
2603
+ if (!Array.isArray(t))
2604
+ return console.warn("URLs must be an array:", t), [];
2605
+ const i = [];
2606
+ for (const o of t) {
2607
+ const s = o.split("/").pop() || o;
2608
+ if (!e.isAllowed(s)) {
2609
+ this.log(`Skipping filtered URL: ${o}`);
2610
+ continue;
2611
+ }
2612
+ this.validateUrls ? await this.validateUrl(o) ? i.push(o) : console.warn(`Skipping invalid/missing URL: ${o}`) : i.push(o);
2613
+ }
2614
+ return i;
2615
+ }
2616
+ /**
2617
+ * Process a path-based source
2618
+ * @param basePath - Base path (relative or absolute)
2619
+ * @param files - Array of filenames
2620
+ * @param filter - Filter to apply to discovered images
2621
+ * @returns Promise resolving to array of validated URLs
2622
+ */
2623
+ async processPath(t, e, i) {
2624
+ if (!Array.isArray(e))
2625
+ return console.warn("files must be an array:", e), [];
2626
+ const o = [];
2627
+ for (const s of e) {
2628
+ if (!i.isAllowed(s)) {
2629
+ this.log(`Skipping filtered file: ${s}`);
2630
+ continue;
2631
+ }
2632
+ const r = this.constructUrl(t, s);
2633
+ this.validateUrls ? await this.validateUrl(r) ? o.push(r) : console.warn(`Skipping invalid/missing file: ${r}`) : o.push(r);
2634
+ }
2635
+ return o;
2636
+ }
2637
+ /**
2638
+ * Process a JSON endpoint source
2639
+ * Fetches a JSON endpoint that returns { images: string[] }
2640
+ * @param url - JSON endpoint URL
2641
+ * @param filter - Filter to apply to discovered images
2642
+ * @returns Promise resolving to array of validated URLs
2643
+ */
2644
+ async processJson(t, e) {
2645
+ this.log(`Fetching JSON endpoint: ${t}`);
2646
+ const i = new AbortController(), o = setTimeout(() => i.abort(), 1e4);
2647
+ try {
2648
+ const s = await fetch(t, { signal: i.signal });
2649
+ if (clearTimeout(o), !s.ok)
2650
+ throw new Error(`HTTP ${s.status} fetching ${t}`);
2651
+ const r = await s.json();
2652
+ if (!r || !Array.isArray(r.images))
2653
+ throw new Error('JSON source must return JSON with shape { "images": ["url1", "url2", ...] }');
2654
+ return this.log(`JSON endpoint returned ${r.images.length} image(s)`), await this.processUrls(r.images, e);
2655
+ } catch (s) {
2656
+ throw clearTimeout(o), s instanceof Error && s.name === "AbortError" ? new Error(`Timeout fetching JSON endpoint: ${t}`) : s;
2657
+ }
2658
+ }
2659
+ /**
2660
+ * Validate a single URL using HEAD request
2661
+ * @param url - URL to validate
2662
+ * @returns Promise resolving to true if valid and accessible
2663
+ */
2664
+ async validateUrl(t) {
2665
+ if (this.validationMethod === "none")
2666
+ return !0;
2667
+ if (this.validationMethod === "simple")
2668
+ try {
2669
+ return typeof window < "u" ? new URL(t, window.location.origin) : new URL(t), !0;
2670
+ } catch {
2671
+ return !1;
2672
+ }
2673
+ if (typeof window > "u")
2674
+ return !0;
2675
+ if (!(t.startsWith(window.location.origin) || t.startsWith("/")))
2676
+ return this.log(`Skipping validation for cross-origin URL: ${t}`), !0;
2677
+ try {
2678
+ const i = new AbortController(), o = setTimeout(() => i.abort(), this.validationTimeout), s = await fetch(t, {
2679
+ method: "HEAD",
2680
+ signal: i.signal
2681
+ });
2682
+ return clearTimeout(o), s.ok ? !0 : (this.log(`Validation failed for ${t}: HTTP ${s.status}`), !1);
2683
+ } catch (i) {
2684
+ return i instanceof Error && (i.name === "AbortError" ? this.log(`Validation timeout for ${t}`) : this.log(`Validation failed for ${t}:`, i.message)), !1;
2685
+ }
2686
+ }
2687
+ /**
2688
+ * Construct full URL from basePath and filename
2689
+ * @param basePath - Base path (relative or absolute)
2690
+ * @param filename - Filename to append
2691
+ * @returns Complete URL
2692
+ */
2693
+ constructUrl(t, e) {
2694
+ const i = t.replace(/\/$/, "");
2695
+ if (this.isAbsoluteUrl(t))
2696
+ return `${i}/${e}`;
2697
+ if (typeof window > "u")
2698
+ return `${i}/${e}`;
2699
+ const o = window.location.origin, r = (t.startsWith("/") ? t : "/" + t).replace(/\/$/, "");
2700
+ return `${o}${r}/${e}`;
2701
+ }
2702
+ /**
2703
+ * Check if URL is absolute (contains protocol)
2704
+ * @param url - URL to check
2705
+ * @returns True if absolute URL
2706
+ */
2707
+ isAbsoluteUrl(t) {
2708
+ try {
2709
+ return new URL(t), !0;
2710
+ } catch {
2711
+ return !1;
2712
+ }
2713
+ }
2714
+ /**
2715
+ * Debug logging helper
2716
+ * @param args - Arguments to log
2717
+ */
2718
+ log(...t) {
2719
+ this.debugLogging && typeof console < "u" && console.log(...t);
2720
+ }
2721
+ }
2722
+ class ze {
2723
+ constructor(t) {
2724
+ if (this._prepared = !1, this._discoveredUrls = [], this.loaders = t.loaders, this.debugLogging = t.debugLogging ?? !1, !this.loaders || this.loaders.length === 0)
2725
+ throw new Error("CompositeLoader requires at least one loader to be configured");
2726
+ this.log(`CompositeLoader initialized with ${this.loaders.length} loader(s)`);
2727
+ }
2728
+ /**
2729
+ * Prepare all loaders in parallel and combine their results
2730
+ * @param filter - Filter to apply to discovered images
2731
+ */
2732
+ async prepare(t) {
2733
+ this._discoveredUrls = [], this.log(`Preparing ${this.loaders.length} loader(s) in parallel`);
2734
+ const e = this.loaders.map((i, o) => i.prepare(t).then(() => {
2735
+ this.log(`Loader ${o} prepared with ${i.imagesLength()} images`);
2736
+ }).catch((s) => {
2737
+ console.warn(`Loader ${o} failed to prepare:`, s);
2738
+ }));
2739
+ await Promise.all(e);
2740
+ for (const i of this.loaders)
2741
+ if (i.isPrepared()) {
2742
+ const o = i.imageURLs();
2743
+ this._discoveredUrls.push(...o);
2744
+ }
2745
+ this._prepared = !0, this.log(`CompositeLoader prepared with ${this._discoveredUrls.length} total images`);
2746
+ }
2747
+ /**
2748
+ * Get the combined number of discovered images
2749
+ * @throws Error if called before prepare()
2750
+ */
2751
+ imagesLength() {
2752
+ if (!this._prepared)
2753
+ throw new Error("CompositeLoader.imagesLength() called before prepare()");
2754
+ return this._discoveredUrls.length;
2755
+ }
2756
+ /**
2757
+ * Get the combined ordered list of image URLs
2758
+ * @throws Error if called before prepare()
2759
+ */
2760
+ imageURLs() {
2761
+ if (!this._prepared)
2762
+ throw new Error("CompositeLoader.imageURLs() called before prepare()");
2763
+ return [...this._discoveredUrls];
2764
+ }
2765
+ /**
2766
+ * Check if the loader has been prepared
2767
+ */
2768
+ isPrepared() {
2769
+ return this._prepared;
2770
+ }
2771
+ /**
2772
+ * Debug logging helper
2773
+ * @param args - Arguments to log
2774
+ */
2775
+ log(...t) {
2776
+ this.debugLogging && typeof console < "u" && console.log("[CompositeLoader]", ...t);
2777
+ }
2778
+ }
2779
+ class Fe {
2780
+ /**
2781
+ * Create a new ImageFilter
2782
+ * @param extensions - Array of allowed file extensions (without dots)
2783
+ * Defaults to common image formats if not provided
2784
+ */
2785
+ constructor(t) {
2786
+ this.allowedExtensions = t || [
2787
+ "jpg",
2788
+ "jpeg",
2789
+ "png",
2790
+ "gif",
2791
+ "webp",
2792
+ "bmp"
2793
+ ];
2794
+ }
2795
+ /**
2796
+ * Check if a filename has an allowed extension
2797
+ * @param filename - The filename to check (can include path or query string)
2798
+ * @returns True if the file extension is allowed
2799
+ */
2800
+ isAllowed(t) {
2801
+ const i = t.split("?")[0].split(".").pop()?.toLowerCase();
2802
+ return i ? this.allowedExtensions.includes(i) : !1;
2803
+ }
2804
+ /**
2805
+ * Get the list of allowed extensions
2806
+ * @returns Array of allowed extensions
2807
+ */
2808
+ getAllowedExtensions() {
2809
+ return [...this.allowedExtensions];
2810
+ }
2811
+ // Future expansion methods:
2812
+ // isAllowedSize(sizeBytes: number): boolean
2813
+ // isAllowedDate(date: Date): boolean
2814
+ // isAllowedDimensions(width: number, height: number): boolean
2815
+ }
2816
+ const De = `
2817
+ .fbn-ic-gallery {
2818
+ position: relative;
2819
+ width: 100%;
2820
+ height: 100%;
2821
+ overflow: hidden;
2822
+ perspective: 1000px;
2823
+ }
2824
+
2825
+ .fbn-ic-image {
2826
+ position: absolute;
2827
+ cursor: pointer;
2828
+ transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1),
2829
+ box-shadow 0.6s cubic-bezier(0.4, 0, 0.2, 1),
2830
+ filter 0.3s cubic-bezier(0.4, 0, 0.2, 1),
2831
+ opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
2832
+ border 0.3s cubic-bezier(0.4, 0, 0.2, 1),
2833
+ outline 0.3s cubic-bezier(0.4, 0, 0.2, 1),
2834
+ z-index 0s 0.6s;
2835
+ will-change: transform;
2836
+ user-select: none;
2837
+ backface-visibility: hidden;
2838
+ -webkit-backface-visibility: hidden;
2839
+ }
2840
+
2841
+ .fbn-ic-image.fbn-ic-focused {
2842
+ z-index: 1000;
2843
+ transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1),
2844
+ box-shadow 0.6s cubic-bezier(0.4, 0, 0.2, 1),
2845
+ filter 0.3s cubic-bezier(0.4, 0, 0.2, 1),
2846
+ opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
2847
+ border 0.3s cubic-bezier(0.4, 0, 0.2, 1),
2848
+ outline 0.3s cubic-bezier(0.4, 0, 0.2, 1),
2849
+ z-index 0s 0s;
2850
+ will-change: auto;
2851
+ }
2852
+
2853
+ .fbn-ic-counter {
2854
+ position: fixed;
2855
+ bottom: 24px;
2856
+ left: 50%;
2857
+ transform: translateX(-50%);
2858
+ z-index: 10001;
2859
+ pointer-events: none;
2860
+ }
2861
+
2862
+ .fbn-ic-hidden {
2863
+ display: none !important;
2864
+ }
2865
+ `;
2866
+ function Oe() {
2867
+ if (typeof document > "u") return;
2868
+ const n = "fbn-ic-functional-styles";
2869
+ if (document.getElementById(n)) return;
2870
+ const t = document.createElement("style");
2871
+ t.id = n, t.textContent = De, document.head.appendChild(t);
2872
+ }
2873
+ let $e = class {
2874
+ constructor(t = {}) {
2875
+ this.fullConfig = Yt(t), t.container instanceof HTMLElement ? (this.containerRef = t.container, this.containerId = null) : (this.containerRef = null, this.containerId = t.container || "imageCloud"), this.imagesLoaded = !1, this.imageElements = [], this.imageLayouts = [], this.currentImageHeight = 225, this.currentFocusIndex = null, this.hoveredImage = null, this.resizeTimeout = null, this.displayQueue = [], this.queueInterval = null, this.loadGeneration = 0, this.loadingElAutoCreated = !1, this.errorElAutoCreated = !1, this.counterEl = null, this.counterElAutoCreated = !1, this.animationEngine = new Vt(this.fullConfig.animation), this.layoutEngine = new ye({
2876
+ layout: this.fullConfig.layout,
2877
+ image: this.fullConfig.image
2878
+ }), this.zoomEngine = new Ee(this.fullConfig.interaction.focus, this.animationEngine, this.fullConfig.styling), this.defaultStyles = st(this.fullConfig.styling?.default), this.hoverStyles = st(this.fullConfig.styling?.hover), this.defaultClassName = this.fullConfig.styling?.default?.className, this.hoverClassName = this.fullConfig.styling?.hover?.className;
2879
+ const e = this.fullConfig.animation.entry || y.animation.entry;
2880
+ this.entryAnimationEngine = new re(
2881
+ e,
2882
+ this.fullConfig.layout.algorithm
2883
+ ), this.swipeEngine = null, this.imageFilter = this.createImageFilter(), this.imageLoader = this.createLoader(), this.containerEl = null, this.loadingEl = null, this.errorEl = null;
2884
+ }
2885
+ /**
2886
+ * Create image filter based on shared loader config
2887
+ */
2888
+ createImageFilter() {
2889
+ const t = this.fullConfig.config.loaders?.allowedExtensions;
2890
+ return new Fe(t);
2891
+ }
2892
+ /**
2893
+ * Create appropriate image loader based on config
2894
+ * Processes loaders array, merges shared config, wraps in CompositeLoader if needed
2895
+ */
2896
+ createLoader() {
2897
+ const t = this.fullConfig.loaders, e = this.fullConfig.config.loaders ?? {};
2898
+ if (!t || t.length === 0)
2899
+ throw new Error("No loaders configured. Provide `images`, `loaders`, or both.");
2900
+ const i = t.map((o) => this.createLoaderFromEntry(o, e));
2901
+ return i.length === 1 ? i[0] : new ze({
2902
+ loaders: i,
2903
+ debugLogging: this.fullConfig.config.debug?.loaders
2904
+ });
2905
+ }
2906
+ /**
2907
+ * Create a single loader from a LoaderEntry, merging shared config
2908
+ */
2909
+ createLoaderFromEntry(t, e) {
2910
+ if ("static" in t) {
2911
+ const i = t.static, o = {
2912
+ ...i,
2913
+ validateUrls: i.validateUrls ?? e.validateUrls,
2914
+ validationTimeout: i.validationTimeout ?? e.validationTimeout,
2915
+ validationMethod: i.validationMethod ?? e.validationMethod,
2916
+ allowedExtensions: i.allowedExtensions ?? e.allowedExtensions,
2917
+ debugLogging: i.debugLogging ?? this.fullConfig.config.debug?.loaders
2918
+ };
2919
+ return new Me(o);
2920
+ } else if ("googleDrive" in t) {
2921
+ const i = t.googleDrive, o = {
2922
+ ...i,
2923
+ allowedExtensions: i.allowedExtensions ?? e.allowedExtensions,
2924
+ debugLogging: i.debugLogging ?? this.fullConfig.config.debug?.loaders
2925
+ };
2926
+ return new Le(o);
2927
+ } else
2928
+ throw new Error(`Unknown loader entry: ${JSON.stringify(t)}`);
2929
+ }
2930
+ /**
2931
+ * Initialize the gallery
2932
+ */
2933
+ async init() {
2934
+ try {
2935
+ if (Oe(), this.containerRef)
2936
+ this.containerEl = this.containerRef;
2937
+ else if (this.containerEl = document.getElementById(this.containerId), !this.containerEl)
2938
+ throw new Error(`Container #${this.containerId} not found`);
2939
+ this.containerEl.classList.add("fbn-ic-gallery"), this.swipeEngine = new mt(this.containerEl, {
2940
+ onNext: () => this.navigateToNextImage(),
2941
+ onPrev: () => this.navigateToPreviousImage(),
2942
+ onDragOffset: (t) => this.zoomEngine.setDragOffset(t),
2943
+ onDragEnd: (t) => {
2944
+ t ? this.zoomEngine.clearDragOffset(!1) : this.zoomEngine.clearDragOffset(!0, Ce);
2945
+ }
2946
+ }), this.setupUI(), this.setupEventListeners(), this.logDebug("ImageCloud initialized"), await this.loadImages();
2947
+ } catch (t) {
2948
+ console.error("Gallery initialization failed:", t), this.errorEl && t instanceof Error && this.showError("Gallery failed to initialize: " + t.message);
2949
+ }
2950
+ }
2951
+ setupUI() {
2952
+ const t = this.fullConfig.rendering.ui;
2953
+ t.showLoadingSpinner && (t.loadingElement ? (this.loadingEl = this.resolveElement(t.loadingElement), this.loadingElAutoCreated = !1) : (this.loadingEl = this.createDefaultLoadingElement(), this.loadingElAutoCreated = !0)), t.errorElement ? (this.errorEl = this.resolveElement(t.errorElement), this.errorElAutoCreated = !1) : (this.errorEl = this.createDefaultErrorElement(), this.errorElAutoCreated = !0), t.showImageCounter && (t.counterElement ? (this.counterEl = this.resolveElement(t.counterElement), this.counterElAutoCreated = !1) : (this.counterEl = this.createDefaultCounterElement(), this.counterElAutoCreated = !0));
2954
+ }
2955
+ resolveElement(t) {
2956
+ return t instanceof HTMLElement ? t : document.getElementById(t);
2957
+ }
2958
+ createDefaultLoadingElement() {
2959
+ const t = document.createElement("div");
2960
+ t.className = "fbn-ic-loading fbn-ic-hidden";
2961
+ const e = document.createElement("div");
2962
+ e.className = "fbn-ic-spinner", t.appendChild(e);
2963
+ const i = document.createElement("p");
2964
+ return i.textContent = "Loading images...", t.appendChild(i), this.containerEl.appendChild(t), t;
2965
+ }
2966
+ createDefaultErrorElement() {
2967
+ const t = document.createElement("div");
2968
+ return t.className = "fbn-ic-error fbn-ic-hidden", this.containerEl.appendChild(t), t;
2969
+ }
2970
+ createDefaultCounterElement() {
2971
+ const t = document.createElement("div");
2972
+ return t.className = "fbn-ic-counter fbn-ic-hidden", this.containerEl.appendChild(t), t;
2973
+ }
2974
+ setupEventListeners() {
2975
+ document.addEventListener("keydown", (t) => {
2976
+ t.key === "Escape" ? (this.zoomEngine.unfocusImage(), this.currentFocusIndex = null, this.swipeEngine?.disable(), this.hideCounter()) : t.key === "ArrowRight" ? this.navigateToNextImage() : t.key === "ArrowLeft" ? this.navigateToPreviousImage() : (t.key === "Enter" || t.key === " ") && this.hoveredImage && (this.handleImageClick(this.hoveredImage.element, this.hoveredImage.layout), t.preventDefault());
2977
+ }), document.addEventListener("click", (t) => {
2978
+ this.swipeEngine?.hadRecentTouch() || t.target.closest(".fbn-ic-image") || (this.zoomEngine.unfocusImage(), this.currentFocusIndex = null, this.swipeEngine?.disable(), this.hideCounter());
2979
+ }), window.addEventListener("resize", () => this.handleResize());
2980
+ }
2981
+ /**
2982
+ * Navigate to the next image (Right arrow)
2983
+ */
2984
+ navigateToNextImage() {
2985
+ if (this.currentFocusIndex === null || this.imageElements.length === 0) return;
2986
+ const t = (this.currentFocusIndex + 1) % this.imageLayouts.length, e = this.imageElements.find(
2987
+ (o) => o.dataset.imageId === String(t)
2988
+ );
2989
+ if (!e) return;
2990
+ const i = this.imageLayouts[t];
2991
+ i && (this.currentFocusIndex = t, this.handleImageClick(e, i), this.updateCounter(t));
2992
+ }
2993
+ /**
2994
+ * Navigate to the previous image (Left arrow)
2995
+ */
2996
+ navigateToPreviousImage() {
2997
+ if (this.currentFocusIndex === null || this.imageElements.length === 0) return;
2998
+ const t = (this.currentFocusIndex - 1 + this.imageLayouts.length) % this.imageLayouts.length, e = this.imageElements.find(
2999
+ (o) => o.dataset.imageId === String(t)
3000
+ );
3001
+ if (!e) return;
3002
+ const i = this.imageLayouts[t];
3003
+ i && (this.currentFocusIndex = t, this.handleImageClick(e, i), this.updateCounter(t));
3004
+ }
3005
+ /**
3006
+ * Navigate to a specific image by index
3007
+ */
3008
+ handleResize() {
3009
+ this.imagesLoaded && (this.resizeTimeout !== null && clearTimeout(this.resizeTimeout), this.resizeTimeout = window.setTimeout(() => {
3010
+ const t = this.getImageHeight();
3011
+ t !== this.currentImageHeight ? (this.logDebug(`Window resized to new breakpoint (height: ${t}px). Reloading images...`), this.loadImages()) : this.logDebug("Window resized (no breakpoint change)");
3012
+ }, 500));
3013
+ }
3014
+ getImageHeight() {
3015
+ const t = window.innerWidth, e = this.fullConfig.layout.responsive, o = this.fullConfig.image.sizing?.maxSize ?? 400;
3016
+ return e ? t <= e.mobile.maxWidth ? Math.min(100, o) : t <= e.tablet.maxWidth ? Math.min(180, o) : Math.min(225, o) : t <= 767 ? Math.min(100, o) : t <= 1199 ? Math.min(180, o) : Math.min(225, o);
3017
+ }
3018
+ /**
3019
+ * Get container bounds for layout calculations
3020
+ */
3021
+ getContainerBounds() {
3022
+ return this.containerEl ? {
3023
+ width: this.containerEl.offsetWidth,
3024
+ height: this.containerEl.offsetHeight || window.innerHeight * 0.7
3025
+ } : { width: window.innerWidth, height: window.innerHeight * 0.7 };
3026
+ }
3027
+ /**
3028
+ * Load images using the unified loader interface
3029
+ */
3030
+ async loadImages() {
3031
+ try {
3032
+ this.showLoading(!0), this.hideError(), this.clearImageCloud(), await this.imageLoader.prepare(this.imageFilter);
3033
+ const t = this.imageLoader.imagesLength();
3034
+ let e = this.imageLoader.imageURLs();
3035
+ if (t === 0) {
3036
+ this.showError("No images found."), this.showLoading(!1);
3037
+ return;
3038
+ }
3039
+ const i = this.getContainerBounds(), o = this.getImageHeight(), s = window.innerWidth;
3040
+ this.logDebug(`Adaptive sizing input: container=${i.width}x${i.height}px, images=${t}, responsiveMax=${o}px`);
3041
+ const r = this.layoutEngine.calculateAdaptiveSize(
3042
+ i,
3043
+ t,
3044
+ o,
3045
+ s
3046
+ );
3047
+ this.logDebug(`Adaptive sizing result: height=${r.height}px`), await this.createImageCloud(e, r.height), this.showLoading(!1), this.imagesLoaded = !0;
3048
+ } catch (t) {
3049
+ console.error("Error loading images:", t), t instanceof Error && this.showError(t.message || "Failed to load images."), this.showLoading(!1);
3050
+ }
3051
+ }
3052
+ /**
3053
+ * Helper for debug logging
3054
+ */
3055
+ logDebug(...t) {
3056
+ this.fullConfig.config.debug?.enabled && typeof console < "u" && console.log(...t);
3057
+ }
3058
+ async createImageCloud(t, e) {
3059
+ if (!this.containerEl) return;
3060
+ const i = this.getContainerBounds();
3061
+ this.currentImageHeight = e;
3062
+ const o = this.loadGeneration, s = this.layoutEngine.generateLayout(t.length, i, { fixedHeight: e });
3063
+ this.imageLayouts = s, this.displayQueue = [];
3064
+ let r = 0;
3065
+ const a = (c) => {
3066
+ this.containerEl && (this.containerEl.appendChild(c), this.imageElements.push(c), requestAnimationFrame(() => {
3067
+ if (c.offsetWidth, c.style.opacity = this.defaultStyles.opacity ?? "1", c.dataset.startX && (this.entryAnimationEngine.requiresJSAnimation() || this.entryAnimationEngine.requiresJSRotation() || this.entryAnimationEngine.requiresJSScale() || c.dataset.startRotation !== c.dataset.rotation || c.dataset.startScale !== c.dataset.scale)) {
3068
+ const d = {
3069
+ x: parseFloat(c.dataset.startX),
3070
+ y: parseFloat(c.dataset.startY)
3071
+ }, f = {
3072
+ x: parseFloat(c.dataset.endX),
3073
+ y: parseFloat(c.dataset.endY)
3074
+ }, b = parseFloat(c.dataset.imageWidth), p = parseFloat(c.dataset.imageHeight), g = parseFloat(c.dataset.rotation), m = parseFloat(c.dataset.scale), E = c.dataset.startRotation ? parseFloat(c.dataset.startRotation) : g, v = c.dataset.startScale ? parseFloat(c.dataset.startScale) : m, w = this.entryAnimationEngine.getTiming();
3075
+ oe({
3076
+ element: c,
3077
+ startPosition: d,
3078
+ endPosition: f,
3079
+ pathConfig: this.entryAnimationEngine.getPathConfig(),
3080
+ duration: w.duration,
3081
+ imageWidth: b,
3082
+ imageHeight: p,
3083
+ rotation: g,
3084
+ scale: m,
3085
+ rotationConfig: this.entryAnimationEngine.getRotationConfig(),
3086
+ startRotation: E,
3087
+ scaleConfig: this.entryAnimationEngine.getScaleConfig(),
3088
+ startScale: v
3089
+ });
3090
+ } else {
3091
+ const d = c.dataset.finalTransform || "";
3092
+ c.style.transform = d;
3093
+ }
3094
+ const h = parseInt(c.dataset.imageId || "0");
3095
+ if (this.fullConfig.config.debug?.enabled && h < 3) {
3096
+ const d = c.dataset.finalTransform || "";
3097
+ console.log(`Image ${h} final state:`, {
3098
+ left: c.style.left,
3099
+ top: c.style.top,
3100
+ width: c.style.width,
3101
+ height: c.style.height,
3102
+ computedWidth: c.offsetWidth,
3103
+ computedHeight: c.offsetHeight,
3104
+ transform: d,
3105
+ pathType: this.entryAnimationEngine.getPathType()
3106
+ });
3107
+ }
3108
+ }), r++);
3109
+ }, l = () => {
3110
+ if (this.logDebug("Starting queue processing, enabled:", this.fullConfig.animation.queue.enabled), !this.fullConfig.animation.queue.enabled) {
3111
+ for (; this.displayQueue.length > 0; ) {
3112
+ const c = this.displayQueue.shift();
3113
+ c && a(c);
3114
+ }
3115
+ return;
3116
+ }
3117
+ this.queueInterval !== null && clearInterval(this.queueInterval), this.queueInterval = window.setInterval(() => {
3118
+ if (o !== this.loadGeneration) {
3119
+ this.queueInterval !== null && (clearInterval(this.queueInterval), this.queueInterval = null);
3120
+ return;
3121
+ }
3122
+ if (this.displayQueue.length > 0) {
3123
+ const c = this.displayQueue.shift();
3124
+ c && a(c);
3125
+ }
3126
+ r >= t.length && this.displayQueue.length === 0 && this.queueInterval !== null && (clearInterval(this.queueInterval), this.queueInterval = null);
3127
+ }, this.fullConfig.animation.queue.interval);
3128
+ };
3129
+ if ("IntersectionObserver" in window && this.containerEl) {
3130
+ const c = new IntersectionObserver((u) => {
3131
+ u.forEach((h) => {
3132
+ h.isIntersecting && (l(), c.disconnect());
3133
+ });
3134
+ }, { threshold: 0.1, rootMargin: "50px" });
3135
+ c.observe(this.containerEl);
3136
+ } else
3137
+ l();
3138
+ this.fullConfig.config.debug?.centers && this.containerEl && (this.containerEl.querySelectorAll(".fbn-ic-debug-center").forEach((c) => c.remove()), s.forEach((c, u) => {
3139
+ const h = document.createElement("div");
3140
+ h.className = "fbn-ic-debug-center", h.style.position = "absolute", h.style.width = "12px", h.style.height = "12px", h.style.borderRadius = "50%", h.style.backgroundColor = "red", h.style.border = "2px solid yellow", h.style.zIndex = "9999", h.style.pointerEvents = "none";
3141
+ const d = c.x, f = c.y;
3142
+ h.style.left = `${d - 6}px`, h.style.top = `${f - 6}px`, h.title = `Image ${u}: center (${Math.round(d)}, ${Math.round(f)})`, this.containerEl.appendChild(h);
3143
+ })), t.forEach((c, u) => {
3144
+ const h = document.createElement("img");
3145
+ h.referrerPolicy = "no-referrer", h.classList.add("fbn-ic-image"), h.dataset.imageId = String(u);
3146
+ const d = s[u];
3147
+ h.style.position = "absolute", h.style.width = "auto", h.style.height = `${e}px`, h.style.left = `${d.x}px`, h.style.top = `${d.y}px`, d.zIndex && (h.style.zIndex = String(d.zIndex)), tt(h, this.defaultStyles), et(h, this.defaultClassName), h.addEventListener("mouseenter", () => {
3148
+ this.hoveredImage = { element: h, layout: d }, this.zoomEngine.isInvolved(h) || (tt(h, this.hoverStyles), et(h, this.hoverClassName));
3149
+ }), h.addEventListener("mouseleave", () => {
3150
+ this.hoveredImage = null, this.zoomEngine.isInvolved(h) || (tt(h, this.defaultStyles), zt(h, this.hoverClassName), et(h, this.defaultClassName));
3151
+ }), h.addEventListener("click", (f) => {
3152
+ f.stopPropagation(), this.handleImageClick(h, d);
3153
+ }), h.style.opacity = "0", h.style.transition = this.entryAnimationEngine.getTransitionCSS(), h.onload = () => {
3154
+ if (o !== this.loadGeneration)
3155
+ return;
3156
+ const f = h.naturalWidth / h.naturalHeight, b = e * f;
3157
+ h.style.width = `${b}px`;
3158
+ const p = { x: d.x, y: d.y }, g = { width: b, height: e }, m = this.entryAnimationEngine.calculateStartPosition(
3159
+ p,
3160
+ g,
3161
+ i,
3162
+ u,
3163
+ t.length
3164
+ ), E = this.entryAnimationEngine.calculateStartRotation(d.rotation), v = this.entryAnimationEngine.calculateStartScale(d.scale), w = this.entryAnimationEngine.buildFinalTransform(
3165
+ d.rotation,
3166
+ d.scale,
3167
+ b,
3168
+ e
3169
+ ), S = this.entryAnimationEngine.buildStartTransform(
3170
+ m,
3171
+ p,
3172
+ d.rotation,
3173
+ d.scale,
3174
+ b,
3175
+ e,
3176
+ E,
3177
+ v
3178
+ );
3179
+ this.fullConfig.config.debug?.enabled && u < 3 && console.log(`Image ${u}:`, {
3180
+ finalPosition: p,
3181
+ imageSize: g,
3182
+ left: d.x,
3183
+ top: d.y,
3184
+ finalTransform: w,
3185
+ renderedWidth: b,
3186
+ renderedHeight: e
3187
+ }), h.style.transform = S, h.dataset.finalTransform = w, (this.entryAnimationEngine.requiresJSAnimation() || this.entryAnimationEngine.requiresJSRotation() || this.entryAnimationEngine.requiresJSScale() || E !== d.rotation || v !== d.scale) && (h.dataset.startX = String(m.x), h.dataset.startY = String(m.y), h.dataset.endX = String(p.x), h.dataset.endY = String(p.y), h.dataset.imageWidth = String(b), h.dataset.imageHeight = String(e), h.dataset.rotation = String(d.rotation), h.dataset.scale = String(d.scale), h.dataset.startRotation = String(E), h.dataset.startScale = String(v)), this.displayQueue.push(h);
3188
+ }, h.onerror = () => r++, h.src = c;
3189
+ });
3190
+ }
3191
+ async handleImageClick(t, e) {
3192
+ if (!this.containerEl) return;
3193
+ const i = this.zoomEngine.isFocused(t), o = {
3194
+ width: this.containerEl.offsetWidth,
3195
+ height: this.containerEl.offsetHeight
3196
+ };
3197
+ if (i)
3198
+ await this.zoomEngine.unfocusImage(), this.currentFocusIndex = null, this.swipeEngine?.disable(), this.hideCounter();
3199
+ else {
3200
+ const s = t.dataset.imageId;
3201
+ this.currentFocusIndex = s !== void 0 ? parseInt(s, 10) : null, this.swipeEngine?.enable(), await this.zoomEngine.focusImage(t, o, e), this.currentFocusIndex !== null && this.updateCounter(this.currentFocusIndex);
3202
+ }
3203
+ }
3204
+ /**
3205
+ * Clear the image cloud and reset state
3206
+ */
3207
+ clearImageCloud() {
3208
+ this.queueInterval !== null && (clearInterval(this.queueInterval), this.queueInterval = null), this.loadGeneration++, this.displayQueue = [], this.containerEl && this.containerEl.querySelectorAll(".fbn-ic-image, .fbn-ic-debug-center").forEach((t) => t.remove()), this.imageElements = [], this.imageLayouts = [], this.currentFocusIndex = null, this.hoveredImage = null, this.layoutEngine.reset(), this.zoomEngine.reset(), this.imagesLoaded = !1;
3209
+ }
3210
+ showLoading(t) {
3211
+ !this.fullConfig.rendering.ui.showLoadingSpinner || !this.loadingEl || (t ? this.loadingEl.classList.remove("fbn-ic-hidden") : this.loadingEl.classList.add("fbn-ic-hidden"));
3212
+ }
3213
+ showError(t) {
3214
+ this.errorEl && (this.errorEl.textContent = t, this.errorEl.classList.remove("fbn-ic-hidden"));
3215
+ }
3216
+ hideError() {
3217
+ this.errorEl && this.errorEl.classList.add("fbn-ic-hidden");
3218
+ }
3219
+ updateCounter(t) {
3220
+ !this.fullConfig.rendering.ui.showImageCounter || !this.counterEl || (this.counterEl.textContent = `${t + 1} of ${this.imageElements.length}`, this.counterEl.classList.remove("fbn-ic-hidden"));
3221
+ }
3222
+ hideCounter() {
3223
+ this.counterEl && this.counterEl.classList.add("fbn-ic-hidden");
3224
+ }
3225
+ /**
3226
+ * Destroy the gallery and clean up resources
3227
+ */
3228
+ destroy() {
3229
+ this.clearImageCloud(), this.loadingElAutoCreated && this.loadingEl && (this.loadingEl.remove(), this.loadingEl = null), this.errorElAutoCreated && this.errorEl && (this.errorEl.remove(), this.errorEl = null), this.counterElAutoCreated && this.counterEl && (this.counterEl.remove(), this.counterEl = null), this.resizeTimeout !== null && clearTimeout(this.resizeTimeout), this.swipeEngine?.destroy();
3230
+ }
3231
+ };
3232
+ const He = $t(
3233
+ function({ className: t, style: e, ...i }, o) {
3234
+ const s = wt(null), r = wt(null);
3235
+ return Ut(o, () => ({
3236
+ get instance() {
3237
+ return r.current;
3238
+ }
3239
+ })), Pt(() => {
3240
+ if (!s.current) return;
3241
+ const a = new $e({
3242
+ container: s.current,
3243
+ ...i
3244
+ });
3245
+ return r.current = a, a.init().catch((l) => {
3246
+ console.error("ImageCloud init failed:", l);
3247
+ }), () => {
3248
+ a.destroy(), r.current = null;
3249
+ };
3250
+ }, [JSON.stringify(i)]), /* @__PURE__ */ Ot("div", { ref: s, className: t, style: e });
3251
+ }
3252
+ );
3253
+ export {
3254
+ He as ImageCloud
3255
+ };
3256
+ //# sourceMappingURL=react.js.map