243 lines
4.9 KiB
Svelte
243 lines
4.9 KiB
Svelte
<script lang="ts">
|
|
interface Props {
|
|
id: number;
|
|
frontImage: string;
|
|
backImage: string;
|
|
flipped?: boolean;
|
|
x?: number;
|
|
y?: number;
|
|
zIndex?: number;
|
|
canvasOffset?: { x: number; y: number };
|
|
canvasZoom?: number;
|
|
onMove?: (id: number, x: number, y: number) => void;
|
|
onClick?: (id: number) => void;
|
|
onDrop?: (id: number, x: number, y: number) => void;
|
|
}
|
|
|
|
let {
|
|
id,
|
|
frontImage,
|
|
backImage,
|
|
flipped = $bindable(false),
|
|
x = $bindable(0),
|
|
y = $bindable(0),
|
|
zIndex = $bindable(0),
|
|
canvasOffset = { x: 0, y: 0 },
|
|
canvasZoom = 1,
|
|
onMove,
|
|
onClick,
|
|
onDrop
|
|
}: Props = $props();
|
|
|
|
let isDragging = $state(false);
|
|
let dragOffset = $state({ x: 0, y: 0 });
|
|
let dragStartPos = $state({ x: 0, y: 0 });
|
|
let hasMoved = $state(false);
|
|
let cardElement: HTMLDivElement;
|
|
|
|
function handleMouseDown(event: MouseEvent) {
|
|
if (event.button !== 0) return;
|
|
|
|
isDragging = true;
|
|
hasMoved = false;
|
|
|
|
// Calculate offset relative to card position in canvas coordinates
|
|
dragOffset = {
|
|
x: (event.clientX - canvasOffset.x) / canvasZoom - x,
|
|
y: (event.clientY - canvasOffset.y) / canvasZoom - y
|
|
};
|
|
dragStartPos = {
|
|
x: event.clientX,
|
|
y: event.clientY
|
|
};
|
|
|
|
event.preventDefault();
|
|
}
|
|
|
|
function handleMouseMove(event: MouseEvent) {
|
|
if (!isDragging) return;
|
|
|
|
const deltaX = Math.abs(event.clientX - dragStartPos.x);
|
|
const deltaY = Math.abs(event.clientY - dragStartPos.y);
|
|
|
|
if (deltaX > 10 || deltaY > 10) {
|
|
hasMoved = true;
|
|
}
|
|
|
|
const newX = (event.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
|
const newY = (event.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
|
|
|
x = newX;
|
|
y = newY;
|
|
|
|
onMove?.(id, newX, newY);
|
|
}
|
|
|
|
function handleMouseUp() {
|
|
if (isDragging) {
|
|
onDrop?.(id, x, y);
|
|
}
|
|
isDragging = false;
|
|
}
|
|
|
|
function handleClick(event: MouseEvent) {
|
|
if (!hasMoved) {
|
|
flipped = !flipped;
|
|
onClick?.(id);
|
|
}
|
|
}
|
|
|
|
function handleTouchStart(event: TouchEvent) {
|
|
if (event.touches.length !== 1) return;
|
|
event.stopPropagation(); // Prevent canvas panning
|
|
|
|
const touch = event.touches[0];
|
|
isDragging = true;
|
|
hasMoved = false;
|
|
|
|
// Calculate offset relative to card position in canvas coordinates
|
|
dragOffset = {
|
|
x: (touch.clientX - canvasOffset.x) / canvasZoom - x,
|
|
y: (touch.clientY - canvasOffset.y) / canvasZoom - y
|
|
};
|
|
dragStartPos = {
|
|
x: touch.clientX,
|
|
y: touch.clientY
|
|
};
|
|
|
|
event.preventDefault();
|
|
}
|
|
|
|
function handleTouchMove(event: TouchEvent) {
|
|
if (!isDragging || event.touches.length !== 1) return;
|
|
|
|
const touch = event.touches[0];
|
|
const deltaX = Math.abs(touch.clientX - dragStartPos.x);
|
|
const deltaY = Math.abs(touch.clientY - dragStartPos.y);
|
|
|
|
if (deltaX > 10 || deltaY > 10) {
|
|
hasMoved = true;
|
|
}
|
|
|
|
const newX = (touch.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
|
const newY = (touch.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
|
|
|
x = newX;
|
|
y = newY;
|
|
|
|
onMove?.(id, newX, newY);
|
|
event.preventDefault();
|
|
}
|
|
|
|
function handleTouchEnd(event: TouchEvent) {
|
|
if (isDragging) {
|
|
onDrop?.(id, x, y);
|
|
if (!hasMoved) {
|
|
flipped = !flipped;
|
|
onClick?.(id);
|
|
}
|
|
}
|
|
isDragging = false;
|
|
event.preventDefault();
|
|
}
|
|
|
|
$effect(() => {
|
|
if (isDragging) {
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
return () => {
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
bind:this={cardElement}
|
|
class="card"
|
|
class:flipped
|
|
class:dragging={isDragging}
|
|
style="left: {x}px; top: {y}px; z-index: {zIndex};"
|
|
onmousedown={handleMouseDown}
|
|
onclick={handleClick}
|
|
ontouchstart={handleTouchStart}
|
|
ontouchmove={handleTouchMove}
|
|
ontouchend={handleTouchEnd}
|
|
role="button"
|
|
tabindex="0"
|
|
>
|
|
<div class="card-inner">
|
|
<div class="card-front">
|
|
<img src={frontImage} alt="Card front" />
|
|
</div>
|
|
<div class="card-back">
|
|
<img src={backImage} alt="Card back" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.card {
|
|
position: absolute;
|
|
width: 220px;
|
|
height: 300px;
|
|
cursor: pointer;
|
|
perspective: 1000px;
|
|
user-select: none;
|
|
}
|
|
|
|
.card.dragging {
|
|
cursor: grabbing;
|
|
z-index: 1000 !important;
|
|
}
|
|
|
|
.card-inner {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
text-align: center;
|
|
transition: transform 0.6s;
|
|
transform-style: preserve-3d;
|
|
}
|
|
|
|
.card.flipped .card-inner {
|
|
transform: rotateY(180deg);
|
|
}
|
|
|
|
.card-front,
|
|
.card-back {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
backface-visibility: hidden;
|
|
border-radius: 27px;
|
|
overflow: hidden;
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.card-front {
|
|
background: white;
|
|
}
|
|
|
|
.card-back {
|
|
transform: rotateY(180deg);
|
|
background: white;
|
|
}
|
|
|
|
.card img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: 30px;
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(-2px);
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.card.dragging:hover {
|
|
transform: none;
|
|
}
|
|
</style> |