Hyden logo in white

Loading...

Retro loading bar
Character of Dustin and a polar bear looking at a website.

Behind The Scenes

Curious how this site was made? Well your in luck. Not only have I created this behind the scenes look, but you can also view the entire repo over on GitHub.

Tech Stack

For this project I used NextJS exclusively for my server (deployed on Vercel) and front-end. My database is built into the project rather than connecting to an SQL/NoSQL source. I Used react-three-fiber to implement my 3D effects, and blender to model/animate all of the 3d objects themselves.

I've been itching to try out the new NextJS 13 beta, and I figured this would be a great place to do so. It did however mean a lot of bugs to deal with! At the time of writing, the create-next-app doesn't play nicely with React and causes hydration errors out of the gate. But with all these bugs aside, Next13 is a huge leap forward in my opinion.

Whenever possible, I develop as lean as I can. Below you can see my package.json file. There really isn't much here aside from some type declarations, default NextJS packages, threeJS packages for the 3D effects, and a few utility packages.

11"dependencies": {
12  "@react-three/drei": "^9.57.0",
13  "@react-three/fiber": "^8.11.9",
14  "@react-three/postprocessing": "^2.7.1",
15  "@types/node": "18.14.6",
16  "@types/react": "18.0.28",
17  "@types/react-dom": "18.0.11",
18  "@types/three": "^0.149.0",
19  "@vercel/analytics": "^0.1.11",
20  "eslint": "8.35.0",
21  "eslint-config-next": "13.2.3",
22  "next": "13.2.4",
23  "react": "^18.2.0",
24  "react-dom": "18.2.0",
25  "react-syntax-highlighter": "^15.5.0",
26  "react-use-scroll-direction": "^0.1.0",
27  "three": "^0.150.1",
28  "typescript": "4.9.5"
29}

Subtle Animations

I'm all about the subtle experience. I believe that's what builds to a great experience. One neat subtlety was the simple hover menu on desktop. Hovering anywhere near the menu, will expand it, reducing the clicks a user has to do by 1, while also hiding that information when it's not necessary (a win-win)!

This effect is mostly CSS magic, and it works like this:

140/* large screen effects */
141@media only screen and (min-width: 768px) {
142  .menu-items {
143    display: flex;
144    align-items: center;
145    clip-path: polygon(100% 0, 100% 0, 100% 100%, 100% 100%);
146    /* close speed = 0.35s */
147    transition: clip-path 0.35s var(--smoothing),
148    -webkit-clip-path 0.35s var(--smoothing);
149  }
150  .nav-container:hover .menu-items {
151    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
152    /* open speed = 0.6s */
153    transition: clip-path 0.6s var(--smoothing),
154    -webkit-clip-path 0.6s var(--smoothing);
155  }
156  /* hamburger */
157  .nav-container:hover .menu-icon__bar:nth-of-type(2) {
158    transform: translateX(-494px);
159  }
160  .nav-container:hover .menu-icon__bar:nth-of-type(1) {
161    transform: rotateZ(45deg);
162  }
163  .nav-container:hover .menu-icon__bar:nth-of-type(3) {
164    transform: rotateZ(-45deg);
165  }
166}

Let's Get More Technical

I love discussing the inner workings of the packages used and the CSS that was written, but when it comes down to most people are just looking for the fancy JavaScript that was written to make a specific effect. So, let's talk JavaScript

How Did I Make The Homepage?

Step 1 was simple, I created a faux database and as a TypeScript fanatic I setup a type that would make sure all of my data objects were the same structure. Here is that type:

18export type ProjectType = {
19  title: string
20  featured: boolean
21  featuredVideo: string
22  subtitle: string
23  hideSubtitleOnMobile?: boolean
24  sidebarWidget?: () => React.ReactNode
25  href: string
26  image: {
27    src: string
28    alt: string
29  }
30  industry?: string
31  toolsUsed?: string[]
32  work?: string[]
33  externalLink?: string
34  linkText?: string
35  content: () => React.ReactNode
36}

Next, I made a basic config file that holds all the relevant settings and information that my homepage would need, such as how far away the objects should be from the "player", where the floor is, how much space there should be between panels, and more.

