-
Notifications
You must be signed in to change notification settings - Fork 15
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
Changes from 7 commits
918350c
c4aebba
c119de2
1a6f721
01d5a3b
ccf8dfe
fffb8ab
74afb38
a8a5d94
9f82803
27b64ca
3daff57
42bee79
07dd56a
4e7f180
c090e6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
|
||
let animationFrameId: number | ||
|
||
const animate = () => { | ||
if (dimensions.width > 0 && dimensions.height > 0) { | ||
drawDots(ctx, dots, dimensions, mousePosition, isMobile) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 9f82803 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} /> | ||
} |
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> | ||
) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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
|
||
} |
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 | ||
} |
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 | ||
} |
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 | ||
} |
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 |
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 | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 9f82803
There was a problem hiding this comment.
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