<p>Click on the grid to draw a line</p>
<button onClick="clearLines()">Clear lines</button>
<button onClick="init()">Restart</button>
<label>Draw dots: <input class="draw-dots" type="checkbox" /></label>
<label>Draw lines: <input class="draw-lines" type="checkbox" checked /></label>
body {
  padding: 20px;
  font-family: Helvetica, Arial, sans-serif;
  color: #333;
}

p {
  margin-bottom: 10px;
}

svg {
  overflow: visible;
  margin: 50px auto 20px;
  max-width: 800px;
  display: block;
  position: relative;
}

marker {
  overflow: visible;
}

.coordinate-line {
  stroke: #ddd;
}

marker,
.vector {
  stroke: #abc;
  fill: #abc;
}
View Compiled
const gridStep = 20;
const gridSize = 15;
const maxVelocity = gridStep;
const size = gridSize * gridStep;
const drawDotsInput = document.querySelector('.draw-dots');
const drawDotsLines = document.querySelector('.draw-lines');


const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");

svg.setAttribute("viewBox", `0 0 ${size} ${size}`);
document.querySelector("body").appendChild(svg);

const vectors = [];

const directionVector = {
  x: maxVelocity * 0.5,
  y: 0
};

function getRandomVector() {
  const xSign = Math.random() > 0.5 ? 1 : -1;
  const ySign = Math.random() > 0.5 ? 1 : -1;

  return {
    x: Math.random() * xSign * maxVelocity + directionVector.x,
    y: Math.random() * ySign * maxVelocity + directionVector.y
  };
}

function roundToDecimal(number, decimalPlaces = 2) {
  return parseFloat(number.toFixed(decimalPlaces));
}

function drawDot(dot, color = "#333") {
  if (drawDotsInput.checked) {
    svg.innerHTML += `
      <circle
        class="generated-dot"
        fill="${color}" 
        cx="${dot.x}" 
        cy="${dot.y}" 
        r="${size / 200}"
      />
    `;
  }
}

function addVectors(v1, v2) {
  return {
    x: v1.x + v2.x,
    y: v1.y + v2.y
  };
}

function multiplyVector(v, factor) {
  return {
    x: v.x * factor,
    y: v.y * factor
  };
}

function getDistance(v1, v2) {
  const x = v1.x - v2.x;
  const y = v1.y - v2.y;

  return Math.sqrt(x * x + y * y) / gridStep;
}

function drawNextDot(dot) {
  const nearVectors = [];

  const searchRadiusFactor = 1.5;

  for (let x = 0; x <= gridSize; x++) {
    const xCoord = x * gridStep;

    for (let y = 0; y <= gridSize; y++) {
      const yCoord = y * gridStep;
      const start = { x: xCoord, y: yCoord };

      if (isDotInCircle(start, dot, gridStep * searchRadiusFactor)) {
        nearVectors.push({
          start,
          vector: vectors[x][y]
        });
      }
    }
  }

  if (nearVectors.length === 0) {
    return;
  }

  let nextDot = { ...dot };

  nearVectors.forEach(vector => {
    const distance = getDistance(dot, vector.start);

    nextDot = addVectors(
      nextDot,
      multiplyVector(vector.vector, searchRadiusFactor - distance)
    );
  });

  drawDot(nextDot);
  line.push([nextDot.x, nextDot.y]);
  dotsDrawn++;

  if (
    nextDot.x > size * 1.1 ||
    nextDot.y > size * 1.1 ||
    nextDot.x < size * -0.1 ||
    nextDot.y < size * -0.1
  ) {
    return;
  }

  if (dotsDrawn < maxDots) {
    drawNextDot(nextDot);
  }
}

function isDotInCircle(dot, circleCenter, radius) {
  const x = dot.x - circleCenter.x;
  const y = dot.y - circleCenter.y;

  return x * x + y * y < radius * radius;
}

function getRandomColor() {
  const goldenRatio = 0.618033988749895 * 360;
  let h = Math.random() * 360;
  
  h += goldenRatio;
  h %= 360;
  return `hsl(${ parseInt(h, 10) }, 70%, 60%)`;
}

const maxDots = 100;
let dotsDrawn = 0;
let line = [];

svg.addEventListener("click", e => {
  const rect = svg.getBoundingClientRect();
  const x = e.pageX - (rect.left + window.pageXOffset);
  const y = e.pageY - (rect.top + window.pageYOffset);
  const factor = rect.width / size;

  dotsDrawn = 0;
  line = [];

  const start = {
    x: x / factor,
    y: y / factor
  };

  line.push([start.x, start.y]);

  drawDot(start);

  drawNextDot(start);

  svg.innerHTML += svgPath(line, bezierCommand);
  /*`
    <path 
      class="generated-line"
      fill="none"
      stroke="${ getRandomColor() }" 
      d="${line.join(" ")}" 
      stroke-width="${(gridStep / 30).toFixed(2)}" 
    />`;
  
  */
});

