add interactions for hero models - desktop

This commit is contained in:
amirhouieh
2023-06-01 02:42:12 +03:30
committed by Hossein Mehrabi
parent 0ec93e6b8c
commit 8f53200f24
20 changed files with 400 additions and 124 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"start": "docusaurus start -p 3002",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
@@ -1,2 +1,7 @@
:root {
}
body,
html {
overflow-x: hidden;
}
@@ -8,22 +8,40 @@ import {
FeatureList,
Showcase,
HeroModel,
HeroInfo,
} from '../components/mdx'
<Hero
size="large"
title="The web3 managing standard"
description="Waku is a decentralized communications network"
>
<HeroActions>
<HeroAction href="https://docs.waku.org" target="_blank">
Build on Waku
</HeroAction>
<HeroAction variant="filled" href="https://docs.waku.org" target="_blank">
Run Waku
</HeroAction>
</HeroActions>
<HeroModel modelUrl="/hero/bust-hi.glb" layout="floating" renderer="ascii" />
<Hero size="large">
<HeroInfo>
<HeroTitle>The web3 managing standard</HeroTitle>
{/*
<HeroDescription>
Waku is a decentralized communications network that enables private, censorship-resistant messaging for web3 applications.
</HeroDescription>
* */}
<HeroDescription>
WAKU IS A DECENTRALIZED COMMUNICATIONS NETWORK.
</HeroDescription>
<HeroActions>
<HeroAction href="https://docs.waku.org" target="_blank">
Build on Waku
</HeroAction>
<HeroAction variant="filled" href="https://docs.waku.org" target="_blank">
Run Waku
</HeroAction>
</HeroActions>
</HeroInfo>
<HeroModel
modelUrl="/hero/bulanding-generic-hand-01.glb"
layout="floating"
renderer="ascii"
initialX={-0.9831336272264352}
initialY={-0.14592188008227222}
rotateSpeed={0.1}
asciiConfig={{
characters: ' .,:;ox%# ||| ||| waku @',
}}
/>
</Hero>
<FeatureList
@@ -1,5 +1,6 @@
.mdx-feature-list {
border-top: 1px solid rgb(var(--lsd-surface-secondary));
position: relative;
}
.mdx-feature-list__title {
@@ -1,13 +1,10 @@
.mdx-hero {
position: relative;
border-top: 1px solid rgb(var(--lsd-surface-secondary));
padding-top: 8rem;
padding-bottom: 2rem;
}
.mdx-hero--large {
padding-top: 2rem;
padding-bottom: 2rem;
border-top: none;
//padding-top: 2rem;
//padding-bottom: 2rem;
//border-top: none;
}
@@ -1,19 +1,14 @@
import clsx from 'clsx'
import React from 'react'
import { HeroDescription, HeroTitle } from '../index'
import { HeroContext } from './Hero.context'
import './Hero.scss'
export type HeroProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> & {
size?: 'large' | 'medium' | 'small'
title?: React.ReactNode
description?: React.ReactNode
}
export const Hero: React.FC<HeroProps> = ({
size = 'medium',
title,
description,
className,
children,
...props
@@ -24,8 +19,6 @@ export const Hero: React.FC<HeroProps> = ({
className={clsx(className, 'mdx-hero', `mdx-hero--${size}`)}
{...props}
>
{title && <HeroTitle>{title}</HeroTitle>}
{description && <HeroDescription>{description}</HeroDescription>}
{children}
</div>
</HeroContext.Provider>
@@ -4,4 +4,7 @@
flex-direction: row;
gap: 0 1rem;
align-items: center;
position: relative;
z-index: 98;
}
@@ -6,25 +6,27 @@ import './HeroDescription.scss'
export type HeroDescriptionProps = TypographyProps & {
size?: 'small' | 'medium' | 'large'
uppercase?: boolean
}
export const HeroDescription: React.FC<HeroDescriptionProps> = ({
size: sizeProp,
className,
uppercase = false,
children,
...props
}) => {
const ctx = useHero()
const size = sizeProp ?? (ctx ? ctx.size : 'medium')
return (
<Typography
variant={size === 'small' ? 'h5' : 'h1'}
variant={size === 'small' ? 'h5' : 'h2'}
className={clsx(
className,
'mdx-hero-description',
`mdx-hero-description--${size}`,
uppercase ? 'mdx-hero-description--uppercase' : '',
)}
{...props}
>
@@ -0,0 +1,17 @@
.mdx-hero-info {
padding: var(--logos-hero-info-padding-bottom) 0
var(--logos-hero-info-padding-bottom) 0;
height: var(--logos-hero-info-height);
display: flex;
flex-direction: column;
justify-content: end;
border-bottom: 1px solid rgb(var(--lsd-surface-secondary));
margin-bottom: calc(100vh - var(--logos-hero-info-height));
transition: margin-bottom 0.05s ease-in-out;
}
.mdx-hero-info--large {
}
@@ -0,0 +1,61 @@
import clsx from 'clsx'
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react'
import { useHero } from '../Hero/Hero.context'
import './HeroInfo.scss'
import { useScrollY } from '../../../lib/useScrollY'
import { calcScrollThreshold, mapFloat } from '../../../lib/ui.utils'
export type HeroInfoProps = PropsWithChildren<
React.HTMLAttributes<HTMLDivElement>
> & {
size?: 'medium' | 'large' | 'small'
}
const calcMaxOffsetY = () => {
return window.innerHeight * 0.33
}
export const HeroInfo: React.FC<HeroInfoProps> = ({
size: sizeProp,
className,
children,
...props
}) => {
const ctx = useHero()
const size = sizeProp ? sizeProp : ctx ? ctx.size : 'medium'
const scrollY = useScrollY()
const ref = useRef<HTMLDivElement>()
const [initialMarginBottom, setInitialMarginBottom] = useState<number | null>(
null,
)
const getMarginBottom = () => {
const offsetY = mapFloat(
scrollY,
0,
calcScrollThreshold(),
0,
calcMaxOffsetY(),
)
return initialMarginBottom ? initialMarginBottom - offsetY : 0
}
useEffect(() => {
if (typeof window === 'undefined' || !(ref && ref.current)) return
setInitialMarginBottom(
parseFloat(window.getComputedStyle(ref.current).marginBottom),
)
}, [])
return (
<div
className={clsx(className, 'mdx-hero-info', `mdx-hero-info--${size}`)}
style={{
...(initialMarginBottom ? { marginBottom: getMarginBottom() } : {}),
}}
ref={ref}
{...(props as any)}
>
{children}
</div>
)
}
@@ -0,0 +1 @@
export * from './HeroInfo'
@@ -1,55 +1,51 @@
.mdx-hero-model {
border-top: 1px solid rgb(var(--lsd-surface-secondary));
margin-top: 2rem;
height: 452px;
width: 100vw;
transform: translateX(
calc(-1 * calc(calc(100vw - 1440px + calc(var(--content-padding)) * 2) / 2))
);
position: absolute;
top: calc(var(--logos-hero-info-height) - 10px);
& > div {
position: absolute;
.mdx-hero-model--inner {
position: relative;
width: 100%;
height: 1400px;
z-index: -1;
top: 200px;
z-index: 0;
left: 0;
}
& > div:first-child {
top: 0;
//transition: translate 0.6s ease-in-out;
canvas {
pointer-events: none;
}
}
& > div:last-child {
pointer-events: none;
background-image: linear-gradient(
179.61deg,
rgba(0, 0, 0, 0) 0.34%,
#000000 70%
);
opacity: 1;
}
}
.mdx-hero-model--floating {
}
.mdx-hero-model--cropped {
overflow: hidden;
& > div {
position: relative;
top: -200px;
z-index: 0;
}
.mdx-hero-model--floating {
.mdx-hero-model--inner {
canvas {
pointer-events: unset !important;
}
}
& > div:last-child {
display: none;
}
}
.mdx-hero-model--3d {
overflow: visible !important;
.mdx-hero-model--inner {
position: relative;
}
.mdx-hero-model--inner {
> div {
position: absolute;
top: 50%;
left: 50%;
transform: scale(1.2) translate(-50%, -50%);
}
}
}
.mdx-hero-model--ascii {
@@ -59,11 +55,19 @@
border: none !important;
font-family: monospace;
}
tr {
//text-align: center;
//font-size: 12px;
//letter-spacing: 1px;
}
}
}
.mdx-hero-model--shade {
width: 100%;
height: 600px;
position: absolute;
background: linear-gradient(
0deg,
rgb(0, 0, 0) 37%,
rgb(0, 0, 0) 0%,
rgba(0, 0, 0, 0) 100%
);
bottom: 500px;
left: 0;
}
@@ -2,62 +2,150 @@ import { OrbitControls, useGLTF } from '@react-three/drei'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import clsx from 'clsx'
import React, {
PropsWithChildren,
Suspense,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import * as THREE from 'three'
import { AsciiEffect } from 'three-stdlib'
import './HeroModel.scss'
import {
calcScrollThreshold,
isTouchDevice,
mapFloat,
random,
} from '../../../lib/ui.utils'
import { useScrollY } from '../../../lib/useScrollY'
let ROTATE_SPEED = 0.1
const MIN_ROTATE_SPEED = 0.1
const MAX_ROTATE_SPEED = 1
const MAX_Z = 20 * 1.4 // or another value that suits your needs
const MIN_Z = 0.2 * 1.4
const MIN_Y = -0.25
const MAX_Y = 0.25
const MIN_X = -0.25
const MAX_X = 0.25
const MIN_ZOOM = 0.22
const RESIZE_SPEED_FACTOR = 0.9
const INITIAL_ZOOM = 3
const calcZoom = (scrollY = 0, initZ = INITIAL_ZOOM) => {
return Math.max(
mapFloat(
scrollY,
0,
calcScrollThreshold() * RESIZE_SPEED_FACTOR,
initZ,
MIN_ZOOM,
),
MIN_ZOOM,
)
}
const calcRotateSpeed = (scrollY = 0, speed) => {
const sFactor = mapFloat(scrollY, 0, window.innerHeight, 1, 10)
return Math.max(Math.min(speed * sFactor, MAX_ROTATE_SPEED), MIN_ROTATE_SPEED)
}
function Model({ url, ...props }) {
// useGLTF suspends the component, it literally stops processing
const { camera } = useThree()
const { scene } = useGLTF(url) as any
const size = useMemo(() => {
const box = new THREE.Box3().setFromObject(scene)
return box.getSize(new THREE.Vector3())
}, [scene])
useLayoutEffect(() => {
if (!camera) return
const c = camera as THREE.PerspectiveCamera
const fov = c.fov * (Math.PI / 180)
const distance = Math.abs(Math.max(size.x, size.y) / Math.sin(fov / 2))
c.position.setZ(distance)
}, [size, camera])
// useLayoutEffect(() => {
// if (!camera) return
// const c = camera as THREE.PerspectiveCamera
// const fov = c.fov * (Math.PI / 180)
// const distance = Math.abs(Math.max(size.x, size.y) / Math.sin(fov / 2))
// c.position.setZ(distance)
// }, [size, camera])
return (
<primitive
object={scene}
{...props}
position={[0, (size.y / -10) * 3, 0]}
// position={[0, (10 / -10) * 3, 0]}
/>
)
}
function Rotate(props) {
const ref = useRef<any>()
useFrame((state) => (ref.current.rotation.y = state.clock.elapsedTime * 0.1))
type RotateProps = PropsWithChildren<{
initialZ?: number
rotateSpeed?: number
enableZoom?: boolean
enableRotateOnScroll?: boolean
}>
return <group ref={ref} {...props} scale={1.4} />
function Movements({
initialZ,
rotateSpeed = ROTATE_SPEED,
enableZoom = true,
enableRotateOnScroll = true,
...props
}: RotateProps) {
const ref = useRef<any>()
const scrollY = useScrollY()
useFrame((state, delta) => {
// const f = mapFloat(scrollY, 0, 1000, 1, 2);
// ref.current.rotation.x += delta * (ROTATE_SPEED);
// ref.current.rotation.y += delta * (ROTATE_SPEED);
const speed = enableRotateOnScroll
? calcRotateSpeed(scrollY, rotateSpeed)
: rotateSpeed
ref.current.rotation.y += delta * speed
})
return (
<group
ref={ref}
{...props}
scale={enableZoom ? calcZoom(scrollY, initialZ) : initialZ}
/>
)
}
function useAsciiEffect({
renderIndex = 1,
bgColor = 'black',
fgColor = 'white',
// characters = ' .:-+*=%@#',
characters = ' l.o.g.o.s',
invert = false,
color = false,
resolution = 0.2,
}) {
type AsciiConfigs = {
characters?: string
invert?: boolean
color?: boolean
bgColor?: string
fgColor?: string
resolution?: number
renderIndex?: number
}
const defaultAsciiConfigs = {
renderIndex: 1,
bgColor: 'rgb(var(--lsd-surface-primary))',
fgColor: 'rgb(var(--lsd-text-primary))',
characters: ' l.o.g.o.s',
invert: false,
color: false,
resolution: 0.2,
}
type AsciiEffectProps = {
config?: AsciiConfigs
initialZ?: number
}
function useAsciiEffect({ config = {}, initialZ }: AsciiEffectProps) {
const {
renderIndex = defaultAsciiConfigs.renderIndex,
characters = defaultAsciiConfigs.characters,
invert = defaultAsciiConfigs.invert,
color = defaultAsciiConfigs.color,
resolution = defaultAsciiConfigs.resolution,
bgColor = defaultAsciiConfigs.bgColor,
fgColor = defaultAsciiConfigs.fgColor,
} = config
// Reactive state
const { size, gl, scene, camera, viewport } = useThree()
const { gl, size, scene, camera, viewport } = useThree()
const effect = useMemo(() => {
const effect = new AsciiEffect(gl, characters, {
@@ -72,10 +160,9 @@ function useAsciiEffect({
return effect
}, [characters, invert, color, resolution])
// Styling
useLayoutEffect(() => {
effect.domElement.style.color = fgColor
effect.domElement.style.backgroundColor = bgColor
// effect.domElement.style.backgroundColor = bgColor
}, [fgColor, bgColor])
// Append on mount, remove on unmount
@@ -90,9 +177,6 @@ function useAsciiEffect({
// Set size
useEffect(() => {
camera.position.setX(2)
camera.position.setY(2)
effect.setSize(size.width, size.height)
}, [effect, size])
@@ -102,15 +186,8 @@ function useAsciiEffect({
}, renderIndex)
}
const AsciiRenderer = ({
fgColor,
bgColor,
}: {
fgColor: string
bgColor: string
}) => {
useAsciiEffect({ fgColor, bgColor })
const AsciiRenderer = (props: AsciiEffectProps) => {
useAsciiEffect(props)
return <></>
}
@@ -118,6 +195,14 @@ export type HeroModelProps = React.HTMLAttributes<HTMLDivElement> & {
modelUrl: string
layout?: 'floating' | 'cropped'
renderer?: '3d' | 'ascii'
asciiConfig?: AsciiConfigs
initialZ?: number
initialX?: number
initialY?: number
rotateSpeed?: number
enableZoom?: boolean
enableRotateOnScroll?: boolean
withParallelEffect?: boolean
}
export const HeroModel: React.FC<HeroModelProps> = ({
@@ -126,8 +211,30 @@ export const HeroModel: React.FC<HeroModelProps> = ({
modelUrl,
className,
children,
asciiConfig,
initialX,
initialY,
initialZ,
rotateSpeed = ROTATE_SPEED,
enableZoom,
enableRotateOnScroll,
withParallelEffect,
...props
}) => {
const scrollY = useScrollY()
const initialPosition = useMemo(() => {
if (typeof window === 'undefined') return new THREE.Vector3(0, 0, 0)
if (renderer === 'ascii') {
return new THREE.Vector3(
initialX || random(MIN_X, MAX_X),
initialY || random(MIN_Y, MAX_Y),
0,
)
}
// if not ascii then use the max z
return new THREE.Vector3(3.1, initialY || random(MIN_Y, MAX_Y), 0)
}, [initialX, initialY, initialZ, renderer])
return (
<div
className={clsx(
@@ -138,32 +245,53 @@ export const HeroModel: React.FC<HeroModelProps> = ({
)}
{...props}
>
<div>
<div
className={`mdx-hero-model--inner`}
style={{
// a nice parallax effect based on scroll position
...(withParallelEffect
? { transform: `translateY(${scrollY * 0.1}px)` }
: {}),
}}
>
<Canvas
dpr={[1, 2]}
camera={{
fov: 50,
position: initialPosition,
}}
>
<directionalLight position={[10, 10, 0]} intensity={1.5} />
<directionalLight position={[-10, 10, 5]} intensity={1} />
<directionalLight position={[-10, 10, 0]} intensity={1.5} />
<directionalLight position={[-10, 10, 5]} intensity={0.3} />
<directionalLight position={[-10, 20, 0]} intensity={1.5} />
<directionalLight position={[100, -10, 0]} intensity={0.25} />
<Suspense fallback={<></>}>
<Rotate>
<Movements
initialZ={initialZ}
enableZoom={enableZoom}
enableRotateOnScroll={enableRotateOnScroll}
>
<Model url={modelUrl} />
</Rotate>
</Movements>
</Suspense>
<OrbitControls />
{!isTouchDevice() && <OrbitControls enableZoom={false} />}
{renderer === 'ascii' && (
<AsciiRenderer
fgColor="rgb(var(--lsd-text-primary))"
bgColor="rgb(var(--lsd-surface-primary))"
/>
<AsciiRenderer config={asciiConfig} initialZ={initialZ} />
)}
</Canvas>
</div>
<div></div>
<div
className={'mdx-hero-model--shade'}
style={{
opacity: mapFloat(
scrollY,
0,
calcScrollThreshold() * RESIZE_SPEED_FACTOR,
0,
1,
),
}}
/>
</div>
)
}
@@ -7,3 +7,4 @@ export * from './HeroDescription'
export * from './HeroModel'
export * from './HeroTitle'
export * from './Showcase'
export * from './HeroInfo'
@@ -298,6 +298,11 @@ a code {
--ifm-navbar-item-padding-vertical: 8px;
--ifm-navbar-item-padding-horizontal: 12px;
--logos-navbar-height: 64px;
--logos-hero-info-height: 420px;
--logos-hero-info-padding-top: 32px;
--logos-hero-info-padding-bottom: 32px;
}
.card,
@@ -314,18 +319,23 @@ a code {
.sub1 {
font-size: var(--ifm-h1-font-size);
}
.sub2 {
font-size: var(--ifm-h2-font-size);
}
.sub3 {
font-size: var(--ifm-h3-font-size);
}
.sub4 {
font-size: var(--ifm-h4-font-size);
}
.sub5 {
font-size: var(--ifm-h5-font-size);
}
.sub6 {
font-size: var(--ifm-h6-font-size);
}
@@ -841,6 +851,10 @@ small {
}
}
.main-wrapper > div {
display: block;
}
.navbar__left-items {
display: none;
}
@@ -0,0 +1,21 @@
export const random = (min, max) => {
return Math.random() * (max - min) + min
}
export const mapFloat = (value, inMin, inMax, outMin, outMax) => {
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin
}
export const calcScrollThreshold = () => {
return window.innerHeight * 0.4
}
export const isTouchDevice = () => {
if (typeof window === 'undefined') return false
return (
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
// @ts-ignore
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
)
}
@@ -0,0 +1,10 @@
import { useWindowEventListener } from './useWindowEventListener'
import { useState } from 'react'
export const useScrollY = () => {
const [scrollY, setScrollY] = useState(0)
useWindowEventListener('scroll', () => {
setScrollY(window.scrollY)
})
return scrollY
}