How to make curved rectangles in ThreeJS to show the relationship between two lists?

Viewed 441

I have two lists of data on the screen, one on the left and one on the right, I'm trying to show the relationship between the two lists. The lists should look somewhat like the ones in the middle of this image. This image is from this webpage https://react-three-fiber-website-playground.vercel.app/ but this has been done in D3 I can see. I need to do the same in Three.js, ideally react-three-fiber. Would bezier curves help? Is there a proper name for this type of data visualization?

enter image description here


Just to show the current lines are drawing nicely based upon the example code given in answer here.

enter image description here

2 Answers

As an option, use CubicBezierCurve and bend PlaneGeometry in accordance to the curve:

body{
  overflow: hidden;
  margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three@0.133.1";

let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(0, 0, 10);
camera.lookAt(scene.position);
let renderer = new THREE.WebGLRenderer({
  antialias: true
});
renderer.setSize(innerWidth, innerHeight);
renderer.setClearColor(0x202020);
document.body.appendChild(renderer.domElement);

let basePoints = [ // start, end points
  new THREE.Vector3(-2, -3),
  new THREE.Vector3(2, 3)
];
let pathPoints = [
  basePoints[0],
  new THREE.Vector2().addVectors(basePoints[0], basePoints[1]).multiplyScalar(0.5).setY(basePoints[0].y),
  new THREE.Vector2().addVectors(basePoints[0], basePoints[1]).multiplyScalar(0.5).setY(basePoints[1].y),
  basePoints[1]
]
let curve = new THREE.CubicBezierCurve(pathPoints[0], pathPoints[1], pathPoints[2], pathPoints[3]);
let resultPoints = curve.getSpacedPoints(50);

let line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(resultPoints), new THREE.LineBasicMaterial({
  color: "yellow"
}));
scene.add(line);

let width = [1, 3]; // start, end

let g = new THREE.PlaneGeometry(1, 1, 50, 1);
let pos = g.attributes.position;
for (let i = 0; i <= 50; i++) {
  let lrp = THREE.MathUtils.lerp(width[0], width[1], i / 50);
  pos.setXYZ(
    i, resultPoints[i].x, resultPoints[i].y + lrp * 0.5 * Math.sign(pos.getY(i)), 0
  );
  pos.setXYZ(
    i + 51, resultPoints[i].x, resultPoints[i].y + lrp * 0.5 * Math.sign(pos.getY(i + 51)), 0
  )
}
let m = new THREE.MeshBasicMaterial({
  color: "aqua",
  wireframe: true
});
let o = new THREE.Mesh(g, m);
scene.add(o);

window.addEventListener("resize", onResize);

renderer.setAnimationLoop(_ => {
  renderer.render(scene, camera);
})

function onResize(event) {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
}

</script>

This is the same code (not exactly!) as prisoner849's answer modified to run in react-three-fiber, I use this now in my project and it is working. I think my forwardRef is not necessary, its just that I have useImperativeHandle in there for a reason. This is a non-answer to the question as its a copy of his code just translated.

import React, {useLayoutEffect, useRef} from 'react'
import * as THREE from 'three'
import { useThree } from 'react-three-fiber'
const { forwardRef, useImperativeHandle } = React;

export const LineMapping = (forwardRef(({}, ref) => {
    const {
        camera,
        gl: { domElement }
    } = useThree()

    const ref3 = useRef()
    const ref2 = useRef()
    const meshRef = useRef()

    const randomFloat = (a: number, b: number): number => {
        return (Math.random() * (b-a)) + a
    }

    let width = [randomFloat(0.3, 2.0), randomFloat(0.3, 0.6)] // start, end
    let basePoints = [
        // start, end points
        new THREE.Vector3(-2, 0.9 + randomFloat(0.0, 3.0)),
        new THREE.Vector3(2, 0.9 + randomFloat(0.0, 3.0))
    ]

    let pathPoints = [
        basePoints[0],
        // @ts-ignore
        new THREE.Vector2().addVectors(basePoints[0], basePoints[1]).multiplyScalar(0.5).setY(basePoints[0].y),
        // @ts-ignore
        new THREE.Vector2().addVectors(basePoints[0], basePoints[1]).multiplyScalar(0.5).setY(basePoints[1].y),
        basePoints[1]
    ]

    // @ts-ignore
    let curve = new THREE.CubicBezierCurve(pathPoints[0], pathPoints[1], pathPoints[2], pathPoints[3])

    let resultPoints = curve.getSpacedPoints(50)
    const lineGeometry = new THREE.BufferGeometry().setFromPoints(resultPoints)

    useLayoutEffect(() => {
        if (ref3.current !== undefined && ref2.current !== undefined) {

            // @ts-ignore
            let pos = ref2.current!.attributes.position
            for (let i = 0; i <= 50; i++) {
                let iMod = i
                let lrp = THREE.MathUtils.lerp(width[0], width[1], i / 50)
                pos.setXYZ(i, resultPoints[iMod].x, resultPoints[iMod].y + lrp * 0.5 * Math.sign(pos.getY(i)), 0)
                pos.setXYZ(iMod + 51, resultPoints[iMod].x, resultPoints[iMod].y + lrp * 0.5 * Math.sign(pos.getY(i + 51)), 0)
            }

        }
    });

    // @ts-ignore
    useImperativeHandle(ref, () => ({
        testMe: () => {


        }
    }));

    return <>
            <group position={[2.2, 0, -5.5]}>

                <group position={[0, 0, 0]} visible={false}>
                <line ref={ref3} geometry={lineGeometry}>
                    <lineDashedMaterial attach="material" color={'#ffffff'} linewidth={1} />
                </line>
                </group>

                <mesh ref={meshRef} receiveShadow>
                    <planeBufferGeometry ref={ref2} attach="geometry" args={[7, 7, 50, 1]} />
                    <meshBasicMaterial attach="material" color="#2892D7" wireframe={false} />
                </mesh>
            </group>
        </>
}))
Related