Skip to content

Commit

Permalink
Creating the Hover3D component and story
Browse files Browse the repository at this point in the history
  • Loading branch information
édouard wautier authored and édouard wautier committed Nov 22, 2023
1 parent 673daaf commit 688ba1b
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 0 deletions.
3 changes: 3 additions & 0 deletions sparkle/src/_index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { SparkleContext } from "./context";
export { SparkleContext };

import { Div3D, Hover3D } from "./components/Hover3D";
export { Div3D, Hover3D };

import { Button } from "./components/Button";
export { Button };

Expand Down
133 changes: 133 additions & 0 deletions sparkle/src/components/Hover3D.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, {
createContext,
useContext,
useEffect,
useRef,
useState,
} from "react";

// Context to share hover state
const Hover3DContext = createContext<{
isHovered: boolean;
setHovered?: (state: boolean) => void;
}>({ isHovered: false });

// Custom hook to use the context
export const useHover3D = () => useContext(Hover3DContext);

interface Hover3DProps {
children: React.ReactNode;
xOffset?: number;
yOffset?: number;
attack?: number;
release?: number;
perspective?: number;
className?: string;
depth?: number;
}

function Hover3D({
children,
xOffset = 10,
yOffset = 10,
attack = 0.1,
release = 0.5,
perspective = 500,
depth = -10,
className = "",
}: Hover3DProps) {
const elementRef = useRef<HTMLDivElement>(null);
const [isHovered, setHovered] = useState(false);
const [transform, setTransform] = useState(
"perspective(500px) translateZ(0px)"
);
const [transition, setTransition] = useState("");

const map = (
value: number,
istart: number,
istop: number,
ostart: number,
ostop: number
) => {
return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
};

useEffect(() => {
const element = elementRef.current;

const handleMouseEnter = () => {
setTransition(`transform ${attack}s`);
setHovered(true);
};

const handleMouseMove = (e: MouseEvent) => {
if (element) {
const rect = element.getBoundingClientRect();
const dx = e.clientX - rect.left;
const dy = e.clientY - rect.top;

const xRot = map(dx, 0, rect.width, -xOffset, xOffset);
const yRot = map(dy, 0, rect.height, yOffset, -yOffset);

setTransform(
`perspective(${perspective}px) rotateX(${yRot}deg) rotateY(${xRot}deg) translateZ(${depth}px)`
);
}
};

const handleMouseLeave = () => {
setTransition(`transform ${release}s`);
setTransform(`perspective(${perspective}px) rotateX(0deg) rotateY(0deg)`);
setHovered(false);
};

element?.addEventListener("mouseenter", handleMouseEnter);
element?.addEventListener("mousemove", handleMouseMove);
element?.addEventListener("mouseleave", handleMouseLeave);

return () => {
element?.removeEventListener("mouseenter", handleMouseEnter);
element?.removeEventListener("mousemove", handleMouseMove);
element?.removeEventListener("mouseleave", handleMouseLeave);
};
}, [attack, release, perspective, xOffset, yOffset]);

return (
<Hover3DContext.Provider value={{ isHovered, setHovered }}>
<div
ref={elementRef}
style={{
transform: transform,
transition: transition,
transformStyle: "preserve-3d",
}}
className={className}
>
{children}
</div>
</Hover3DContext.Provider>
);
}

interface divProps {
depth: number;
children: React.ReactNode;
className?: string;
}

const Div3D = ({ depth, children, className = "" }: divProps) => {
const { isHovered } = useHover3D();
const style = {
transform: `translateZ(${isHovered ? depth : 0}px)`,
transition: "transform 0.5s",
};

return (
<div style={style} className={className}>
{children}
</div>
);
};

export { Div3D, Hover3D };
58 changes: 58 additions & 0 deletions sparkle/src/stories/Hover3D.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Meta } from "@storybook/react";
import React from "react";

import { LogoSquareColor } from "@sparkle/logo/dust";

import { Div3D, GithubLogo, Hover3D, Icon } from "../index_with_tw_base";

const meta = {
title: "Test/Hover3D",
component: Hover3D,
} satisfies Meta<typeof Hover3D>;

export default meta;

export const Hover3DExample = () => (
<div className="s-flex s-gap-4">
<div>
<Hover3D
className="s-rounded-[30px] s-bg-gradient-to-r s-from-cyan-500 s-to-blue-500 s-p-10 s-shadow-xl"
depth={-30}
>
<Div3D depth={20}>Coucou</Div3D>
<Div3D depth={10}>Coucou</Div3D>
<Div3D depth={40}>Coucou</Div3D>
</Hover3D>
</div>
<div>
<Hover3D
className="s-rounded-2xl s-bg-slate-200 s-p-3 s-shadow-xl"
depth={-20}
>
<Div3D depth={50}>
<Icon visual={GithubLogo} size="xl" />
</Div3D>
</Hover3D>
</div>
<div>
<Hover3D className="s-rounded-[24px] s-bg-slate-800 s-p-8">
<Div3D depth={60}>
<Icon visual={LogoSquareColor} size="2xl" />
</Div3D>
</Hover3D>
</div>
<div>
<Hover3D
className="s-relative s-h-44 s-w-44 s-rounded-[32px] s-bg-gradient-to-t s-from-stone-400 s-to-stone-300 s-p-2"
depth={-10}
>
<Div3D depth={25} className="s-absolute s-h-40 s-w-40">
<img src="http://test.edouardwautier.com/layer2.png" />
</Div3D>
<Div3D depth={50} className="s-absolute s-h-40 s-w-40">
<img src="http://test.edouardwautier.com/layer3.png" />
</Div3D>
</Hover3D>
</div>
</div>
);

0 comments on commit 688ba1b

Please sign in to comment.