sonia maklouf
sonia maklouf

Reputation: 2743

arrow with modification point

Hi I'm trying to reproduce a draw.io effect. When you draw an arrow it display a blue point in the middle of the arrow that allows you to create angle between the two lines and it display two blue point that allow you to do the same with the two new line. I have added image below. It'll be easier to understand. I wonder how to code dynamically these blue points that allow to "break" the line enter image description here enter image description here

var ctx = tempcanvas.getContext('2d'),
    mainctx = canvas.getContext('2d'),
    w = canvas.width,
    h = canvas.height,
    x1,
    y1,
    isDown = false;

ctx.translate(0.5, 0.5);

tempcanvas.onmousedown = function(e) {
    var rect = canvas.getBoundingClientRect();
    x1 = e.clientX - rect.left;
    y1 = e.clientY - rect.top;
    isDown = true;
}
tempcanvas.onmouseup = function() {
    isDown = false;
    mainctx.drawImage(tempcanvas, 0, 0);
    ctx.clearRect(0, 0, w, h);
}
tempcanvas.onmousemove = function(e) {

    if (!isDown) return;
    
    var rect = canvas.getBoundingClientRect(),
        x2 = e.clientX - rect.left,
        y2 = e.clientY - rect.top;
    var p0={x1,y1};
    var p1={x2,y2};


    ctx.clearRect(0, 0, w, h);
    drawLineWithArrowhead(p0,p1,25);

}




function drawLineWithArrowhead(p0,p1,headLength){

  
  var PI=Math.PI;
  var degreesInRadians225=225*PI/180;
  var degreesInRadians135=135*PI/180;

  
  var dx=p1.x2-p0.x1;
  var dy=p1.y2-p0.y1;
  var angle=Math.atan2(dy,dx);

  // calc arrowhead points
  var x225=p1.x2+headLength*Math.cos(angle+degreesInRadians225);
  var y225=p1.y2+headLength*Math.sin(angle+degreesInRadians225);
  var x135=p1.x2+headLength*Math.cos(angle+degreesInRadians135);
  var y135=p1.y2+headLength*Math.sin(angle+degreesInRadians135);

  
  ctx.beginPath();
  // draw the line from p0 to p1
  ctx.moveTo(p0.x1,p0.y1);
  ctx.lineTo(p1.x2,p1.y2);
  // draw partial arrowhead at 225 degrees
  ctx.moveTo(p1.x2,p1.y2);
  ctx.lineTo(x225,y225);
  // draw partial arrowhead at 135 degrees
  ctx.moveTo(p1.x1,p1.y1);
  ctx.lineTo(x135,y135);
  // stroke the line and arrowhead
  ctx.stroke();
}
canvas {position:absolute;left:0;top:0}
#canvas {background:#eef}
<canvas id="canvas" width=400 height=400></canvas>
<canvas id="tempcanvas" width=400 height=400></canvas>

Upvotes: 1

Views: 97

Answers (1)

Blindman67
Blindman67

Reputation: 54128

Example snippet

Sorry out of time (Weekend and all) to write a detailed explanation and no point wasting the code, so hope it helps.

const ctx = canvas.getContext("2d");
ctx.bounds = canvas.getBoundingClientRect();
const P2 = (x = 0, y = 0) => ({x, y});
const points = [];
const lineStyle = "#000";
const nearLineStyle = "#0AF";
const lineWidth = 2;
const nearLineWidth = 3;
const pointStyle = "#000";
const nearPointStyle = "#0AF";
const pointLineWidth = 1;
const nearPointLineWidth = 2;
const arrowSize = 18;
const pointSize = 5;
const nearPointSize = 15;
const checkerSize = 256;  // power of two
const checkerCol1 = "#CCC";
const checkerCol2 = "#EEE";
const MIN_SELECT_DIST = 20; // in pixels;
var w = canvas.width, h = canvas.height;
var cw = w / 2, ch = h / 2;
var cursor = "default";
var toolTip = "";
const mouse = { x: 0, y: 0, button: 0 };
const drag = {dragging: false};
requestAnimationFrame(update);

