Proportional text scaling (Konva)

I need to scale a text in both directions but also be able to crop it with ‘middle-right’ or ‘middle-left’ anchor.

The code is below (which is basically modified version of the official example), but there are couple of issues:

  • Transform frame shakes and glitches when I try to minimize by middle anchors
  • It randomly expands to thousands px width
  • Text not always scales correctly

I’m looking for better way to implement it.

var width = window.innerWidth;
var height = window.innerHeight;

var stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

var layer = new Konva.Layer();
stage.add(layer);

// Function to create text nodes
function createTextNode(text, fontSize, x, y) {
  var textNode = new Konva.Text({
    text: text,
    fontSize: fontSize,
    draggable: true,
    wrap: 'none',
    x: x,
    y: y
  });

  var tr = new Konva.Transformer({
    nodes: [textNode],
    rotateEnabled: false,
    flipEnabled: false,
    anchorSize: 8,
    enabledAnchors: [
      'top-left',
      'top-right',
      'middle-right',
      'middle-left',
      'bottom-left',
      'bottom-right'
    ],
    // set minimum width of text
    boundBoxFunc: function(oldBox, newBox) {
      newBox.width = Math.max(40 * textNode.scaleX(), newBox.width);
      return newBox;
    }
  });

  layer.add(textNode);

  textNode.on('transform', function(e) {
    textNode.width(tr.width() / textNode.scaleY());
  })

  layer.add(tr);
  tr.hide();

  textNode.on('click', function(e) {
    tr.show();
  });

  textNode.on('dblclick dbltap', () => {
    // hide text node and transformer:
    textNode.hide();
    tr.hide();

    // create textarea over canvas with absolute position
    // first we need to find position for textarea
    // how to find it?

    // at first lets find position of text node relative to the stage:
    var textPosition = textNode.absolutePosition();

    // so position of textarea will be the sum of positions above:
    var areaPosition = {
      x: stage.container().offsetLeft + textPosition.x,
      y: stage.container().offsetTop + textPosition.y,
    };

    // create textarea and style it
    var textarea = document.createElement('textarea');
    document.body.appendChild(textarea);

    // apply many styles to match text on canvas as close as possible
    // remember that text rendering on canvas and on the textarea can be different
    // and sometimes it is hard to make it 100% the same. But we will try...
    textarea.value = textNode.text();
    textarea.style.position = 'absolute';
    textarea.style.top = areaPosition.y + 'px';
    textarea.style.left = areaPosition.x + 'px';
    textarea.style.width = textNode.width() - textNode.padding() * 2 + 'px';
    textarea.style.height =
      textNode.height() - textNode.padding() * 2 + 5 + 'px';
    textarea.style.fontSize = textNode.fontSize() * textNode.scaleY() + 'px';
    textarea.style.border = 'none';
    textarea.style.padding = '0px';
    textarea.style.margin = '0px';
    textarea.style.overflow = 'hidden';
    textarea.style.background = 'none';
    textarea.style.outline = 'none';
    textarea.style.resize = 'none';
    textarea.style.lineHeight = textNode.lineHeight();
    textarea.style.fontFamily = textNode.fontFamily();
    textarea.style.transformOrigin = 'left top';
    textarea.style.textAlign = textNode.align();
    textarea.style.color = textNode.fill();
    textarea.style.whiteSpace = 'nowrap';
    rotation = textNode.rotation();
    var transform = '';
    if (rotation) {
      transform += 'rotateZ(' + rotation + 'deg)';
    }

    var px = 0;
    // also we need to slightly move textarea on firefox
    // because it jumps a bit
    var isFirefox =
      navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
    if (isFirefox) {
      px += 2 + Math.round(textNode.fontSize() / 20);
    }
    transform += 'translateY(-' + px + 'px)';

    textarea.style.transform = transform;

    // reset height
    textarea.style.height = 'auto';
    // after browsers resized it we can set actual value
    textarea.style.height = textarea.scrollHeight + 3 + 'px';

    textarea.focus();

    function removeTextarea() {
      textarea.parentNode.removeChild(textarea);
      window.removeEventListener('click', handleOutsideClick);
      textNode.show();
      tr.show();
      tr.forceUpdate();
    }

    function setTextareaWidth(newWidth) {
      if (!newWidth) {
        // set width for placeholder
        newWidth = textNode.placeholder.length * textNode.fontSize();
      }
      // some extra fixes on different browsers
      var isSafari = /^((?!chrome|android).)*safari/i.test(
        navigator.userAgent
      );
      var isFirefox =
        navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
      if (isSafari || isFirefox) {
        newWidth = Math.ceil(newWidth);
      }

      var isEdge =
        document.documentMode || /Edge/.test(navigator.userAgent);
      if (isEdge) {
        newWidth += 1;
      }
      textarea.style.width = newWidth + 'px';
    }

    textarea.addEventListener('keydown', function(e) {
      // hide on enter and esc
      if (e.keyCode === 13 || e.keyCode === 27) {
        textNode.text(textarea.value.replaceAll('n', ' '));
        removeTextarea();
      }
    });

    textarea.addEventListener('keydown', function(e) {
      scale = textNode.getAbsoluteScale().x;
      setTextareaWidth(textNode.width() * scale);
      textarea.style.height = 'auto';
      textarea.style.height =
        textarea.scrollHeight + textNode.fontSize() + 'px';
    });

    function handleOutsideClick(e) {
      if (e.target !== textarea) {
        textNode.text(textarea.value);
        removeTextarea();
      }
    }
    setTimeout(() => {
      window.addEventListener('click', handleOutsideClick);
    });
  });

}

// Call the function to create text nodes
createTextNode('Text 1', 20, 50, 50);
createTextNode('Text 2', 20, 150, 150);
createTextNode('Text 3', 20, 250, 250);
body {
  margin: 0;
  padding: 0;
  background-color: #f0f0f0;
}
<script src="https://unpkg.com/[email protected]/konva.min.js"></script>
<div id="container"></div>