mirror of
https://github.com/ArthurDanjou/artchat.git
synced 2026-02-11 04:07:10 +01:00
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.
This commit is contained in:
142
modules/infinite-canvas/components/InfiniteCanvas.vue
Normal file
142
modules/infinite-canvas/components/InfiniteCanvas.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user