Beric
Beric

Reputation: 83

Javascript canvas arrow - arrowhead not pointing at line end

I am using this code to draw an arrow on a canvas:

var arrowCanvas = document.getElementById("arrowCanvas");
var ctx = arrowCanvas.getContext("2d");
drawArrow(ctx, 30, 10, 30, 100);

function drawArrow(ctx, fromx, fromy, tox, toy){
    //variables to be used when creating the arrow
    var headlen = 10;
    ctx.strokeStyle = "#cc0000";
    ctx.fillStyle = "#cc0000";
    ctx.lineWidth = 10;
    var angle = Math.atan2(toy-fromy,tox-fromx);

    //starting path of the arrow from the start square to the end square and drawing the stroke
    ctx.beginPath();
    ctx.moveTo(fromx, fromy);
    ctx.lineTo(tox, toy);
    ctx.stroke();

    //starting a new path from the head of the arrow to one of the sides of the point
    ctx.beginPath();
    ctx.moveTo(tox, toy);
    ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));

    //path from the side point of the arrow, to the other side point
    ctx.lineTo(tox-headlen*Math.cos(angle+Math.PI/7),toy-headlen*Math.sin(angle+Math.PI/7));

    //path from the side point back to the tip of the arrow, and then again to the opposite side point
    ctx.lineTo(tox, toy);
    ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));

    //draws the paths created above
    ctx.stroke();
    ctx.fill();
}

(Code is found in this answer: Draw arrow on canvas tag).

My problem with this code is that because of ctx.lineWidth the arrowHead points over the specified coordinates (tox, toy). In the provided example the arrowhead points onto the canvas y-position 110, due to lineWidth = 10px. I want it to point exactly on the tox, toy-coordinates, independent of the arrows angle.

My solution would be to reduce the initial length of the arrow by the amount of lineWidth, but I failed at considering the arrow angle for it.

Upvotes: 1

Views: 1157

Answers (1)

Blindman67
Blindman67

Reputation: 54128

A little trig is all that is needed. Bellow is a snippet that solves the 3 types of line joins. Bevel, Miter, and round.

The function drawArrow draw a line from x,y,xx,yy with options setting the various dimensions for the arrow head.

The function reads the ctx.lineWidth and ctx.lineJoin values to work out how to move the end point to ensure it does not pass the point.

  • For "round" joins the distance to move in is half the line width
  • For "miter" the distance is half line width divide the sin of half the arrowhead pointy angle
  • For "Bevel" the distance is cos of half the angle of the pointy end times the half line width

const PI = Math.PI;
const PI2 = PI * 2;
function drawArrow(ctx,x,y,xx,yy,options){
    function getDef(name,def){return options[name] !== undefined ? options[name] : def;}
    var w = getDef("width",5); // get settings
    var hs = getDef("headSize",15); // 
    var hw = getDef("headWidth",15); // 
    var dx = xx-x;
    var dy = yy-y;
    var dir = Math.atan2(dy,dx);
    var dist = Math.sqrt(dx*dx+dy*dy);
    var lineWidth = Number(ctx.lineWidth)
    var endMove = ctx.lineWidth/2; // assume round joins
    if(ctx.lineJoin === "miter"){
        endMove = Math.min(ctx.miterLimit,endMove / (hw / Math.sqrt(hs*hs+hw*hw)));
    }else if(ctx.lineJoin === "bevel"){
        endMove = endMove * Math.cos(Math.asin(hs / Math.sqrt(hs*hs+hw*hw)));
    }
    // move canvas coordinates so that the arrow starts at 0,0, 
    ctx.setTransform(1,0,0,1,x,y); 
    ctx.rotate(dir); // and is aligned to x
    dist -= endMove; // shorten for line width
    ctx.beginPath();
    ctx.moveTo(0,-w);
    ctx.lineTo(dist - hs,-w);
    ctx.lineTo(dist - hs,-hw);
    ctx.lineTo(dist ,0);
    ctx.lineTo(dist - hs,hw);
    ctx.lineTo(dist - hs,w);
    ctx.lineTo(0,w);
    ctx.stroke();
    ctx.fill();
}

var arrows = [
    {width : 5, headWidth : 10, headSize : 20, lineWidth : 5,line : "red", fill : "blue",join : "bevel", limit : 100},
    {width : 10, headWidth : 20, headSize : 20, lineWidth : 5,line : "Orange", fill : "blue",join : "miter", limit : 5},
    {width : 10, headWidth : 20, headSize : 20, lineWidth : 5,line : "Green", fill : "blue",join : "round", limit : 0},
]
var tempArrow = {width : 10, headWidth : 20, headSize : 20};
const numArrows = 3;
const mouseClear = 30;  


