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 3 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
121 changes: 121 additions & 0 deletions src/components/DataRoom/IndustryComparison/DotGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { MotionValue } from 'framer-motion'
import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { createDots } from './utils/createDots'
import { updateCanvasDimensions } from './utils/updateCanvasDimensions'
import { drawDots } from './utils/drawDots'
import { lerp } from './utils/lerp'
import { getContainerDimensions } from './utils/getContainerDimensions'

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 = getContainerDimensions(containerRef)
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

const updateDimensions = useCallback(() => {
updateCanvasDimensions(canvasRef, dimensions.width, dimensions.height)
}, [dimensions.width, dimensions.height])

const handleMouseMove = useCallback(
(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 })
}
},
[containerRef],
)

const updateMobileMousePosition = useCallback(() => {
if (scrollYProgress && isMobile && containerRef.current) {
const progress = scrollYProgress.get()
const { height } = containerRef.current.getBoundingClientRect()
setMousePosition({ x: dimensions.width - dimensions.width / 8, y: progress * height })
}
}, [scrollYProgress, isMobile, containerRef, dimensions.width])

const lerpMousePosition = useCallback(() => {
setLerpedMousePosition((prev) => ({
x: lerp(prev.x, mousePosition.x),
y: lerp(prev.y, mousePosition.y),
}))
return requestAnimationFrame(lerpMousePosition)
}, [mousePosition.x, mousePosition.y])

useEffect(() => {
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)
}

const animationFrameId = lerpMousePosition()

return () => {
window.removeEventListener('resize', updateDimensions)
if (isMobile && scrollYProgress) {
scrollYProgress.clearListeners()
} else {
container?.removeEventListener('mousemove', handleMouseMove)
}
cancelAnimationFrame(animationFrameId)
}
}, [
updateDimensions,
handleMouseMove,
updateMobileMousePosition,
lerpMousePosition,
containerRef,
isMobile,
scrollYProgress,
])

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, mousePosition, lerpedMousePosition, isMobile)
animationFrameId = requestAnimationFrame(animate)
}

animate()

return () => {
cancelAnimationFrame(animationFrameId)
}
}, [dots, dimensions, mousePosition, 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
}
44 changes: 44 additions & 0 deletions src/components/DataRoom/IndustryComparison/utils/drawDots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { lerp } from './lerp'

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 },
mousePosition: { x: number; y: number },
lerpedMousePosition: { x: number; y: number },
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
isMobile: boolean,
) => {
ctx.clearRect(0, 0, dimensions.width, dimensions.height)

const maxScale = isMobile ? MOBILE_MAX_SCALE : DESKTOP_MAX_SCALE

ctx.fillStyle = DOT_COLOR

// Update lerpedMousePosition
lerpedMousePosition.x = lerp(lerpedMousePosition.x, mousePosition.x)
lerpedMousePosition.y = lerp(lerpedMousePosition.y, mousePosition.y)

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()
})

return lerpedMousePosition
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const getContainerDimensions = (
containerRef: React.RefObject<HTMLDivElement>,
): { width: number; height: number } => {
if (containerRef.current) {
const { clientWidth, clientHeight } = containerRef.current
return { width: clientWidth, height: clientHeight }
}
return { width: 0, height: 0 }
}
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,18 @@
export const updateCanvasDimensions = (
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
canvasRef: React.RefObject<HTMLCanvasElement>,
width: number,
height: number,
): void => {
if (canvasRef.current) {
const pixelRatio = window.devicePixelRatio || 1
canvasRef.current.width = width * pixelRatio
canvasRef.current.height = height * pixelRatio
canvasRef.current.style.width = `${width}px`
canvasRef.current.style.height = `${height}px`

const ctx = canvasRef.current.getContext('2d')
if (ctx) {
ctx.scale(pixelRatio, pixelRatio)
}
}
}
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
24 changes: 24 additions & 0 deletions src/hooks/useLerpedMousePosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { lerp } from '@/components/DataRoom/IndustryComparison/utils/lerp'
import { useState, useCallback, useEffect } from 'react'

export const useLerpedMousePosition = (mousePosition: { x: number; y: number }) => {
const [lerpedMousePosition, setLerpedMousePosition] = useState({ x: 0, y: 0 })

const lerpMousePosition = useCallback(() => {
setLerpedMousePosition((prev) => ({
x: lerp(prev.x, mousePosition.x),
y: lerp(prev.y, mousePosition.y),
}))
}, [mousePosition.x, mousePosition.y])

useEffect(() => {
const animationFrameId = requestAnimationFrame(function animate() {
lerpMousePosition()
requestAnimationFrame(animate)
})

return () => cancelAnimationFrame(animationFrameId)
}, [lerpMousePosition])

return lerpedMousePosition
}
Loading