Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 🎸 Add Industry Comparison Section #427

Merged
merged 16 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/components/DataRoom/IndustryComparison/DotGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { MotionValue } from 'framer-motion'
import { useEffect, useMemo, useRef, useState } from 'react'
import { createDots } from './utils/createDots'
import { updateCanvasDimensions } from './utils/updateCanvasDimensions'
import { drawDots } from './utils/drawDots'
import { lerp } from './utils/lerp'

export default function DotGrid({
containerRef,
isMobile,
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
scrollYProgress,
}: {
containerRef: React.RefObject<HTMLDivElement>
isMobile: boolean
scrollYProgress?: MotionValue<number>
}) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
const [lerpedMousePosition, setLerpedMousePosition] = useState({ x: 0, y: 0 })

const dots = useMemo(() => createDots(dimensions, isMobile), [dimensions, isMobile])
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
const updateDimensions = () => {
const newDimensions = updateCanvasDimensions(canvasRef, containerRef)
setDimensions(newDimensions)
}

const handleMouseMove = (e: MouseEvent) => {
if (canvasRef.current && containerRef.current) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is canvasRef.current required for the mouse position? The value isn't used inside the handler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in fffb8ab

const rect = containerRef.current.getBoundingClientRect()
setMousePosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })
}
}

const updateMobileMousePosition = () => {
if (scrollYProgress && isMobile && containerRef.current) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use early returns here to reduce indentation, e.g.

Suggested change
if (canvasRef.current && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect()
setMousePosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })
}
}
const updateMobileMousePosition = () => {
if (scrollYProgress && isMobile && containerRef.current) {
if (canvasRef.current && containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
setMousePosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })
}
const updateMobileMousePosition = () => {
if (scrollYProgress && isMobile && containerRef.current) return

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 01d5a3b

const progress = scrollYProgress.get()
const { height } = containerRef.current.getBoundingClientRect()
setMousePosition({ x: dimensions.width - dimensions.width / 8, y: progress * height })
}
}

const lerpMousePosition = () => {
setLerpedMousePosition((prev) => ({
x: lerp(prev.x, mousePosition.x),
y: lerp(prev.y, mousePosition.y),
}))
requestAnimationFrame(lerpMousePosition)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to clean this up with cancelAnimationFrame on unmount.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 01d5a3b

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the dependencies, I'd suggest we move memoise these in useCallbacks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 01d5a3b

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaiu, this is related to drawing the dots? Could we locate them together?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in drawDots util in 01d5a3b


updateDimensions()
window.addEventListener('resize', updateDimensions)
const container = containerRef.current
if (isMobile && scrollYProgress) {
scrollYProgress.onChange(updateMobileMousePosition)
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
} else {
container?.addEventListener('mousemove', handleMouseMove)
}
lerpMousePosition()

return () => {
window.removeEventListener('resize', updateDimensions)
if (isMobile && scrollYProgress) {
scrollYProgress.clearListeners()
} else {
container?.removeEventListener('mousemove', handleMouseMove)
}
}
}, [mousePosition.x, mousePosition.y, containerRef, isMobile, scrollYProgress, dimensions.width])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot going on here in one useEffect. If it's possible, breaking it down would be ideal. Memoising the methods in useCallbacks would help.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 01d5a3b


useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return

const ctx = canvas.getContext('2d')
if (!ctx) return

let animationFrameId: number

const animate = () => {
drawDots(ctx, dots, dimensions, lerpedMousePosition, isMobile)
animationFrameId = requestAnimationFrame(animate)
}

animate()

return () => {
cancelAnimationFrame(animationFrameId)
}
}, [dots, dimensions, lerpedMousePosition, isMobile])

