55 lines
1.5 KiB
TypeScript
55 lines
1.5 KiB
TypeScript
import type { CSSProperties } from "vue";
|
|||
|
|
|
||
|
|
const REST: CSSProperties = {
|
||
|
|
transform: "perspective(1000px) rotateX(0deg) rotateY(0deg) translateY(0px) scale3d(1, 1, 1)",
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Pointer-follow 3D tilt + spotlight position for a card, batched to one update per frame. */
|
||
|
|
export function useCardTilt() {
|
||
|
|
const style = ref<CSSProperties>({});
|
||
|
|
let frame = 0;
|
||
|
|
let el: HTMLElement | null = null;
|
||
|
|
let clientX = 0;
|
||
|
|
let clientY = 0;
|
||
|
|
|
||
|
|
const apply = () => {
|
||
|
|
frame = 0;
|
||
|
|
if (!el) return;
|
||
|
|
const rect = el.getBoundingClientRect();
|
||
|
|
const x = clientX - rect.left;
|
||
|
|
const y = clientY - rect.top;
|
||
|
|
|
||
|
|
el.style.setProperty("--mouse-x", `${x}px`);
|
||
|
|
el.style.setProperty("--mouse-y", `${y}px`);
|
||
|
|
|
||
|
|
const centerX = rect.width / 2;
|
||
|
|
const centerY = rect.height / 2;
|
||
|
|
const rotateX = ((y - centerY) / centerY) * -7;
|
||
|
|
const rotateY = ((x - centerX) / centerX) * 7;
|
||
|
|
|
||
|
|
style.value = {
|
||
|
|
transform: `perspective(1000px) rotateX(${rotateX.toFixed(1)}deg) rotateY(${rotateY.toFixed(1)}deg) translateY(-6px) scale3d(1.02, 1.02, 1.02)`,
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
function onMouseMove(e: MouseEvent) {
|
||
|
|
el = e.currentTarget as HTMLElement;
|
||
|
|
clientX = e.clientX;
|
||
|
|
clientY = e.clientY;
|
||
|
|
if (!frame) frame = requestAnimationFrame(apply);
|
||
|
|
}
|
||
|
|
|
||
|
|
function onMouseLeave() {
|
||
|
|
if (frame) cancelAnimationFrame(frame);
|
||
|
|
frame = 0;
|
||
|
|
el = null;
|
||
|
|
style.value = REST;
|
||
|
|
}
|
||
|
|
|
||
|
|
onBeforeUnmount(() => {
|
||
|
|
if (frame) cancelAnimationFrame(frame);
|
||
|
|
});
|
||
|
|
|
||
|
|
return { style, onMouseMove, onMouseLeave };
|
||
|
|
}
|