Created
November 5, 2020 07:08
-
-
Save phobon/9696c199fe4646e968f157e51fdbaf23 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @jsx jsx */ | |
import { jsx } from "@emotion/react"; | |
import { useRef, useEffect } from "react"; | |
import { Box, BoxProps, Image } from "@phobon/base"; | |
import { motion } from "framer-motion"; | |
const MotionImage = motion.custom(Image); | |
export interface IShiftImageProps { | |
loading?: "eager" | "lazy"; | |
unsized?: boolean; | |
factor?: number; | |
perspective?: number; | |
} | |
export type ShiftImageProps = IShiftImageProps & | |
BoxProps & | |
React.DetailedHTMLProps< | |
React.ImgHTMLAttributes<HTMLImageElement>, | |
HTMLImageElement | |
>; | |
export const ShiftImage: React.FunctionComponent<ShiftImageProps> = ({ | |
src, | |
alt, | |
loading = "lazy", | |
width, | |
height, | |
factor = 8, | |
...props | |
}) => { | |
const sceneRef = useRef<HTMLDivElement>(null); | |
const planeRef = useRef<HTMLImageElement>(null); | |
const hovered = useRef<boolean>(false); | |
const boundingRef = useRef<{ width?: number; height?: number }>(null); | |
useEffect(() => { | |
boundingRef.current = planeRef.current.getBoundingClientRect(); | |
const enter = () => { | |
hovered.current = true; | |
}; | |
const leave = () => { | |
hovered.current = false; | |
requestAnimationFrame(() => { | |
planeRef.current.style.transform = "scale(1) translate3d(0, 0, 0)"; | |
}); | |
}; | |
const layoutShift = (e) => { | |
if (!hovered.current) { | |
return; | |
} | |
const { width, height } = boundingRef.current; | |
const halfWidth = width / 2; | |
const halfHeight = height / 2; | |
// Normalise around origin | |
const normalizeY = ((e.offsetX - halfWidth) / halfWidth) * factor; | |
const normalizeX = ((e.offsetY - halfHeight) / halfHeight) * factor; | |
const transform = `scale(1) translate3d(${normalizeY}px, ${normalizeX}px, 0)`; | |
requestAnimationFrame(() => { | |
planeRef.current.style.transform = transform; | |
}); | |
}; | |
sceneRef.current.addEventListener("mouseenter", enter); | |
sceneRef.current.addEventListener("mouseleave", leave); | |
sceneRef.current.addEventListener("mousemove", layoutShift); | |
return () => { | |
sceneRef.current.removeEventListener("mouseenter", enter); | |
sceneRef.current.removeEventListener("mouseleave", leave); | |
sceneRef.current.removeEventListener("mousemove", layoutShift); | |
}; | |
}, []); | |
return ( | |
<Box | |
width={width} | |
height={height} | |
ref={sceneRef} | |
css={{ | |
position: "relative", | |
transformStyle: "preserve-3d", | |
"> img": { | |
width: "100%", | |
height: "auto", | |
objectFit: "cover", | |
maxWidth: "inherit", | |
maxHeight: "inherit", | |
willChange: "transform", | |
}, | |
}} | |
{...props} | |
> | |
<MotionImage | |
ref={planeRef} | |
src={src} | |
alt={alt} | |
width={width} | |
height={height} | |
loading={loading} | |
css={{ | |
transition: "transform 240ms ease-out", | |
transform: "scale(1) translate3d(0)", | |
}} | |
/> | |
</Box> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment