Skip to content
Advertisement

making each canvas line draggable and droppable

I am able to draw some lines on the canvas. I want to make each line draggable and droppable.

However, it is implemented by storing the positions of different lines in the array, how can I make each of them like an instance that is draggable and droppable? Or did I get it wrong?

You can check out the code here

var storedLines = []

http://jsfiddle.net/m1erickson/NnZ7a/

Thanks a lot!

Advertisement

Answer

Canvas draggables.

To make draggable lines you keep an array of endpoints and an array of lines as indexes to end points.

Then when the user clicks and drags near an end point or line you just update the endpoints by the amount the mouse has moved.

A point, a line, and some lists

First create a simple point structure

const Point2 = (x,y) => ({x,y});  // creates a point

Then an list that will hold points, add points, and what ever else you may need.

const list = {
    items : null,
    add(item) { this.items.push(item); return item },
    eachItem(callback) { 
        var i;
        while(i < this.items.length){
             callback(this.items[i],i);
        }
    }
}

You then create a points list from the list structure

function createList(extend){
    return Object.assign({},list,{items : []},extend);
}

const points = createList();

The line is a set of point indexes

const Line = (p1,p2) => ({p1,p2});
const lines = createList();

Finding the closest

To select a point from a mouse coordinate you need to find the closest point to the mouse.

// this will extend the points list
function getClosestPoint(from ,minDist) {
    var closestPoint;
    this.eachItem(point => {
        const dist = Math.hypot(from.x - point.x, from.y - point.y);
        if(dist < minDist){
            closestPoint = point;
            minDist = dist;
        }
    });
    return closestPoint;
}

The same for the line but that is not as simple. You need a function that finds the distance that a point is from a line segment.

function distanceLineFromPoint(line,point){
    const lx = points.items[line.p1].x;
    const ly = points.items[line.p1].y;
    const v1x = points.items[line.p2].x - lx;
    const v1y = points.items[line.p2].y - ly;
    const v2x = point.x - lx;
    const v2y = point.y - ly;
    // get unit dist of closest point
    const u = (v2x * v1x + v2y * v1y)/(v1y * v1y + v1x * v1x);
    if(u >= 0 && u <= 1){  // is the point on the line
        return Math.hypot(lx + v1x * u - point.x, ly + v1y * u - point.y);
    } else if ( u < 0 ) {  // point is before start
        return Math.hypot(lx - point.x, ly - point.y);
    }
    // point is after end of line
    return Math.hypot(points.items[line.p2].x - point.x, points.items[line.p2].y - point.y);
}

// this will extend the lines list
function getClosestline(from ,minDist) {
    var closestLine;
    this.eachItem(line => {
        const dist = distanceLineFromPoint(line,from);
        if(dist < minDist){
            closestLine = line;
            minDist = dist;
        }
    });
    return closestLine;
}

With these function we should have extended the list object so will recreate them with the extensions.

 const points = createList({getClosest : getClosestPoint});
 const lines = createList({getClosest : getClosestline});

Then the rest is implementing a mouse interface and a rendering function. You add draggable points and lines connecting them. If you click near a line or point you drag them. The snippet shows the rest.

User feedback is important

It is also important to show the correct user feedback. You need to set the cursor, tooltip (via canvas.style.cursor and canvas.title) and highlight objects that will be effected, so that the user knows what actions will happen and to what when they click and drag.

Also you should set the mouse events to the document rather than the canvas as this will capture the mouse drag allowing the user to drag outside the canvas while you still get the mouseup and move events.

Create and drag points and lines.

const ctx = canvas.getContext("2d");
const Point2 = (x,y) => ({x,y});  // creates a point
const Line = (p1,p2) => ({p1,p2});
const setStyle = (style) => eachOf(Object.keys(style), key => { ctx[key] = style[key] } );
const eachOf = (array, callback) => {var i = 0; while (i < array.length && callback(array[i],i ++) !== true ); };


const list = {
    items : null,
    add(item) { this.items.push(item); return item },
    eachItem(callback) { 
        var i = 0;
        while(i < this.items.length){
             callback(this.items[i],i++);
        }
    }
}
function createList(extend){
    return Object.assign({},list,{items : []},extend);
}
// this will extend the points list
function getClosestPoint(from ,minDist) {
    var closestPoint;
    this.eachItem(point => {
        const dist = Math.hypot(from.x - point.x, from.y - point.y);
        if(dist < minDist){
            closestPoint = point;
            minDist = dist;
        }
    });
    return closestPoint;
}
function distanceLineFromPoint(line,point){
    const lx = points.items[line.p1].x;
    const ly = points.items[line.p1].y;
    const v1x = points.items[line.p2].x - lx;
    const v1y = points.items[line.p2].y - ly;
    const v2x = point.x - lx;
    const v2y = point.y - ly;
    // get unit dist of closest point
    const u = (v2x * v1x + v2y * v1y)/(v1y * v1y + v1x * v1x);
    if(u >= 0 && u <= 1){  // is the point on the line
        return Math.hypot(lx + v1x * u - point.x, ly + v1y * u - point.y);
    } else if ( u < 0 ) {  // point is before start
        return Math.hypot(lx - point.x, ly - point.y);
    }
    // point is after end of line
    return Math.hypot(points.items[line.p2].x - point.x, points.items[line.p2].y - point.y);
}
// this will extend the lines list
function getClosestline(from ,minDist) {
    var closestLine;
    this.eachItem(line => {
        const dist = distanceLineFromPoint(line,from);
        if(dist < minDist){
            closestLine = line;
            minDist = dist;
        }
    });
    return closestLine;
}
function drawPoint(point){
    ctx.moveTo(point.x,point.y);
    ctx.rect(point.x - 2,point.y - 2, 4,4);
}
function drawLine(line){
    ctx.moveTo(points.items[line.p1].x,points.items[line.p1].y);
    ctx.lineTo(points.items[line.p2].x,points.items[line.p2].y);
}
function drawLines(){ this.eachItem(line => drawLine(line)) }
function drawPoints(){this.eachItem(point => drawPoint(point)) }

