How to animate curved arrows in threejs?

Hi there,
I am looking for a way to animate curved arrows going from point A to B. They can be static as well. But I am not sure where I should start.
Also before I do animation I am also not sure how I can create a curved object that looks like an arrow.
If anyone can point me to right direction, I would really appreciate it.
Thank you.

If you’re using THREE.Curve. (Which I never but this is what I see from the doc)

A simple idea is to use curve.getPoint( t ) while t value is 0-1 (start to end of the curve)

You just need to animate the t value from 0 to 1 from time to time.

And use t value to update your arrow position

arrow.position = curve.getPoint(t);

// Same as above but avoid creating new Vector3 instance.
curve.getPoint(t, arrow.position);
1 Like

Perhaps these examples will give you a starting point.

From the Collection of examples from discourse.threejs.org

CurvedArrowHelper

discourse.threejs.hofk.de getPoint( ) getPointAt( )

ColorStripeChanging

2021 eXtended eXamples CurveGenerator

:slightly_smiling_face:

1 Like

Thanks for the suggestion by the question. I am close to finishing a multiform geometry (Open source ).

A test resulted in the following
2022-04-06 21.25.28

Not yet perfect!

function  curvedArrowCenterline( h ) {  
   return { x: 0.1 * Math.sin(  pi * h ), z: 0 };      
}

function  arrowOutline( h ) {

    return{ y: h < 0.9 ? h : (  h === 0.9 ? 0.8 : h ), r: h < 0.9 ?  0.1 : (  h === 0.9 ? 0.2 : 0 ) }

} 

UPDATE

Multiform geometry is easier than my Addon. Produces almost infinite many time-varying geometries with functions

It is not a real space curve, but only a horizontal offset of the layers. So a shear per height segment.

In the addon, it is a correctly bent cylinder. However, the arrowhead is not centered due to the height offset. You can create the arrowhead separately and form a group.

The best solution is certainly a specially created geometry. :slightly_smiling_face:

Here is an example of bending a generic mesh around a curve

https://threejs.org/examples/webgl_modifier_curve.html

I have it there MotionAlongCurve used.
( From the Collection of examples from discourse.threejs.org )

But it does not bend a single mesh. :frowning_face: See the following posts.

2022-04-08 11.35.01

Looks like the box needs segmentation greater, than 1, along X-axis.

1 Like

Yeah, needs more vertices in the axes its being curved in.

In fact
2022-04-08 12.19.05

I had no subdivision for the example of the movement.

Then an arrow that is curved can be created easily.
2022-04-08 12.26.53

1 Like

If you need many arrows there is also an instanced version which is useful for animating 100s of objects at the same time such as: XR Koi Garden

2 Likes

Everybody likes Koi fish :slight_smile: Spline + DataTexture

I looked to see if you can make a curved arrow from the official example
three.js examples LineGeometry

Should work.

2022-04-08 18.17.57