return (
<canvas
ref={canvasRef}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
}}
/>
)
}
71 changes: 71 additions & 0 deletions src/components/DataRoom/IndustryComparison/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { BaseBlock } from '@/components/Home/types'
import type { MotionValue } from 'framer-motion'
import { useTransform, motion, useScroll } from 'framer-motion'
import type { ReactNode } from 'react'
import React, { useRef } from 'react'
import css from './styles.module.css'
import { useIsMediumScreen } from '@/hooks/useMaxWidth'
import { Typography } from '@mui/material'
import DotGrid from './DotGrid'

const IndustryComparison = ({ title }: BaseBlock) => {
const backgroundRef = useRef<HTMLDivElement>(null)
const gridContainerRef = useRef<HTMLDivElement>(null)
const isMobile = useIsMediumScreen()

const { scrollYProgress } = useScroll({
target: backgroundRef,
offset: ['start end', 'end start'],
})

return (
<div ref={backgroundRef} className={css.sectionContainer}>
<div className={css.stickyContainer}>
<RightPanel containerRef={gridContainerRef} scrollYProgress={scrollYProgress} isMobile={isMobile}>
<Typography className={css.title} variant="h1">
{title}
</Typography>
<DotGrid containerRef={gridContainerRef} isMobile={isMobile} scrollYProgress={scrollYProgress} />
</RightPanel>
</div>
</div>
)
}

const RightPanel = ({
scrollYProgress,
children,
containerRef,
isMobile,
}: {
scrollYProgress: MotionValue<number>
children: ReactNode
isMobile: boolean
containerRef: React.RefObject<HTMLDivElement>
}) => {
const opacityParams = [0.25, 0.35, 0.65, 0.75]
const translateParams = [0.25, 0.35, 0.65, 0.75]
const opacity = useTransform(scrollYProgress, opacityParams, [0, 1, 1, 0])
const bgTranslate = useTransform(scrollYProgress, translateParams, ['100%', '0%', '0%', '100%'])

return (
<div ref={containerRef} className={css.rightPanelContainer}>
<motion.div
className={css.rightPanelContent}
style={{
opacity: isMobile ? 1 : opacity,
}}
>
{children}
</motion.div>
<motion.div
className={css.rightPanelBG}
style={{
translateX: isMobile ? '0%' : bgTranslate,
}}
></motion.div>
</div>
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we consolidate this component with the one used in the CryptoPunks section? They seem almost identical.

I would suggest to move this component under the /common folder and name it something more descriptive. Example SlidingRightPanel. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not worth doing imo, both the panels have different behaviour for desktop and different behaviour for mobile view and different styling for the content.


export default IndustryComparison
69 changes: 69 additions & 0 deletions src/components/DataRoom/IndustryComparison/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
.sectionContainer {
height: 300vh;
display: flex;
}

.stickyContainer {
position: sticky;
top: 0;
width: 100%;
height: 100dvh;
display: flex;
}

.rightPanelContainer {
width: 100%;
height: 100%;
position: absolute;
right: 0;
color: var(--mui-palette-text-dark);
overflow-x: hidden;
display: flex;
align-items: flex-end;
padding-top: 64px;
}

.rightPanelContent {
z-index: 20;
display: flex;
justify-items: center;
align-items: center;
width: 100%;
padding: 64px;
height: 100%;
gap: 30px;
}

.title {
font-size: 80px;
line-height: 88px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These values are already defined in the h1 variant. I don't think we need this class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 01d5a3b

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can delete the class. Why do you need the z-index here?

z-index: 30;
}

.rightPanelBG {
position: absolute;
inset: 0;
background-color: var(--mui-palette-primary-main);
z-index: 10;
margin-top: 72px;
}

@media (max-width: 900px) {
.rightPanelContainer {
width: 100%;
height: 100%;
position: absolute;
bottom: 0;
}
.rightPanelContent {
padding-left: 32px;
padding-right: 108px;
}
.title {
font-size: 56px;
line-height: 60px;
}
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
.rightPanelBG {
margin-top: 0px;
}
}
16 changes: 16 additions & 0 deletions src/components/DataRoom/IndustryComparison/utils/createDots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const ROWS = 30
const MOBILE_COLS = 15
const DESKTOP_COLS = 60

export const createDots = (dimensions: { width: number; height: number }, isMobile: boolean) => {
const cols = isMobile ? MOBILE_COLS : DESKTOP_COLS
const newDots = []
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < cols; col++) {
const x = (dimensions.width / (cols - 1)) * col + dimensions.width / cols / 2
const y = (dimensions.height / (ROWS - 1)) * row + dimensions.height / ROWS / 2
newDots.push({ x, y })
}
}
return newDots
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
}
35 changes: 35 additions & 0 deletions src/components/DataRoom/IndustryComparison/utils/drawDots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const MAX_SCALE_DISTANCE = 15
const MAX_POSITION_DISTANCE = 100
const MOBILE_MAX_SCALE = 8
const DESKTOP_MAX_SCALE = 12
const DOT_COLOR = '#121312'

