Card stamp arc
Arco interactivo de tarjetas polaroid con física de abanico dinámico al pasar el cursor, ángulos de rotación escalonados, transiciones suaves y sonido.
Arquitectura
Montañas
Vista al océano
Estudio creativo
Retrato
Astro & react CSS transforms Microinteracciones
---
export interface PhotoStamp {
id: number;
src?: string;
alt?: string;
caption?: string;
}
export interface Props {
arc?: number;
spread?: number;
yOffset?: number;
duration?: number;
className?: string;
photos?: PhotoStamp[];
}
const {
arc = 16,
spread = 180,
yOffset = 20,
duration = 0.4,
className = '',
photos
} = Astro.props;
const defaultStamps: PhotoStamp[] = [
{ id: 0, caption: 'Arquitectura', src: 'https://images.unsplash.com/photo-1513694203232-719a280e022f?w=600&auto=format&fit=crop' },
{ id: 1, caption: 'Montañas', src: 'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=600&auto=format&fit=crop' },
{ id: 2, caption: 'Vista al océano', src: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=600&auto=format&fit=crop' },
{ id: 3, caption: 'Estudio creativo', src: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=600&auto=format&fit=crop' },
{ id: 4, caption: 'Retrato', src: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=600&auto=format&fit=crop' }
];
const stampsList = photos && photos.length > 0 ? photos : defaultStamps;
const count = stampsList.length;
const centerIdx = (count - 1) / 2;
const stampConfigs = stampsList.map((stamp, i) => {
const norm = centerIdx > 0 ? (i - centerIdx) / centerIdx : 0;
const idleRotate = norm * 6;
const idleX = norm * 36;
const idleY = Math.abs(norm) * 4;
const targetRotate = norm * arc;
const targetX = norm * spread;
const targetY = Math.abs(norm) * yOffset;
const mobileSpread = 90;
const mobileRotate = norm * 10;
const mobileX = norm * mobileSpread;
const mobileY = Math.abs(norm) * 4;
const dist = Math.abs(i - centerIdx);
return {
...stamp,
norm,
idleRotate,
idleX,
idleY,
targetRotate,
targetX,
targetY,
mobileRotate,
mobileX,
mobileY,
defaultZIndex: Math.round(10 - dist)
};
});
---
<div
class={`card-stamp-arc-root relative w-[6.4rem] h-[8.6rem] sm:w-[9.2rem] sm:h-[11.8rem] cursor-pointer flex items-center justify-center select-none touch-manipulation max-w-full overflow-visible ${className}`}
data-count={count}
>
{stampConfigs.map((stamp, i) => (
<div
class="stamp-card-item absolute inset-0 rounded-lg sm:rounded-xl p-1 pb-1.5 sm:p-1.5 sm:pb-2.5 flex flex-col bg-white text-zinc-800 border border-zinc-200/60 shadow-[0_4px_18px_rgba(0,0,0,0.06)] dark:bg-zinc-900 dark:text-zinc-100 dark:border-zinc-800/60"
style={{
zIndex: stamp.defaultZIndex,
transformOrigin: '50% 100%',
transitionDuration: `${duration}s`,
'--idle-tr': `${stamp.idleRotate}deg`,
'--idle-tx': `${stamp.idleX}px`,
'--idle-ty': `${stamp.idleY}px`,
'--desktop-tr': `${stamp.targetRotate}deg`,
'--desktop-tx': `${stamp.targetX}px`,
'--desktop-ty': `${stamp.targetY}px`,
'--mobile-tr': `${stamp.mobileRotate}deg`,
'--mobile-tx': `${stamp.mobileX}px`,
'--mobile-ty': `${stamp.mobileY}px`
}}
>
{stamp.caption && (
<div class="stamp-tooltip absolute left-1/2 -top-7 sm:-top-9 pointer-events-none z-50 flex justify-center items-center w-max">
<span class="block whitespace-nowrap rounded-lg border border-zinc-200/80 bg-white/95 px-2.5 py-1 text-[10px] sm:text-[11px] font-mono tracking-tight text-zinc-900 shadow-md dark:border-zinc-800 dark:bg-zinc-900/95 dark:text-zinc-100 backdrop-blur-md">
{stamp.caption}
</span>
</div>
)}
<div class="relative w-full h-full rounded bg-zinc-100/90 dark:bg-zinc-800/60 overflow-hidden border border-zinc-200/30 dark:border-zinc-700/30 flex flex-col items-center justify-center pointer-events-none">
{stamp.src ? (
<img
src={stamp.src}
alt={stamp.caption || 'Foto'}
class="w-full h-full object-cover transition-opacity duration-300"
loading="lazy"
/>
) : (
<div class="w-full h-full flex flex-col items-center justify-center p-2 text-center">
<span class="text-[9px] font-mono font-medium text-zinc-400">0{stamp.id + 1}</span>
</div>
)}
</div>
</div>
))}
</div>