Skip to content

Commit

Permalink
feat: added animation for new appearing nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
Rassl committed Dec 11, 2024
1 parent bd56d2c commit aa56a6f
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 122 deletions.
201 changes: 128 additions & 73 deletions src/components/Universe/Graph/Cubes/Text/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Billboard, Plane, Svg, Text } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { useFrame, useThree } from '@react-three/fiber'
import gsap from 'gsap'
import { memo, useEffect, useRef } from 'react'
import { memo, useRef } from 'react'
import { Mesh, MeshBasicMaterial, Vector3 } from 'three'
import { Icons } from '~/components/Icons'
import { useTraceUpdate } from '~/hooks/useTraceUpdate'
Expand All @@ -18,6 +18,8 @@ type Props = {
color: string
hide?: boolean
ignoreDistance: boolean
scale: number
index: number
}

function splitStringIntoThreeParts(text: string): string {
Expand All @@ -38,13 +40,22 @@ function splitStringIntoThreeParts(text: string): string {
return `${firstPart}\n${secondPart}\n${thirdPart}`
}

const offset = { x: 20, y: 20 }

export const TextNode = memo(
(props: Props) => {
const { node, hide, ignoreDistance, color } = props
const { node, hide, ignoreDistance, color, index } = props
const simulation = useGraphStore((s) => s.simulation)
const positionAnimationRef = useRef<GSAPTween | null>(null)
const scaleAnimationRef = useRef<GSAPTween | null>(null)

const finishedSimulationCircle = useRef<boolean>(false)

const svgRef = useRef<Mesh | null>(null)
const ringRef = useRef<Mesh | null>(null)
const circleRef = useRef<Mesh | null>(null)
const wrapperRef = useRef<Mesh | null>(null)
const { camera: perspectiveCamera, size } = useThree()

useTraceUpdate(props)

Expand Down Expand Up @@ -89,30 +100,69 @@ export const TextNode = memo(
}

if (circleRef.current) {
circleRef.current.visible = false
circleRef.current.visible = true
}

checkDistance()
})

useEffect(() => {
if (!ringRef.current) {
return
if (finishedSimulationCircle.current) {
checkDistance()
}

gsap.fromTo(
ringRef.current.scale, // Target
{ x: 1, y: 1, z: 1 }, // From values
{
x: 2,
y: 2,
z: 2, // To values
duration: 1.5, // Animation duration
yoyo: true,
repeat: 1,
},
)
}, [ringRef])
if (wrapperRef.current && simulation) {
const simulationNode = simulation.nodes()[index]

if (!simulationNode) {
return
}

if (!finishedSimulationCircle.current) {
// Define the NDC coordinates for the fixed position
const ndc = new Vector3(
-1 + (offset.x * 2) / size.width, // Adjust for left offset
1 - (offset.y * 2) / size.height, // Adjust for top offset
0, // Near clipping plane
)

// Convert NDC to world space
const worldPosition = ndc.unproject(perspectiveCamera)

// Maintain a fixed distance from the camera
const distanceFromCamera = 5
const direction = worldPosition.sub(perspectiveCamera.position).normalize()
const fixedPosition = perspectiveCamera.position.clone().add(direction.multiplyScalar(distanceFromCamera))

wrapperRef.current.position.copy(fixedPosition)

// Store the largest dimension as the "size" of the mesh

wrapperRef.current.scale.set(0.1, 0.1, 0.1)

wrapperRef.current.visible = false
}

if (simulationNode.fx && !finishedSimulationCircle.current) {
wrapperRef.current.visible = true
finishedSimulationCircle.current = true
positionAnimationRef?.current?.kill()
scaleAnimationRef?.current?.kill()

positionAnimationRef.current = gsap.to(wrapperRef.current.position, {
x: simulationNode.fx, // Destination X coordinate
y: simulationNode.fy, // Destination Y coordinate
z: simulationNode.fz, // Destination Z coordinate
duration: 4, // Animation duration in seconds
ease: 'power2.in', // Easing function
})

scaleAnimationRef.current = gsap.to(wrapperRef.current.scale, {
x: 1, // Destination X coordinate
y: 1, // Destination Y coordinate
z: 1, // Destination Z coordinate
duration: 4.5, // Animation duration in seconds
ease: 'power2.in', // Easing function
})
}
}
})

const primaryColor = normalizedSchemasByType[node.node_type]?.primary_color
const primaryIcon = normalizedSchemasByType[node.node_type]?.icon
Expand All @@ -132,17 +182,20 @@ export const TextNode = memo(
}

