<script src="https://unpkg.com/konva@8/konva.min.js"></script>
<p>Click draw line button then click and drag to add a line with multiple points. <br />
  Drag line points to edit the line.  <br />
  Rinse and repeat. Click clear ro reset.</p>
<p><b>Try a long stright line to see the simplification effect</b></p>
<p><button id='reset'>Clear</button> 
  <button id='drawline'>Draw line</button></p>
<p><span id='info'>.</span></p>


<div id="container"></div> 
body {
  margin: 20px;
  padding: 0;
  overflow: hidden;
  background-color: #f0f0f0;
}
#container {
  border: 1px solid silver;
  width: 600px;
  height: 400px;
}
 

/*
 (c) 2017, Vladimir Agafonkin
 Simplify.js, a high-performance JS polyline simplification library
 mourner.github.io/simplify-js
*/

(function () { 'use strict';

// to suit your point format, run search/replace for '.x' and '.y';
// for 3D version, see 3d branch (configurability would draw significant performance overhead)

// square distance between 2 points
function getSqDist(p1, p2) {

    var dx = p1.x - p2.x,
        dy = p1.y - p2.y;

    return dx * dx + dy * dy;
}

// square distance from a point to a segment
function getSqSegDist(p, p1, p2) {

    var x = p1.x,
        y = p1.y,
        dx = p2.x - x,
        dy = p2.y - y;

    if (dx !== 0 || dy !== 0) {

        var t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);

        if (t > 1) {
            x = p2.x;
            y = p2.y;

        } else if (t > 0) {
            x += dx * t;
            y += dy * t;
        }
    }

    dx = p.x - x;
    dy = p.y - y;

    return dx * dx + dy * dy;
}
// rest of the code doesn't care about point format

// basic distance-based simplification
function simplifyRadialDist(points, sqTolerance) {

    var prevPoint = points[0],
        newPoints = [prevPoint],
        point;

    for (var i = 1, len = points.length; i < len; i++) {
        point = points[i];

        if (getSqDist(point, prevPoint) > sqTolerance) {
            newPoints.push(point);
            prevPoint = point;
        }
    }

    if (prevPoint !== point) newPoints.push(point);

    return newPoints;
}

function simplifyDPStep(points, first, last, sqTolerance, simplified) {
    var maxSqDist = sqTolerance,
        index;

    for (var i = first + 1; i < last; i++) {
        var sqDist = getSqSegDist(points[i], points[first], points[last]);

        if (sqDist > maxSqDist) {
            index = i;
            maxSqDist = sqDist;
        }
    }

    if (maxSqDist > sqTolerance) {
        if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified);
        simplified.push(points[index]);
        if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified);
    }
}

// simplification using Ramer-Douglas-Peucker algorithm
function simplifyDouglasPeucker(points, sqTolerance) {
    var last = points.length - 1;

    var simplified = [points[0]];
    simplifyDPStep(points, 0, last, sqTolerance, simplified);
    simplified.push(points[last]);

    return simplified;
}

// both algorithms combined for awesome performance
function simplify(points, tolerance, highestQuality) {

    if (points.length <= 2) return points;

    var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1;

    points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
    points = simplifyDouglasPeucker(points, sqTolerance);

    return points;
}

// export as AMD module / Node module / browser or worker variable
if (typeof define === 'function' && define.amd) define(function() { return simplify; });
else if (typeof module !== 'undefined') {
    module.exports = simplify;
    module.exports.default = simplify;
} else if (typeof self !== 'undefined') self.simplify = simplify;
else window.simplify = simplify;

})();


/*
* From here onwards we set up the stage and its contents.
*/
const stage = new Konva.Stage({
        container: 'container',
        width: $('#container').innerWidth(),
        height: $('#container').innerHeight() 
      }),
      layer = new Konva.Layer(),
      anchor = new Konva.Rect({width: 10, height: 10, fill: 'blue'})

stage.add(layer);

let 
    info = $('#info'), // het the handle to the html info element for showing point stats
    lineCnt = -1,  // global used to could lines and generate anchor names
    points = [],   // list of raw points
    simplifiedPts = [],  // simplified list of points
    state = '',   // we will need to know when we are drawing and when dragging
    moving_anchor,  // global to hold the current anchor when dragging
    mouseOffset = {x: 0, y: 0}, // from corner of dragging anchor
    lines = [], currentLine = 0;

/*
* This function draws a path based on a list of points.
*/
function drawPoints(){
 
  // If we drew this path before then remove the current point markers ready to redraw
  // Note we have to do this because the simplification process could move the points
  const kids = layer.find('.anchor' + currentLine);  
  for (let i = 0; i < kids.length; i++){
    kids[i].remove();
  }

  // Make the drawing commands for the simplified, simple SVG commands line M = move to, L = line to
  let pathData = "";
  for (let i = 0; i < lines[currentLine].points.length; i++){
    let pt = lines[currentLine].points[i];
    switch (i){
      case 0:
        pathData+="M " + pt.x + " " + pt.y;
        break;
        
      default:
        pathData+="L " + pt.x + " " + pt.y;
        break;
    }   
    const newAnchor = anchor.clone({x: pt.x - 5, y: pt.y - 5, name: 'anchor' + currentLine});
    newAnchor.setAttrs({lineNo: currentLine, anchorNo: i}); // store the point sequence in the shape so we can get it on mousedown
    layer.add(newAnchor); // add anchor rect to the layer
  }
   
  // set the new path data into the paths objects.
  lines[currentLine].path.data(pathData);  
  
}

/*
* This function is called each time the mousemove event is fired.
* Note that before we draw the points we will simplify them, removing any unneeded points.
*/
function addPoint(pt){
  
  // store the new raw point
  points.push(pt);

  // simplify the raw points to make an equiv line with fewer points.
  lines[currentLine].points = simplify(points);

  drawPoints();

  info.html('Raw points ' + points.length + ', simplified to ' + lines[currentLine].points.length)
  
}

stage.on('mousedown', function(evt){

  let shape = evt.target;
  const pt = stage.getPointerPosition();
  if (state === "beforeDraw"){
    state = 'drawing';
  }
  else {
    if (shape.name().length > 0)
    {
      state='moving';
      moving_anchor = shape;  
      let anchor_no = moving_anchor.getAttr('anchorNo');
      let pt = lines[currentLine].points[anchor_no];
      mouseOffset = {x: shape.x() - pt.x, y: shape.y() - pt.y};
      
    }
  }
})

// user draws with mouse held down and moving
stage.on('mousemove', function (e) { 
 
  const pt = stage.getPointerPosition();
  if (state === 'drawing'){ 
    addPoint(pt)    
  }
  if (state === 'moving'){ 
    currentLine = moving_anchor.getAttr('lineNo')
    let anchor_no = moving_anchor.getAttr('anchorNo');
    moving_anchor.position({x: pt.x + mouseOffset.x, y: pt.y + mouseOffset.y});
    lines[currentLine].points[anchor_no] = pt;
    drawPoints();
  }
 
})

// user ends drawing with mouse up  
stage.on('mouseup', function (e) { 
  state = "";
  reset(false)
})

// reset button
$('#reset').on('click', function(){
  reset(true)
 
})
// reset the stage and points lists
function reset(clear){
  if (clear){
    layer.removeChildren();     
  }
  points = []; 
  
 
  
}
reset(true);

$('#drawline').on('click', function(){
  state = 'beforeDraw';
  lineCnt++;
  path = new Konva.Path({stroke: 'blue', strokeWidth: 1 });
  layer.add(path);
  currentLine = lineCnt;
  lines[lineCnt] = {path: path, points: []};
}) 
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js