@beautifi/plugin 0.1.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.
@@ -0,0 +1,1619 @@
1
+ class LivePhotoError extends Error {
2
+ constructor(message, code, originalError) {
3
+ super(message);
4
+ this.code = code;
5
+ this.originalError = originalError;
6
+ this.name = "LivePhotoError";
7
+ if (Error.captureStackTrace) {
8
+ Error.captureStackTrace(this, LivePhotoError);
9
+ }
10
+ }
11
+ }
12
+ const DEFAULT_OPTIONS = {
13
+ endpoint: "https://us-central1-magic-mirror-427812.cloudfunctions.net/beautifi-animate/animate",
14
+ selector: "img",
15
+ intensity: "subtle",
16
+ type: "auto",
17
+ loop: true,
18
+ respectReducedMotion: true,
19
+ debug: false,
20
+ threshold: 0.1,
21
+ rootMargin: "50px",
22
+ timeout: 1e4,
23
+ maxRetries: 3,
24
+ mode: "auto",
25
+ videoEndpoint: "https://us-central1-magic-mirror-427812.cloudfunctions.net/beautifi-animate"
26
+ };
27
+ const DEFAULT_ANIMATION_CONFIG = {
28
+ enabled: true,
29
+ intensity: "subtle",
30
+ type: "auto",
31
+ loop: true,
32
+ delay: 0
33
+ };
34
+ function generateId() {
35
+ return `lmp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
36
+ }
37
+ function parseDataAttributes(element) {
38
+ const enabled = element.dataset.live !== "false";
39
+ const intensityAttr = element.dataset.liveIntensity;
40
+ const intensity = intensityAttr === "subtle" || intensityAttr === "medium" || intensityAttr === "strong" ? intensityAttr : DEFAULT_ANIMATION_CONFIG.intensity;
41
+ const typeAttr = element.dataset.liveType;
42
+ const type = typeAttr === "breathing" || typeAttr === "parallax" || typeAttr === "sway" || typeAttr === "auto" ? typeAttr : DEFAULT_ANIMATION_CONFIG.type;
43
+ const loop = element.dataset.liveLoop !== "false";
44
+ const delay = parseInt(element.dataset.liveDelay || "0", 10) || 0;
45
+ return {
46
+ enabled,
47
+ intensity,
48
+ type,
49
+ loop,
50
+ delay
51
+ };
52
+ }
53
+ class ImageDetector {
54
+ constructor(selector = "img", debug = false) {
55
+ this.trackedImages = /* @__PURE__ */ new Map();
56
+ this.processedElements = /* @__PURE__ */ new WeakSet();
57
+ this.mutationObserver = null;
58
+ this.onImageDetected = null;
59
+ this.selector = selector;
60
+ this.debug = debug;
61
+ }
62
+ /**
63
+ * Set callback for when new images are detected
64
+ */
65
+ setOnImageDetected(callback) {
66
+ this.onImageDetected = callback;
67
+ }
68
+ /**
69
+ * Scan the DOM for images matching the selector
70
+ */
71
+ scan() {
72
+ const elements = document.querySelectorAll(this.selector);
73
+ const newImages = [];
74
+ elements.forEach((element) => {
75
+ if (this.processedElements.has(element)) {
76
+ return;
77
+ }
78
+ const config = parseDataAttributes(element);
79
+ if (!config.enabled) {
80
+ this.processedElements.add(element);
81
+ return;
82
+ }
83
+ const trackedImage = {
84
+ element,
85
+ id: generateId(),
86
+ state: "idle",
87
+ config
88
+ };
89
+ this.trackedImages.set(trackedImage.id, trackedImage);
90
+ this.processedElements.add(element);
91
+ newImages.push(trackedImage);
92
+ if (this.debug) {
93
+ console.log("[LiveMyPhotos] Detected image:", element.src, config);
94
+ }
95
+ });
96
+ return newImages;
97
+ }
98
+ /**
99
+ * Start observing the DOM for dynamically added images
100
+ */
101
+ observe() {
102
+ if (this.mutationObserver) {
103
+ return;
104
+ }
105
+ this.mutationObserver = new MutationObserver((mutations) => {
106
+ let shouldScan = false;
107
+ for (const mutation of mutations) {
108
+ if (mutation.type === "childList") {
109
+ for (const node of mutation.addedNodes) {
110
+ if (node instanceof HTMLImageElement || node instanceof Element && node.querySelector(this.selector)) {
111
+ shouldScan = true;
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ if (shouldScan) break;
117
+ }
118
+ if (shouldScan) {
119
+ const newImages = this.scan();
120
+ newImages.forEach((image) => {
121
+ if (this.onImageDetected) {
122
+ this.onImageDetected(image);
123
+ }
124
+ });
125
+ }
126
+ });
127
+ this.mutationObserver.observe(document.body, {
128
+ childList: true,
129
+ subtree: true
130
+ });
131
+ }
132
+ /**
133
+ * Get all tracked images
134
+ */
135
+ getTrackedImages() {
136
+ return Array.from(this.trackedImages.values());
137
+ }
138
+ /**
139
+ * Get a tracked image by ID
140
+ */
141
+ getTrackedImage(id) {
142
+ return this.trackedImages.get(id);
143
+ }
144
+ /**
145
+ * Update a tracked image's state
146
+ */
147
+ updateImageState(id, updates) {
148
+ const image = this.trackedImages.get(id);
149
+ if (image) {
150
+ Object.assign(image, updates);
151
+ }
152
+ }
153
+ /**
154
+ * Clean up and stop observing
155
+ */
156
+ destroy() {
157
+ if (this.mutationObserver) {
158
+ this.mutationObserver.disconnect();
159
+ this.mutationObserver = null;
160
+ }
161
+ this.trackedImages.clear();
162
+ this.onImageDetected = null;
163
+ }
164
+ }
165
+ class ViewportObserver {
166
+ constructor(callback, options = {}) {
167
+ this.observer = null;
168
+ this.visibilityHandler = null;
169
+ this.observedElements = /* @__PURE__ */ new Map();
170
+ this.isDocumentVisible = true;
171
+ this.callback = callback;
172
+ this.threshold = options.threshold ?? 0.1;
173
+ this.rootMargin = options.rootMargin ?? "50px";
174
+ this.debug = options.debug ?? false;
175
+ this.isDocumentVisible = typeof document !== "undefined" ? !document.hidden : true;
176
+ }
177
+ /**
178
+ * Initialize the observer
179
+ */
180
+ init() {
181
+ if (this.observer) {
182
+ return;
183
+ }
184
+ this.observer = new IntersectionObserver(
185
+ (entries) => {
186
+ entries.forEach((entry) => {
187
+ const image = this.observedElements.get(entry.target);
188
+ if (image && this.isDocumentVisible) {
189
+ if (this.debug) {
190
+ console.log(
191
+ "[LiveMyPhotos] Viewport:",
192
+ entry.isIntersecting ? "enter" : "leave",
193
+ image.element.src
194
+ );
195
+ }
196
+ this.callback(image, entry.isIntersecting);
197
+ }
198
+ });
199
+ },
200
+ {
201
+ threshold: this.threshold,
202
+ rootMargin: this.rootMargin
203
+ }
204
+ );
205
+ this.visibilityHandler = () => {
206
+ const wasVisible = this.isDocumentVisible;
207
+ this.isDocumentVisible = !document.hidden;
208
+ if (wasVisible !== this.isDocumentVisible) {
209
+ if (this.debug) {
210
+ console.log("[LiveMyPhotos] Document visibility:", this.isDocumentVisible);
211
+ }
212
+ this.observedElements.forEach((image) => {
213
+ this.callback(image, this.isDocumentVisible);
214
+ });
215
+ }
216
+ };
217
+ document.addEventListener("visibilitychange", this.visibilityHandler);
218
+ }
219
+ /**
220
+ * Start observing an image
221
+ */
222
+ observe(image) {
223
+ if (!this.observer) {
224
+ this.init();
225
+ }
226
+ if (!this.observedElements.has(image.element)) {
227
+ this.observedElements.set(image.element, image);
228
+ this.observer.observe(image.element);
229
+ }
230
+ }
231
+ /**
232
+ * Stop observing an image
233
+ */
234
+ unobserve(image) {
235
+ if (this.observer && this.observedElements.has(image.element)) {
236
+ this.observer.unobserve(image.element);
237
+ this.observedElements.delete(image.element);
238
+ }
239
+ }
240
+ /**
241
+ * Check if document is currently visible
242
+ */
243
+ isVisible() {
244
+ return this.isDocumentVisible;
245
+ }
246
+ /**
247
+ * Clean up resources
248
+ */
249
+ destroy() {
250
+ if (this.observer) {
251
+ this.observer.disconnect();
252
+ this.observer = null;
253
+ }
254
+ if (this.visibilityHandler) {
255
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
256
+ this.visibilityHandler = null;
257
+ }
258
+ this.observedElements.clear();
259
+ }
260
+ }
261
+ const DEFAULT_CONFIG$2 = {
262
+ timeout: 1e4,
263
+ maxRetries: 3,
264
+ enableCache: true,
265
+ debug: false
266
+ };
267
+ const CACHE_KEY_PREFIX = "lmp_cache_";
268
+ const CACHE_TTL = 30 * 24 * 60 * 60 * 1e3;
269
+ class GeminiApiClient {
270
+ constructor(config) {
271
+ this.config = {
272
+ ...DEFAULT_CONFIG$2,
273
+ ...config
274
+ };
275
+ }
276
+ /**
277
+ * Fetch animation data for an image
278
+ *
279
+ * @param imageUrl - URL of the image to animate
280
+ * @param options - Animation options
281
+ * @returns Promise resolving to animation response
282
+ */
283
+ async fetch(imageUrl, options = {}) {
284
+ const imageHash = await this.computeHash(imageUrl);
285
+ if (this.config.enableCache) {
286
+ const cached = this.getFromCache(imageHash);
287
+ if (cached) {
288
+ if (this.config.debug) {
289
+ console.log("[GeminiApiClient] Cache hit for:", imageUrl);
290
+ }
291
+ return { ...cached, cacheStatus: "hit" };
292
+ }
293
+ }
294
+ const request = {
295
+ imageUrl,
296
+ imageHash,
297
+ type: options.type ?? "auto",
298
+ intensity: options.intensity ?? "subtle",
299
+ loop: options.loop ?? true
300
+ };
301
+ const response = await this.fetchWithRetry(request);
302
+ if (response.success && this.config.enableCache) {
303
+ this.saveToCache(imageHash, response);
304
+ }
305
+ return { ...response, cacheStatus: "miss" };
306
+ }
307
+ /**
308
+ * Fetch with retry logic and exponential backoff
309
+ */
310
+ async fetchWithRetry(request, attempt = 0) {
311
+ try {
312
+ return await this.fetchWithTimeout(request);
313
+ } catch (error) {
314
+ const isRetryable = error instanceof LivePhotoError && (error.code === "NETWORK_ERROR" || error.code === "TIMEOUT_ERROR");
315
+ if (isRetryable && attempt < this.config.maxRetries) {
316
+ const delay = Math.pow(2, attempt) * 1e3;
317
+ if (this.config.debug) {
318
+ console.log(
319
+ `[GeminiApiClient] Retry ${attempt + 1}/${this.config.maxRetries} after ${delay}ms`
320
+ );
321
+ }
322
+ await this.sleep(delay);
323
+ return this.fetchWithRetry(request, attempt + 1);
324
+ }
325
+ throw error;
326
+ }
327
+ }
328
+ /**
329
+ * Fetch with timeout handling
330
+ */
331
+ async fetchWithTimeout(request) {
332
+ const controller = new AbortController();
333
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
334
+ try {
335
+ const response = await fetch(this.config.endpoint, {
336
+ method: "POST",
337
+ headers: {
338
+ "Content-Type": "application/json"
339
+ },
340
+ body: JSON.stringify(request),
341
+ signal: controller.signal
342
+ });
343
+ clearTimeout(timeoutId);
344
+ if (!response.ok) {
345
+ if (response.status === 429) {
346
+ throw new LivePhotoError(
347
+ "Rate limit exceeded",
348
+ "RATE_LIMIT_ERROR"
349
+ );
350
+ }
351
+ throw new LivePhotoError(
352
+ `API error: ${response.status} ${response.statusText}`,
353
+ "API_ERROR"
354
+ );
355
+ }
356
+ const data = await response.json();
357
+ return data;
358
+ } catch (error) {
359
+ clearTimeout(timeoutId);
360
+ if (error instanceof LivePhotoError) {
361
+ throw error;
362
+ }
363
+ if (error instanceof Error) {
364
+ if (error.name === "AbortError") {
365
+ throw new LivePhotoError(
366
+ `Request timed out after ${this.config.timeout}ms`,
367
+ "TIMEOUT_ERROR",
368
+ error
369
+ );
370
+ }
371
+ throw new LivePhotoError(
372
+ `Network error: ${error.message}`,
373
+ "NETWORK_ERROR",
374
+ error
375
+ );
376
+ }
377
+ throw new LivePhotoError("Unknown error occurred", "UNKNOWN_ERROR");
378
+ }
379
+ }
380
+ /**
381
+ * Compute SHA-256 hash of a string (for cache keys)
382
+ */
383
+ async computeHash(input) {
384
+ if (typeof crypto !== "undefined" && crypto.subtle) {
385
+ const encoder = new TextEncoder();
386
+ const data = encoder.encode(input);
387
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
388
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
389
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
390
+ }
391
+ let hash = 0;
392
+ for (let i = 0; i < input.length; i++) {
393
+ const char = input.charCodeAt(i);
394
+ hash = (hash << 5) - hash + char;
395
+ hash = hash & hash;
396
+ }
397
+ return Math.abs(hash).toString(16);
398
+ }
399
+ /**
400
+ * Get cached response if valid
401
+ */
402
+ getFromCache(imageHash) {
403
+ if (typeof localStorage === "undefined") {
404
+ return null;
405
+ }
406
+ try {
407
+ const cacheKey = CACHE_KEY_PREFIX + imageHash;
408
+ const cached = localStorage.getItem(cacheKey);
409
+ if (!cached) {
410
+ return null;
411
+ }
412
+ const parsed = JSON.parse(cached);
413
+ if (Date.now() - parsed.timestamp > CACHE_TTL) {
414
+ localStorage.removeItem(cacheKey);
415
+ return null;
416
+ }
417
+ return parsed.data;
418
+ } catch {
419
+ return null;
420
+ }
421
+ }
422
+ /**
423
+ * Save response to cache
424
+ */
425
+ saveToCache(imageHash, response) {
426
+ if (typeof localStorage === "undefined") {
427
+ return;
428
+ }
429
+ try {
430
+ const cacheKey = CACHE_KEY_PREFIX + imageHash;
431
+ const cached = {
432
+ data: response,
433
+ timestamp: Date.now()
434
+ };
435
+ localStorage.setItem(cacheKey, JSON.stringify(cached));
436
+ } catch {
437
+ if (this.config.debug) {
438
+ console.warn("[GeminiApiClient] Failed to cache response");
439
+ }
440
+ }
441
+ }
442
+ /**
443
+ * Clear all cached responses
444
+ */
445
+ clearCache() {
446
+ if (typeof localStorage === "undefined") {
447
+ return;
448
+ }
449
+ const keysToRemove = [];
450
+ for (let i = 0; i < localStorage.length; i++) {
451
+ const key = localStorage.key(i);
452
+ if (key && key.startsWith(CACHE_KEY_PREFIX)) {
453
+ keysToRemove.push(key);
454
+ }
455
+ }
456
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
457
+ }
458
+ /**
459
+ * Sleep utility for retry delays
460
+ */
461
+ sleep(ms) {
462
+ return new Promise((resolve) => setTimeout(resolve, ms));
463
+ }
464
+ }
465
+ const INTENSITY_MULTIPLIERS = {
466
+ subtle: 0.3,
467
+ medium: 0.6,
468
+ strong: 1
469
+ };
470
+ const ANIMATION_PRESETS = {
471
+ breathing: {
472
+ translateX: 0,
473
+ translateY: 0,
474
+ scale: 0.02,
475
+ rotate: 0,
476
+ duration: 3e3
477
+ },
478
+ parallax: {
479
+ translateX: 5,
480
+ translateY: 3,
481
+ scale: 0.01,
482
+ rotate: 0,
483
+ duration: 4e3
484
+ },
485
+ sway: {
486
+ translateX: 3,
487
+ translateY: 0,
488
+ scale: 0,
489
+ rotate: 2,
490
+ duration: 2500
491
+ }
492
+ };
493
+ class AnimationRenderer {
494
+ constructor(config = {}) {
495
+ this.animations = /* @__PURE__ */ new Map();
496
+ this.config = {
497
+ frameRate: config.frameRate ?? 60,
498
+ debug: config.debug ?? false
499
+ };
500
+ }
501
+ /**
502
+ * Start animation playback for an image
503
+ *
504
+ * @param trackedImage - The tracked image to animate
505
+ * @param animationData - Animation data from Gemini API
506
+ * @param options - Animation options
507
+ */
508
+ play(trackedImage, animationData, options = {}) {
509
+ const { id, element } = trackedImage;
510
+ this.stop(id);
511
+ element.style.willChange = "transform";
512
+ element.style.transformOrigin = "center center";
513
+ const state = {
514
+ trackedImage,
515
+ animationData,
516
+ currentFrame: 0,
517
+ animationFrameId: null,
518
+ startTime: performance.now(),
519
+ isPaused: false,
520
+ intensity: options.intensity ?? trackedImage.config.intensity,
521
+ loop: options.loop ?? trackedImage.config.loop,
522
+ onComplete: options.onComplete
523
+ };
524
+ this.animations.set(id, state);
525
+ if (this.config.debug) {
526
+ console.log("[AnimationRenderer] Starting animation for:", id);
527
+ }
528
+ this.renderLoop(id);
529
+ }
530
+ /**
531
+ * Pause animation for an image
532
+ */
533
+ pause(imageId) {
534
+ const state = this.animations.get(imageId);
535
+ if (state && !state.isPaused) {
536
+ state.isPaused = true;
537
+ if (state.animationFrameId !== null) {
538
+ cancelAnimationFrame(state.animationFrameId);
539
+ state.animationFrameId = null;
540
+ }
541
+ if (this.config.debug) {
542
+ console.log("[AnimationRenderer] Paused animation for:", imageId);
543
+ }
544
+ }
545
+ }
546
+ /**
547
+ * Resume animation for an image
548
+ */
549
+ resume(imageId) {
550
+ const state = this.animations.get(imageId);
551
+ if (state && state.isPaused) {
552
+ state.isPaused = false;
553
+ state.startTime = performance.now() - this.getElapsedTime(state);
554
+ this.renderLoop(imageId);
555
+ if (this.config.debug) {
556
+ console.log("[AnimationRenderer] Resumed animation for:", imageId);
557
+ }
558
+ }
559
+ }
560
+ /**
561
+ * Stop animation for an image and reset transforms
562
+ */
563
+ stop(imageId) {
564
+ const state = this.animations.get(imageId);
565
+ if (state) {
566
+ if (state.animationFrameId !== null) {
567
+ cancelAnimationFrame(state.animationFrameId);
568
+ }
569
+ state.trackedImage.element.style.transform = "";
570
+ state.trackedImage.element.style.willChange = "";
571
+ this.animations.delete(imageId);
572
+ if (this.config.debug) {
573
+ console.log("[AnimationRenderer] Stopped animation for:", imageId);
574
+ }
575
+ }
576
+ }
577
+ /**
578
+ * Check if an image is currently animating
579
+ */
580
+ isPlaying(imageId) {
581
+ const state = this.animations.get(imageId);
582
+ return state !== void 0 && !state.isPaused;
583
+ }
584
+ /**
585
+ * Get all currently animating image IDs
586
+ */
587
+ getActiveAnimations() {
588
+ return Array.from(this.animations.keys());
589
+ }
590
+ /**
591
+ * Stop all animations and clean up
592
+ */
593
+ destroy() {
594
+ for (const imageId of this.animations.keys()) {
595
+ this.stop(imageId);
596
+ }
597
+ this.animations.clear();
598
+ }
599
+ /**
600
+ * Main render loop using requestAnimationFrame
601
+ */
602
+ renderLoop(imageId) {
603
+ const state = this.animations.get(imageId);
604
+ if (!state || state.isPaused) {
605
+ return;
606
+ }
607
+ const elapsed = performance.now() - state.startTime;
608
+ const duration = state.animationData.duration || 3e3;
609
+ const progress = elapsed % duration / duration;
610
+ if (!state.loop && elapsed >= duration) {
611
+ this.stop(imageId);
612
+ if (state.onComplete) {
613
+ state.onComplete();
614
+ }
615
+ return;
616
+ }
617
+ this.renderFrame(state, progress);
618
+ state.animationFrameId = requestAnimationFrame(() => {
619
+ this.renderLoop(imageId);
620
+ });
621
+ }
622
+ /**
623
+ * Render a single animation frame
624
+ */
625
+ renderFrame(state, progress) {
626
+ const { trackedImage, animationData, intensity } = state;
627
+ const multiplier = INTENSITY_MULTIPLIERS[intensity];
628
+ let transform;
629
+ if (animationData.frames && animationData.frames.length > 0) {
630
+ const frameIndex = Math.floor(progress * animationData.frames.length);
631
+ const frame = animationData.frames[Math.min(frameIndex, animationData.frames.length - 1)];
632
+ transform = frame.transform;
633
+ } else {
634
+ const animType = animationData.detectedType || "breathing";
635
+ if (animType !== "auto") {
636
+ transform = this.generateTransformFromType(animType, progress);
637
+ } else {
638
+ transform = this.generateTransformFromType("breathing", progress);
639
+ }
640
+ }
641
+ const translateX = transform.translateX * multiplier;
642
+ const translateY = transform.translateY * multiplier;
643
+ const scale = 1 + transform.scale * multiplier;
644
+ const rotate = transform.rotate * multiplier;
645
+ trackedImage.element.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale}) rotate(${rotate}deg)`;
646
+ }
647
+ /**
648
+ * Generate transform values for a given animation type
649
+ */
650
+ generateTransformFromType(type, progress) {
651
+ const preset = ANIMATION_PRESETS[type] || ANIMATION_PRESETS.breathing;
652
+ const oscillation = Math.sin(progress * Math.PI * 2);
653
+ return {
654
+ translateX: preset.translateX * oscillation,
655
+ translateY: preset.translateY * oscillation,
656
+ scale: preset.scale * oscillation,
657
+ rotate: preset.rotate * oscillation
658
+ };
659
+ }
660
+ /**
661
+ * Calculate elapsed time for a paused animation
662
+ */
663
+ getElapsedTime(state) {
664
+ const duration = state.animationData.duration || 3e3;
665
+ return state.currentFrame / state.animationData.frameRate * 1e3 % duration;
666
+ }
667
+ }
668
+ const DEFAULT_CONFIG$1 = {
669
+ apiKey: "",
670
+ debug: false,
671
+ pollInterval: 3e3,
672
+ maxPollAttempts: 60,
673
+ cacheTTL: 30 * 24 * 60 * 60 * 1e3
674
+ // 30 days
675
+ };
676
+ const VIDEO_CACHE_PREFIX = "beautifi_video_";
677
+ class VeoClient {
678
+ constructor(config) {
679
+ this.pendingOperations = /* @__PURE__ */ new Map();
680
+ this.config = {
681
+ ...DEFAULT_CONFIG$1,
682
+ ...config
683
+ };
684
+ }
685
+ /**
686
+ * Start video generation for an image
687
+ *
688
+ * @param request - Video generation request
689
+ * @returns Promise resolving to operation ID
690
+ */
691
+ async startGeneration(request) {
692
+ var _a;
693
+ const cacheKey = this.computeCacheKey(request.imageUrl);
694
+ const cached = this.getFromCache(cacheKey);
695
+ if (cached) {
696
+ this.log(`📦 Video cached: ${cacheKey.slice(0, 8)}...`);
697
+ return {
698
+ success: true,
699
+ status: "completed",
700
+ videoUrl: cached
701
+ };
702
+ }
703
+ try {
704
+ const endpoint = this.config.endpoint.replace("/animate", "/animate-auto");
705
+ this.log(`🎬 Starting video generation for: ${request.imageUrl.slice(0, 50)}...`);
706
+ const headers = {
707
+ "Content-Type": "application/json"
708
+ };
709
+ if (this.config.apiKey) {
710
+ headers["X-API-Key"] = this.config.apiKey;
711
+ }
712
+ const response = await fetch(endpoint, {
713
+ method: "POST",
714
+ headers,
715
+ body: JSON.stringify({
716
+ imageUrl: request.imageUrl,
717
+ animationPrompt: request.animationPrompt,
718
+ aspectRatio: request.aspectRatio,
719
+ durationSeconds: request.durationSeconds
720
+ })
721
+ });
722
+ if (!response.ok) {
723
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
724
+ }
725
+ const data = await response.json();
726
+ if (!data.success) {
727
+ throw new Error(((_a = data.error) == null ? void 0 : _a.message) || "Video generation failed");
728
+ }
729
+ this.log(`🚀 Generation started: ${data.operationId}`);
730
+ return data;
731
+ } catch (error) {
732
+ this.log(`❌ Start generation error: ${error}`);
733
+ return {
734
+ success: false,
735
+ error: {
736
+ code: "GENERATION_ERROR",
737
+ message: error instanceof Error ? error.message : "Failed to start generation"
738
+ }
739
+ };
740
+ }
741
+ }
742
+ /**
743
+ * Poll for video generation status until complete
744
+ *
745
+ * @param operationId - Operation ID to poll
746
+ * @param imageUrl - Original image URL (for caching)
747
+ * @param onProgress - Callback for progress updates
748
+ * @returns Promise resolving to video URL or null on failure
749
+ */
750
+ async pollUntilComplete(operationId, imageUrl, onProgress) {
751
+ var _a;
752
+ const controller = new AbortController();
753
+ this.pendingOperations.set(operationId, controller);
754
+ try {
755
+ let attempt = 0;
756
+ while (attempt < this.config.maxPollAttempts) {
757
+ if (controller.signal.aborted) {
758
+ this.log(`🛑 Polling aborted: ${operationId}`);
759
+ return null;
760
+ }
761
+ const result = await this.checkStatus(operationId);
762
+ if (onProgress) {
763
+ onProgress(result.status || "unknown", attempt);
764
+ }
765
+ if (result.status === "completed" && result.videoUrl) {
766
+ this.log(`✅ Video ready: ${result.videoUrl.slice(0, 50)}...`);
767
+ const cacheKey = this.computeCacheKey(imageUrl);
768
+ this.saveToCache(cacheKey, result.videoUrl);
769
+ return result.videoUrl;
770
+ }
771
+ if (result.status === "error" || !result.success) {
772
+ this.log(`❌ Generation failed: ${(_a = result.error) == null ? void 0 : _a.message}`);
773
+ return null;
774
+ }
775
+ const backoffMs = Math.min(
776
+ this.config.pollInterval * Math.pow(1.5, Math.min(attempt, 5)),
777
+ 15e3
778
+ );
779
+ this.log(`⏳ Polling (${attempt + 1}/${this.config.maxPollAttempts}): ${result.status}`);
780
+ await this.sleep(backoffMs);
781
+ attempt++;
782
+ }
783
+ this.log(`⏰ Polling timeout: ${operationId}`);
784
+ return null;
785
+ } finally {
786
+ this.pendingOperations.delete(operationId);
787
+ }
788
+ }
789
+ /**
790
+ * Check the status of a video generation operation
791
+ */
792
+ async checkStatus(operationId) {
793
+ try {
794
+ const endpoint = this.config.endpoint.replace("/animate", "/generate-video/status").replace(/\/$/, "");
795
+ const headers = {};
796
+ if (this.config.apiKey) {
797
+ headers["X-API-Key"] = this.config.apiKey;
798
+ }
799
+ const response = await fetch(`${endpoint}/${operationId}`, { headers });
800
+ if (!response.ok) {
801
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
802
+ }
803
+ return await response.json();
804
+ } catch (error) {
805
+ return {
806
+ success: false,
807
+ status: "error",
808
+ error: {
809
+ code: "POLL_ERROR",
810
+ message: error instanceof Error ? error.message : "Status check failed"
811
+ }
812
+ };
813
+ }
814
+ }
815
+ /**
816
+ * Cancel a pending video generation operation
817
+ */
818
+ cancelOperation(operationId) {
819
+ const controller = this.pendingOperations.get(operationId);
820
+ if (controller) {
821
+ controller.abort();
822
+ this.pendingOperations.delete(operationId);
823
+ this.log(`🛑 Cancelled: ${operationId}`);
824
+ }
825
+ }
826
+ /**
827
+ * Cancel all pending operations
828
+ */
829
+ cancelAllOperations() {
830
+ for (const [id, controller] of this.pendingOperations) {
831
+ controller.abort();
832
+ this.log(`🛑 Cancelled: ${id}`);
833
+ }
834
+ this.pendingOperations.clear();
835
+ }
836
+ /**
837
+ * Get cached video URL for an image
838
+ */
839
+ getCachedVideo(imageUrl) {
840
+ const cacheKey = this.computeCacheKey(imageUrl);
841
+ return this.getFromCache(cacheKey);
842
+ }
843
+ /**
844
+ * Compute cache key from image URL
845
+ */
846
+ computeCacheKey(imageUrl) {
847
+ let hash = 0;
848
+ for (let i = 0; i < imageUrl.length; i++) {
849
+ const char = imageUrl.charCodeAt(i);
850
+ hash = (hash << 5) - hash + char;
851
+ hash = hash & hash;
852
+ }
853
+ return Math.abs(hash).toString(36);
854
+ }
855
+ /**
856
+ * Get video URL from localStorage cache
857
+ */
858
+ getFromCache(cacheKey) {
859
+ if (typeof localStorage === "undefined") return null;
860
+ try {
861
+ const raw = localStorage.getItem(VIDEO_CACHE_PREFIX + cacheKey);
862
+ if (!raw) return null;
863
+ const cached = JSON.parse(raw);
864
+ if (Date.now() - cached.timestamp > this.config.cacheTTL) {
865
+ localStorage.removeItem(VIDEO_CACHE_PREFIX + cacheKey);
866
+ return null;
867
+ }
868
+ return cached.videoUrl;
869
+ } catch {
870
+ return null;
871
+ }
872
+ }
873
+ /**
874
+ * Save video URL to localStorage cache
875
+ */
876
+ saveToCache(cacheKey, videoUrl) {
877
+ if (typeof localStorage === "undefined") return;
878
+ try {
879
+ const cached = {
880
+ videoUrl,
881
+ timestamp: Date.now()
882
+ };
883
+ localStorage.setItem(VIDEO_CACHE_PREFIX + cacheKey, JSON.stringify(cached));
884
+ this.log(`💾 Cached: ${cacheKey}`);
885
+ } catch {
886
+ }
887
+ }
888
+ /**
889
+ * Clear all cached videos
890
+ */
891
+ clearCache() {
892
+ if (typeof localStorage === "undefined") return;
893
+ const keysToRemove = [];
894
+ for (let i = 0; i < localStorage.length; i++) {
895
+ const key = localStorage.key(i);
896
+ if (key == null ? void 0 : key.startsWith(VIDEO_CACHE_PREFIX)) {
897
+ keysToRemove.push(key);
898
+ }
899
+ }
900
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
901
+ this.log(`🧹 Cleared ${keysToRemove.length} cached videos`);
902
+ }
903
+ /**
904
+ * Sleep utility
905
+ */
906
+ sleep(ms) {
907
+ return new Promise((resolve) => setTimeout(resolve, ms));
908
+ }
909
+ /**
910
+ * Debug logging
911
+ */
912
+ log(message) {
913
+ if (this.config.debug) {
914
+ console.log(`[VeoClient] ${message}`);
915
+ }
916
+ }
917
+ }
918
+ const DEFAULT_CONFIG = {
919
+ transitionDuration: 500,
920
+ debug: false,
921
+ autoPlay: true,
922
+ loop: true,
923
+ muted: true
924
+ };
925
+ class VideoRenderer {
926
+ constructor(config = {}) {
927
+ this.activeVideos = /* @__PURE__ */ new Map();
928
+ this.config = {
929
+ ...DEFAULT_CONFIG,
930
+ ...config
931
+ };
932
+ }
933
+ /**
934
+ * Prepare a video overlay for an image
935
+ *
936
+ * @param imageId - Unique ID for tracking
937
+ * @param imageElement - The image element to overlay
938
+ * @param videoUrl - URL of the video to play
939
+ * @param onFallback - Callback if video fails to load
940
+ * @returns Promise that resolves when video is ready to play
941
+ */
942
+ async prepareVideo(imageId, imageElement, videoUrl, onFallback) {
943
+ if (this.activeVideos.has(imageId)) {
944
+ this.log(`⚠️ Video already prepared: ${imageId}`);
945
+ return true;
946
+ }
947
+ try {
948
+ const wrapper = this.createWrapper(imageElement);
949
+ const video = this.createVideoElement(videoUrl);
950
+ wrapper.insertBefore(video, imageElement);
951
+ const entry = {
952
+ imageElement,
953
+ videoElement: video,
954
+ wrapperElement: wrapper,
955
+ state: "loading",
956
+ onFallback
957
+ };
958
+ this.activeVideos.set(imageId, entry);
959
+ await this.waitForVideoReady(video, entry);
960
+ this.log(`✅ Video prepared: ${imageId}`);
961
+ return true;
962
+ } catch (error) {
963
+ this.log(`❌ Prepare failed: ${imageId} - ${error}`);
964
+ this.cleanup(imageId);
965
+ onFallback == null ? void 0 : onFallback();
966
+ return false;
967
+ }
968
+ }
969
+ /**
970
+ * Play video and transition from CSS animation
971
+ */
972
+ play(imageId) {
973
+ var _a;
974
+ const entry = this.activeVideos.get(imageId);
975
+ if (!entry) {
976
+ this.log(`⚠️ No video found: ${imageId}`);
977
+ return false;
978
+ }
979
+ if (entry.state === "error") {
980
+ this.log(`⚠️ Video in error state: ${imageId}`);
981
+ return false;
982
+ }
983
+ try {
984
+ this.transitionToVideo(entry);
985
+ entry.videoElement.play().catch((err) => {
986
+ var _a2;
987
+ this.log(`❌ Play failed: ${err}`);
988
+ entry.state = "error";
989
+ (_a2 = entry.onFallback) == null ? void 0 : _a2.call(entry);
990
+ });
991
+ entry.state = "playing";
992
+ this.log(`▶️ Playing: ${imageId}`);
993
+ return true;
994
+ } catch (error) {
995
+ this.log(`❌ Play error: ${error}`);
996
+ entry.state = "error";
997
+ (_a = entry.onFallback) == null ? void 0 : _a.call(entry);
998
+ return false;
999
+ }
1000
+ }
1001
+ /**
1002
+ * Pause video playback
1003
+ */
1004
+ pause(imageId) {
1005
+ const entry = this.activeVideos.get(imageId);
1006
+ if (!entry || entry.state !== "playing") {
1007
+ return false;
1008
+ }
1009
+ entry.videoElement.pause();
1010
+ entry.state = "paused";
1011
+ this.log(`⏸️ Paused: ${imageId}`);
1012
+ return true;
1013
+ }
1014
+ /**
1015
+ * Resume video playback
1016
+ */
1017
+ resume(imageId) {
1018
+ const entry = this.activeVideos.get(imageId);
1019
+ if (!entry || entry.state !== "paused") {
1020
+ return false;
1021
+ }
1022
+ entry.videoElement.play().catch(() => {
1023
+ entry.state = "error";
1024
+ });
1025
+ entry.state = "playing";
1026
+ this.log(`▶️ Resumed: ${imageId}`);
1027
+ return true;
1028
+ }
1029
+ /**
1030
+ * Stop video and remove overlay
1031
+ */
1032
+ stop(imageId) {
1033
+ const entry = this.activeVideos.get(imageId);
1034
+ if (!entry) return;
1035
+ this.transitionToImage(entry);
1036
+ setTimeout(() => {
1037
+ this.cleanup(imageId);
1038
+ }, this.config.transitionDuration);
1039
+ }
1040
+ /**
1041
+ * Check if video is playing for an image
1042
+ */
1043
+ isPlaying(imageId) {
1044
+ const entry = this.activeVideos.get(imageId);
1045
+ return (entry == null ? void 0 : entry.state) === "playing";
1046
+ }
1047
+ /**
1048
+ * Check if video is prepared for an image
1049
+ */
1050
+ isPrepared(imageId) {
1051
+ return this.activeVideos.has(imageId);
1052
+ }
1053
+ /**
1054
+ * Get current playback state
1055
+ */
1056
+ getState(imageId) {
1057
+ var _a;
1058
+ return ((_a = this.activeVideos.get(imageId)) == null ? void 0 : _a.state) || null;
1059
+ }
1060
+ /**
1061
+ * Destroy all videos and cleanup
1062
+ */
1063
+ destroy() {
1064
+ for (const imageId of this.activeVideos.keys()) {
1065
+ this.cleanup(imageId);
1066
+ }
1067
+ this.log(`🧹 Destroyed all videos`);
1068
+ }
1069
+ /**
1070
+ * Create wrapper element around image
1071
+ */
1072
+ createWrapper(imageElement) {
1073
+ var _a;
1074
+ const existingWrapper = imageElement.parentElement;
1075
+ if (existingWrapper == null ? void 0 : existingWrapper.classList.contains("beautifi-video-wrapper")) {
1076
+ return existingWrapper;
1077
+ }
1078
+ const wrapper = document.createElement("div");
1079
+ wrapper.className = "beautifi-video-wrapper";
1080
+ wrapper.style.cssText = `
1081
+ position: relative;
1082
+ display: inline-block;
1083
+ width: ${imageElement.offsetWidth}px;
1084
+ height: ${imageElement.offsetHeight}px;
1085
+ overflow: hidden;
1086
+ `;
1087
+ (_a = imageElement.parentNode) == null ? void 0 : _a.insertBefore(wrapper, imageElement);
1088
+ wrapper.appendChild(imageElement);
1089
+ imageElement.style.cssText += `
1090
+ position: relative;
1091
+ z-index: 2;
1092
+ transition: opacity ${this.config.transitionDuration}ms ease-in-out;
1093
+ `;
1094
+ return wrapper;
1095
+ }
1096
+ /**
1097
+ * Create video element with proper attributes
1098
+ */
1099
+ createVideoElement(videoUrl) {
1100
+ const video = document.createElement("video");
1101
+ video.src = videoUrl;
1102
+ video.muted = this.config.muted;
1103
+ video.loop = this.config.loop;
1104
+ video.playsInline = true;
1105
+ video.preload = "auto";
1106
+ video.crossOrigin = "anonymous";
1107
+ video.style.cssText = `
1108
+ position: absolute;
1109
+ top: 0;
1110
+ left: 0;
1111
+ width: 100%;
1112
+ height: 100%;
1113
+ object-fit: cover;
1114
+ z-index: 1;
1115
+ opacity: 0;
1116
+ transition: opacity ${this.config.transitionDuration}ms ease-in-out;
1117
+ `;
1118
+ return video;
1119
+ }
1120
+ /**
1121
+ * Wait for video to be ready to play
1122
+ */
1123
+ waitForVideoReady(video, entry) {
1124
+ return new Promise((resolve, reject) => {
1125
+ const timeout = setTimeout(() => {
1126
+ reject(new Error("Video load timeout"));
1127
+ }, 3e4);
1128
+ video.addEventListener("canplaythrough", () => {
1129
+ clearTimeout(timeout);
1130
+ entry.state = "buffering";
1131
+ resolve();
1132
+ }, { once: true });
1133
+ video.addEventListener("error", () => {
1134
+ clearTimeout(timeout);
1135
+ entry.state = "error";
1136
+ reject(new Error("Video load error"));
1137
+ }, { once: true });
1138
+ video.load();
1139
+ });
1140
+ }
1141
+ /**
1142
+ * Transition from image to video
1143
+ */
1144
+ transitionToVideo(entry) {
1145
+ entry.videoElement.style.opacity = "1";
1146
+ entry.imageElement.style.opacity = "0";
1147
+ entry.imageElement.style.animation = "none";
1148
+ }
1149
+ /**
1150
+ * Transition from video back to image
1151
+ */
1152
+ transitionToImage(entry) {
1153
+ entry.imageElement.style.opacity = "1";
1154
+ entry.videoElement.style.opacity = "0";
1155
+ entry.videoElement.pause();
1156
+ }
1157
+ /**
1158
+ * Cleanup video element and restore image
1159
+ */
1160
+ cleanup(imageId) {
1161
+ const entry = this.activeVideos.get(imageId);
1162
+ if (!entry) return;
1163
+ entry.imageElement.style.opacity = "1";
1164
+ entry.imageElement.style.animation = "";
1165
+ entry.imageElement.style.position = "";
1166
+ entry.imageElement.style.zIndex = "";
1167
+ entry.imageElement.style.transition = "";
1168
+ entry.videoElement.remove();
1169
+ if (entry.wrapperElement.classList.contains("beautifi-video-wrapper")) {
1170
+ const parent = entry.wrapperElement.parentNode;
1171
+ if (parent) {
1172
+ parent.insertBefore(entry.imageElement, entry.wrapperElement);
1173
+ entry.wrapperElement.remove();
1174
+ }
1175
+ }
1176
+ this.activeVideos.delete(imageId);
1177
+ this.log(`🧹 Cleaned up: ${imageId}`);
1178
+ }
1179
+ /**
1180
+ * Debug logging
1181
+ */
1182
+ log(message) {
1183
+ if (this.config.debug) {
1184
+ console.log(`[VideoRenderer] ${message}`);
1185
+ }
1186
+ }
1187
+ }
1188
+ class beautifiPlugin {
1189
+ constructor() {
1190
+ this.initialized = false;
1191
+ this.options = null;
1192
+ this.detector = null;
1193
+ this.viewportObserver = null;
1194
+ this.apiClient = null;
1195
+ this.renderer = null;
1196
+ this.veoClient = null;
1197
+ this.videoRenderer = null;
1198
+ this.reducedMotion = false;
1199
+ this.pendingVideoGenerations = /* @__PURE__ */ new Map();
1200
+ }
1201
+ // imageId -> operationId
1202
+ /**
1203
+ * Initialize the plugin with configuration options
1204
+ *
1205
+ * @param options - Plugin configuration
1206
+ * @throws Error if apiKey is not provided
1207
+ */
1208
+ init(options) {
1209
+ if (!options.apiKey) {
1210
+ throw new Error("beautifi: apiKey is required");
1211
+ }
1212
+ if (typeof window !== "undefined" && window.matchMedia) {
1213
+ this.reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1214
+ }
1215
+ this.options = {
1216
+ ...DEFAULT_OPTIONS,
1217
+ ...options
1218
+ };
1219
+ if (this.reducedMotion && this.options.respectReducedMotion) {
1220
+ if (this.options.debug) {
1221
+ console.log("[beautifi] Reduced motion preference detected, animations disabled");
1222
+ }
1223
+ this.initialized = true;
1224
+ return;
1225
+ }
1226
+ this.detector = new ImageDetector(this.options.selector, this.options.debug);
1227
+ this.apiClient = new GeminiApiClient({
1228
+ endpoint: this.options.endpoint || "https://us-central1-seedgpt-planter.cloudfunctions.net/beautifi-animate/animate",
1229
+ timeout: this.options.timeout,
1230
+ maxRetries: this.options.maxRetries,
1231
+ enableCache: true,
1232
+ debug: this.options.debug
1233
+ });
1234
+ this.renderer = new AnimationRenderer({
1235
+ debug: this.options.debug
1236
+ });
1237
+ if (this.options.mode !== "css") {
1238
+ this.veoClient = new VeoClient({
1239
+ endpoint: this.options.videoEndpoint || this.options.endpoint,
1240
+ debug: this.options.debug
1241
+ });
1242
+ this.videoRenderer = new VideoRenderer({
1243
+ debug: this.options.debug,
1244
+ transitionDuration: 500
1245
+ });
1246
+ if (this.options.debug) {
1247
+ console.log("[beautifi] Video mode enabled:", this.options.mode);
1248
+ }
1249
+ }
1250
+ this.viewportObserver = new ViewportObserver(
1251
+ (image, isIntersecting) => {
1252
+ this.handleVisibilityChange(image, isIntersecting);
1253
+ },
1254
+ {
1255
+ threshold: this.options.threshold,
1256
+ rootMargin: this.options.rootMargin,
1257
+ debug: this.options.debug
1258
+ }
1259
+ );
1260
+ this.detector.setOnImageDetected((image) => {
1261
+ this.viewportObserver.observe(image);
1262
+ });
1263
+ this.viewportObserver.init();
1264
+ const images = this.detector.scan();
1265
+ images.forEach((image) => {
1266
+ this.viewportObserver.observe(image);
1267
+ });
1268
+ this.detector.observe();
1269
+ this.initialized = true;
1270
+ if (this.options.debug) {
1271
+ console.log("[beautifi] Initialized with options:", this.options);
1272
+ console.log("[beautifi] Detected", images.length, "images");
1273
+ }
1274
+ }
1275
+ /**
1276
+ * Handle image entering/leaving viewport
1277
+ */
1278
+ handleVisibilityChange(image, isIntersecting) {
1279
+ if (!this.options) return;
1280
+ if (isIntersecting) {
1281
+ if (image.state === "idle") {
1282
+ this.queueAnimation(image);
1283
+ } else if (image.state === "paused") {
1284
+ this.resumeAnimation(image);
1285
+ }
1286
+ } else {
1287
+ if (image.state === "playing") {
1288
+ this.pauseAnimation(image);
1289
+ }
1290
+ }
1291
+ }
1292
+ /**
1293
+ * Queue an image for animation
1294
+ */
1295
+ async queueAnimation(image) {
1296
+ var _a, _b, _c;
1297
+ if (this.detector) {
1298
+ this.detector.updateImageState(image.id, { state: "loading" });
1299
+ }
1300
+ if ((_a = this.options) == null ? void 0 : _a.debug) {
1301
+ console.log("[beautifi] Queuing animation for:", image.element.src);
1302
+ }
1303
+ if (image.config.delay && image.config.delay > 0) {
1304
+ await new Promise((resolve) => setTimeout(resolve, image.config.delay));
1305
+ }
1306
+ const imageMode = image.element.dataset.beautifiMode || ((_b = this.options) == null ? void 0 : _b.mode) || "auto";
1307
+ try {
1308
+ const animationData = await this.apiClient.fetch(image.element.src, {
1309
+ type: image.config.type,
1310
+ intensity: image.config.intensity,
1311
+ loop: image.config.loop
1312
+ });
1313
+ if (!animationData.success) {
1314
+ throw new LivePhotoError(
1315
+ animationData.error || "Animation generation failed",
1316
+ "API_ERROR"
1317
+ );
1318
+ }
1319
+ if (this.detector) {
1320
+ this.detector.updateImageState(image.id, {
1321
+ state: "playing",
1322
+ animationData
1323
+ });
1324
+ }
1325
+ if (imageMode !== "video") {
1326
+ this.renderer.play(image, animationData, {
1327
+ intensity: image.config.intensity,
1328
+ loop: image.config.loop,
1329
+ onComplete: () => {
1330
+ if (this.detector) {
1331
+ this.detector.updateImageState(image.id, { state: "idle" });
1332
+ }
1333
+ this.emitEvent("animationComplete", image);
1334
+ }
1335
+ });
1336
+ this.emitEvent("animationStart", image);
1337
+ }
1338
+ if (imageMode !== "css" && this.veoClient && this.videoRenderer) {
1339
+ this.startVideoGeneration(image);
1340
+ }
1341
+ } catch (error) {
1342
+ const livePhotoError = error instanceof LivePhotoError ? error : new LivePhotoError(
1343
+ error instanceof Error ? error.message : "Unknown error",
1344
+ "UNKNOWN_ERROR",
1345
+ error instanceof Error ? error : void 0
1346
+ );
1347
+ if (this.detector) {
1348
+ this.detector.updateImageState(image.id, {
1349
+ state: "error",
1350
+ error: livePhotoError
1351
+ });
1352
+ }
1353
+ if ((_c = this.options) == null ? void 0 : _c.debug) {
1354
+ console.error("[beautifi] Animation error:", livePhotoError);
1355
+ }
1356
+ this.emitEvent("animationError", image, livePhotoError);
1357
+ }
1358
+ }
1359
+ /**
1360
+ * Start video generation in background
1361
+ */
1362
+ async startVideoGeneration(image) {
1363
+ var _a, _b, _c, _d, _e;
1364
+ if (!this.veoClient || !this.videoRenderer) return;
1365
+ const cachedVideo = this.veoClient.getCachedVideo(image.element.src);
1366
+ if (cachedVideo) {
1367
+ if ((_a = this.options) == null ? void 0 : _a.debug) {
1368
+ console.log("[beautifi] Using cached video for:", image.id);
1369
+ }
1370
+ this.transitionToVideo(image, cachedVideo);
1371
+ return;
1372
+ }
1373
+ this.emitVideoEvent("videoStart", image);
1374
+ if ((_b = this.options) == null ? void 0 : _b.debug) {
1375
+ console.log("[beautifi] Starting video generation for:", image.id);
1376
+ }
1377
+ try {
1378
+ const result = await this.veoClient.startGeneration({
1379
+ imageUrl: image.element.src
1380
+ });
1381
+ if (!result.success || !result.operationId) {
1382
+ throw new Error(((_c = result.error) == null ? void 0 : _c.message) || "Failed to start video generation");
1383
+ }
1384
+ if (result.status === "completed" && result.videoUrl) {
1385
+ this.transitionToVideo(image, result.videoUrl);
1386
+ return;
1387
+ }
1388
+ this.pendingVideoGenerations.set(image.id, result.operationId);
1389
+ const videoUrl = await this.veoClient.pollUntilComplete(
1390
+ result.operationId,
1391
+ image.element.src,
1392
+ (status, attempt) => {
1393
+ var _a2;
1394
+ if ((_a2 = this.options) == null ? void 0 : _a2.debug) {
1395
+ console.log(`[beautifi] Video poll ${image.id}: ${status} (attempt ${attempt})`);
1396
+ }
1397
+ }
1398
+ );
1399
+ this.pendingVideoGenerations.delete(image.id);
1400
+ if (videoUrl) {
1401
+ this.transitionToVideo(image, videoUrl);
1402
+ } else {
1403
+ if ((_d = this.options) == null ? void 0 : _d.debug) {
1404
+ console.log("[beautifi] Video generation failed, keeping CSS animation:", image.id);
1405
+ }
1406
+ this.emitVideoEvent("videoError", image);
1407
+ }
1408
+ } catch (error) {
1409
+ this.pendingVideoGenerations.delete(image.id);
1410
+ if ((_e = this.options) == null ? void 0 : _e.debug) {
1411
+ console.error("[beautifi] Video generation error:", error);
1412
+ }
1413
+ this.emitVideoEvent("videoError", image);
1414
+ }
1415
+ }
1416
+ /**
1417
+ * Transition from CSS animation to video
1418
+ */
1419
+ async transitionToVideo(image, videoUrl) {
1420
+ var _a, _b;
1421
+ if (!this.videoRenderer) return;
1422
+ if ((_a = this.options) == null ? void 0 : _a.debug) {
1423
+ console.log("[beautifi] Transitioning to video:", image.id);
1424
+ }
1425
+ const prepared = await this.videoRenderer.prepareVideo(
1426
+ image.id,
1427
+ image.element,
1428
+ videoUrl,
1429
+ () => {
1430
+ var _a2;
1431
+ if ((_a2 = this.options) == null ? void 0 : _a2.debug) {
1432
+ console.log("[beautifi] Video fallback to CSS:", image.id);
1433
+ }
1434
+ }
1435
+ );
1436
+ if (prepared) {
1437
+ (_b = this.renderer) == null ? void 0 : _b.stop(image.id);
1438
+ this.videoRenderer.play(image.id);
1439
+ this.emitVideoEvent("videoReady", image, videoUrl);
1440
+ }
1441
+ }
1442
+ /**
1443
+ * Pause animation for an image
1444
+ */
1445
+ pauseAnimation(image) {
1446
+ var _a;
1447
+ if (this.renderer) {
1448
+ this.renderer.pause(image.id);
1449
+ }
1450
+ if (this.detector) {
1451
+ this.detector.updateImageState(image.id, { state: "paused" });
1452
+ }
1453
+ if ((_a = this.options) == null ? void 0 : _a.debug) {
1454
+ console.log("[beautifi] Pausing animation for:", image.element.src);
1455
+ }
1456
+ this.emitEvent("animationPause", image);
1457
+ }
1458
+ /**
1459
+ * Resume animation for an image
1460
+ */
1461
+ resumeAnimation(image) {
1462
+ var _a;
1463
+ if (this.renderer) {
1464
+ this.renderer.resume(image.id);
1465
+ }
1466
+ if (this.detector) {
1467
+ this.detector.updateImageState(image.id, { state: "playing" });
1468
+ }
1469
+ if ((_a = this.options) == null ? void 0 : _a.debug) {
1470
+ console.log("[beautifi] Resuming animation for:", image.element.src);
1471
+ }
1472
+ this.emitEvent("animationResume", image);
1473
+ }
1474
+ /**
1475
+ * Emit lifecycle events
1476
+ */
1477
+ emitEvent(type, image, error) {
1478
+ if (typeof CustomEvent !== "undefined") {
1479
+ const detail = {
1480
+ type,
1481
+ timestamp: Date.now(),
1482
+ element: image.element,
1483
+ imageId: image.id,
1484
+ ...error && { error }
1485
+ };
1486
+ const event = new CustomEvent(`beautifi:${type}`, { detail });
1487
+ document.dispatchEvent(event);
1488
+ }
1489
+ }
1490
+ /**
1491
+ * Emit video-related events
1492
+ */
1493
+ emitVideoEvent(type, image, videoUrl) {
1494
+ if (typeof CustomEvent !== "undefined") {
1495
+ const detail = {
1496
+ type,
1497
+ timestamp: Date.now(),
1498
+ element: image.element,
1499
+ imageId: image.id,
1500
+ ...videoUrl && { videoUrl }
1501
+ };
1502
+ const event = new CustomEvent(`beautifi:${type}`, { detail });
1503
+ document.dispatchEvent(event);
1504
+ }
1505
+ }
1506
+ /**
1507
+ * Check if the plugin has been initialized
1508
+ */
1509
+ isInitialized() {
1510
+ return this.initialized;
1511
+ }
1512
+ /**
1513
+ * Get current options (for debugging)
1514
+ */
1515
+ getOptions() {
1516
+ return this.options;
1517
+ }
1518
+ /**
1519
+ * Get all tracked images
1520
+ */
1521
+ getTrackedImages() {
1522
+ var _a;
1523
+ return ((_a = this.detector) == null ? void 0 : _a.getTrackedImages()) ?? [];
1524
+ }
1525
+ /**
1526
+ * Destroy the plugin and clean up resources
1527
+ */
1528
+ destroy() {
1529
+ if (this.veoClient) {
1530
+ this.veoClient.cancelAllOperations();
1531
+ this.veoClient = null;
1532
+ }
1533
+ if (this.videoRenderer) {
1534
+ this.videoRenderer.destroy();
1535
+ this.videoRenderer = null;
1536
+ }
1537
+ if (this.renderer) {
1538
+ this.renderer.destroy();
1539
+ this.renderer = null;
1540
+ }
1541
+ if (this.detector) {
1542
+ this.detector.destroy();
1543
+ this.detector = null;
1544
+ }
1545
+ if (this.viewportObserver) {
1546
+ this.viewportObserver.destroy();
1547
+ this.viewportObserver = null;
1548
+ }
1549
+ this.apiClient = null;
1550
+ this.pendingVideoGenerations.clear();
1551
+ this.initialized = false;
1552
+ this.options = null;
1553
+ }
1554
+ }
1555
+ const plugin = new beautifiPlugin();
1556
+ const beautifi = {
1557
+ /**
1558
+ * Initialize the plugin
1559
+ */
1560
+ init: (options) => plugin.init(options),
1561
+ /**
1562
+ * Check if initialized
1563
+ */
1564
+ isInitialized: () => plugin.isInitialized(),
1565
+ /**
1566
+ * Get current options
1567
+ */
1568
+ getOptions: () => plugin.getOptions(),
1569
+ /**
1570
+ * Get tracked images
1571
+ */
1572
+ getTrackedImages: () => plugin.getTrackedImages(),
1573
+ /**
1574
+ * Destroy and clean up
1575
+ */
1576
+ destroy: () => plugin.destroy()
1577
+ };
1578
+ const LiveMyPhotos = beautifi;
1579
+ if (typeof document !== "undefined") {
1580
+ document.addEventListener("DOMContentLoaded", () => {
1581
+ const scriptTag = document.querySelector(
1582
+ 'script[data-api-key][src*="beautifi"], script[data-api-key][src*="live-my-photos"]'
1583
+ );
1584
+ if (scriptTag) {
1585
+ const apiKey = scriptTag.dataset.apiKey;
1586
+ const autoInit = scriptTag.dataset.autoInit !== "false";
1587
+ if (apiKey && autoInit) {
1588
+ beautifi.init({
1589
+ apiKey,
1590
+ selector: scriptTag.dataset.selector,
1591
+ intensity: scriptTag.dataset.intensity,
1592
+ type: scriptTag.dataset.type,
1593
+ loop: scriptTag.dataset.loop !== "false",
1594
+ debug: scriptTag.dataset.debug === "true",
1595
+ mode: scriptTag.dataset.mode || "auto"
1596
+ });
1597
+ }
1598
+ }
1599
+ });
1600
+ }
1601
+ if (typeof window !== "undefined") {
1602
+ window.beautifi = beautifi;
1603
+ window.LiveMyPhotos = beautifi;
1604
+ }
1605
+ export {
1606
+ AnimationRenderer,
1607
+ DEFAULT_ANIMATION_CONFIG,
1608
+ DEFAULT_OPTIONS,
1609
+ GeminiApiClient,
1610
+ ImageDetector,
1611
+ LiveMyPhotos,
1612
+ LivePhotoError,
1613
+ VeoClient,
1614
+ VideoRenderer,
1615
+ ViewportObserver,
1616
+ beautifi,
1617
+ beautifi as default
1618
+ };
1619
+ //# sourceMappingURL=beautifi.esm.js.map