function clearLines() {
  document.querySelectorAll(".generated-line, .generated-dot").forEach(line => {
    svg.removeChild(line);
  });
}

function init() {
  svg.innerHTML = `
    <marker 
      id="arrow" 
      viewBox="0 0 10 10" 
      refX="5" 
      refY="5"
      markerWidth="${gridStep / 5}" 
      markerHeight="${gridStep / 5}"
      orient="auto-start-reverse"
      stroke-linecap="round"
    >
      <path d="M 0 0 L 10 5 L 0 10 Z" />
    </marker>
  `;

  for (let x = 0; x <= gridSize; x++) {
    vectors[x] = [];

    const coord = x * gridStep;

    svg.innerHTML += `
      <path
        class="coordinate-line"
        d="M ${0} ${coord} L ${size} ${coord}" 
        stroke-width="${gridStep / 100}"
      />
      <path
        class="coordinate-line"
        d="M ${coord} ${0} L ${coord} ${size}" 
        stroke-width="${gridStep / 100}"
      />
    `;

    for (let y = 0; y <= gridSize; y++) {
      vectors[x][y] = getRandomVector();
    }
  }

  vectors.forEach((row, x) => {
    row.forEach((vector, y) => {
      const xCoord = x * gridStep;
      const yCoord = y * gridStep;
      svg.innerHTML += `
        <path 
          class="vector"
          d="M ${xCoord} ${yCoord} L ${(xCoord + vector.x).toFixed(2)} ${ (yCoord + vector.y).toFixed(2) }" 
          marker-end="url(#arrow)"
          stroke-width="${(gridStep / 30).toFixed(2)}"
          stroke-linecap="round"
        />
      `;
    });
  });
}

init();

// The smoothing ratio
const smoothing = 0.15;

// Properties of a line 
// I:  - pointA (array) [x,y]: coordinates
//     - pointB (array) [x,y]: coordinates
// O:  - (object) { length: l, angle: a }: properties of the line
const lineFn = (pointA, pointB) => {
  const lengthX = pointB[0] - pointA[0]
  const lengthY = pointB[1] - pointA[1]
  return {
    length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
    angle: Math.atan2(lengthY, lengthX)
  }
}

// Position of a control point 
// I:  - current (array) [x, y]: current point coordinates
//     - previous (array) [x, y]: previous point coordinates
//     - next (array) [x, y]: next point coordinates
//     - reverse (boolean, optional): sets the direction
// O:  - (array) [x,y]: a tuple of coordinates
const controlPoint = (current, previous, next, reverse) => {

  // When 'current' is the first or last point of the array
  // 'previous' or 'next' don't exist.
  // Replace with 'current'
  const p = previous || current
  const n = next || current

  // Properties of the opposed-line
  const o = lineFn(p, n)

  // If is end-control-point, add PI to the angle to go backward
  const angle = o.angle + (reverse ? Math.PI : 0)
  const length = o.length * smoothing

  // The control point position is relative to the current point
  const x = current[0] + Math.cos(angle) * length
  const y = current[1] + Math.sin(angle) * length
  return [x, y]
}

// Create the bezier curve command 
// I:  - point (array) [x,y]: current point coordinates
//     - i (integer): index of 'point' in the array 'a'
//     - a (array): complete array of points coordinates
// O:  - (string) 'C x2,y2 x1,y1 x,y': SVG cubic bezier C command
const bezierCommand = (point, i, a) => {

  // start control point
  const cps = controlPoint(a[i - 1], a[i - 2], point)

  // end control point
  const cpe = controlPoint(point, a[i - 1], a[i + 1], true)
  return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`
}

// Render the svg <path> element 
// I:  - points (array): points coordinates
//     - command (function)
//       I:  - point (array) [x,y]: current point coordinates
//           - i (integer): index of 'point' in the array 'a'
//           - a (array): complete array of points coordinates
//       O:  - (string) a svg path command
// O:  - (string): a Svg <path> element
const svgPath = (points, command) => {
  if (drawDotsLines.checked) {
    // build the d attributes by looping over the points
    const d = points.reduce((acc, point, i, a) => i === 0
      ? `M ${point[0]},${point[1]}`
      : `${acc} ${command(point, i, a)}`
    , '')
    return `<path class="generated-line" d="${d}" fill="none" stroke="${ getRandomColor() }" />`
  }
}

/*
for (let x = 0; x < gridSize; x++) { 
  const coord = x * gridStep;
      
  svg.innerHTML += `
    <path
      class="coordinate-line"
      d="M ${ 0 } ${ coord } L ${ coord } ${ size }" 
      stroke-width="${ gridStep / 50 }"
    />
    <path
      class="coordinate-line"
      d="M ${ coord } ${ 0 } L ${ size } ${ coord }" 
      stroke-width="${ gridStep / 50 }"
    />
  `;
}
*/
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.