Search code examples
javascriptjqueryhtml5-canvas

Canvas text click not working. How to fix?


JS Code -

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var $canvas = $("#canvas");
var BB=canvas.getBoundingClientRect();
var offsetX = BB.left;
var offsetY = BB.top;
var mx;
var my;
var texts = [];
var images = [];
var dragF = -1;
var mode = "none";

function print(log) {
    console.log(log)
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for(const { text, x, y, width, height } of texts) {
        ctx.fillText(text, x, y);
        // trace
        ctx.strokeRect(x, y, width, height);
    }
}

function addNewText(string_text, arrayname) {
    var y = texts.length * 20 + 20;
    var text = {
        text: string_text,
        x: 20,
        y: y,
        name: arrayname
    };
    ctx.font = "32px verdana";
    ctx.textBaseline = "top";
    text.width = ctx.measureText(text.text).width;
    text.height = 32;

    texts.push(text);
    draw();
}

function hitDrag(x,y,textIndex) {
    var r=texts[textIndex];
    return (x>r.x && x<r.x+r.width && y>r.y && y<r.y+r.height);
}

function myDown(e) {
    e.preventDefault();
    e.stopPropagation();

    mx=parseInt(e.clientX-offsetX);
    my=parseInt(e.clientY-offsetY);

    for(var i=0;i<texts.length;i++){
        if(hitDrag(mx,my,i)){
            print("found");
            dragF = i;
        }
    }
}

$("#canvas").mousedown(function(e) { 
    myDown(e);
});

addNewText("Hello world", "text 1");

So I followed @Kaiido advice with the BBox and I changed textBaseline to "top". Which fits perfectly. Now when I run his code snipper under my help forum, it seems to work and print "found" (which shows it works). When I run it, it doesn't seem to work. What could be the reason for this.

His Code working: https://gyazo.com/511bf35523fcb3ea8a26c2b088530f99

My coding not working: https://gyazo.com/743c38f6a33a7f1f4513bac361c23588

HTML Code -

<div id="middle_container">
            <div class="center_container">
                <canvas id="canvas"></canvas>
            </div>
        </div>

CSS Code -

  #canvas {
        height: 667px;
        width: 800px;
        touch-action: auto;
        cursor: inherit;
        visibility: visible;
        border: 1px solid grey;
    }

