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 7 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
50 changes: 50 additions & 0 deletions src/components/DataRoom/IndustryComparison/DotGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { MotionValue } from 'framer-motion'
import { useEffect, useRef } from 'react'
import { createDots } from './utils/createDots'
import { drawDots } from './utils/drawDots'
import { useIsMediumScreen } from '@/hooks/useMaxWidth'
import css from './styles.module.css'
import useContainerSize from '@/hooks/useContainerSize'
import { updateCanvas } from './utils/updateCanvas'
import useMousePosition from './utils/useMousePosition'

export default function DotGrid({
containerRef,
scrollYProgress,
}: {
containerRef: React.RefObject<HTMLDivElement>
scrollYProgress?: MotionValue<number>
}) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const isMobile = useIsMediumScreen()
const dimensions = useContainerSize(containerRef)
const mousePosition = useMousePosition(canvasRef, dimensions, scrollYProgress)

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

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

const dots = createDots(dimensions, isMobile)
updateCanvas(canvas, ctx, dimensions.width, dimensions.height)
Copy link
Member

Choose a reason for hiding this comment

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

This is also being called unconditionally without any change in the dimensions.

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 9f82803

Copy link
Member

Choose a reason for hiding this comment

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

How did you address it ? drawDots still being called unconditionally at every possible frame and updating the canvas context for every dot


let animationFrameId: number

const animate = () => {
if (dimensions.width > 0 && dimensions.height > 0) {
drawDots(ctx, dots, dimensions, mousePosition, isMobile)
Copy link
Member

Choose a reason for hiding this comment

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

This function is expensive to be called non-stop. Can you only redraw the dots when necessary? (i.e, when the mouse moves or when the resolution change)

Copy link
Member

Choose a reason for hiding this comment

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

Can you also explore a way to not have to draw the whole grid when the mouse moves but only the dots that are changing size?

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 9f82803

Copy link
Member

Choose a reason for hiding this comment

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

How did you address it?

}
animationFrameId = requestAnimationFrame(animate)
}

animate()

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

return <canvas ref={canvasRef} className={css.canvasStyles} />
}
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} 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
79 changes: 79 additions & 0 deletions src/components/DataRoom/IndustryComparison/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
.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: 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;
}

.canvasStyles {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin-top: 72px;
}

.title {
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;
}
.canvasStyles {
margin-top: 64px;
}
.title {
font-size: 56px;
line-height: 60px;
}
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
.rightPanelBG {
margin-top: 0px;
}
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
}
24 changes: 24 additions & 0 deletions src/components/DataRoom/IndustryComparison/utils/createDots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Creates an array of dot coordinates based on given dimensions and device type.
* @param dimensions - The width and height of the container.
* @param isMobile - Boolean indicating if the device is mobile.
* @returns An array of objects containing x and y coordinates for each dot.
*/
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved

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
}
45 changes: 45 additions & 0 deletions src/components/DataRoom/IndustryComparison/utils/drawDots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { lerp } from '@/lib/lerp'

type Position = { x: number; y: number }

const MAX_SCALE_DISTANCE = 15
const MOBILE_MAX_SCALE = 8
const DESKTOP_MAX_SCALE = 12
const DOT_COLOR = '#121312'
const LERP_FACTOR = 0.07

// Initialize lerpedMousePosition outside the function
let lerpedMousePosition: Position = { x: 0, y: 0 }

export const drawDots = (
ctx: CanvasRenderingContext2D,
dots: Position[],
dimensions: { width: number; height: number },
mousePosition: Position,
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, LERP_FACTOR)
lerpedMousePosition.y = lerp(lerpedMousePosition.y, mousePosition.y, LERP_FACTOR)

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

ctx.beginPath()
ctx.arc(dot.x, dot.y, 1 * scale, 0, 2 * Math.PI)
ctx.fill()
})

return lerpedMousePosition
}
15 changes: 15 additions & 0 deletions src/components/DataRoom/IndustryComparison/utils/updateCanvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Updates the canvas size and scale to account for device pixel ratio.
* @param canvas - The HTML canvas element to update
* @param ctx - The 2D rendering context of the canvas
* @param width - The desired width of the canvas in CSS pixels
* @param height - The desired height of the canvas in CSS pixels
*/
export function updateCanvas(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, width: number, height: number) {
const dpr = window.devicePixelRatio || 1
canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
ctx.scale(dpr, dpr)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState, useEffect } from 'react'
import type { MotionValue } from 'framer-motion'
import { useIsMediumScreen } from '@/hooks/useMaxWidth'

/**
* Custom hook to track mouse position or simulate it based on scroll progress.
* @param canvasRef - Reference to the canvas element.
* @param dimensions - Object containing width and height of the container.
* @param scrollYProgress - MotionValue for scroll progress, used on mobile devices.
* @returns An object with x and y coordinates representing either:
* - Actual mouse position relative to the canvas (on desktop)
* - Simulated position based on scroll progress (on mobile)
*/

DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
export default function useMousePosition(
canvasRef: React.RefObject<HTMLCanvasElement>,
dimensions: { width: number; height: number },
scrollYProgress?: MotionValue<number>,
) {
const isMobile = useIsMediumScreen()
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })

useEffect(() => {
if (isMobile && scrollYProgress) {
const updatePositionMobile = () => {
const progress = scrollYProgress.get()
setMousePosition({ x: dimensions.width - dimensions.width / 8, y: progress * dimensions.height })
}
return scrollYProgress.on('change', updatePositionMobile)
} else {
const canvas = canvasRef.current
const updatePositionDesktop = (e: MouseEvent) => {
const rect = canvas?.getBoundingClientRect()
if (rect) {
setMousePosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })
}
}
canvas?.addEventListener('mousemove', updatePositionDesktop)
return () => canvas?.removeEventListener('mousemove', updatePositionDesktop)
}
}, [canvasRef, isMobile, scrollYProgress, dimensions])

return mousePosition
}
4 changes: 4 additions & 0 deletions src/content/dataroom.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
"href": "https://dune.com/queries/3737066"
}
},
{
"component": "DataRoom/IndustryComparison",
"title": "How we compare<br/>to others industry leaders"
},
{
"component": "DataRoom/AssetsSecured",
"caption": "CEX",
Expand Down
23 changes: 23 additions & 0 deletions src/hooks/useContainerSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useState, useEffect, type RefObject } from 'react'

function useContainerSize(ref: RefObject<HTMLElement>): { width: number; height: number } {
const [size, setSize] = useState({ width: 0, height: 0 })

useEffect(() => {
if (!ref.current) return

const updateSize = (entries: ResizeObserverEntry[]) => {
const { width, height } = entries[0].contentRect
setSize({ width, height })
}

const resizeObserver = new ResizeObserver(updateSize)
resizeObserver.observe(ref.current)

return () => resizeObserver.disconnect()
}, [ref])

return size
}

export default useContainerSize
12 changes: 12 additions & 0 deletions src/lib/lerp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Performs linear interpolation between two numbers.
*
* @param {number} start - The starting value.
* @param {number} end - The ending value.
* @param {number} factor - The interpolation factor (0-1).
* @returns {number} The interpolated value.
*/

export const lerp = (start: number, end: number, factor: number) => {
return start + (end - start) * factor
}
Loading