3export const scrollConfig = {
4  offsetFromPlayer: 8,
5  spacing: 40,
6  floorPos: -4.5,
7  scrollSpeed: 0.025
8  showHideDepth: 0.35,
9  totalScrollHeight: () =>
10    (scrollConfig.spacing *
11      data.reduce((acc, current) => acc + (current.featured ? 1 : 0), 0) -
12      40) /
13    scrollConfig.scrollSpeed,
14  pixelsPerProject: () => scrollConfig.spacing / scrollConfig.scrollSpeed,
15}

Above you can see I also calculated the necessary scroll height based on the spacing of each element, the number of items that are featured, and a modified based on how quickly the items scroll in the 3d world. Then, with this information I directly modified the homepage div to set it's height to the config.totalScrollHeight():

Creating this config file really helps to simplify development as all my logic pulls from these values, thus keeping everything in sync (such as my page height, and the components used to render the 3D world)

The next step was to create a basic hook that keeps track of the scroll position (pretty standard stuff).

11export default function useScrollPosition() {
12  const [scrollPosition, setScrollPosition] = useState(0)
13
14  const handleScroll = () => {
15    const position = window.pageYOffset
16    setScrollPosition(position)
17  }
18
19  // set the initial scroll position of the projects + watch for scroll
20  useEffect(() => {
21    handleScroll()
22    window.addEventListener("scroll", handleScroll, { passive: true })
23    return () => {
24      window.removeEventListener("scroll", handleScroll)
25    }
26  }, [])
27
28  return scrollPosition
29}

From there, I just needed to apply this scroll position to my 3D "div" that contained all of the projects on the homepage. React-Three-Fiber has a handy dandy hook that lets me hook into the 3d render pipeline and modify values quite easily. Thus, all I needed to do was the following:

11const { width } = useWindowDimensions()
12const group = useRef<THREE.Group>(null)
13const scrollPosition = useScrollPosition()
14
15const featuredItems = data.filter((item) => item.featured)
16
17useFrame((state, delta) => {
18  if (group.current) {
19    return group.current.position.set(
20      config.offsetFromPlayer + (width >= 768 ? 6 : 0),
21      config.floorPos,
22      -scrollPosition * config.scrollSpeed
23    )
24  }
25})

To explain the above, I also coded a useWindowDimensions hook so that I could move the world relative to the screen width (woo for responsiveness 🎉). Then I needed a ref to the object so I called this "group" since Three JS refers to groups of objects as "group". Finally, ThreeJS' "useFrame" hook allowed me to modify the 3D. So, I calculated the X, Y, and Z positions based on my config settings and saved scroll position, and injected them into the useFrame hook.

So What About The Word Effect?

Having the words appear or disappear has nothing to do with the 3d world, and instead relies entirely on my config file. Based on the scroll position, I determine if any of the featured projects are "in frame", and if so, I show their information. You can view the entire WorldOverlay.tsx file here.

Mouse Effect

I typically recommend against customizing the mouse for a traditional website. It breaks the normal flow that users are familiar with. But, since this is a portfolio I thought I should customize every aspect (such as the scroll bar and mouse) to showcase what I "can" do.

11export default function Mouse() {
12  const { x, y } = useMousePosition()
13  const { mouse, threeMouse } = useMouseContext()
14  const doEffect = threeMouse || mouse?.nodeName === "A" || mouse?.closest("a")
15
16  return (
17    <span
18      className={`${styles.mouse} ${doEffect ? styles.hovering : ""}`}
19      style={{ transform: `translate(${x}px, ${y}px)` }}
20    />
21  )
22}

The snippet above shows all the logic necessary to have the mouse effect work. I look at where my mouse is, determine if I'm hovering over anything clickable, and then apply a class based on that logic. The rest is CSS.

Final Comments

There was a lot of small details that went into this project that I haven't even begun to cover. My favourite detail is simply that you can click the 3d character on the homepage and he will react to being clicked! I even implemented a cross-fade animation effect so that the animations fade into each other, rather than appearing like an old Gameboy Colour game.

Here's the full repo on GitHub once again, take a look through it. Once you're ready, reach out and let's discuss how I can help you.

Navigate

Three people sitting on a chair

Digital Main Street

Web Design • Brand Identity • Brand Strategy • Digital Marketing

string

Contact me