import { useEffect, useState, useRef, useCallback } from 'react';
import { ForceGraph3D } from 'react-force-graph';
import styles from './CyberlinksGraph.module.scss';

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 }: Props) {
  const [isRendering, setRendering] = useState(true);
  const [touched, setTouched] = useState(false);

  const fgRef = useRef();

  // 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 });
  }, [fgRef]);

  // 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);
  }, [fgRef, 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);
      }
    };
  }, [fgRef]);

  // orbit camera
  useEffect(() => {
    if (!fgRef.current || touched || isRendering) {
      return;
    }

    let angle = 0;

    let interval = null;

    const timeout = setTimeout(() => {
      interval = setInterval(() => {
        fgRef.current.cameraPosition({
          x: DEFAULT_CAMERA_DISTANCE * Math.sin(angle),
          z: DEFAULT_CAMERA_DISTANCE * Math.cos(angle),
        });
        angle += Math.PI / 3000;
      }, 10);
    }, CAMERA_ZOOM_IN_EFFECT_DURATION + CAMERA_ZOOM_IN_EFFECT_DELAY);

    return () => {
      clearTimeout(timeout);
      clearInterval(interval);
    };
  }, [fgRef, 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
      );
    },
    [fgRef]
  );

  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
      );
    },
    [fgRef]
  );

  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,
      }}
    >
      {isRendering && (
        <div className={styles.loaderWrapper}>rendering data...</div>
      )}

      <ForceGraph3D
        height={size}
        width={size}
        ref={fgRef}
        graphData={data}
        showNavInfo={false}
        backgroundColor="#000000"
        warmupTicks={420}
        cooldownTicks={0}
        enableNodeDrag={false}
        enablePointerInteraction
        enableNavigationControls
        nodeLabel="id"
        nodeColor={() => 'rgba(0,100,235,1)'}
        nodeOpacity={1.0}
        nodeRelSize={8}
        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={handleNodeRightClick}
        onNodeRightClick={handleNodeClick}
        onLinkClick={handleLinkRightClick}
        onLinkRightClick={handleLinkClick}
        onEngineStop={handleEngineStop}
      />
    </div>
  );
}

export default CyberlinksGraph;

Synonyms

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

Neighbours