cyb/src/features/cyberlinks/CyberlinksGraph/CyberlinksGraph.tsx

import { useCallback, useEffect, useRef, useState } from 'react';
import ForceGraph3D from 'react-force-graph-3d';
import GraphActionBar from '../graph/GraphActionBar/GraphActionBar';

import styles from './CyberlinksGraph.module.scss';
import GraphHoverInfo from './GraphHoverInfo/GraphHoverInfo';

type Props = {
  data: any;
  // currentAddress?: string;
  size?: number;
};

// before zoom in
const INITIAL_CAMERA_DISTANCE = 2500;
const DEFAULT_CAMERA_DISTANCE = 1300;
const CAMERA_ZOOM_IN_EFFECT_DURATION = 5000;
const CAMERA_ZOOM_IN_EFFECT_DELAY = 500;

function CyberlinksGraph({ data, size, minVersion }: Props) {
  const [isRendering, setRendering] = useState(true);
  const [touched, setTouched] = useState(false);
  const [hoverNode, setHoverNode] = useState(null);

  const fgRef = useRef<any>();

  // debug, remove later
  useEffect(() => {
    if (isRendering) {
      console.time('rendering');
    } else {
      console.timeEnd('rendering');
    }
  }, [isRendering]);

  // initial camera position, didn't find via props
  useEffect(() => {
    if (!fgRef.current) {
      return;
    }
    fgRef.current.cameraPosition({ z: INITIAL_CAMERA_DISTANCE });
  }, []);

  // initial loading camera zoom effect
  useEffect(() => {
    if (!fgRef.current || isRendering) {
      return;
    }

    setTimeout(() => {
      if (!fgRef.current) {
        return;
      }

      fgRef.current.cameraPosition(
        { z: DEFAULT_CAMERA_DISTANCE },
        null,
        CAMERA_ZOOM_IN_EFFECT_DURATION
      );
    }, CAMERA_ZOOM_IN_EFFECT_DELAY);
  }, [isRendering]);

  useEffect(() => {
    if (!fgRef.current) {
      return;
    }

    function onTouch() {
      setTouched(true);
    }

    fgRef.current.controls().addEventListener('start', onTouch);

    return () => {
      if (fgRef.current) {
        fgRef.current.controls().removeEventListener('start', onTouch);
      }
    };
  }, []);

  // orbit camera using requestAnimationFrame (avoids 10ms setInterval CPU drain)
  useEffect(() => {
    if (!fgRef.current || touched || isRendering) {
      return;
    }

    let angle = 0;
    let rafId: number | null = null;
    let lastTime = 0;

    const orbit = (time: number) => {
      if (time - lastTime >= 33) { // ~30fps
        lastTime = time;
        if (fgRef.current) {
          fgRef.current.cameraPosition({
            x: DEFAULT_CAMERA_DISTANCE * Math.sin(angle),
            z: DEFAULT_CAMERA_DISTANCE * Math.cos(angle),
          });
          angle += Math.PI / 900; // same visual speed at 30fps
        }
      }
      rafId = requestAnimationFrame(orbit);
    };

    const timeout = setTimeout(() => {
      rafId = requestAnimationFrame(orbit);
    }, CAMERA_ZOOM_IN_EFFECT_DURATION + CAMERA_ZOOM_IN_EFFECT_DELAY);

    return () => {
      clearTimeout(timeout);
      if (rafId !== null) cancelAnimationFrame(rafId);
    };
  }, [touched, isRendering]);

  const handleNodeClick = useCallback((node) => {
    if (!fgRef.current) {
      return;
    }

    const distance = 300;
    const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);

    fgRef.current.cameraPosition(
      { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio },
      node,
      5000
    );
  }, []);

  const handleLinkClick = useCallback((link) => {
    if (!fgRef.current) {
      return;
    }

    const node = link.target;
    const distance = 300;
    const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);

    fgRef.current.cameraPosition(
      { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio },
      node,
      5000
    );
  }, []);

  const handleNodeRightClick = useCallback((node) => {
    window.open(`${window.location.origin}/ipfs/${node.id}`, '_blank');
  }, []);

  const handleLinkRightClick = useCallback((link) => {
    window.open(`${window.location.origin}/network/bostrom/tx/${link.name}`, '_blank');
  }, []);

  const handleEngineStop = useCallback(() => {
    console.log('ForceGraph3D engine stopped!');
    setRendering(false);
  }, []);

  return (
    <div
      style={{
        minHeight: size,
        position: 'relative',
      }}
    >
      {isRendering && <div className={styles.loaderWrapper}>rendering data...</div>}

      <ForceGraph3D
        height={size}
        width={size}
        ref={fgRef}
        graphData={data}
        showNavInfo={false}
        backgroundColor="rgba(0, 0, 0, 0)"
        warmupTicks={420}
        cooldownTicks={0}
        enableNodeDrag={false}
        enablePointerInteraction
        enableNavigationControls
        // nodeLabel="id"
        nodeColor={() => 'rgba(0,100,235,1)'}
        nodeOpacity={1.0}
        nodeRelSize={8}
        onNodeHover={setHoverNode}
        linkColor={
          // not working
          (_link) =>
            // link.subject && link.subject === currentAddress
            //   ? 'red'
            'rgba(9,255,13,1)'
        }
        linkLabel=""
        linkWidth={4}
        linkCurvature={0.2}
        linkOpacity={0.7}
        linkDirectionalParticles={1}
        linkDirectionalParticleColor={() => 'rgba(9,255,13,1)'}
        linkDirectionalParticleWidth={4}
        linkDirectionalParticleSpeed={0.015}
        // linkDirectionalArrowRelPos={1}
        // linkDirectionalArrowLength={10}
        // linkDirectionalArrowColor={() => 'rgba(9,255,13,1)'}

        onNodeClick={!minVersion ? handleNodeRightClick : undefined}
        onNodeRightClick={handleNodeClick}
        onLinkClick={handleLinkRightClick}
        onLinkRightClick={handleLinkClick}
        onEngineStop={handleEngineStop}
      />

      {!minVersion && (
        <>
          <GraphHoverInfo
            node={hoverNode}
            camera={fgRef.current?.camera()}
            size={size || window.innerWidth}
          />
          <GraphActionBar />
        </>
      )}
    </div>
  );
}

export default CyberlinksGraph;

Synonyms

pussy-ts/src/features/cyberlinks/CyberlinksGraph/CyberlinksGraph.tsx

Neighbours