JSFiddle - React, Tailwind, and code Playground

HTML

<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>

<script id="vertexShader" type="x-shader/x-vertex">
		precision highp float;

		uniform float sineTime;

		uniform mat4 modelViewMatrix;
		uniform mat4 projectionMatrix;

		attribute vec3 position;
		attribute vec3 offset;
		attribute vec4 color;
		attribute vec4 orientationStart;
		attribute vec4 orientationEnd;

		varying vec3 vPosition;
		varying vec4 vColor;

		void main(){

			vPosition = offset * max( abs( sineTime * 2.0 + 1.0 ), 0.5 ) + position;
			vec4 orientation = normalize( mix( orientationStart, orientationEnd, sineTime ) );
			vec3 vcV = cross( orientation.xyz, vPosition );
			vPosition = vcV * ( 2.0 * orientation.w ) + ( cross( orientation.xyz, vcV ) * 2.0 + vPosition );

			vColor = color;

			gl_Position = projectionMatrix * modelViewMatrix * vec4( vPosition, 1.0 );

		}

	</script>

	<script id="fragmentShader" type="x-shader/x-fragment">

		precision highp float;

		uniform float time;

		varying vec3 vPosition;
		varying vec4 vColor;

		void main() {

			vec4 color = vec4( vColor );
			color.r += sin( vPosition.x * 10.0 + time ) * 0.5;

			gl_FragColor = color;

		}

	</script>

CSS

body {
	  margin: 0;
}

JavaScript

let camera, scene, renderer;

init();
animate();

function init() {

  camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10);
  camera.position.z = 2;

  scene = new THREE.Scene();

  // geometry

  const vector = new THREE.Vector4();

  const instances = 50000;

  const positions = [];
  const offsets = [];
  const colors = [];
  const orientationsStart = [];
  const orientationsEnd = [];

  positions.push(0.025, -0.025, 0);
  positions.push(-0.025, 0.025, 0);
  positions.push(0, 0, 0.025);

  // instanced attributes

  for (let i = 0; i < instances; i++) {

    // offsets

    offsets.push(Math.floor( Math.random() * 255 ) , Math.floor( Math.random() * 255 ), Math.floor( Math.random() * 255 ) );

    // colors

    colors.push(Math.random(), Math.random(), Math.random(), Math.random());

    // orientation start

    vector.set(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1);
    vector.normalize();

    orientationsStart.push(vector.x, vector.y, vector.z, vector.w);

    // orientation end

    vector.set(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1);
    vector.normalize();

    orientationsEnd.push(vector.x, vector.y, vector.z, vector.w);

  }

  const geometry = new THREE.InstancedBufferGeometry();
  geometry.instanceCount = instances; // set so its initalized for dat.GUI, will be set in first draw otherwise

  geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));

  geometry.setAttribute('offset', new THREE.InstancedBufferAttribute(new Uint8Array(offsets), 3, true));
  geometry.setAttribute('color', new THREE.InstancedBufferAttribute(new Float32Array(colors), 4));
  geometry.setAttribute('orientationStart', new THREE.InstancedBufferAttribute(new Float32Array(orientationsStart), 4));
  geometry.setAttribute('orientationEnd', new THREE.InstancedBufferAttribute(new Float32Array(orientationsEnd), 4));

  //...