Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- ====== GlobalImageTransition.svelte =====
- <script lang="ts">
- import { browser } from '$app/environment';
- interface TransitionState {
- isActive: boolean;
- fromRect: DOMRect | null;
- toRect: DOMRect | null;
- imageUrl: string;
- onComplete?: () => void;
- }
- let transitionState = $state<TransitionState>({
- isActive: false,
- fromRect: null,
- toRect: null,
- imageUrl: '',
- onComplete: undefined
- });
- // Global function to start transition
- export function startImageTransition(
- sourceElement: HTMLElement,
- targetPosition: { left: number; top: number; width: number; height: number },
- imageUrl: string,
- onComplete?: () => void
- ) {
- if (!browser || !sourceElement) return false;
- const sourceRect = sourceElement.getBoundingClientRect();
- transitionState = {
- isActive: true,
- fromRect: sourceRect,
- toRect: new DOMRect(
- targetPosition.left,
- targetPosition.top,
- targetPosition.width,
- targetPosition.height
- ),
- imageUrl,
- onComplete
- };
- // Wait for overlay element to be ready, then start animation
- const startAnimation = () => {
- if (!overlayElement) {
- // Retry if overlay not ready
- requestAnimationFrame(startAnimation);
- return;
- }
- // Set initial position
- overlayElement.style.left = `${sourceRect.left}px`;
- overlayElement.style.top = `${sourceRect.top}px`;
- overlayElement.style.width = `${sourceRect.width}px`;
- overlayElement.style.height = `${sourceRect.height}px`;
- overlayElement.style.opacity = '1';
- // Force reflow
- overlayElement.offsetHeight;
- // Animate to target
- requestAnimationFrame(() => {
- if (overlayElement && transitionState.toRect) {
- overlayElement.style.left = `${transitionState.toRect.left}px`;
- overlayElement.style.top = `${transitionState.toRect.top}px`;
- overlayElement.style.width = `${transitionState.toRect.width}px`;
- overlayElement.style.height = `${transitionState.toRect.height}px`;
- }
- });
- };
- // Start animation
- startAnimation();
- // Complete transition - but keep clone visible until explicitly destroyed
- setTimeout(() => {
- if (transitionState.onComplete) {
- transitionState.onComplete();
- }
- }, 300);
- return true;
- }
- let overlayElement = $state<HTMLDivElement>();
- // Listen for destroy event
- if (browser) {
- (window as any).startImageTransition = startImageTransition;
- window.addEventListener('destroyImageTransition', () => {
- transitionState = {
- isActive: false,
- fromRect: null,
- toRect: null,
- imageUrl: '',
- onComplete: undefined
- };
- });
- }
- </script>
- {#if transitionState?.isActive}
- <div
- bind:this={overlayElement}
- class="fixed z-[200] overflow-hidden rounded-lg shadow-2xl transition-all duration-300 ease-out"
- style="pointer-events: none;"
- >
- <img src={transitionState?.imageUrl || ''} alt="" class="h-full w-full object-cover" />
- </div>
- {/if}
- ====== GenResultGrid.svelte (relevant code only) =====
- async function handlePickImage(imageIndex: number, event?: MouseEvent) {
- if (isPickAnimating) return;
- event?.stopPropagation();
- const image = images[imageIndex];
- const imageElement = imageElements[imageIndex];
- if (!image || !imageElement) {
- onPickImage(imageIndex);
- return;
- }
- isPickAnimating = true;
- // 1. Cache source position (viewport coords)
- const sourceRect = imageElement.getBoundingClientRect();
- // 2. Calculate target position based on actual grid size (not picked view)
- const gridRect = gridContainerElement.getBoundingClientRect();
- const targetWidth = gridRect.width;
- const targetHeight = gridRect.height;
- const targetLeft = gridRect.left;
- const targetTop = gridRect.top;
- // 3. Start animation to calculated target
- const startGlobalImageTransition = (window as any).startGlobalImageTransition;
- const mockSourceElement = { getBoundingClientRect: () => sourceRect } as HTMLElement;
- // Capture current scroll position
- const currentScrollY = window.scrollY;
- // 1. CREATE CLONE with independent lifecycle
- startGlobalImageTransition(
- mockSourceElement,
- { left: targetLeft, top: targetTop, width: targetWidth, height: targetHeight },
- image.url,
- () => {
- // 3. ANIMATION DONE, now SWITCH
- onPickImage(imageIndex);
- // 4. DESTROY clone after delay (new view loads)
- setTimeout(() => {
- // Signal to GlobalImageTransition to destroy clone
- const destroyEvent = new CustomEvent('destroyImageTransition');
- window.dispatchEvent(destroyEvent);
- // Local state cleanup - do this AFTER clone is destroyed
- isPickAnimating = false;
- }, 500);
- }
- );
- }
- ====== GenResultPick.svelte (relevant code only) =====
- <script lang="ts">
- import { fade } from 'svelte/transition';
- import { Button } from '$lib/components/ui/button';
- import TooltipSlim from '$lib/components/tooltips/TooltipSlim.svelte';
- import ActionButton from '$lib/components/ui/ActionButton.svelte';
- import PickedImageFullscreen from '$lib/components/ui/PickedImageFullscreen.svelte';
- import VideoPlayer from '$lib/components/ui/VideoPlayer.svelte';
- import type { GenResultData } from '$lib/types';
- // SVG imports
- import downloadIcon from '$lib/components/icons/download.svg?raw';
- import imagePlaceholder from '$lib/components/icons/image_placeholder.svg?raw';
- import infoIcon from '$lib/components/icons/info.svg?raw';
- interface Props {
- pickedImage: any;
- result: GenResultData | undefined;
- isAuthenticated: boolean;
- failedImages: Set<string>;
- onShowDebugInfo: (debugInfo: any) => void;
- onUnpickImage: () => void;
- onImageAction: (actionType: string, imageIndex: string) => void;
- onDownloadImage: (imageUrl: string) => void;
- showBackButton?: boolean;
- }
- let {
- pickedImage,
- result,
- isAuthenticated,
- failedImages,
- onShowDebugInfo,
- onUnpickImage,
- onImageAction,
- onDownloadImage,
- showBackButton = true
- }: Props = $props();
- let pickedImageDimensions = $state({ width: 0, height: 0 });
- let imageLoaded = $state(false);
- let imageContainer = $state<HTMLDivElement>();
- let showFullscreenModal = $state(false);
- // Reset image state when picked image changes
- $effect(() => {
- if (pickedImage) {
- pickedImageDimensions = { width: 0, height: 0 };
- imageLoaded = false;
- // Check if image is already loaded (cached) - fix for cached images
- setTimeout(() => {
- const img = document.getElementById('picked-image-img') as HTMLImageElement;
- if (img && img.complete && img.naturalWidth > 0) {
- pickedImageDimensions = { width: img.naturalWidth, height: img.naturalHeight };
- imageLoaded = true;
- }
- }, 0);
- }
- });
- function handlePickedImageLoad(event: Event) {
- const img = event.target as HTMLImageElement;
- pickedImageDimensions = {
- width: img.naturalWidth,
- height: img.naturalHeight
- };
- imageLoaded = true;
- }
- function handleImageError(imageUrl: string) {
- failedImages.add(imageUrl);
- failedImages = new Set(failedImages); // Trigger reactivity
- imageLoaded = true; // Show the error state, hide loading skeleton
- }
- function isImageFailed(imageUrl: string) {
- return failedImages.has(imageUrl);
- }
- function showImageFullscreen() {
- showFullscreenModal = true;
- }
- function closeFullscreenModal() {
- showFullscreenModal = false;
- }
- function isVideoContent(image: any): boolean {
- return (
- image.content_type === 'video' ||
- (image.url && (image.url.includes('.mp4') || image.url.includes('.webm')))
- );
- }
- </script>
- {#if pickedImage && pickedImage.url}
- <!-- Picked Image Expanded View -->
- <div class="space-y-4">
- <!-- Image Container - Same size as grid would be -->
- <div class="relative mx-auto max-w-2xl">
- <!-- Loading skeleton -->
- {#if !imageLoaded}
- <div
- class="border-border/20 relative aspect-square overflow-hidden rounded-lg border bg-gray-100 dark:bg-gray-800"
- >
- <div class="flex h-full w-full items-center justify-center">
- <div class="animate-pulse text-gray-400">
- {@html imagePlaceholder}
- </div>
- </div>
- </div>
- {/if}
- <div
- bind:this={imageContainer}
- id="picked-image-container"
- class="border-border/20 relative aspect-square cursor-pointer overflow-hidden rounded-lg opacity-100 transition-opacity duration-300"
- onclick={(e) => {
- e.stopPropagation();
- showImageFullscreen();
- }}
- onkeydown={(e) => {
- if (pickedImage?.url && e.key === 'Enter') {
- e.preventDefault();
- showImageFullscreen();
- }
- }}
- role="button"
- tabindex="0"
- aria-label="View image in fullscreen"
- >
Advertisement
Add Comment
Please, Sign In to add comment