2

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

Vanquished Wombat
  • 9,075
  • 5
  • 28
  • 67
Bruno Jurado
  • 31
  • 1
  • 4
  • Are you sure the font is actually loaded into the browser at the point when you set the node size? I think you already will know this - font loading is an async operation similar to image loading where you are pulling a file across the internet which takes time to arrive. What are you using for font arrival observation? – Vanquished Wombat Apr 12 '22 at 15:24
  • Hey, thank you for your comment, yes I did a lot of work to get the font loading and changing to work correctly. Basically I fetch the CSS font faces from google for the selected font, then I add the CSS to the page. At the Text node level, when the font changes, I check every 200ms if the new font is loaded, at which point I apply the new font (with a computedFontFamily prop) so I'm sure the font is loaded and ready in the browser. I'm pretty confident this is not related to the font loading. Also, when I dynamically lower the lineHeight, the text slowly becomes clipped. – Bruno Jurado Apr 12 '22 at 15:33

2 Answers2

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
});};
Bruno Jurado
  • 31
  • 1
  • 4
  • The caching behavior I proposed breaks when the node is rotated, a simple workaround is to use the offset property of the cache method, passing the highest value between the height and the width of the canvas: `node.cache({offset: Math.max(width, height)});` – Bruno Jurado Apr 13 '22 at 22:26
  • The cached images were so big it made the app laggy, so I ended up using this compromise: `node.cache({offset: 50});` – Bruno Jurado Apr 13 '22 at 23:37
0

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>
Vanquished Wombat
  • 9,075
  • 5
  • 28
  • 67