<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/how-to-animate-curved-arrows-in-threejs/36808/10 -->
	<head>
		<title> WebglLinesFat </title>
		<meta charset="utf-8">
	</head>

	<body>

		<script type="module">

			import * as THREE from '../jsm/three.module.139.js';
			import Stats from '../jsm/stats.module.139.js';
			import { OrbitControls } from '../jsm/OrbitControls.139.js';
			import { LineMaterial } from '../jsm/LineMaterial.139.js';
            import { LineSegments2 } from '../jsm/LineSegments2.139.js';
			import { Line2 } from '../jsm/Line2.139.js';
			import { LineGeometry } from '../jsm/LineGeometry.139.js';
 
			let line, segments ;
			let renderer, scene, camera,controls;

			let matLine ;
			let stats ;

			init();
			animate();

			function init() {

				renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
				renderer.setPixelRatio( window.devicePixelRatio );
				//renderer.setClearColor( 0x000000, 0.0 );
				renderer.setSize( window.innerWidth, window.innerHeight );
				document.body.appendChild( renderer.domElement );

				scene = new THREE.Scene();

				camera = new THREE.PerspectiveCamera( 65, window.innerWidth / window.innerHeight, 0.001, 1000 );
				camera.position.set( 0, 5, 25 );
 
				controls = new OrbitControls( camera, renderer.domElement );
  
				// Position and THREE.Color Data

				const positions = [];
				//const colors = [];
				const points = [];
				for ( let i = - 20; i < 20; i ++ ) {

					const t = i / 3;
					//points.push( new THREE.Vector3( t * Math.sin( 2 * t ), t, t * Math.cos( 2 * t ) ) );
                    points.push( new THREE.Vector3( t * Math.sin(  t / 6 ), t, -t   ) );
                    
				}

				const spline = new THREE.CatmullRomCurve3( points );
                const divisions = Math.round( points.length ); //const divisions = Math.round( 3 * points.length );
				const point = new THREE.Vector3();
				//const color = new THREE.Color();

				for ( let i = 0, l = divisions; i < l; i ++ ) {

					const t = i / l;

					spline.getPoint( t, point );
					positions.push( point.x, point.y, point.z );

					//color.setHSL( t, 1.0, 0.5 );
					//colors.push( color.r, color.g, color.b );

				}

				const lineGeometry = new LineGeometry();
				lineGeometry.setPositions( positions );
				//lineGeometry.setColors( colors );
 
				matLine = new LineMaterial( {

					color:  0x0000ff, // 0xffffff,
					linewidth: 0.75, // in world units with size attenuation, pixels otherwise
					worldUnits: true,
					//vertexColors: true,

					//resolution:  // to be set by renderer, eventually
					//alphaToCoverage: true,

				} );
 
 

				line = new Line2( lineGeometry, matLine );
				// line.computeLineDistances();
				// line.scale.set( 1, 1, 1 );
                line.scale.set( 1.2, 1.2, 1.2 );
				scene.add( line );

				//const geo = new THREE.BufferGeometry();
				//geo.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
				//geo.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );

				window.addEventListener( 'resize', onWindowResize );
				onWindowResize();

				stats = new Stats();
				document.body.appendChild( stats.dom );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );


			}

			function animate() {

				requestAnimationFrame( animate );

				stats.update();

				// main scene

				//renderer.setClearColor( 0x000000, 0 );

				//renderer.setViewport( 0, 0, window.innerWidth, window.innerHeight );

				// renderer will set this eventually
				//matLine.resolution.set( window.innerWidth, window.innerHeight ); // resolution of the viewport
 
				renderer.render( scene, camera );
			}

		</script>

	</body>

</html>
1 Like

And here is the arrow.
2022-04-08 20.34.35

But you have to create the peak separately, otherwise it is only correct from a distance.

for ( let i = - 25; i <= 0.4; i ++ ) {
   points.push( new THREE.Vector3( i / 4 * Math.sin(  i / 20 ), 0, -i / 2 ) );
}
 
for ( let i = - 5; i <=  5; i ++ ) {
    points.push( new THREE.Vector3( i / 5 , 0, Math.abs( i / 5 ) ) ); 
}

const matLine = new LineMaterial( {
	color:  0xff00ff,
	linewidth: 0.01,
} );

UPDATE:
I just noticed that the visible arrow thickness remains constant as the camera moves further away. I must have removed something from the template that is obviously important. :thinking:

1 Like

Try to uncomment this line.

Then the complete line disappears. I’ll probably have to simplify the original example again piece by piece to find the spot.

UPDATE:
I obviously got confused with my various test versions. In one it works. Must bring order into it. :frowning_face:

This is how it works
ArrowLinesFat

2022-04-09 20.34.25

I didn’t get, why does it have to be fat lines?
Is usual THREE.Line() not an option?

… me neither.
Looked at how to do it with a simple line.

CurvedArrowWithTip

With it you can build a beautiful net.

You can make the balls as transparent bubbles and position things in them.
Here only as a wireframe.

The arrow hits the balls on the straight line connecting the centers.



<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/how-to-animate-curved-arrows-in-threejs/36808/17 -->
<head>
	<meta charset="utf-8">
	<title> CurvedArrowWithTip </title>
     <style>	
        body { 
        margin: 0;
        overflow: hidden;
        }
    </style>
</head>

