Search code examples
kineticjs

How can I get textBaseline="alphabetic" behavior in Kineticjs?


I'm trying to align text in a way that canvas context textBaseline property set to "alphabetic" does. I can't get exactly same effect with kineticjs.

var letters = [
    { symbol: "A", x: 3.0, size: 20 },
    { symbol: "B", x: 36.3, size: 30 },
    { symbol: "C", x: 86.3, size: 40 },
    { symbol: "D", x: 158.6, size: 50 },
    { symbol: "E", x: 248.9, size: 40 },
    { symbol: "F", x: 315.5, size: 30 },
    { symbol: "G", x: 361.3, size: 20 } ];
// How kineticjs renders the text
(function actual() {
    var stage = new Kinetic.Stage({ container: "mycontainer",  width: 400,  height: 100 }),
		layer = new Kinetic.Layer(),
        baseline = 60;
	letters.forEach(function(letter) { 
    	layer.add(new Kinetic.Text({
        	x: letter.x,
        	y: baseline - letter.size,
        	text: letter.symbol,
        	fontSize: letter.size,         
         	fill: 'black',
     	}));     
 	});
    // Baseline visualization
 	layer.add(new Kinetic.Line({
    	points: [0, baseline, 400, baseline ],
    	stroke: "red"
 	}));
 	stage.add(layer);
})();
// How I would like it to render the text
(function expected() {
    var c = document.getElementById("mycanvas"),
    	ctx = c.getContext("2d"),
        baseline = 60;
  	ctx.textBaseline = "alphabetic"; // redundant as it's actually default behaviour 	
 	letters.forEach(function(letter) {
        ctx.font = letter.size + "px Arial";
        ctx.fillText(letter.symbol, letter.x, baseline); 
    });    
    // Baseline visualization
    ctx.strokeStyle = "red";
 	ctx.moveTo(0, baseline);
 	ctx.lineTo(400, baseline);
 	ctx.stroke();
})();
<script src="https://cdn.lukej.me/kineticjs/5.1.0/kinetic.min.js"></script>

<div id="mycontainer"></div>

<canvas id="mycanvas" width="400" height="150" style="position: absolute; left: 10px; top: 100px">

Same code on jsfiddle.

I'm aware of this question however I haven't found the right way of calculating the offset for alphabetic baseline.


Solution

  • The proper "y" position can be calculated as follows:

    y = baseline - fontSize + descent;

    where "descent" can be obtained using "getTextHeight" function from this answer

    The code below gives pixel perfect results in Firefox and decent ones in Chrome.

    var letters = [
        { symbol: "A", x: 3.0, size: 20 },
        { symbol: "B", x: 36.3, size: 30 },
        { symbol: "C", x: 86.3, size: 40 },
        { symbol: "D", x: 158.6, size: 50 },
        { symbol: "E", x: 248.9, size: 40 },
        { symbol: "F", x: 315.5, size: 30 },
        { symbol: "G", x: 361.3, size: 20 } ];
    // How kineticjs renders the text
    (function actual() {
        var stage = new Kinetic.Stage({ container: "mycontainer",  width: 400,  height: 100 }),
    		layer = new Kinetic.Layer(),
            baseline = 60;
    	letters.forEach(function(letter) { 
        	layer.add(new Kinetic.Text({
            	x: letter.x,
            	y: baseline - letter.size + getMetrics("Arial", letter.size).descent,
            	text: letter.symbol,
            	fontSize: letter.size,         
             	fill: 'black',
         	}));     
     	});
        // Baseline visualization
     	layer.add(new Kinetic.Line({
        	points: [0, baseline, 400, baseline ],
        	stroke: "red"
     	}));
     	stage.add(layer);
        function getMetrics(fontFamily, fontSize) { 
            var $text = $("<span>Hg</span>").css({  
                "fontFamily": fontFamily,  
                "font-size": fontSize + "px", 
                "line-height": "normal" 
            }); 
            var $block = $("<div></div>").css({ 
                "display": "inline-block", 
                "width": "1px", 
                "height": "0px" 
            }); 
            var $div = $("<div></div>"); 
            $div.append($text, $block); 
            $("body").append($div); 
            try { 
                var result = {}; 
                $block.css({ verticalAlign: "baseline" }); 
                result.ascent = $block.offset().top - $text.offset().top; 
                $block.css({ verticalAlign: "bottom" }); 
                result.height = $block.offset().top - $text.offset().top; 
                result.descent = result.height - result.ascent; 
            } finally { 
                $div.remove(); 
            } 
            return result; 
     }; 
    })();
    // How I would like it to render the text
    (function expected() {
        var c = document.getElementById("mycanvas"),
        	ctx = c.getContext("2d"),
            baseline = 60;
      	ctx.textBaseline = "alphabetic"; // redundant as it's actually default behaviour 	
     	letters.forEach(function(letter) {
            ctx.font = letter.size + "px Arial";
            ctx.fillText(letter.symbol, letter.x, baseline); 
        });    
        // Baseline visualization
        ctx.strokeStyle = "red";
     	ctx.moveTo(0, baseline);
     	ctx.lineTo(400, baseline);
     	ctx.stroke();
    })();
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdn.lukej.me/kineticjs/5.1.0/kinetic.min.js"></script>
    
    <div id="mycontainer"></div>
    
    <canvas id="mycanvas" width="400" height="150" style="position: absolute; left: 10px; top: 100px">