JSFiddle - React, Tailwind, and code Playground

HTML

<!DOCTYPE html>
<meta charset="utf-8">

<head>
  <title>Ball of string</title>
</head>
<style>
  body {
    background-color: #39474F;
  }

</style>

<body>
  <canvas id="content"></canvas>
</body>

JavaScript

var context = document.getElementById('content').getContext('2d');

var width = window.innerWidth;
var height = window.innerHeight;
var size = d3.min([width, height]);

d3.select('#content')
  .attr('width', width + 'px')
  .attr('height', height + 'px');

context.lineWidth = 1;
context.strokeStyle = '#FFFFFF';

var projection = d3.geoOrthographic()
  .scale(0.45 * size)
  .translate([0.5 * width, 0.5 * height]);

var geoGenerator = d3.geoPath()
  .projection(projection)
  .context(context)
  .pointRadius(.8);

var geojson = {
  type: 'Feature',
  geometry: {
    type: 'MultiLineString',
    coordinates: []
  },
};

const lngPoints = 48;
const lngMinus = lngPoints / 2;
const lngDistance = 360 / lngPoints;

const latPoints = 36;
const latMinus = latPoints / 2;
const latDistance = 180 / latPoints;

const lngs = [...Array(lngPoints + 1).keys()].map(x => (x - lngMinus) * lngDistance);
const lats = [...Array(latPoints + 1).keys()].map(x => (x - latMinus) * latDistance);

lngs.forEach(lng => {
  let lngShift = lng;
  let lngV = 0;
  let lngA = (Math.random()-0.5) / 10;
  if (lng > 120) {
      lngShift += Math.random() * 1 + 0.5;
      lngV = (Math.random()) / 10;
  }
  const lngs = [];
  lats.forEach(lat => {
    if (lat >= 0) {
        lngV += lngA;
        lngShift += lngV;
    } else {
        lngV += (lngShift - lng) / (-lat) + lngA;
        lngShift += lngV;
    }
    lngs.push([lngShift, lat]);
  })
  geojson.geometry.coordinates.push(lngs);
})

function update(t) {
  projection.rotate([t / 15, -40]);

	context.setLineDash([2, 15]);
  context.clearRect(0, 0, width, height);
  context.beginPath();
  geoGenerator(geojson);
  context.stroke();

  window.requestAnimationFrame(update);
}

window.requestAnimationFrame(update);