@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.
- package/dist/beautifi-lite.cjs.js +2 -0
- package/dist/beautifi-lite.cjs.js.map +1 -0
- package/dist/beautifi-lite.esm.js +212 -0
- package/dist/beautifi-lite.esm.js.map +1 -0
- package/dist/beautifi-lite.min.js +2 -0
- package/dist/beautifi-lite.min.js.map +1 -0
- package/dist/beautifi-lite.umd.js +2 -0
- package/dist/beautifi-lite.umd.js.map +1 -0
- package/dist/beautifi.cjs.js +2 -0
- package/dist/beautifi.cjs.js.map +1 -0
- package/dist/beautifi.esm.js +1619 -0
- package/dist/beautifi.esm.js.map +1 -0
- package/dist/beautifi.min.js +2 -0
- package/dist/beautifi.min.js.map +1 -0
- package/dist/beautifi.umd.js +2 -0
- package/dist/beautifi.umd.js.map +1 -0
- package/dist/sri-hashes.json +46 -0
- package/dist/types/AnimationRenderer.d.ts +74 -0
- package/dist/types/GeminiApiClient.d.ts +67 -0
- package/dist/types/ImageDetector.d.ts +44 -0
- package/dist/types/VeoClient.d.ts +124 -0
- package/dist/types/VideoRenderer.d.ts +105 -0
- package/dist/types/ViewportObserver.d.ts +42 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/lite.d.ts +86 -0
- package/dist/types/types.d.ts +204 -0
- package/package.json +79 -0
|
@@ -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
|