dayofjudgement

debugging expand animation flicker

Aug 20th, 2025
97
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 8.78 KB | Source Code | 0 0
  1. ====== GlobalImageTransition.svelte =====
  2. <script lang="ts">
  3.     import { browser } from '$app/environment';
  4.  
  5.     interface TransitionState {
  6.         isActive: boolean;
  7.         fromRect: DOMRect | null;
  8.         toRect: DOMRect | null;
  9.         imageUrl: string;
  10.         onComplete?: () => void;
  11.     }
  12.  
  13.     let transitionState = $state<TransitionState>({
  14.         isActive: false,
  15.         fromRect: null,
  16.         toRect: null,
  17.         imageUrl: '',
  18.         onComplete: undefined
  19.     });
  20.  
  21.     // Global function to start transition
  22.     export function startImageTransition(
  23.         sourceElement: HTMLElement,
  24.         targetPosition: { left: number; top: number; width: number; height: number },
  25.         imageUrl: string,
  26.         onComplete?: () => void
  27.     ) {
  28.         if (!browser || !sourceElement) return false;
  29.  
  30.         const sourceRect = sourceElement.getBoundingClientRect();
  31.  
  32.         transitionState = {
  33.             isActive: true,
  34.             fromRect: sourceRect,
  35.             toRect: new DOMRect(
  36.                 targetPosition.left,
  37.                 targetPosition.top,
  38.                 targetPosition.width,
  39.                 targetPosition.height
  40.             ),
  41.             imageUrl,
  42.             onComplete
  43.         };
  44.  
  45.         // Wait for overlay element to be ready, then start animation
  46.         const startAnimation = () => {
  47.             if (!overlayElement) {
  48.                 // Retry if overlay not ready
  49.                 requestAnimationFrame(startAnimation);
  50.                 return;
  51.             }
  52.  
  53.             // Set initial position
  54.             overlayElement.style.left = `${sourceRect.left}px`;
  55.             overlayElement.style.top = `${sourceRect.top}px`;
  56.             overlayElement.style.width = `${sourceRect.width}px`;
  57.             overlayElement.style.height = `${sourceRect.height}px`;
  58.             overlayElement.style.opacity = '1';
  59.  
  60.             // Force reflow
  61.             overlayElement.offsetHeight;
  62.  
  63.             // Animate to target
  64.             requestAnimationFrame(() => {
  65.                 if (overlayElement && transitionState.toRect) {
  66.                     overlayElement.style.left = `${transitionState.toRect.left}px`;
  67.                     overlayElement.style.top = `${transitionState.toRect.top}px`;
  68.                     overlayElement.style.width = `${transitionState.toRect.width}px`;
  69.                     overlayElement.style.height = `${transitionState.toRect.height}px`;
  70.                 }
  71.             });
  72.         };
  73.  
  74.         // Start animation
  75.         startAnimation();
  76.  
  77.         // Complete transition - but keep clone visible until explicitly destroyed
  78.         setTimeout(() => {
  79.             if (transitionState.onComplete) {
  80.                 transitionState.onComplete();
  81.             }
  82.         }, 300);
  83.  
  84.         return true;
  85.     }
  86.  
  87.     let overlayElement = $state<HTMLDivElement>();
  88.  
  89.     // Listen for destroy event
  90.     if (browser) {
  91.         (window as any).startImageTransition = startImageTransition;
  92.  
  93.         window.addEventListener('destroyImageTransition', () => {
  94.             transitionState = {
  95.                 isActive: false,
  96.                 fromRect: null,
  97.                 toRect: null,
  98.                 imageUrl: '',
  99.                 onComplete: undefined
  100.             };
  101.         });
  102.     }
  103. </script>
  104.  
  105. {#if transitionState?.isActive}
  106.     <div
  107.         bind:this={overlayElement}
  108.         class="fixed z-[200] overflow-hidden rounded-lg shadow-2xl transition-all duration-300 ease-out"
  109.         style="pointer-events: none;"
  110.     >
  111.         <img src={transitionState?.imageUrl || ''} alt="" class="h-full w-full object-cover" />
  112.     </div>
  113. {/if}
  114.  
  115.  
  116. ====== GenResultGrid.svelte (relevant code only) =====
  117. async function handlePickImage(imageIndex: number, event?: MouseEvent) {
  118.     if (isPickAnimating) return;
  119.     event?.stopPropagation();
  120.  
  121.     const image = images[imageIndex];
  122.     const imageElement = imageElements[imageIndex];
  123.  
  124.     if (!image || !imageElement) {
  125.         onPickImage(imageIndex);
  126.         return;
  127.     }
  128.  
  129.     isPickAnimating = true;
  130.  
  131.     // 1. Cache source position (viewport coords)
  132.     const sourceRect = imageElement.getBoundingClientRect();
  133.  
  134.     // 2. Calculate target position based on actual grid size (not picked view)
  135.     const gridRect = gridContainerElement.getBoundingClientRect();
  136.     const targetWidth = gridRect.width;
  137.     const targetHeight = gridRect.height;
  138.     const targetLeft = gridRect.left;
  139.     const targetTop = gridRect.top;
  140.  
  141.     // 3. Start animation to calculated target
  142.     const startGlobalImageTransition = (window as any).startGlobalImageTransition;
  143.     const mockSourceElement = { getBoundingClientRect: () => sourceRect } as HTMLElement;
  144.  
  145.     // Capture current scroll position
  146.     const currentScrollY = window.scrollY;
  147.  
  148.     // 1. CREATE CLONE with independent lifecycle
  149.     startGlobalImageTransition(
  150.         mockSourceElement,
  151.         { left: targetLeft, top: targetTop, width: targetWidth, height: targetHeight },
  152.         image.url,
  153.         () => {
  154.             // 3. ANIMATION DONE, now SWITCH
  155.             onPickImage(imageIndex);
  156.  
  157.             // 4. DESTROY clone after delay (new view loads)
  158.             setTimeout(() => {
  159.                 // Signal to GlobalImageTransition to destroy clone
  160.                 const destroyEvent = new CustomEvent('destroyImageTransition');
  161.                 window.dispatchEvent(destroyEvent);
  162.  
  163.                 // Local state cleanup - do this AFTER clone is destroyed
  164.                 isPickAnimating = false;
  165.             }, 500);
  166.         }
  167.     );
  168. }
  169.  
  170.  
  171. ====== GenResultPick.svelte (relevant code only) =====
  172. <script lang="ts">
  173.     import { fade } from 'svelte/transition';
  174.     import { Button } from '$lib/components/ui/button';
  175.     import TooltipSlim from '$lib/components/tooltips/TooltipSlim.svelte';
  176.     import ActionButton from '$lib/components/ui/ActionButton.svelte';
  177.     import PickedImageFullscreen from '$lib/components/ui/PickedImageFullscreen.svelte';
  178.     import VideoPlayer from '$lib/components/ui/VideoPlayer.svelte';
  179.     import type { GenResultData } from '$lib/types';
  180.  
  181.     // SVG imports
  182.     import downloadIcon from '$lib/components/icons/download.svg?raw';
  183.     import imagePlaceholder from '$lib/components/icons/image_placeholder.svg?raw';
  184.     import infoIcon from '$lib/components/icons/info.svg?raw';
  185.  
  186.     interface Props {
  187.         pickedImage: any;
  188.         result: GenResultData | undefined;
  189.         isAuthenticated: boolean;
  190.         failedImages: Set<string>;
  191.         onShowDebugInfo: (debugInfo: any) => void;
  192.         onUnpickImage: () => void;
  193.         onImageAction: (actionType: string, imageIndex: string) => void;
  194.         onDownloadImage: (imageUrl: string) => void;
  195.         showBackButton?: boolean;
  196.     }
  197.  
  198.     let {
  199.         pickedImage,
  200.         result,
  201.         isAuthenticated,
  202.         failedImages,
  203.         onShowDebugInfo,
  204.         onUnpickImage,
  205.         onImageAction,
  206.         onDownloadImage,
  207.         showBackButton = true
  208.     }: Props = $props();
  209.  
  210.     let pickedImageDimensions = $state({ width: 0, height: 0 });
  211.     let imageLoaded = $state(false);
  212.     let imageContainer = $state<HTMLDivElement>();
  213.     let showFullscreenModal = $state(false);
  214.  
  215.     // Reset image state when picked image changes
  216.     $effect(() => {
  217.         if (pickedImage) {
  218.             pickedImageDimensions = { width: 0, height: 0 };
  219.             imageLoaded = false;
  220.  
  221.             // Check if image is already loaded (cached) - fix for cached images
  222.             setTimeout(() => {
  223.                 const img = document.getElementById('picked-image-img') as HTMLImageElement;
  224.                 if (img && img.complete && img.naturalWidth > 0) {
  225.                     pickedImageDimensions = { width: img.naturalWidth, height: img.naturalHeight };
  226.                     imageLoaded = true;
  227.                 }
  228.             }, 0);
  229.         }
  230.     });
  231.  
  232.     function handlePickedImageLoad(event: Event) {
  233.         const img = event.target as HTMLImageElement;
  234.         pickedImageDimensions = {
  235.             width: img.naturalWidth,
  236.             height: img.naturalHeight
  237.         };
  238.         imageLoaded = true;
  239.     }
  240.  
  241.     function handleImageError(imageUrl: string) {
  242.         failedImages.add(imageUrl);
  243.         failedImages = new Set(failedImages); // Trigger reactivity
  244.         imageLoaded = true; // Show the error state, hide loading skeleton
  245.     }
  246.  
  247.     function isImageFailed(imageUrl: string) {
  248.         return failedImages.has(imageUrl);
  249.     }
  250.  
  251.     function showImageFullscreen() {
  252.         showFullscreenModal = true;
  253.     }
  254.  
  255.     function closeFullscreenModal() {
  256.         showFullscreenModal = false;
  257.     }
  258.  
  259.     function isVideoContent(image: any): boolean {
  260.         return (
  261.             image.content_type === 'video' ||
  262.             (image.url && (image.url.includes('.mp4') || image.url.includes('.webm')))
  263.         );
  264.     }
  265. </script>
  266.  
  267. {#if pickedImage && pickedImage.url}
  268.     <!-- Picked Image Expanded View -->
  269.     <div class="space-y-4">
  270.         <!-- Image Container - Same size as grid would be -->
  271.         <div class="relative mx-auto max-w-2xl">
  272.             <!-- Loading skeleton -->
  273.             {#if !imageLoaded}
  274.                 <div
  275.                     class="border-border/20 relative aspect-square overflow-hidden rounded-lg border bg-gray-100 dark:bg-gray-800"
  276.                 >
  277.                     <div class="flex h-full w-full items-center justify-center">
  278.                         <div class="animate-pulse text-gray-400">
  279.                             {@html imagePlaceholder}
  280.                         </div>
  281.                     </div>
  282.                 </div>
  283.             {/if}
  284.             <div
  285.                 bind:this={imageContainer}
  286.                 id="picked-image-container"
  287.                 class="border-border/20 relative aspect-square cursor-pointer overflow-hidden rounded-lg opacity-100 transition-opacity duration-300"
  288.                 onclick={(e) => {
  289.                     e.stopPropagation();
  290.                     showImageFullscreen();
  291.                 }}
  292.                 onkeydown={(e) => {
  293.                     if (pickedImage?.url && e.key === 'Enter') {
  294.                         e.preventDefault();
  295.                         showImageFullscreen();
  296.                     }
  297.                 }}
  298.                 role="button"
  299.                 tabindex="0"
  300.                 aria-label="View image in fullscreen"
  301.             >
  302.  
Advertisement
Add Comment
Please, Sign In to add comment