<body></body>

<script src='../js/three.139.js'></script>
<script src='../js/OrbitControls.139.js'></script>

<script>

// @author hofk

const scene = new THREE.Scene( );

const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setClearColor( 0xdedede, 1.0 );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const camera = new THREE.PerspectiveCamera( 65, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.position.set( -1, 3, 4 );

const controls = new THREE.OrbitControls( camera, renderer.domElement );

window.addEventListener( "resize", event => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix( );
                                             renderer.setSize(innerWidth, innerHeight ); } );
                                             
const gridHelper = new THREE.GridHelper( 5, 10 );
scene.add( gridHelper );
 
// input data .................................
const v0 = new THREE.Vector3( -1.9, 0.2, 0.4 );
const r0 = 0.16;
const v1 = new THREE.Vector3(  1.7, 0.5, 0.1 );
const r1 = 0.35;
const bend = 0.8; // 0 .. +-1 .. +-2 ...
const φ  = -0.9;  // radiant,  test with 0
//.............................................

const mesh0 = new THREE.Mesh( new THREE.SphereGeometry( r0 ), new THREE.MeshBasicMaterial( { color:  0x11ab22 , wireframe: true } ) );
mesh0.position.set( v0.x, v0.y, v0.z ) ;
scene.add( mesh0 );

const mesh1 = new THREE.Mesh( new THREE.SphereGeometry( r1 ), new THREE.MeshBasicMaterial( { color:  0x11ab22 , wireframe: true } ) );
mesh1.position.set( v1.x, v1.y, v1.z ) ;
scene.add( mesh1 );

const  cpc = 100; // bended: curve points count
let points = [];  // curve points
const points3 = [];
let k, b, n, r;

const v = new THREE.Vector3( ).subVectors( v1, v0 );
const vn = v.clone( ).normalize( );
const vlen = v.length( );
v0.add( v.normalize( ).multiplyScalar( r0 ) );
v1.sub( v.normalize( ).multiplyScalar( r1 ) ); 
const vv = new THREE.Vector3( ).subVectors( v1, v0 ).divideScalar( 2 );

// orthogonal see   http://lolengine.net/blog/2013/09/21/picking-orthogonal-vector-combing-coconuts
k = ( Math.abs( vn.x ) + 0. ) % 1; // fract
b = new THREE.Vector3( -vn.y, vn.x - k * vn.z, k * vn.y ).normalize( ); // binormal

if( φ !== 0 ) {

	const n = new THREE.Vector3( ).crossVectors( vn, b ); // normal
	r = new THREE.Vector3( ).addVectors( b.multiplyScalar( Math.cos( φ ) ), n.multiplyScalar( Math.sin( φ ) ) ); // rotated
	
}

const vm = new THREE.Vector3( ).add( v0 ).add( vv ).add( ( φ === 0 ? b : r ).multiplyScalar( vlen / 4 ).multiplyScalar( bend ) );

points3.push( v0, vm, v1 ); 
points = new THREE.CatmullRomCurve3( points3 ).getPoints( cpc );

const line = new THREE.Line( new THREE.BufferGeometry( ).setFromPoints( points ), new THREE.LineBasicMaterial( { color: 0xff1122 } ) );

const tipHeight = 0.2;
const tipGeometry =  new THREE.ConeGeometry(  tipHeight / 4, tipHeight );
tipGeometry.translate( 0, -tipHeight / 2, 0 );

const tipMesh = new THREE.Mesh( tipGeometry , new THREE.MeshBasicMaterial( { color:  0xff1122 , wireframe: true } ) );

const vt = new THREE.Vector3( ).subVectors( points[ cpc ], points[ cpc - Math.floor( 0.05 * cpc ) ] ).normalize( );

tipMesh.quaternion.setFromUnitVectors( new THREE.Vector3( 0, 1, 0 ), vt );
tipMesh.position.set( v1.x, v1.y, v1.z );
line.add( tipMesh ); // scene.add( tipMesh );

scene.add( line );

animate();

function animate() {

	requestAnimationFrame( animate );
	renderer.render( scene, camera );
}

</script>
</html>
1 Like