const points = createList({
  getClosest : getClosestPoint,
  draw : drawPoints,
});
const lines = createList({
  getClosest : getClosestline,
  draw : drawLines,
});
const mouse  = {x : 0, y : 0, button : false, drag : false, dragStart : false, dragEnd : false, dragStartX : 0, dragStartY : 0}
function mouseEvents(e){
	mouse.x = e.pageX;
	mouse.y = e.pageY;
	const lb = mouse.button;
	mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
	if(lb !== mouse.button){
		if(mouse.button){
			mouse.drag = true;
			mouse.dragStart = true;
			mouse.dragStartX = mouse.x;
			mouse.dragStartY = mouse.y;
		}else{
			mouse.drag = false;
			mouse.dragEnd = true;
		}
	}
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars 
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center 
var ch = h / 2;
var globalTime;
var closestLine;
var closestPoint;
var pointDrag; // true is dragging a point else dragging a line
var dragOffsetX;
var dragOffsetY;
var cursor;
var toolTip;
var helpCount = 0;
const minDist = 20;
const lineStyle = {
  lineWidth : 2,
  strokeStyle : "green",
}
const pointStyle = {
  lineWidth : 1,
  strokeStyle : "blue",
}
const highlightStyle = {
  lineWidth : 3,
  strokeStyle : "red",
}
const font = {
  font : "18px arial",
  fillStyle : "black",
  textAlign : "center",
}


// main update function
function update(timer){
    cursor = "crosshair";
    toolTip = helpCount < 2 ? "Click drag to create a line" : "";
    globalTime = timer;
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
	if(w !== innerWidth || h !== innerHeight){
		cw = (w = canvas.width = innerWidth) / 2;
		ch = (h = canvas.height = innerHeight) / 2;
	}else{
		ctx.clearRect(0,0,w,h);
	}
  if(mouse.drag=== false){
    closestLine = undefined;
    closestPoint = points.getClosest(mouse,minDist);
    if(closestPoint === undefined){
       closestLine = lines.getClosest(mouse,minDist);
    }
    if(closestPoint || closestLine){
       toolTip = "Click drag to move " + (closestPoint ? "point" : "line");     
       cursor = "move";
    }
  }
  if(mouse.dragStart){
    if(closestPoint){
      dragOffsetX = closestPoint.x - mouse.x;
      dragOffsetY =  closestPoint.y - mouse.y;
      pointDrag = true;
    
    }else if( closestLine){
      dragOffsetX = points.items[closestLine.p1].x - mouse.x;
      dragOffsetY = points.items[closestLine.p1].y - mouse.y;
      pointDrag = false;
    
    } else {
      points.add(Point2(mouse.x,mouse.y));
      closestPoint = points.add(Point2(mouse.x,mouse.y));
      closestLine = lines.add(Line(points.items.length-2,points.items.length-1));
      dragOffsetX = 0;
      dragOffsetY = 0;
      pointDrag = true;
      helpCount += 1;
      
    }
    mouse.dragStart = false;
  
  }else if(mouse.drag){
      cursor = 'none';
      if(pointDrag){
        closestPoint.x = mouse.x + dragOffsetX;
        closestPoint.y = mouse.y + dragOffsetY;
      }else{
        const dx = mouse.x- mouse.dragStartX;
        const dy = mouse.y -mouse.dragStartY;
        mouse.dragStartX = mouse.x;
        mouse.dragStartY = mouse.y;
        points.items[closestLine.p1].x +=  dx;
        points.items[closestLine.p1].y +=  dy;
        points.items[closestLine.p2].x +=  dx;
        points.items[closestLine.p2].y +=  dy;        
      }
  }else{
  
  
  }
  // draw all points and lines
  setStyle(lineStyle);
  ctx.beginPath();
  lines.draw();
  ctx.stroke();
  setStyle(pointStyle);
  ctx.beginPath();
  points.draw();
  ctx.stroke();

  
  // draw highlighted point or line
  setStyle(highlightStyle);
  ctx.beginPath();
  if(closestLine){ drawLine(closestLine) }
  if(closestPoint){ drawPoint(closestPoint) }
  
  ctx.stroke();
      
  
  if(helpCount < 2){
     setStyle(font);
     ctx.fillText(toolTip,cw,30);
  }
  
  
  canvas.style.cursor = cursor;
  if(helpCount < 5){
      canvas.title = toolTip;
  }else{
      canvas.title = "";
  }
  requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { 
  position : absolute; 
  top : 0px; 
  left : 0px; 
}
<canvas id="canvas"></canvas>
User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement