react-3d-carousel-fullcontrol 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.js +34 -0
- package/dist/App.test.js +11 -0
- package/dist/Carousel.js +697 -0
- package/dist/index.js +22 -0
- package/dist/reportWebVitals.js +27 -0
- package/dist/setupTests.js +3 -0
- package/package.json +14 -2
- package/public/favicon.ico +0 -0
- package/public/index.html +0 -43
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +0 -25
- package/public/robots.txt +0 -3
- package/src/App.js +0 -25
- package/src/App.test.js +0 -8
- package/src/Carousel.jsx +0 -684
- package/src/index.js +0 -17
- package/src/reportWebVitals.js +0 -13
- package/src/setupTests.js +0 -5
- /package/{src → dist}/App.css +0 -0
- /package/{src → dist}/index.css +0 -0
- /package/{src → dist}/logo.svg +0 -0
package/src/Carousel.jsx
DELETED
|
@@ -1,684 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, useCallback, memo } from "react";
|
|
2
|
-
import { motion } from "framer-motion";
|
|
3
|
-
|
|
4
|
-
// ─── Particle system ────────────────────────────────────────────────────────
|
|
5
|
-
function Particles() {
|
|
6
|
-
const count = 80;
|
|
7
|
-
const particles = useRef(
|
|
8
|
-
Array.from({ length: count }, (_, i) => ({
|
|
9
|
-
id: i,
|
|
10
|
-
x: Math.random() * 100,
|
|
11
|
-
y: Math.random() * 100,
|
|
12
|
-
size: Math.random() * 2.5 + 0.5,
|
|
13
|
-
opacity: Math.random() * 0.5 + 0.1,
|
|
14
|
-
dur: Math.random() * 12 + 8,
|
|
15
|
-
delay: Math.random() * -20,
|
|
16
|
-
}))
|
|
17
|
-
).current;
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<div
|
|
21
|
-
style={{
|
|
22
|
-
position: "absolute",
|
|
23
|
-
inset: 0,
|
|
24
|
-
overflow: "hidden",
|
|
25
|
-
pointerEvents: "none",
|
|
26
|
-
zIndex: 1,
|
|
27
|
-
}}
|
|
28
|
-
>
|
|
29
|
-
{particles.map((p) => (
|
|
30
|
-
<motion.div
|
|
31
|
-
key={p.id}
|
|
32
|
-
style={{
|
|
33
|
-
position: "absolute",
|
|
34
|
-
left: `${p.x}%`,
|
|
35
|
-
top: `${p.y}%`,
|
|
36
|
-
width: p.size,
|
|
37
|
-
height: p.size,
|
|
38
|
-
borderRadius: "50%",
|
|
39
|
-
background: `rgba(180,160,255,${p.opacity})`,
|
|
40
|
-
}}
|
|
41
|
-
animate={{
|
|
42
|
-
y: [0, -30, 0],
|
|
43
|
-
opacity: [p.opacity, p.opacity * 2, p.opacity],
|
|
44
|
-
}}
|
|
45
|
-
transition={{
|
|
46
|
-
duration: p.dur,
|
|
47
|
-
repeat: Infinity,
|
|
48
|
-
ease: "easeInOut",
|
|
49
|
-
delay: p.delay,
|
|
50
|
-
}}
|
|
51
|
-
/>
|
|
52
|
-
))}
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ─── Light blobs ────────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
function LightBlobs({ mouseX, mouseY }) {
|
|
60
|
-
return (
|
|
61
|
-
<div
|
|
62
|
-
style={{
|
|
63
|
-
position: "absolute",
|
|
64
|
-
inset: 0,
|
|
65
|
-
pointerEvents: "none",
|
|
66
|
-
zIndex: 0,
|
|
67
|
-
overflow: "hidden",
|
|
68
|
-
}}
|
|
69
|
-
>
|
|
70
|
-
<div
|
|
71
|
-
style={{
|
|
72
|
-
position: "absolute",
|
|
73
|
-
width: 700,
|
|
74
|
-
height: 700,
|
|
75
|
-
borderRadius: "50%",
|
|
76
|
-
left: mouseX - 350,
|
|
77
|
-
top: mouseY - 350,
|
|
78
|
-
background:
|
|
79
|
-
"radial-gradient(circle, rgba(100,80,200,0.12) 0%, transparent 70%)",
|
|
80
|
-
transition: "left 0.6s ease, top 0.6s ease",
|
|
81
|
-
pointerEvents: "none",
|
|
82
|
-
}}
|
|
83
|
-
/>
|
|
84
|
-
<motion.div
|
|
85
|
-
style={{
|
|
86
|
-
position: "absolute",
|
|
87
|
-
width: 600,
|
|
88
|
-
height: 600,
|
|
89
|
-
borderRadius: "50%",
|
|
90
|
-
top: "-10%",
|
|
91
|
-
left: "10%",
|
|
92
|
-
background:
|
|
93
|
-
"radial-gradient(circle, rgba(80,40,180,0.18) 0%, transparent 70%)",
|
|
94
|
-
filter: "blur(60px)",
|
|
95
|
-
}}
|
|
96
|
-
animate={{ scale: [1, 1.15, 1], x: [0, 40, 0] }}
|
|
97
|
-
transition={{ duration: 18, repeat: Infinity, ease: "easeInOut" }}
|
|
98
|
-
/>
|
|
99
|
-
<motion.div
|
|
100
|
-
style={{
|
|
101
|
-
position: "absolute",
|
|
102
|
-
width: 500,
|
|
103
|
-
height: 500,
|
|
104
|
-
borderRadius: "50%",
|
|
105
|
-
bottom: "-5%",
|
|
106
|
-
right: "5%",
|
|
107
|
-
background:
|
|
108
|
-
"radial-gradient(circle, rgba(40,100,200,0.14) 0%, transparent 70%)",
|
|
109
|
-
filter: "blur(80px)",
|
|
110
|
-
}}
|
|
111
|
-
animate={{ scale: [1, 1.2, 1], y: [0, -50, 0] }}
|
|
112
|
-
transition={{ duration: 14, repeat: Infinity, ease: "easeInOut", delay: 3 }}
|
|
113
|
-
/>
|
|
114
|
-
<motion.div
|
|
115
|
-
style={{
|
|
116
|
-
position: "absolute",
|
|
117
|
-
width: 400,
|
|
118
|
-
height: 400,
|
|
119
|
-
borderRadius: "50%",
|
|
120
|
-
top: "40%",
|
|
121
|
-
left: "40%",
|
|
122
|
-
background:
|
|
123
|
-
"radial-gradient(circle, rgba(160,60,220,0.08) 0%, transparent 70%)",
|
|
124
|
-
filter: "blur(100px)",
|
|
125
|
-
}}
|
|
126
|
-
animate={{ scale: [1, 1.3, 1] }}
|
|
127
|
-
transition={{ duration: 22, repeat: Infinity, ease: "easeInOut", delay: 7 }}
|
|
128
|
-
/>
|
|
129
|
-
</div>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ─── Single carousel card ────────────────────────────────────────────────────
|
|
134
|
-
|
|
135
|
-
const CarouselCard = memo(function CarouselCard({
|
|
136
|
-
image,
|
|
137
|
-
index,
|
|
138
|
-
totalAngle,
|
|
139
|
-
opacity,
|
|
140
|
-
angleStep,
|
|
141
|
-
radius,
|
|
142
|
-
cardWidth,
|
|
143
|
-
cardHeight,
|
|
144
|
-
}) {
|
|
145
|
-
const angle = index * angleStep;
|
|
146
|
-
const [hovered, setHovered] = useState(false);
|
|
147
|
-
|
|
148
|
-
const normalizedAngle = ((totalAngle + angle) % 360 + 360) % 360;
|
|
149
|
-
const frontness = Math.max(0, Math.cos((normalizedAngle * Math.PI) / 180));
|
|
150
|
-
|
|
151
|
-
return (
|
|
152
|
-
<div
|
|
153
|
-
style={{
|
|
154
|
-
position: "absolute",
|
|
155
|
-
width: `${cardWidth}px`,
|
|
156
|
-
height: `${cardHeight}px`,
|
|
157
|
-
left: `-${cardWidth / 2}px`,
|
|
158
|
-
top: `-${cardHeight / 2}px`,
|
|
159
|
-
transform: `rotateY(${angle}deg) translateZ(${radius}px)`,
|
|
160
|
-
transformStyle: "preserve-3d",
|
|
161
|
-
backfaceVisibility: "visible",
|
|
162
|
-
WebkitBackfaceVisibility: "visible",
|
|
163
|
-
opacity: opacity || 1,
|
|
164
|
-
willChange: "transform, opacity",
|
|
165
|
-
}}
|
|
166
|
-
>
|
|
167
|
-
<motion.div
|
|
168
|
-
onHoverStart={() => setHovered(true)}
|
|
169
|
-
onHoverEnd={() => setHovered(false)}
|
|
170
|
-
animate={{
|
|
171
|
-
y: hovered ? -12 : 0,
|
|
172
|
-
}}
|
|
173
|
-
transition={{ type: "spring", stiffness: 300, damping: 22 }}
|
|
174
|
-
style={{
|
|
175
|
-
width: "100%",
|
|
176
|
-
height: "100%",
|
|
177
|
-
borderRadius: "0px",
|
|
178
|
-
overflow: "hidden",
|
|
179
|
-
position: "relative",
|
|
180
|
-
cursor: "pointer",
|
|
181
|
-
willChange: "transform",
|
|
182
|
-
boxShadow: hovered
|
|
183
|
-
? `0 0 30px rgba(160,120,255,0.8), 0 0 60px rgba(100,80,220,0.6), 0 0 100px rgba(60,40,180,0.4), 0 20px 40px rgba(0,0,0,0.8)`
|
|
184
|
-
: `0 10px 20px rgba(0,0,0,0.5)`,
|
|
185
|
-
transition: "box-shadow 0.4s ease",
|
|
186
|
-
}}
|
|
187
|
-
>
|
|
188
|
-
<img
|
|
189
|
-
src={image.src}
|
|
190
|
-
alt={image.label || `Card ${index + 1}`}
|
|
191
|
-
loading="lazy"
|
|
192
|
-
style={{
|
|
193
|
-
width: "100%",
|
|
194
|
-
height: "100%",
|
|
195
|
-
objectFit: "cover",
|
|
196
|
-
display: "block",
|
|
197
|
-
borderRadius: "0px",
|
|
198
|
-
filter: hovered
|
|
199
|
-
? "brightness(1.2) saturate(1.3) contrast(1.1)"
|
|
200
|
-
: `brightness(${0.65 + frontness * 0.3}) saturate(1.05)`,
|
|
201
|
-
transition: "filter 0.4s ease",
|
|
202
|
-
}}
|
|
203
|
-
/>
|
|
204
|
-
|
|
205
|
-
<div
|
|
206
|
-
style={{
|
|
207
|
-
position: "absolute",
|
|
208
|
-
inset: 0,
|
|
209
|
-
background:
|
|
210
|
-
"linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.55) 100%)",
|
|
211
|
-
zIndex: 2,
|
|
212
|
-
}}
|
|
213
|
-
/>
|
|
214
|
-
|
|
215
|
-
{image.label && (
|
|
216
|
-
<div
|
|
217
|
-
style={{
|
|
218
|
-
position: "absolute",
|
|
219
|
-
bottom: 0,
|
|
220
|
-
left: 0,
|
|
221
|
-
right: 0,
|
|
222
|
-
padding: "20px 16px 16px",
|
|
223
|
-
zIndex: 4,
|
|
224
|
-
background:
|
|
225
|
-
"linear-gradient(0deg, rgba(0,0,0,0.7) 0%, transparent 100%)",
|
|
226
|
-
}}
|
|
227
|
-
>
|
|
228
|
-
<div
|
|
229
|
-
style={{
|
|
230
|
-
fontFamily: "'SF Pro Display', 'Helvetica Neue', sans-serif",
|
|
231
|
-
fontSize: "13px",
|
|
232
|
-
fontWeight: 600,
|
|
233
|
-
color: "rgba(255,255,255,0.9)",
|
|
234
|
-
letterSpacing: "0.08em",
|
|
235
|
-
textTransform: "uppercase",
|
|
236
|
-
}}
|
|
237
|
-
>
|
|
238
|
-
{image.label}
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
)}
|
|
242
|
-
|
|
243
|
-
<motion.div
|
|
244
|
-
animate={hovered ? { opacity: [0, 0.15, 0], x: ["-100%", "200%"] } : { opacity: 0 }}
|
|
245
|
-
transition={{ duration: 0.7, ease: "easeInOut" }}
|
|
246
|
-
style={{
|
|
247
|
-
position: "absolute",
|
|
248
|
-
inset: 0,
|
|
249
|
-
background:
|
|
250
|
-
"linear-gradient(105deg, transparent 40%, rgba(255,255,255,0.4) 50%, transparent 60%)",
|
|
251
|
-
zIndex: 5,
|
|
252
|
-
}}
|
|
253
|
-
/>
|
|
254
|
-
</motion.div>
|
|
255
|
-
|
|
256
|
-
<div
|
|
257
|
-
style={{
|
|
258
|
-
position: "absolute",
|
|
259
|
-
top: "105%",
|
|
260
|
-
left: "5%",
|
|
261
|
-
right: "5%",
|
|
262
|
-
height: "80px",
|
|
263
|
-
background: `url(${image.src}) center/cover no-repeat`,
|
|
264
|
-
borderRadius: "0px",
|
|
265
|
-
transform: "scaleY(-1)",
|
|
266
|
-
opacity: 0.12 + frontness * 0.08,
|
|
267
|
-
maskImage: "linear-gradient(to bottom, rgba(0,0,0,0.6) 0%, transparent 100%)",
|
|
268
|
-
WebkitMaskImage: "linear-gradient(to bottom, rgba(0,0,0,0.6) 0%, transparent 100%)",
|
|
269
|
-
filter: "blur(3px)",
|
|
270
|
-
pointerEvents: "none",
|
|
271
|
-
}}
|
|
272
|
-
/>
|
|
273
|
-
</div>
|
|
274
|
-
);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// ─── Main reusable carousel component ────────────────────────────────────────
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* 3D Carousel with cylinder-style rotation and full axis control
|
|
281
|
-
* Cards arranged in a circle and the entire cylinder rotates
|
|
282
|
-
*
|
|
283
|
-
* @param {Object} props
|
|
284
|
-
* @param {Array<{src: string, label?: string}>} props.images
|
|
285
|
-
* @param {Object} [props.defaultRotation] - Default rotation { x: -20, y: 10, z: 20 }
|
|
286
|
-
* @param {boolean} [props.controlled=false] - Return to default position on release
|
|
287
|
-
* @param {function} [props.onRotationChange] - Callback with current rotation
|
|
288
|
-
* @param {number} [props.autoRotateSpeed=0.3] - Auto rotation speed (degrees per frame)
|
|
289
|
-
* @param {Object} [props.autoRotateAxes] - Which axes to auto-rotate { x: false, y: true, z: false }
|
|
290
|
-
* @param {boolean} [props.pauseOnDrag=true] - Pause auto-rotation while dragging
|
|
291
|
-
* @param {number} [props.cardWidth=220]
|
|
292
|
-
* @param {number} [props.cardHeight=300]
|
|
293
|
-
* @param {number} [props.gap=2]
|
|
294
|
-
* @param {number} [props.perspective=2000]
|
|
295
|
-
* @param {number} [props.sensitivity=0.4] - Mouse drag sensitivity
|
|
296
|
-
* @param {boolean} [props.showDragHint=true]
|
|
297
|
-
* @param {boolean} [props.glowEffect=true] - Enable glow effect on hover
|
|
298
|
-
* @param {string} [props.className=""]
|
|
299
|
-
* @param {Object} [props.style={}]
|
|
300
|
-
*/
|
|
301
|
-
function Carousel({
|
|
302
|
-
images = [],
|
|
303
|
-
defaultRotation = { x: -20, y: 10, z: 20 },
|
|
304
|
-
controlled = false,
|
|
305
|
-
onRotationChange,
|
|
306
|
-
autoRotateSpeed = 0.3,
|
|
307
|
-
autoRotateAxes = { x: false, y: true, z: false },
|
|
308
|
-
pauseOnDrag = true,
|
|
309
|
-
cardWidth = 220,
|
|
310
|
-
cardHeight = 300,
|
|
311
|
-
gap = 2,
|
|
312
|
-
perspective = 2000,
|
|
313
|
-
sensitivity = 0.4,
|
|
314
|
-
showDragHint = true,
|
|
315
|
-
glowEffect = true,
|
|
316
|
-
className = "",
|
|
317
|
-
style = {},
|
|
318
|
-
}) {
|
|
319
|
-
const rotationRef = useRef({
|
|
320
|
-
x: defaultRotation.x ?? -20,
|
|
321
|
-
y: defaultRotation.y ?? 10,
|
|
322
|
-
z: defaultRotation.z ?? 20,
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const rafRef = useRef(null);
|
|
326
|
-
const isDragging = useRef(false);
|
|
327
|
-
const lastMousePos = useRef({ x: 0, y: 0 });
|
|
328
|
-
const velocityRef = useRef({ x: 0, y: 0, z: 0 });
|
|
329
|
-
const [renderRotation, setRenderRotation] = useState({ ...rotationRef.current });
|
|
330
|
-
const returnAnimRef = useRef(null);
|
|
331
|
-
|
|
332
|
-
const cardCount = images.length;
|
|
333
|
-
const angleStep = cardCount > 0 ? 360 / cardCount : 0;
|
|
334
|
-
const radius = cardCount > 0 ? ((cardWidth + gap) * cardCount) / (2 * Math.PI) : 0;
|
|
335
|
-
|
|
336
|
-
// Sync with defaultRotation prop changes
|
|
337
|
-
useEffect(() => {
|
|
338
|
-
if (!isDragging.current) {
|
|
339
|
-
rotationRef.current = {
|
|
340
|
-
x: defaultRotation.x ?? -20,
|
|
341
|
-
y: defaultRotation.y ?? 10,
|
|
342
|
-
z: defaultRotation.z ?? 20,
|
|
343
|
-
};
|
|
344
|
-
setRenderRotation({ ...rotationRef.current });
|
|
345
|
-
}
|
|
346
|
-
}, [defaultRotation.x, defaultRotation.y, defaultRotation.z]);
|
|
347
|
-
|
|
348
|
-
// Animation loop
|
|
349
|
-
useEffect(() => {
|
|
350
|
-
if (cardCount === 0) return;
|
|
351
|
-
|
|
352
|
-
let lastTime = performance.now();
|
|
353
|
-
|
|
354
|
-
const tick = (now) => {
|
|
355
|
-
const delta = Math.min(now - lastTime, 32);
|
|
356
|
-
lastTime = now;
|
|
357
|
-
|
|
358
|
-
const isCurrentlyDragging = isDragging.current;
|
|
359
|
-
|
|
360
|
-
if (!isCurrentlyDragging) {
|
|
361
|
-
// Apply friction to all velocities
|
|
362
|
-
velocityRef.current.x *= 0.94;
|
|
363
|
-
velocityRef.current.y *= 0.94;
|
|
364
|
-
velocityRef.current.z *= 0.94;
|
|
365
|
-
|
|
366
|
-
if (Math.abs(velocityRef.current.x) < 0.001) velocityRef.current.x = 0;
|
|
367
|
-
if (Math.abs(velocityRef.current.y) < 0.001) velocityRef.current.y = 0;
|
|
368
|
-
if (Math.abs(velocityRef.current.z) < 0.001) velocityRef.current.z = 0;
|
|
369
|
-
|
|
370
|
-
if (!controlled) {
|
|
371
|
-
// Free mode: apply auto-rotation to all specified axes
|
|
372
|
-
if (autoRotateAxes.x) rotationRef.current.x += autoRotateSpeed * (delta / 16.67);
|
|
373
|
-
if (autoRotateAxes.y) rotationRef.current.y += autoRotateSpeed * (delta / 16.67);
|
|
374
|
-
if (autoRotateAxes.z) rotationRef.current.z += autoRotateSpeed * (delta / 16.67);
|
|
375
|
-
|
|
376
|
-
// Add momentum
|
|
377
|
-
rotationRef.current.x += velocityRef.current.x;
|
|
378
|
-
rotationRef.current.y += velocityRef.current.y;
|
|
379
|
-
rotationRef.current.z += velocityRef.current.z;
|
|
380
|
-
} else {
|
|
381
|
-
// Controlled mode: apply momentum and return to default
|
|
382
|
-
rotationRef.current.x += velocityRef.current.x;
|
|
383
|
-
rotationRef.current.y += velocityRef.current.y;
|
|
384
|
-
rotationRef.current.z += velocityRef.current.z;
|
|
385
|
-
|
|
386
|
-
// If velocities are near zero, return to default position
|
|
387
|
-
if (Math.abs(velocityRef.current.x) < 0.01 &&
|
|
388
|
-
Math.abs(velocityRef.current.y) < 0.01 &&
|
|
389
|
-
Math.abs(velocityRef.current.z) < 0.01) {
|
|
390
|
-
|
|
391
|
-
const returnSpeed = 0.08;
|
|
392
|
-
const targetX = defaultRotation.x ?? -20;
|
|
393
|
-
const targetY = defaultRotation.y ?? 10;
|
|
394
|
-
const targetZ = defaultRotation.z ?? 20;
|
|
395
|
-
|
|
396
|
-
rotationRef.current.x += (targetX - rotationRef.current.x) * returnSpeed;
|
|
397
|
-
rotationRef.current.y += (targetY - rotationRef.current.y) * returnSpeed;
|
|
398
|
-
rotationRef.current.z += (targetZ - rotationRef.current.z) * returnSpeed;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
setRenderRotation({ ...rotationRef.current });
|
|
404
|
-
|
|
405
|
-
if (onRotationChange) {
|
|
406
|
-
onRotationChange({ ...rotationRef.current });
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
413
|
-
return () => {
|
|
414
|
-
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
415
|
-
if (returnAnimRef.current) cancelAnimationFrame(returnAnimRef.current);
|
|
416
|
-
};
|
|
417
|
-
}, [controlled, defaultRotation, autoRotateSpeed, autoRotateAxes, pauseOnDrag, onRotationChange, cardCount]);
|
|
418
|
-
|
|
419
|
-
// Interaction handlers
|
|
420
|
-
const handlePointerDown = useCallback((clientX, clientY) => {
|
|
421
|
-
isDragging.current = true;
|
|
422
|
-
lastMousePos.current = { x: clientX, y: clientY };
|
|
423
|
-
velocityRef.current = { x: 0, y: 0, z: 0 };
|
|
424
|
-
|
|
425
|
-
if (returnAnimRef.current) {
|
|
426
|
-
cancelAnimationFrame(returnAnimRef.current);
|
|
427
|
-
returnAnimRef.current = null;
|
|
428
|
-
}
|
|
429
|
-
}, []);
|
|
430
|
-
|
|
431
|
-
const handlePointerMove = useCallback((clientX, clientY) => {
|
|
432
|
-
if (!isDragging.current) return;
|
|
433
|
-
|
|
434
|
-
const dx = clientX - lastMousePos.current.x;
|
|
435
|
-
const dy = clientY - lastMousePos.current.y;
|
|
436
|
-
|
|
437
|
-
// Horizontal drag = Y axis rotation (spin the cylinder)
|
|
438
|
-
rotationRef.current.y += dx * sensitivity;
|
|
439
|
-
|
|
440
|
-
// Vertical drag = X axis rotation (tilt the cylinder forward/backward)
|
|
441
|
-
rotationRef.current.x -= dy * sensitivity * 0.6;
|
|
442
|
-
|
|
443
|
-
// Diagonal drag = Z axis rotation (roll/twist the cylinder)
|
|
444
|
-
if (Math.abs(dx) > 2 && Math.abs(dy) > 2) {
|
|
445
|
-
const diagonalComponent = (dx * dy) / Math.sqrt(dx * dx + dy * dy);
|
|
446
|
-
rotationRef.current.z += diagonalComponent * sensitivity * 0.3;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Store velocities for momentum
|
|
450
|
-
velocityRef.current.y = dx * sensitivity * 0.5;
|
|
451
|
-
velocityRef.current.x = -dy * sensitivity * 0.3;
|
|
452
|
-
velocityRef.current.z = (dx * dy / Math.sqrt(dx * dx + dy * dy || 1)) * sensitivity * 0.2;
|
|
453
|
-
|
|
454
|
-
// Clamp rotations to reasonable ranges
|
|
455
|
-
rotationRef.current.x = Math.max(-75, Math.min(75, rotationRef.current.x));
|
|
456
|
-
rotationRef.current.z = Math.max(-45, Math.min(45, rotationRef.current.z));
|
|
457
|
-
|
|
458
|
-
lastMousePos.current = { x: clientX, y: clientY };
|
|
459
|
-
}, [sensitivity]);
|
|
460
|
-
|
|
461
|
-
const handlePointerUp = useCallback(() => {
|
|
462
|
-
isDragging.current = false;
|
|
463
|
-
}, []);
|
|
464
|
-
|
|
465
|
-
// Mouse events
|
|
466
|
-
const onMouseDown = useCallback((e) => {
|
|
467
|
-
e.preventDefault();
|
|
468
|
-
handlePointerDown(e.clientX, e.clientY);
|
|
469
|
-
}, [handlePointerDown]);
|
|
470
|
-
|
|
471
|
-
const onMouseMove = useCallback((e) => {
|
|
472
|
-
handlePointerMove(e.clientX, e.clientY);
|
|
473
|
-
}, [handlePointerMove]);
|
|
474
|
-
|
|
475
|
-
const onMouseUp = useCallback(() => {
|
|
476
|
-
handlePointerUp();
|
|
477
|
-
}, [handlePointerUp]);
|
|
478
|
-
|
|
479
|
-
// Touch events
|
|
480
|
-
const onTouchStart = useCallback((e) => {
|
|
481
|
-
handlePointerDown(e.touches[0].clientX, e.touches[0].clientY);
|
|
482
|
-
}, [handlePointerDown]);
|
|
483
|
-
|
|
484
|
-
const onTouchMove = useCallback((e) => {
|
|
485
|
-
handlePointerMove(e.touches[0].clientX, e.touches[0].clientY);
|
|
486
|
-
}, [handlePointerMove]);
|
|
487
|
-
|
|
488
|
-
const onTouchEnd = useCallback(() => {
|
|
489
|
-
handlePointerUp();
|
|
490
|
-
}, [handlePointerUp]);
|
|
491
|
-
|
|
492
|
-
// Empty state
|
|
493
|
-
if (cardCount === 0) {
|
|
494
|
-
return (
|
|
495
|
-
<div className={className} style={{ position: "relative", width: "100%", height: "100%", ...style }}>
|
|
496
|
-
<div style={{
|
|
497
|
-
position: "absolute",
|
|
498
|
-
top: "50%",
|
|
499
|
-
left: "50%",
|
|
500
|
-
transform: "translate(-50%, -50%)",
|
|
501
|
-
color: "rgba(255,255,255,0.5)",
|
|
502
|
-
fontSize: "16px"
|
|
503
|
-
}}>
|
|
504
|
-
No images to display
|
|
505
|
-
</div>
|
|
506
|
-
</div>
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Sort cards by depth for proper rendering order (back to front)
|
|
511
|
-
const sortedImages = [...images].map((image, i) => {
|
|
512
|
-
const angle = i * angleStep;
|
|
513
|
-
const totalAngle = (renderRotation.y + angle) % 360;
|
|
514
|
-
const zDepth = Math.cos((totalAngle * Math.PI) / 180);
|
|
515
|
-
return { image, index: i, zDepth };
|
|
516
|
-
}).sort((a, b) => a.zDepth - b.zDepth);
|
|
517
|
-
|
|
518
|
-
return (
|
|
519
|
-
<div
|
|
520
|
-
className={className}
|
|
521
|
-
style={{
|
|
522
|
-
position: "relative",
|
|
523
|
-
width: "100%",
|
|
524
|
-
height: "100%",
|
|
525
|
-
perspective: `${perspective}px`,
|
|
526
|
-
perspectiveOrigin: "50% 50%",
|
|
527
|
-
cursor: isDragging.current ? "grabbing" : "grab",
|
|
528
|
-
userSelect: "none",
|
|
529
|
-
touchAction: "none",
|
|
530
|
-
...style,
|
|
531
|
-
}}
|
|
532
|
-
onMouseDown={onMouseDown}
|
|
533
|
-
onMouseMove={onMouseMove}
|
|
534
|
-
onMouseUp={onMouseUp}
|
|
535
|
-
onMouseLeave={onMouseUp}
|
|
536
|
-
onTouchStart={onTouchStart}
|
|
537
|
-
onTouchMove={onTouchMove}
|
|
538
|
-
onTouchEnd={onTouchEnd}
|
|
539
|
-
>
|
|
540
|
-
{/* Outer container - tilts the entire cylinder on X and Z axes */}
|
|
541
|
-
<div
|
|
542
|
-
style={{
|
|
543
|
-
position: "absolute",
|
|
544
|
-
top: "50%",
|
|
545
|
-
left: "50%",
|
|
546
|
-
transformStyle: "preserve-3d",
|
|
547
|
-
transform: `translate(-50%, -50%) rotateX(${renderRotation.x}deg) rotateZ(${renderRotation.z}deg)`,
|
|
548
|
-
willChange: "transform",
|
|
549
|
-
}}
|
|
550
|
-
>
|
|
551
|
-
{/* Inner container - rotates the cards around Y axis (cylinder spin) */}
|
|
552
|
-
<div
|
|
553
|
-
style={{
|
|
554
|
-
position: "absolute",
|
|
555
|
-
top: "50%",
|
|
556
|
-
left: "50%",
|
|
557
|
-
transformStyle: "preserve-3d",
|
|
558
|
-
transform: `translate(-50%, -50%) rotateY(${renderRotation.y}deg)`,
|
|
559
|
-
willChange: "transform",
|
|
560
|
-
}}
|
|
561
|
-
>
|
|
562
|
-
{/* Render cards in sorted order (back to front) */}
|
|
563
|
-
{sortedImages.map(({ image, index, zDepth }) => (
|
|
564
|
-
<CarouselCard
|
|
565
|
-
key={index}
|
|
566
|
-
image={image}
|
|
567
|
-
index={index}
|
|
568
|
-
totalAngle={renderRotation.y}
|
|
569
|
-
opacity={0.3 + (zDepth + 1) * 0.35}
|
|
570
|
-
angleStep={angleStep}
|
|
571
|
-
radius={radius}
|
|
572
|
-
cardWidth={cardWidth}
|
|
573
|
-
cardHeight={cardHeight}
|
|
574
|
-
/>
|
|
575
|
-
))}
|
|
576
|
-
</div>
|
|
577
|
-
</div>
|
|
578
|
-
|
|
579
|
-
{showDragHint && (
|
|
580
|
-
<motion.div
|
|
581
|
-
initial={{ opacity: 0 }}
|
|
582
|
-
animate={{ opacity: 1 }}
|
|
583
|
-
transition={{ delay: 2.5, duration: 1 }}
|
|
584
|
-
style={{
|
|
585
|
-
position: "absolute",
|
|
586
|
-
bottom: "28px",
|
|
587
|
-
left: "50%",
|
|
588
|
-
transform: "translateX(-50%)",
|
|
589
|
-
zIndex: 10,
|
|
590
|
-
color: "rgba(255,255,255,0.25)",
|
|
591
|
-
fontSize: "11px",
|
|
592
|
-
letterSpacing: "0.2em",
|
|
593
|
-
textTransform: "uppercase",
|
|
594
|
-
textAlign: "center",
|
|
595
|
-
pointerEvents: "none",
|
|
596
|
-
}}
|
|
597
|
-
>
|
|
598
|
-
drag to rotate • auto-rotating
|
|
599
|
-
</motion.div>
|
|
600
|
-
)}
|
|
601
|
-
</div>
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// ─── Full page component with backgrounds ────────────────────────────────────
|
|
606
|
-
|
|
607
|
-
function CarouselWithBackground({
|
|
608
|
-
images,
|
|
609
|
-
defaultRotation,
|
|
610
|
-
controlled,
|
|
611
|
-
onRotationChange,
|
|
612
|
-
...carouselProps
|
|
613
|
-
}) {
|
|
614
|
-
const containerRef = useRef(null);
|
|
615
|
-
const [mouse, setMouse] = useState({ x: 0, y: 0 });
|
|
616
|
-
|
|
617
|
-
useEffect(() => {
|
|
618
|
-
const onResize = () => {
|
|
619
|
-
setMouse(prev => ({ ...prev }));
|
|
620
|
-
};
|
|
621
|
-
window.addEventListener("resize", onResize);
|
|
622
|
-
return () => window.removeEventListener("resize", onResize);
|
|
623
|
-
}, []);
|
|
624
|
-
|
|
625
|
-
const onMouseMove = useCallback((e) => {
|
|
626
|
-
setMouse({ x: e.clientX, y: e.clientY });
|
|
627
|
-
}, []);
|
|
628
|
-
|
|
629
|
-
return (
|
|
630
|
-
<div
|
|
631
|
-
ref={containerRef}
|
|
632
|
-
onMouseMove={onMouseMove}
|
|
633
|
-
style={{
|
|
634
|
-
position: "relative",
|
|
635
|
-
width: "100vw",
|
|
636
|
-
height: "100vh",
|
|
637
|
-
background: "#000",
|
|
638
|
-
overflow: "hidden",
|
|
639
|
-
fontFamily: "'SF Pro Display', 'Helvetica Neue', sans-serif",
|
|
640
|
-
}}
|
|
641
|
-
>
|
|
642
|
-
<LightBlobs mouseX={mouse.x} mouseY={mouse.y} />
|
|
643
|
-
<Particles />
|
|
644
|
-
|
|
645
|
-
<div
|
|
646
|
-
style={{
|
|
647
|
-
position: "absolute",
|
|
648
|
-
inset: 0,
|
|
649
|
-
background:
|
|
650
|
-
"radial-gradient(ellipse 60% 40% at 50% 65%, rgba(80,50,160,0.18) 0%, transparent 70%)",
|
|
651
|
-
pointerEvents: "none",
|
|
652
|
-
zIndex: 1,
|
|
653
|
-
}}
|
|
654
|
-
/>
|
|
655
|
-
|
|
656
|
-
<div
|
|
657
|
-
style={{
|
|
658
|
-
position: "absolute",
|
|
659
|
-
bottom: 0,
|
|
660
|
-
left: 0,
|
|
661
|
-
right: 0,
|
|
662
|
-
height: "35%",
|
|
663
|
-
background:
|
|
664
|
-
"linear-gradient(to top, rgba(20,10,40,0.5) 0%, transparent 100%)",
|
|
665
|
-
pointerEvents: "none",
|
|
666
|
-
zIndex: 2,
|
|
667
|
-
}}
|
|
668
|
-
/>
|
|
669
|
-
|
|
670
|
-
<div style={{ position: "absolute", inset: 0, zIndex: 5 }}>
|
|
671
|
-
<Carousel
|
|
672
|
-
images={images}
|
|
673
|
-
defaultRotation={defaultRotation}
|
|
674
|
-
controlled={controlled}
|
|
675
|
-
onRotationChange={onRotationChange}
|
|
676
|
-
{...carouselProps}
|
|
677
|
-
/>
|
|
678
|
-
</div>
|
|
679
|
-
</div>
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
export { Carousel, CarouselWithBackground };
|
|
684
|
-
export default Carousel;
|
package/src/index.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import ReactDOM from 'react-dom/client';
|
|
3
|
-
import './index.css';
|
|
4
|
-
import App from './App';
|
|
5
|
-
import reportWebVitals from './reportWebVitals';
|
|
6
|
-
|
|
7
|
-
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
8
|
-
root.render(
|
|
9
|
-
<React.StrictMode>
|
|
10
|
-
<App />
|
|
11
|
-
</React.StrictMode>
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
// If you want to start measuring performance in your app, pass a function
|
|
15
|
-
// to log results (for example: reportWebVitals(console.log))
|
|
16
|
-
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
17
|
-
reportWebVitals();
|
package/src/reportWebVitals.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const reportWebVitals = onPerfEntry => {
|
|
2
|
-
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
3
|
-
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
4
|
-
getCLS(onPerfEntry);
|
|
5
|
-
getFID(onPerfEntry);
|
|
6
|
-
getFCP(onPerfEntry);
|
|
7
|
-
getLCP(onPerfEntry);
|
|
8
|
-
getTTFB(onPerfEntry);
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export default reportWebVitals;
|