export const drawDots = (
ctx: CanvasRenderingContext2D,
dots: { x: number; y: number }[],
dimensions: { width: number; height: number },
lerpedMousePosition: { x: number; y: number },
isMobile: boolean,
) => {
ctx.clearRect(0, 0, dimensions.width, dimensions.height)

const maxScale = isMobile ? MOBILE_MAX_SCALE : DESKTOP_MAX_SCALE

ctx.fillStyle = DOT_COLOR

dots.forEach((dot) => {
const dx = lerpedMousePosition.x - dot.x
const dy = lerpedMousePosition.y - dot.y
const distance = Math.sqrt(dx * dx + dy * dy)

// Apply scale effect
const scale = Math.max(1, maxScale - distance / MAX_SCALE_DISTANCE)
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved

// Calculate force for translation
const force = Math.max(0, (MAX_POSITION_DISTANCE - distance) / MAX_POSITION_DISTANCE)
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved

ctx.beginPath()
ctx.arc(dot.x, dot.y, 1 * scale, 0, 2 * Math.PI)
ctx.fill()
})
}
5 changes: 5 additions & 0 deletions src/components/DataRoom/IndustryComparison/utils/lerp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const LERP_FACTOR = 0.1

export const lerp = (start: number, end: number) => {
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
return start + (end - start) * LERP_FACTOR
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const updateCanvasDimensions = (
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
canvasRef: React.RefObject<HTMLCanvasElement>,
containerRef: React.RefObject<HTMLDivElement>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing the ref, I think we can use the element directly, e.g.

Suggested change
canvasRef: React.RefObject<HTMLCanvasElement>,
containerRef: React.RefObject<HTMLDivElement>,
canvas: HTMLCanvasElement,
container: HTMLDivElement,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in updateCanvas util in 01d5a3b

) => {
if (canvasRef.current && containerRef.current) {
const { clientWidth, clientHeight } = containerRef.current
const pixelRatio = window.devicePixelRatio || 1
canvasRef.current.width = clientWidth * pixelRatio
canvasRef.current.height = clientHeight * pixelRatio
canvasRef.current.style.width = `${clientWidth}px`
canvasRef.current.style.height = `${clientHeight}px`

const ctx = canvasRef.current.getContext('2d')
if (ctx) {
ctx.scale(pixelRatio, pixelRatio)
}
return { width: clientWidth, height: clientHeight }
}
return { width: 0, height: 0 }
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest we split this - it's updating the canvas, but also getting the dimensions of the container (used in the component). I'd suggest we have two functions: something like getContainerDimensions (and maybe we don't need to store that in state) and then a function to update the canvas.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by using useContainerSize hook in 01d5a3b

4 changes: 4 additions & 0 deletions src/content/dataroom.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
"href": "https://dune.com/queries/3737066"
}
},
{
"component": "DataRoom/IndustryComparison",
"title": "How we compare<br/>to others industry leaders"
},
{
"component": "DataRoom/ExternalLinksGrid",
"title": "Dune Boards",
Expand Down
Loading