Solution

  • The problem is that you are using the default textBaseline = "alphabetic", this makes the y value correspond to the bottom of glyphs like o.

    You can see that your text BBox is wrong by tracing it:

    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    var $canvas = $("#canvas");
    var BB=canvas.getBoundingClientRect();
    var offsetX = BB.left;
    var offsetY = BB.top;
    var mx;
    var my;
    var texts = [];
    var images = [];
    var dragF = -1;
    var mode = "none";
    var print = console.log;
    
    function addNewText(string_text, arrayname) {
        var y = texts.length * 40 + 40;
        var text = {
            text: string_text,
            x: 20,
            y: y,
            name: arrayname
        };
        ctx.font = "32px verdana";
        text.width = ctx.measureText(text.text).width;
        text.height = 32;
    
        texts.push(text);
        draw();
    }
    
    function hitDrag(x,y,textIndex) {
        var r=texts[textIndex];
        return (x>r.x && x<r.x+r.width && y>r.y && y<r.y+r.height);
    }
    
    function myDown(e) {
        e.preventDefault();
        e.stopPropagation();
    
        mx=parseInt(e.clientX-offsetX);
        my=parseInt(e.clientY-offsetY);
    
        for(var i=0;i<texts.length;i++){
            if(hitDrag(mx,my,i)){
                print("found");
                dragF = i;
            }
        }
    }
    
    $("#canvas").mousedown(function(e) { 
        myDown(e);
    });
    
    function draw() {
      for(const { text, x, y, width, height } of texts) {
        ctx.fillText(text, x, y);
        // trace the text's BBox
        ctx.strokeRect(x, y, width, height);
      }
    }
    
    addNewText("Hello world", "text 1");
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <canvas id="canvas"></canvas>

    Setting this textBaseline to "top" would minimize the issue at lower cost, but that won't be a perfect fit, moreover if you use special glyphs.

    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    var $canvas = $("#canvas");
    var BB=canvas.getBoundingClientRect();
    var offsetX = BB.left;
    var offsetY = BB.top;
    var mx;
    var my;
    var texts = [];
    var images = [];
    var dragF = -1;
    var mode = "none";
    var print = console.log;
    
    function addNewText(string_text, arrayname) {
        var y = texts.length * 40 + 40;
        var text = {
            text: string_text,
            x: 20,
            y: y,
            name: arrayname
        };
        ctx.font = "32px verdana";
        ctx.textBaseline = "top";
        text.width = ctx.measureText(text.text).width;
        text.height = 32;
    
        texts.push(text);
        draw();
    }
    
    function hitDrag(x,y,textIndex) {
        var r=texts[textIndex];
        return (x>r.x && x<r.x+r.width && y>r.y && y<r.y+r.height);
    }
    
    function myDown(e) {
        e.preventDefault();
        e.stopPropagation();
    
        mx=parseInt(e.clientX-offsetX);
        my=parseInt(e.clientY-offsetY);
    
        for(var i=0;i<texts.length;i++){
            if(hitDrag(mx,my,i)){
                print("found");
                dragF = i;
            }
        }
    }
    
    $("#canvas").mousedown(function(e) { 
        myDown(e);
    });
    
    function draw() {
      for(const { text, x, y, width, height } of texts) {
        ctx.fillText(text, x, y);
        // trace the text's BBox
        ctx.strokeRect(x, y, width, height);
      }
    }
    
    addNewText("Hello world", "text 1");
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <canvas id="canvas"></canvas>

    All major browsers finally support the actualBoundingBoxXXX properties of the TextMetrics interface, so we can now get precise text BBox:

    function getTextBBox( ctx, text ) {
      const metrics = ctx.measureText( text );
      const left = metrics.actualBoundingBoxLeft * -1;
      const top = metrics.actualBoundingBoxAscent * -1;
      const right = metrics.actualBoundingBoxRight;
      const bottom = metrics.actualBoundingBoxDescent;
      const width = right - left;
      const height = bottom - top;
      return { left, top, right, bottom, width, height };
    }
    

    function getTextBBox(ctx, text) {
      const metrics = ctx.measureText(text);
      const left = metrics.actualBoundingBoxLeft * -1;
      const top = metrics.actualBoundingBoxAscent * -1;
      const right = metrics.actualBoundingBoxRight;
      const bottom = metrics.actualBoundingBoxDescent;
      const width = right - left;
      const height = bottom - top;
      return { left, top, right, bottom, width, height };
    }
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    var $canvas = $("#canvas");
    var BB=canvas.getBoundingClientRect();
    var offsetX = BB.left;
    var offsetY = BB.top;
    var mx;
    var my;
    var texts = [];
    var images = [];
    var dragF = -1;
    var mode = "none";
    var print = console.log;
    
    function addNewText(string_text, arrayname) {
        ctx.font = "32px verdana";
        ctx.textBaseline = "top";
        const bbox = getTextBBox(ctx, string_text);
        const prevText = texts[ texts.length - 1 ];
        const text = {
            text: string_text,
            x: 20,
            y: (prevText ? (prevText.y + prevText.bbox.bottom) : 32) + 2,
            bbox,
            name: arrayname
        };
        texts.push(text);
        draw();
    }
    
    function hitDrag(mx,my,textIndex) {
        const { x, y, bbox: { left, right, top, bottom } } = texts[textIndex];
        return (
          mx > x + left &&
          mx < x + right &&
          my > y + top &&
          my < y + bottom
        );
    }
    
    function myDown(e) {
        e.preventDefault();
        e.stopPropagation();
    
        mx=parseInt(e.clientX-offsetX);
        my=parseInt(e.clientY-offsetY);
    
        for(var i=0;i<texts.length;i++){
            if(hitDrag(mx,my,i)){
                print("found");
                dragF = i;
            }
        }
    }
    
    $("#canvas").mousedown(function(e) { 
        myDown(e);
    });
    
    function draw() {
      for(const { text, x, y, bbox } of texts) {
        ctx.fillText(text, x, y);
        // trace the text's BBox
        ctx.strokeRect(x + bbox.left, y + bbox.top, bbox.width, bbox.height);
      }
    }
    
    addNewText("Hello world", "text 1");
    addNewText("Works fine?", "text 2");
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <canvas id="canvas"></canvas>

    Now, if you need to find if you clicked on an actual painted pixel of a glyph, you'd need to get the ImageData of the canvas, I already shown how to do this in an other answer, so I won't repeat it here.