Files
artchat/modules/infinite-canvas/composables/useImagePreloader.ts
Arthur DANJOU 5dadb20607 feat(infinite-canvas): add infinite canvas component with drag and zoom functionality
- Implemented InfiniteCanvas.vue for rendering an infinite canvas with drag and zoom capabilities.
- Created useInfiniteCanvas composable for managing canvas state and interactions.
- Added useImagePreloader composable for preloading images and videos.
- Introduced constants for physics, touch interactions, viewport settings, and zoom defaults.
- Developed utility functions for touch handling and media type detection.
- Defined TypeScript types for canvas items, grid items, and composables.
- Registered components and composables in the Nuxt module.
- Added screenshot generation functionality for content files.
- Updated package.json to include capture-website dependency.
2025-09-05 11:01:11 +02:00

117 lines
3.1 KiB
TypeScript

/* eslint-disable ts/no-use-before-define */
/**
* @fileoverview Composable for preloading images and videos
*/
import type { ImagePreloaderOptions, UseImagePreloaderReturn } from '../types'
import { isVideo } from '../utils'
/**
* Preloads a single media file (image or video)
* @param src Media URL to preload
* @returns Promise that resolves when loaded
*/
function preloadMedia(src: string): Promise<void> {
return new Promise((resolve) => {
if (isVideo(src)) {
const video = document.createElement('video')
video.preload = 'metadata'
video.muted = true
const handleLoad = () => {
video.removeEventListener('loadedmetadata', handleLoad)
video.removeEventListener('error', handleError)
resolve()
}
const handleError = () => {
video.removeEventListener('loadedmetadata', handleLoad)
video.removeEventListener('error', handleError)
resolve() // Don't reject to avoid breaking the flow
}
video.addEventListener('loadedmetadata', handleLoad)
video.addEventListener('error', handleError)
video.src = src
}
else {
const img = new Image()
const handleLoad = () => {
img.removeEventListener('load', handleLoad)
img.removeEventListener('error', handleError)
resolve()
}
const handleError = () => {
img.removeEventListener('load', handleLoad)
img.removeEventListener('error', handleError)
resolve() // Don't reject to avoid breaking the flow
}
img.addEventListener('load', handleLoad)
img.addEventListener('error', handleError)
img.src = src
}
})
}
/**
* Composable for preloading multiple media files with progress tracking
* @param options Configuration options
* @returns Preloader state and controls
*/
export function useImagePreloader(options: ImagePreloaderOptions): UseImagePreloaderReturn {
const { images, onProgress, onComplete } = options
const progress = ref(0)
const loadedCount = ref(0)
const isLoading = ref(true)
const isComplete = ref(false)
/**
* Updates progress and triggers callbacks
*/
const updateProgress = () => {
const newProgress = loadedCount.value / images.length
progress.value = newProgress
onProgress?.(newProgress)
if (loadedCount.value === images.length) {
isComplete.value = true
isLoading.value = false
onComplete?.()
}
}
/**
* Starts the preloading process for all media files
*/
const startPreloading = async () => {
if (images.length === 0) {
progress.value = 1
isComplete.value = true
isLoading.value = false
onComplete?.()
return
}
// Preload all media in parallel with individual progress tracking
const preloadPromises = images.map(async (src) => {
await preloadMedia(src)
loadedCount.value++
updateProgress()
})
await Promise.all(preloadPromises)
}
return {
progress: readonly(progress),
loadedCount: readonly(loadedCount),
isLoading: readonly(isLoading),
isComplete: readonly(isComplete),
startPreloading,
}
}