UNPKG

@fxshelf/card-orbit

Version:

Path-constrained 3D card orbit carousel for React and Vue — rise, arc, exit. No Framer Motion / GSAP. Copy-as-source friendly.

336 lines (329 loc) • 10.6 kB
"use strict"; var CardOrbit = (() => { var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { CardOrbit: () => CardOrbit, CardOrbitMobile: () => CardOrbitMobile, DEFAULT_ACTIVE_WINDOW: () => DEFAULT_ACTIVE_WINDOW, DEFAULT_AUTO_SPEED: () => DEFAULT_AUTO_SPEED, DEFAULT_DRAG_SENSITIVITY: () => DEFAULT_DRAG_SENSITIVITY, DEFAULT_PERSPECTIVE: () => DEFAULT_PERSPECTIVE, F: () => F, G: () => G, cardTransformStyle: () => cardTransformStyle, createOrbit: () => createOrbit, samplePath: () => samplePath }); // src/browser/react.ts var globalReact = globalThis.React; if (!globalReact) { throw new Error( '[card-orbit] browser build: global "React" not found. Load a UMD build of React (react@18.3.x) before card-orbit.' ); } var Fragment = globalReact.Fragment; var createElement = globalReact.createElement; var useEffect = globalReact.useEffect; var useRef = globalReact.useRef; var useState = globalReact.useState; // src/core/path.ts var RISE = 280; var ARC = Math.PI / 2 * 90; var EXIT = 240; var PATH_LEN = RISE + ARC + EXIT; var F = RISE / PATH_LEN; var G = (RISE + ARC) / PATH_LEN; var DEFAULT_ACTIVE_WINDOW = 0.72; var DEFAULT_AUTO_SPEED = 45e-6; var DEFAULT_DRAG_SENSITIVITY = 8e-4; var DEFAULT_PERSPECTIVE = 3400; function samplePath(local, active) { const fade = active ? Math.min( Math.min(1, Math.max(0, local / 0.06)), Math.min(1, Math.max(0, (1 - local) / 0.08)) ) : 0; const depth = active ? Math.sin( (local < F ? local / (2 * F) : 0.5 + (local - F) / (2 * (1 - F))) * Math.PI ) : 0; let x; let y; if (local < F) { x = -10; y = -300 + local / F * RISE; } else if (local < G) { const ang = (180 + (local - F) / (G - F) * 90) * (Math.PI / 180); x = 80 + 90 * Math.cos(ang); y = -20 - 90 * Math.sin(ang); } else { x = 80 + (local - G) / (1 - G) * EXIT; y = 70; } return { x, y, depth, fade, local, active }; } function cardTransformStyle(point) { const { x, y, depth, fade, local, active } = point; return { zIndex: Math.round(20 + 80 * depth), opacity: active ? fade * (0.4 + 0.6 * depth) : 0, transform: [ `translate3d(calc(-50% + ${x}%), calc(-50% + ${y}%), ${-1e3 + 1280 * depth}px)`, `rotateX(${4 - 2 * depth}deg)`, `rotateY(${-3 + 6 * local}deg)`, "rotateZ(0deg)", `scale(${0.44 + 0.74 * depth})` ].join(" "), pointerEvents: depth > 0.72 ? "auto" : "none" }; } // src/core/createOrbit.ts function createOrbit(options) { let autoSpeed = options.autoSpeed ?? DEFAULT_AUTO_SPEED; let dragSensitivity = options.dragSensitivity ?? DEFAULT_DRAG_SENSITIVITY; const onProgress = options.onProgress; let progress = 0; let dragging = false; let lastX = null; let lastY = null; let frame = 0; let last = 0; let el = null; let running = false; const emit = () => onProgress(progress); const onPointerDown = (e) => { lastX = e.clientX; lastY = e.clientY; dragging = true; el?.setPointerCapture(e.pointerId); }; const onPointerMove = (e) => { if (!dragging || lastX === null || lastY === null) return; const dx = e.clientX - lastX; const dy = e.clientY - lastY; const delta = dy + 0.25 * dx; progress = (progress + dragSensitivity * delta + 1) % 1; emit(); lastX = e.clientX; lastY = e.clientY; }; const endDrag = (e) => { lastX = null; lastY = null; dragging = false; if (el?.hasPointerCapture?.(e.pointerId)) { el.releasePointerCapture(e.pointerId); } }; const tick = (now) => { const dt = now - last; last = now; if (!dragging) { progress = (progress + autoSpeed * dt) % 1; emit(); } frame = requestAnimationFrame(tick); }; const startLoop = () => { if (running) return; running = true; last = performance.now(); frame = requestAnimationFrame(tick); }; const stopLoop = () => { if (!running) return; running = false; cancelAnimationFrame(frame); frame = 0; }; const detach = () => { if (!el) return; el.removeEventListener("pointerdown", onPointerDown); el.removeEventListener("pointermove", onPointerMove); el.removeEventListener("pointerup", endDrag); el.removeEventListener("pointercancel", endDrag); el.removeEventListener("lostpointercapture", endDrag); el = null; }; return { attach(target) { detach(); el = target; el.addEventListener("pointerdown", onPointerDown); el.addEventListener("pointermove", onPointerMove); el.addEventListener("pointerup", endDrag); el.addEventListener("pointercancel", endDrag); el.addEventListener("lostpointercapture", endDrag); startLoop(); }, detach, setOptions(partial) { if (partial.autoSpeed !== void 0) autoSpeed = partial.autoSpeed; if (partial.dragSensitivity !== void 0) { dragSensitivity = partial.dragSensitivity; } }, destroy() { stopLoop(); detach(); dragging = false; lastX = null; lastY = null; }, getProgress() { return progress; } }; } // src/browser/jsx-runtime.ts function jsx(type, props, key) { const create = createElement; return key === void 0 ? create(type, props) : create(type, { ...props, key }); } // src/react/CardOrbit.tsx function CardOrbit({ images, alts, className, style, stageClassName, cardClassName, imageClassName, autoSpeed = DEFAULT_AUTO_SPEED, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, activeWindow = DEFAULT_ACTIVE_WINDOW, perspective = DEFAULT_PERSPECTIVE, ariaLabel = "Card orbit carousel", desktopOnly = true }) { const [progress, setProgress] = useState(0); const rootRef = useRef(null); const orbitRef = useRef(null); const hasImages = images.length > 0; useEffect(() => { if (!hasImages) return; const node = rootRef.current; if (!node) return; const orbit = createOrbit({ onProgress: setProgress }); orbitRef.current = orbit; orbit.attach(node); return () => { orbit.destroy(); orbitRef.current = null; }; }, [hasImages]); useEffect(() => { orbitRef.current?.setOptions({ autoSpeed, dragSensitivity }); }, [autoSpeed, dragSensitivity]); if (!hasImages) return null; const rootClass = [ "card-orbit", desktopOnly ? "card-orbit--desktop-only" : "", className ?? "" ].filter(Boolean).join(" "); return /* @__PURE__ */ jsx( "div", { ref: rootRef, className: rootClass, style: { perspective: `${perspective}px`, ...style }, "aria-label": ariaLabel, children: /* @__PURE__ */ jsx( "div", { className: ["card-orbit__stage", stageClassName].filter(Boolean).join(" "), children: images.map((src, index) => { const phase = (index / images.length + progress) % 1; const active = phase > 0 && phase < activeWindow; const local = active ? phase / activeWindow : 0; const point = samplePath(local, active); return /* @__PURE__ */ jsx( "figure", { className: ["card-orbit__card", cardClassName].filter(Boolean).join(" "), style: cardTransformStyle(point), children: /* @__PURE__ */ jsx( "img", { src, alt: alts?.[index] ?? `Card ${index + 1}`, width: 1200, height: 780, className: ["card-orbit__image", imageClassName].filter(Boolean).join(" "), draggable: false, decoding: "async" } ) }, `${src}-${index}` ); }) } ) } ); } // src/react/CardOrbitMobile.tsx function CardOrbitMobile({ images, alts, className, style, trackClassName, cardClassName, imageClassName, hideOnDesktop = true, durationSec = 28 }) { if (images.length === 0) return null; const loop = [...images, ...images]; const rootClass = [ "card-orbit-mobile", hideOnDesktop ? "card-orbit-mobile--desktop-hide" : "", className ?? "" ].filter(Boolean).join(" "); return /* @__PURE__ */ jsx("div", { className: rootClass, style, children: /* @__PURE__ */ jsx( "div", { className: ["card-orbit-mobile__track", trackClassName].filter(Boolean).join(" "), style: { animationDuration: `${durationSec}s` }, children: loop.map((src, i) => /* @__PURE__ */ jsx( "figure", { className: ["card-orbit-mobile__card", cardClassName].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx( "img", { src, alt: alts?.[i % images.length] ?? "", className: ["card-orbit-mobile__image", imageClassName].filter(Boolean).join(" "), draggable: false } ) }, `${src}-${i}` )) } ) }); } return __toCommonJS(src_exports); })(); //# sourceMappingURL=card-orbit.iife.js.map