Skip to content

Instantly share code, notes, and snippets.

@phobon
Created November 5, 2020 07:08
Show Gist options
  • Save phobon/9696c199fe4646e968f157e51fdbaf23 to your computer and use it in GitHub Desktop.
Save phobon/9696c199fe4646e968f157e51fdbaf23 to your computer and use it in GitHub Desktop.
/** @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