서사형 덱빌딩 게임 프로토 타입
This commit is contained in:
72
client/src/components/SwipeCard.jsx
Normal file
72
client/src/components/SwipeCard.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, useMotionValue, useTransform, useAnimation } from 'framer-motion';
|
||||
import './SwipeCard.css';
|
||||
|
||||
export default function SwipeCard({ card, onSwipe, phase }) {
|
||||
const x = useMotionValue(0);
|
||||
const controls = useAnimation();
|
||||
|
||||
// Dynamic friction based on phase
|
||||
const friction = phase === 3 ? 1.5 : phase === 2 ? 1.2 : 0.8;
|
||||
|
||||
// Rotations and opacity
|
||||
const rotate = useTransform(x, [-200, 200], [-15 * friction, 15 * friction]);
|
||||
const leftOpacity = useTransform(x, [-100, -50, 0], [1, 0.5, 0]);
|
||||
const rightOpacity = useTransform(x, [0, 50, 100], [0, 0.5, 1]);
|
||||
|
||||
const [exitX, setExitX] = useState(0);
|
||||
|
||||
// Reset when card changes
|
||||
useEffect(() => {
|
||||
setExitX(0);
|
||||
x.set(0);
|
||||
controls.start({ x: 0, opacity: 1, rotate: 0, transition: { duration: 0 } });
|
||||
}, [card, controls, x]);
|
||||
|
||||
const handleDragEnd = async (event, info) => {
|
||||
const threshold = 100;
|
||||
if (info.offset.x > threshold) {
|
||||
setExitX(300);
|
||||
await controls.start({ x: 300, opacity: 0, transition: { duration: 0.3 } });
|
||||
onSwipe('right');
|
||||
} else if (info.offset.x < -threshold) {
|
||||
setExitX(-300);
|
||||
await controls.start({ x: -300, opacity: 0, transition: { duration: 0.3 } });
|
||||
onSwipe('left');
|
||||
} else {
|
||||
// Snap back
|
||||
controls.start({ x: 0, y: 0, transition: { type: 'spring', stiffness: 300, damping: 20 } });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card-container">
|
||||
<motion.div
|
||||
className="swipe-card"
|
||||
drag="x"
|
||||
dragConstraints={{ left: 0, right: 0 }}
|
||||
style={{ x, rotate }}
|
||||
onDragEnd={handleDragEnd}
|
||||
animate={controls}
|
||||
whileTap={{ scale: 0.98, cursor: 'grabbing' }}
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-header">
|
||||
<span className="card-category">[{card.type}]</span>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p className="card-text">{card.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Swipe Intents */}
|
||||
<motion.div className="swipe-intent left-intent" style={{ opacity: leftOpacity }}>
|
||||
<span>{card.left?.text}</span>
|
||||
</motion.div>
|
||||
<motion.div className="swipe-intent right-intent" style={{ opacity: rightOpacity }}>
|
||||
<span>{card.right?.text}</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user