@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.
75 lines (65 loc) • 1.86 kB
text/typescript
export const RISE = 280
export const ARC = (Math.PI / 2) * 90
export const EXIT = 240
export const PATH_LEN = RISE + ARC + EXIT
export const F = RISE / PATH_LEN
export const G = (RISE + ARC) / PATH_LEN
export const DEFAULT_ACTIVE_WINDOW = 0.72
export const DEFAULT_AUTO_SPEED = 45e-6
export const DEFAULT_DRAG_SENSITIVITY = 8e-4
export const DEFAULT_PERSPECTIVE = 3400
export type PathPoint = {
x: number
y: number
depth: number
fade: number
local: number
active: boolean
}
export function samplePath(local: number, active: boolean): PathPoint {
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: number
let y: number
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 }
}
export function cardTransformStyle(point: PathPoint): {
zIndex: number
opacity: number
transform: string
pointerEvents: 'auto' | 'none'
} {
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}%), ${-1000 + 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',
}
}