function mouseEvents(e) {
    mouse.x = e.pageX - ctx.bounds.left - scrollX;
    mouse.y = e.pageY - ctx.bounds.top - scrollY;
    if (e.type === "mousedown") { mouse.button |= 1 << (e.which - 1) }
    else if (e.type === "mouseup") { mouse.button &= ~(1 << (e.which - 1)) }
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const checkerboard = (()=> {
    const s = checkerSize, s2 = s / 2;
    const c = document.createElement("canvas");
    c.height = c.width = checkerSize;
    const ctx = c.getContext("2d", {alpha: false});
    ctx.fillStyle = checkerCol1;
    ctx.fillRect(0,0,s, s);
    ctx.fillStyle = checkerCol2;
    ctx.fillRect(0,0,s2,s2);
    ctx.fillRect(s2,s2,s2,s2);
    ctx.globalAlpha = 0.25;
    var ss = s2;
    while(ss > 8) {
        ctx.fillStyle = ctx.createPattern(c, "repeat");  
        ctx.setTransform(1/8,0,0,1/8,0,0);
        ctx.fillRect(0,0,s * 8,s * 8);
        ss /= 2;
    }
    return ctx.createPattern(c, "repeat");   
})();

function nearestPointLine(points, point, minDist){   // fills nearest object with nearest point and line to point if within minDist.
    var i = 0, p1, dist;
    nearest.reset(minDist);
    const v1 = P2();
    const v2 = P2();
    const v3 = P2();
    for (const p of points) {
        v2.x = point.x - p.x;
        v2.y = point.y - p.y;
        dist = (v2.x * v2.x + v2.y * v2.y) ** 0.5;
        if(dist < nearest.point.dist) {
            nearest.point.dist = dist;
            nearest.point.p = p;
            nearest.point.idx = i;
        }           
        if (p1) {
            v1.x = p1.x - p.x;
            v1.y = p1.y - p.y;
            v2.x = point.x - p.x;
            v2.y = point.y - p.y;
            const u = (v2.x * v1.x + v2.y * v1.y) / (v1.y * v1.y + v1.x * v1.x);
           
            if (u >= 0 && u <= 1) { // is closest poin on line segment
                v3.x = p.x + v1.x * u;
                v3.y = p.y + v1.y * u;
                //ctx.fillRect(v3.x, v3.y, 5, 5)
                dist = ((v3.y - point.y) ** 2 + (v3.x - point.x) ** 2) ** 0.5;
                if(dist < nearest.line.dist) {
                    nearest.line.dist = dist;
                    nearest.line.p1 = p1;
                    nearest.line.p2 = p;
                    nearest.line.idx = i;
                    nearest.line.onLine.x = v3.x;
                    nearest.line.onLine.y = v3.y;
                }
            }
        }
        p1 = p;
        i ++;
    }
    if (nearest.point.idx > -1 && nearest.point.dist / 2 <= nearest.line.dist) {        
        nearest.active = nearest.point;
        nearest.near = true;
    } else if (nearest.line.idx > -1) {
        nearest.active = nearest.line;
        nearest.near = true;
    }
}
function drawLine(p1, p2) {
    ctx.moveTo(p1.x, p1.y);
    ctx.lineTo(p2.x, p2.y);
}
function drawLineArrow(p1, p2) {
    var nx = p1.x - p2.x;
    var ny = p1.y - p2.y;
    const d =( nx * nx + ny * ny) ** 0.5;
    if(d > arrowSize) {
        nx /= d;
        ny /= d;
        ctx.setTransform(-nx, -ny, ny, -nx, p2.x, p2.y);
        ctx.beginPath()
        ctx.fillStyle = ctx.strokeStyle;
        ctx.moveTo(0, 0);
        ctx.lineTo(-arrowSize, arrowSize / 2);
        ctx.lineTo(-arrowSize, -arrowSize / 2);
        ctx.fill();
        ctx.setTransform(1,0,0,1,0,0);
    }
}
function drawPoint(p, size = pointSize) {
    ctx.rect(p.x - size / 2, p.y - size / 2, size, size);
}
function drawLines(points) {
    var p1;
    ctx.strokeStyle = lineStyle;
    ctx.lineWidth = lineWidth;
    ctx.beginPath()
    for(const p of points) {
        if (p1) { drawLine(p1 ,p) }
        p1 = p;
    }
    ctx.stroke();
    if(points.length > 1) {
        drawLineArrow(points[points.length - 2], p1);
    }
}
function drawPoints(points) {
    ctx.strokeStyle = pointStyle;
    ctx.lineWidth = pointLineWidth;
    ctx.beginPath()
    for(const p of points) { drawPoint(p) }
    ctx.stroke();
}
function sizeCanvas() { 
    if (w !== innerWidth || h !== innerHeight) {
        cw = (w = canvas.width = innerWidth) / 2;
        ch = (h = canvas.height = innerHeight) / 2;
        ctx.bounds = canvas.getBoundingClientRect();
    }
}
const nearest = {
    point: { isPoint: true },
    line: { onLine: P2() },
    reset(minDist) {
        nearest.point.dist = minDist;
        nearest.point.idx = -1;
        nearest.line.dist = minDist;
        nearest.line.idx = -1;
        nearest.active = null;
        nearest.near = false;
    },
    draw() {
        const a = nearest.active;
        if (a) {
            if (a.isPoint) {
                ctx.strokeStyle = nearPointStyle;
                ctx.lineWidth = nearPointLineWidth;
                ctx.beginPath()
                drawPoint(a.p, nearPointSize);
                ctx.stroke();           
            } else {
                ctx.strokeStyle = nearLineStyle;
                ctx.lineWidth = nearLineWidth;
                ctx.beginPath()
                drawLine(a.p1, a.p2);
                ctx.stroke();       
                ctx.strokeStyle = nearPointStyle;
                ctx.lineWidth = nearPointLineWidth;
                ctx.beginPath()
                drawPoint(a.onLine, nearPointSize);
                ctx.stroke();   
            }
        }
    }           
}
function update() {
    cursor = "crosshair";
    toolTip = "";
    ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
    ctx.globalAlpha = 1; // reset alpha
    sizeCanvas();
    ctx.fillStyle = checkerboard;
    ctx.fillRect(0, 0, w, h);
    if (!drag.dragging) { 
        nearestPointLine(points, mouse, MIN_SELECT_DIST);
        if (nearest.near && nearest.active.isPoint) { cursor = "move"; toolTip = "Drag to move point"}
        else if (nearest.near) { cursor = "crosshair"; toolTip = "Click/drag to cut and drag new point"  }
        else { 
            if (points.length < 2) {
                cursor = "crosshair"; 
                toolTip ="Click to add point";
             } else {
                cursor = "default"; 
                toolTip = "";
             }
         }
    }
    drawLines(points);
    drawPoints(points);
    nearest.draw();
    if((mouse.button & 1) === 1) {
        if (!drag.dragging) {
            if(points.length < 2 && !nearest.near) {
                points.push(P2(mouse.x, mouse.y));
                mouse.button = 0;
            } else if (nearest.near) {
                if (nearest.active.isPoint) {
                    drag.point = nearest.active.p;
                } else {
                    drag.point = P2(nearest.active.onLine.x, nearest.active.onLine.y);
                    points.splice(nearest.active.idx, 0, drag.point);
                    nearestPointLine(points, drag.point, 20);
                }
                drag.offX = drag.point.x - mouse.x;
                drag.offY = drag.point.y - mouse.y;
                drag.dragging = true;
            }
        }
        if(drag.dragging) {
            drag.point.x = drag.offX + mouse.x;
            drag.point.y = drag.offY + mouse.y;
            drag.point.x = drag.point.x < 1 ? 1 : drag.point.x > w - 2 ? w - 2 : drag.point.x;
            drag.point.y = drag.point.y < 1 ? 1 : drag.point.y > h - 2 ? h - 2 : drag.point.y;
            cursor = "none";
        }
    } else if((mouse.button & 1) === 0) {
        drag.dragging = false;
        drag.point = null;
    }
    canvas.title = toolTip;
    canvas.style.cursor = cursor;
    requestAnimationFrame(update);
}
canvas {
  position: absolute;
  top: 0px;
  left: 0px;
}
<canvas id="canvas"></canvas>

Upvotes: 1

Related Questions