Files
artchat/modules/infinite-canvas/components/CanvasLoader.vue
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.3 KiB
Vue

<script setup lang="ts">
import type { LoaderProps } from '../types'
const props = withDefaults(defineProps<LoaderProps>(), {
title: 'Loading Canvas',
description: 'Preparing your visual journey...',
})
const RADIUS = 45
const CIRCUMFERENCE = 2 * Math.PI * RADIUS
/**
* Calculate progress percentage for display
*/
const percent = computed(() => Math.round(props.progress * 100))
/**
* Calculate stroke dash offset for circular progress
*/
const dashOffset = computed(() => CIRCUMFERENCE * (1 - props.progress))
/**
* Animation props for the content reveal
*/
const contentAnimation = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5, delay: 0.2 },
}
/**
* Check if loading is complete
*/
const isComplete = computed(() => props.progress >= 1)
</script>
<template>
<Transition
enter-active-class="transition-all duration-500 ease-out"
leave-active-class="transition-all duration-700 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="isVisible"
class="fixed inset-0 z-50 flex items-center justify-center bg-default"
>
<div class="pointer-events-none fixed inset-0 size-full overflow-hidden">
<div class="noise pointer-events-none absolute inset-[-200%] size-[400%] bg-[url('/noise.png')] opacity-[4%]" />
</div>
<div class="relative flex flex-col items-center space-y-8">
<div class="relative">
<svg
class="size-24 -rotate-90 transform"
viewBox="0 0 100 100"
>
<circle
cx="50"
cy="50"
:r="RADIUS"
fill="none"
stroke="rgba(255, 255, 255, 0.1)"
stroke-width="2"
/>
<circle
cx="50"
cy="50"
:r="RADIUS"
fill="none"
stroke="rgba(255, 255, 255, 0.8)"
stroke-width="2"
stroke-linecap="round"
:stroke-dasharray="CIRCUMFERENCE"
:stroke-dashoffset="dashOffset"
class="transition-all duration-300 ease-out"
/>
</svg>
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-xl font-medium text-highlighted">
{{ percent }}%
</span>
</div>
</div>
<div class="text-center">
<Motion v-bind="contentAnimation">
<h2 class="text-2xl font-light text-highlighted mb-2">
{{ title }}
</h2>
<p class="text-sm text-highlighted/60">
{{ description }}
</p>
</Motion>
</div>
<div class="flex space-x-2">
<div
v-for="i in 3"
:key="i"
class="size-2 rounded-full bg-inverted/40"
:class="{ 'animate-pulse': !isComplete }"
:style="{ animationDelay: `${i * 0.2}s` }"
/>
</div>
</div>
<div class="pointer-events-none absolute inset-0">
<div class="absolute left-1/2 top-1/2 size-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-inverted/5 blur-3xl" />
</div>
</div>
</Transition>
</template>