'Text truncation issue in a Konva.Text shape with low lineHeight?

I'm creating an app very similar to Canva, or the Polotno studio using Konva React. I'm facing a very annoying situation concerning Text shapes:

When the lineHeight is inferior to 1.2, the height of the shape is computed at a lower value than the text it contains, therefore the top of the text is hidden because it is outside of the bounding box of the shape.

I'm using a padding of 0, and height is on "auto" so it is computed from the length of the text and the width of the node.

I think this is a predictable behavior that make sense in some way, BUT on the Polotno Studio (using React Konva) the developer seem to have used a workaround (see the image below): we can see the Transformer node is bounding to the Text shape but the outside text is still visible, this is the result I want to achieve. On my example, you can't see anything that is outside the Transformer (which matches the X, Y, width and height of the Text node).

Image: Comparison example between Polotno Studio's behavior and my project

I tried adding padding in inverse proportion of the lineHeight value, but I'd prefer not using padding if possible. I also tried to change verticalAlign. I'm also refreshing the cache of the node on every important update.

In the example above, the font used is Roboto, fontSize is 64px and lineHeight of 0.5.

Konva library is truly amazing BTW 🤝 Please help 🥲



Solution 1:[1]

Well, the culprit was pretty sneaky ?:

If we add a Konva.Filter to a Text shape we need to call the node.cache() method which creates a "frozen" version of the node content, based on it's x, y, width and height: that's why the text gets clipped, it is not possible to bleed outside of it's bouding box anymore.

This is normal behavior, however that's not what we want here.

Here is my workaround:

Use the options of the cache() method to cache the node with the size of the entire canvas. That way if the blur bleeds very far, it'll still be rendered. I don't think there is a performance issue doing that, since the rest of the image is "empty", and in my case I don't have hundreds of them anyway. Here is the method I use:

const cacheNodeLayerSize = () => {
  const { width, height } = pageGroupJSON; // my container
  const node: Konva.Text = shapeRef.current; // created with React's useRef() on the node
  node.cache({
  x: -node.x(),
  y: -node.y(),
  width, // full width of the canvas (or container you are working in)
  height, // full height
});};

Solution 2:[2]

Here's a demo that does what you are doing but does NOT show the same issue at 1.2 line height for Roboto. It does show that effect for line height < 1 but that is as expected.

You didn't post any code so I can't tell you where your bug lies. Double check that the font is really-really-really loaded when you do any measuring or add the transformer. Do not assume that it is. The transformer will react to changes to the props of the nodes it has been assigned but it will not observe a slow async arrival of a font family. In that case you will get measurements for the default font (Arial) and visibly you will see the font that you loaded - or you might even see a flicker from one font face to the next but that depends on network latency.

The demo includes a way to load fonts via Google's webfonts.js which you might find useful rather than using your 200ms polling.

The snippet is also over at CodePen.

  WebFont.load({
    google: { 
      families: [
        'Anton',
        'Bad Script',
        'Catamaran', 
        'Droid Sans', 
        'Droid Serif', 
        'Hammersmith One',  
        'Hanalei', 
        'IM Fell Double Pica',
        'Lobster',
        'Merriweather',
        'Noto Sans JP', 
        'Open Sans', 
        'Pangolin',
        'Roboto', 
        'Shadows Into Light',
        'Stalinist One',
        'Ubuntu',
        'Ultra'
      ] 
    },
    fontloading: function(familyName, fvd) {
      console.log('Loading font [' + familyName + ']')
    },
    fontactive: function(familyName, fvd) {
      console.log('Loaded font [' + familyName + ']')
      $('#fontName').append('<option value="' + familyName + '">' + familyName + '</option>'); 
    },
  }); 
 

const stage = new Konva.Stage({
        container: 'container',
        width: window.innerWidth,
        height: window.innerHeight 
      }),
      layer = new Konva.Layer(),
      
       // Create the text node 
      textObj = new Konva.Text({
        x: 20,
        y: 100,
        fill:'black',
        draggable: true
      }),
      
      // prepare a transformer
      transformer = new Konva.Transformer(),
      
      // make a rect to show the extent of the text
      rect = new Konva.Rect({
        stroke: 'red',
        listening: false
      })
    
      
// Add layer to stage and group to layer
stage.add(layer);
layer.add(textObj, transformer, rect);         

textObj.on('dragmove', function(){
  reset();
})

transformer.on('transform', function(){
  reset();
})


// function to do the drawing. Could easily be accomodated into a class 
function reset()
{
 
  $('#lineHeightVal').html(parseFloat($('#lineHeight').val()));
   
  textObj.setAttrs({
    text: $('#displayText').val(), 
    fontFamily: $('#fontName').val(), 
    fontSize: parseFloat($('#fontSize').val()),
    lineHeight: parseFloat($('#lineHeight').val())
  });  

  let w = textObj.width(),
      h = textObj.height();

  // position and size rect to illustrate size of text
  rect.setAttrs({
    x: textObj.x(), 
    y: textObj.y(), 
    width: w, 
    height: h, 
    scaleX: textObj.scaleX(),
    scaleY: textObj.scaleY(),
    rotation: textObj.rotation()
  });

  transformer.nodes([textObj])

  stage.setAttrs(
    {
      scaleX: parseFloat($('#scale').val()), 
      scaleY: parseFloat($('#scale').val())
    });
  
}
 

 

 

$('.inputThang').on('input', function(e){
  
  reset();
  
})

$('#displayText').on('blur', function(){
  
  reset();
  
})


$('#lineHeight').on('input', function(){
  
  reset();
  
})

// call go once to show on load
reset();
body {
  margin: 20px;
  padding: 0;
  overflow: hidden;
  background-color: #f0f0f0;
}
#displayText {
  width: 550px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva@8/konva.min.js"></script>
<p>Text: <input id='displayText' value="Wombats are a scatty creature"> </p>
 

<p>
   <label for='fontSize'>Font Size (Pts)</label>
  <select id='fontSize' class='inputThang'>
    <option >200</option>
    <option>180</option>
    <option>160</option>
    <option >140</option>
    <option>120</option>
    <option selected>100</option>
    <option>75</option>
    <option>66</option>
    <option >48</option>
    <option >32</option>
    <option>24</option>
    <option>16</option>
    <option>12</option>
    <option>10</option>
    <option>6</option>
  </select>
  <select id='fontName' class='inputThang'>
    <option selected value='Arial'>Arial</option>    
    <option value='Verdana'>Verdana</option>  
    <option value='Tahoma'>Tahoma</option>  
    <option value='Calibri'>Calibri</option>
    <option value='Trebuchet MS'>Trebuchet MS</option>
    <option value='Times New Roman'>Times New Roman</option>
    <option value='Georgia'>Georgia</option>
    <option value='Garamond'>Garamond</option>
    <option value='Courier New'>Courier New</option>
    <option value='Brush Script MT'>Brush Script MT</option>
    
  </select>   
</p>
<p>
   <label for='lineHeight'>Line height</label>
   <input id='lineHeight' type="range" min="0.2" max="2.4" value="1.2" step="0.2">
  <span id='lineHeightVal' style='margin-right: 10px;'></span>
<p>
  <label for='scale'>Stage scale</label>
    <select id='scale' class='inputThang'>
    <option >0.25</option>
    <option>0.5</option>
    <option>1</option>
    <option>2</option>
    <option>3</option>
    <option>4</option>
    <option>5</option>
    <option>10</option>
    <option>20</option>
  </select>   
</p>      
<div id="container"></div>
   <script src="http://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js"></script>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Bruno Jurado
Solution 2 Vanquished Wombat