return (
<Billboard follow lockX={false} lockY={false} lockZ={false} name="billboard" userData={node}>
<mesh ref={ringRef} name={node.ref_id} userData={node} visible={!hide}>
<mesh ref={circleRef} position={[0, 0, -2]} visible={false}>
<circleGeometry args={[30, 32]} />
<meshBasicMaterial color={nodeColor} opacity={0.5} transparent />
</mesh>

{node.properties?.image_url && texture ? (
<Plane args={[10 * 2, 10 * 2]} scale={2}>
<shaderMaterial
fragmentShader={`
<mesh key={node.ref_id} ref={wrapperRef} name="wr2" scale={node.scale || 1} userData={node}>
<boxGeometry args={[40, 40, 40]} />
<meshStandardMaterial opacity={0} transparent />
<Billboard follow lockX={false} lockY={false} lockZ={false} name="billboard" userData={node}>
<mesh ref={ringRef} name={node.ref_id} userData={node} visible={!hide}>
<mesh ref={circleRef} position={[0, 0, -2]} visible={false}>
<circleGeometry args={[30, 32]} />
<meshBasicMaterial color={nodeColor} opacity={0.5} transparent />
</mesh>

{node.properties?.image_url && texture ? (
<Plane args={[10 * 2, 10 * 2]} scale={2}>
<shaderMaterial
fragmentShader={`
uniform sampler2D u_texture;
uniform float u_radius;
varying vec2 vUv;
Expand All @@ -157,52 +210,54 @@ export const TextNode = memo(
}
}
`}
uniforms={uniforms}
vertexShader={`
uniforms={uniforms}
vertexShader={`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`}
/>
</Plane>
) : (
<Svg
ref={svgRef}
name="svg"
onUpdate={(svg) => {
svg.traverse((child) => {
if (child instanceof Mesh) {
// Apply dynamic color to meshes
// eslint-disable-next-line no-param-reassign
child.material = new MeshBasicMaterial({ color })
}
})
}}
position={[-15, 15, 0]}
scale={2}
src={`svg-icons/${iconName}.svg`}
strokeMaterial={{ color: 'yellow' }}
userData={node}
/>
</Plane>
) : (
<Svg
ref={svgRef}
name="svg"
onUpdate={(svg) => {
svg.traverse((child) => {
if (child instanceof Mesh) {
// Apply dynamic color to meshes
// eslint-disable-next-line no-param-reassign
child.material = new MeshBasicMaterial({ color })
}
})
}}
position={[-15, 15, 0]}
scale={2}
src={`svg-icons/${iconName}.svg`}
strokeMaterial={{ color: 'yellow' }}
userData={node}
/>
)}

{sanitizedNodeName && (
<Text
color={color}
fillOpacity={1}
name="text"
position={[0, -65, 0]}
scale={20}
userData={node}
{...fontProps}
>
{splitStringIntoThreeParts(sanitizedNodeName)}
</Text>
)}
</mesh>
</Billboard>
)}

{sanitizedNodeName && (
<Text
color={color}
fillOpacity={1}
name="text"
position={[0, -65, 0]}
scale={1}
userData={node}
{...fontProps}
fontSize={20}
>
{splitStringIntoThreeParts(sanitizedNodeName)}
</Text>
)}
</mesh>
</Billboard>
</mesh>
)
},
(prevProps, nextProps) =>
Expand Down
22 changes: 10 additions & 12 deletions src/components/Universe/Graph/Cubes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,20 @@ export const Cubes = memo(() => {
>
<RelevanceBadges />
<group name="simulation-3d-group__nodes" visible={!hideUniverse}>
{data?.nodes.map((node: NodeExtended) => {
{data?.nodes.map((node: NodeExtended, index) => {
const hide = !!selectedNode && (relativeIds.includes(node.ref_id) || selectedNode.ref_id === node.ref_id)
const color = COLORS_MAP[nodeTypes.indexOf(node.node_type)] || colors.white

return (
<mesh key={node.ref_id} name="wr2" scale={node.scale || 1} userData={node}>
<boxGeometry args={[40, 40, 40]} />
<meshStandardMaterial opacity={0} transparent />
<TextNode
key={node.ref_id}
color={color}
hide={hideUniverse || hide}
ignoreDistance={false}
node={node}
/>
</mesh>
<TextNode
key={node.ref_id}
color={color}
hide={hideUniverse || hide}
ignoreDistance={false}
index={index}
node={node}
scale={node.scale || 1}
/>
)
})}
</group>
Expand Down
51 changes: 14 additions & 37 deletions src/components/Universe/Graph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,24 @@ export const Graph = () => {
cameraSettled.current = true
}
}
})

simulation.on('end', () => {
const nodesVector = simulation.nodes().map((i: NodeExtended) => {
// eslint-disable-next-line no-param-reassign
i.fx = i.x
// eslint-disable-next-line no-param-reassign
i.fy = i.y
// eslint-disable-next-line no-param-reassign
i.fz = i.z

return new Vector3(i.x, i.y, i.z)
})

if (groupRef.current) {
const gr = groupRef.current.getObjectByName('simulation-3d-group__nodes') as Group
const grPoints = groupRef.current.getObjectByName('simulation-3d-group__node-points') as Group
const grConnections = groupRef.current.getObjectByName('simulation-3d-group__connections') as Group

if (gr) {
gr.children.forEach((mesh, index) => {
const simulationNode = simulation.nodes()[index]

if (simulationNode) {
mesh.position.set(simulationNode.x, simulationNode.y, simulationNode.z)
}
})
}

if (grPoints) {
grPoints.children[0].children.forEach((mesh, index) => {
const simulationNode = simulation.nodes()[index]
Expand All @@ -114,10 +116,6 @@ export const Graph = () => {
})
}

if (simulation.alpha() > 1) {
return
}

if (grConnections) {
linksPositionRef.current.clear()

Expand Down Expand Up @@ -174,31 +172,10 @@ export const Graph = () => {
})
}
}
})

simulation.on('end', () => {
const nodesVector = simulation.nodes().map((i: NodeExtended) => {
// eslint-disable-next-line no-param-reassign
i.fx = i.x
// eslint-disable-next-line no-param-reassign
i.fy = i.y
// eslint-disable-next-line no-param-reassign
i.fz = i.z

return new Vector3(i.x, i.y, i.z)
})

const boundingBox = new Box3().setFromPoints(nodesVector)

const boundingSphere = new Sphere()

boundingBox.getBoundingSphere(boundingSphere)

const sphereRadius = boundingSphere.radius

setGraphRadius(sphereRadius * 1.5)

cameraSettled.current = false
console.log(boundingBox)
})
}, [dataInitial, simulation, setGraphRadius, normalizedSchemasByType])

Expand Down

0 comments on commit aa56a6f

Please sign in to comment.