// main update function
function display(){
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    ctx.clearRect(0,0,w,h);
    ctx.lineWidth = 1;
    ctx.strokeStyle = "black";
    ctx.beginPath();
    ctx.arc(mouse.x,mouse.y,mouseClear,0,PI2);
    ctx.miterLimit = 1000;
    ctx.stroke();
    for(var i = 0; i < numArrows; i ++){
        var x = cw + Math.cos((i/numArrows)*PI2) * cw *1.8;
        var y = ch + Math.sin((i/numArrows)*PI2) * ch *1.8;
        var dir = Math.atan2(y-mouse.y,x-mouse.x);
        var xx = mouse.x + Math.cos(dir) * mouseClear;
        var yy = mouse.y + Math.sin(dir) * mouseClear;
        var scaleLine = (Math.sin(globalTime/1000)+1.1) * 2;
        var style = arrows[i%arrows.length];
        var arrowHead = (Math.sin(globalTime/770)+1.1) * 2;
        var arrowSize = (Math.sin(globalTime/1370)+1.1) * 2;
        ctx.lineWidth = style.lineWidth * scaleLine;
        ctx.strokeStyle = style.line;
        ctx.fillStyle = style.fill;
        ctx.lineJoin = style.join;
        tempArrow.headWidth = style.headSize * arrowHead; 
        tempArrow.headSize = style.headSize * arrowSize;
        
        
        drawArrow(ctx,x,y,xx,yy,tempArrow);
    }

}






//==============================================================================
// From here down part of answer just boiler room stuff
// can be ignored.













/** SimpleFullCanvasMouse.js begin **/

var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;



;(function(){
    const RESIZE_DEBOUNCE_TIME = 100;
    var  createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
    createCanvas = function () {
        var c,
        cs;
        cs = (c = document.createElement("canvas")).style;
        cs.position = "absolute";
        cs.top = cs.left = "0px";
        cs.zIndex = 1000;
        document.body.appendChild(c);
        return c;
    }
    resizeCanvas = function () {
        if (canvas === undefined) {
            canvas = createCanvas();
        }
        canvas.width = innerWidth;
        canvas.height = innerHeight;
        ctx = canvas.getContext("2d");
        if (typeof setGlobals === "function") {
            setGlobals();
        }
        if (typeof onResize === "function") {
            if(firstRun){
                onResize();
                firstRun = false;
            }else{
                resizeCount += 1;
                setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
            }
        }
    }
    function debounceResize() {
        resizeCount -= 1;
        if (resizeCount <= 0) {
            onResize();
        }
    }
    setGlobals = function () {
        cw = (w = canvas.width) / 2;
        ch = (h = canvas.height) / 2;
    }
    mouse = (function () {
        function preventDefault(e) {
            e.preventDefault();
        }
        var mouse = {
            x : 0,
            y : 0,
            w : 0,
            alt : false,
            shift : false,
            ctrl : false,
            buttonRaw : 0,
            over : false,
            bm : [1, 2, 4, 6, 5, 3],
            active : false,
            bounds : null,
            crashRecover : null,
            mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
        };
        var m = mouse;
        function mouseMove(e) {
            var t = e.type;
            m.bounds = m.element.getBoundingClientRect();
            m.x = e.pageX - m.bounds.left;
            m.y = e.pageY - m.bounds.top;
            m.alt = e.altKey;
            m.shift = e.shiftKey;
            m.ctrl = e.ctrlKey;
            if (t === "mousedown") {
                m.buttonRaw |= m.bm[e.which - 1];
            } else if (t === "mouseup") {
                m.buttonRaw &= m.bm[e.which + 2];
            } else if (t === "mouseout") {
                m.buttonRaw = 0;
                m.over = false;
            } else if (t === "mouseover") {
                m.over = true;
            } else if (t === "mousewheel") {
                m.w = e.wheelDelta;
            } else if (t === "DOMMouseScroll") {
                m.w = -e.detail;
            }
            if (m.callbacks) {
                m.callbacks.forEach(c => c(e));
            }
            e.preventDefault();
        }
        m.addCallback = function (callback) {
            if (typeof callback === "function") {
                if (m.callbacks === undefined) {
                    m.callbacks = [callback];
                } else {
                    m.callbacks.push(callback);
                }
            }
        }
        m.start = function (element) {
            if (m.element !== undefined) {
                m.removeMouse();
            }
            m.element = element === undefined ? document : element;
            m.mouseEvents.forEach(n => {
                m.element.addEventListener(n, mouseMove);
            });
            m.element.addEventListener("contextmenu", preventDefault, false);
            m.active = true;
        }
        m.remove = function () {
            if (m.element !== undefined) {
                m.mouseEvents.forEach(n => {
                    m.element.removeEventListener(n, mouseMove);
                });
                m.element.removeEventListener("contextmenu", preventDefault);
                m.element = m.callbacks = undefined;
                m.active = false;
            }
        }
        return mouse;
    })();



    function update(timer) { // Main update loop
        if(ctx === undefined){
            return;
        }
        globalTime = timer;
        display(); // call demo code
        requestAnimationFrame(update);
    }
    setTimeout(function(){
        resizeCanvas();
        mouse.start(canvas, true);
        window.addEventListener("resize", resizeCanvas);
        requestAnimationFrame(update);
    },0);
})();


 

Upvotes: 5

Related Questions