mirror of
https://github.com/ArthurDanjou/artchat.git
synced 2026-01-14 13:54:01 +01:00
- 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.
143 lines
3.8 KiB
Vue
143 lines
3.8 KiB
Vue
<script setup lang="ts" generic="T extends CanvasItem">
|
|
import type { CanvasItem, InfiniteCanvasEmits, InfiniteCanvasProps } from '../types'
|
|
import { useInfiniteCanvas } from '../composables/useInfiniteCanvas'
|
|
import { PHYSICS } from '../constants'
|
|
|
|
const props = withDefaults(defineProps<InfiniteCanvasProps<T>>(), {
|
|
baseGap: 40,
|
|
})
|
|
|
|
const emit = defineEmits<InfiniteCanvasEmits<T>>()
|
|
|
|
const containerRef = ref<HTMLElement | null>(null)
|
|
|
|
const {
|
|
offset,
|
|
zoom,
|
|
visibleItems,
|
|
gridItems,
|
|
containerDimensions,
|
|
canvasBounds,
|
|
canClick,
|
|
updateDimensions,
|
|
handlePointerDown: handlePointerDownCore,
|
|
handlePointerMove: handlePointerMoveCore,
|
|
handlePointerUp: handlePointerUpCore,
|
|
handleWheel,
|
|
handleTouchStart,
|
|
handleTouchMove,
|
|
handleTouchEnd,
|
|
navigateTo,
|
|
} = useInfiniteCanvas({
|
|
items: props.items as CanvasItem[],
|
|
baseGap: props.baseGap,
|
|
zoomOptions: props.zoomOptions,
|
|
containerRef,
|
|
})
|
|
|
|
// Track drag state for click handling
|
|
const isCurrentlyDragging = ref(false)
|
|
const dragStartPosition = ref<{ x: number, y: number }>({ x: 0, y: 0 })
|
|
const totalDragDistance = ref(0)
|
|
|
|
function handlePointerDown(event: PointerEvent) {
|
|
handlePointerDownCore(event.clientX, event.clientY)
|
|
isCurrentlyDragging.value = false
|
|
dragStartPosition.value = { x: event.clientX, y: event.clientY }
|
|
totalDragDistance.value = 0
|
|
}
|
|
|
|
function handlePointerMove(event: PointerEvent) {
|
|
const currentPos = { x: event.clientX, y: event.clientY }
|
|
const distance = Math.sqrt(
|
|
(currentPos.x - dragStartPosition.value.x) ** 2
|
|
+ (currentPos.y - dragStartPosition.value.y) ** 2,
|
|
)
|
|
|
|
if (distance > PHYSICS.DRAG_THRESHOLD) {
|
|
isCurrentlyDragging.value = true
|
|
}
|
|
|
|
totalDragDistance.value = distance
|
|
handlePointerMoveCore(event.clientX, event.clientY)
|
|
}
|
|
|
|
function handlePointerUp(event: PointerEvent) {
|
|
handlePointerUpCore(event.clientX, event.clientY)
|
|
|
|
if (isCurrentlyDragging.value) {
|
|
setTimeout(() => {
|
|
isCurrentlyDragging.value = false
|
|
}, 100)
|
|
}
|
|
}
|
|
|
|
function handleItemClick(item: T | undefined, index: number) {
|
|
if (item && canClick.value && !isCurrentlyDragging.value && totalDragDistance.value <= PHYSICS.DRAG_THRESHOLD) {
|
|
emit('itemClick', item, index)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
updateDimensions()
|
|
useResizeObserver(containerRef, updateDimensions)
|
|
})
|
|
|
|
defineExpose({
|
|
offset,
|
|
zoom,
|
|
visibleItems,
|
|
gridItems,
|
|
containerDimensions,
|
|
canvasBounds,
|
|
updateDimensions,
|
|
navigateTo,
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="containerRef"
|
|
class="relative size-full overflow-hidden touch-none select-none"
|
|
style="touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;"
|
|
@pointerdown="handlePointerDown"
|
|
@pointermove="handlePointerMove"
|
|
@pointerup="handlePointerUp"
|
|
@pointercancel="handlePointerUp"
|
|
@wheel="handleWheel"
|
|
@touchstart="handleTouchStart"
|
|
@touchmove="handleTouchMove"
|
|
@touchend="handleTouchEnd"
|
|
>
|
|
<div
|
|
class="absolute transform-gpu will-change-transform"
|
|
:style="{
|
|
transform: `translate3d(${offset.x}px, ${offset.y}px, 0) scale(${zoom})`,
|
|
backfaceVisibility: 'hidden',
|
|
perspective: '1000px',
|
|
}"
|
|
>
|
|
<div
|
|
v-for="(gridItem, visibleIndex) in visibleItems"
|
|
:key="gridItem.index"
|
|
class="absolute transform-gpu will-change-transform"
|
|
:style="{
|
|
left: `${gridItem.position.x}px`,
|
|
top: `${gridItem.position.y}px`,
|
|
width: `${gridItem.width}px`,
|
|
height: `${gridItem.height}px`,
|
|
backfaceVisibility: 'hidden',
|
|
}"
|
|
>
|
|
<slot
|
|
v-if="props.items[gridItem.index]"
|
|
:item="props.items[gridItem.index]"
|
|
:index="gridItem.index"
|
|
:visible-index
|
|
:on-item-click="() => handleItemClick(props.items[gridItem.index], gridItem.index)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|