Skip to main content

Scaling image to fit a fixed area on canvas

How to scale image to fit available area without its stretching?

The demo demonstrates how to use crop property of Konva.Image to emulate object-fit: cover of CSS.

The crop property allows you to use only specified area of source image to draw into the canvas. If you do the correct calculations, then the resulting image can be drawn without any stretching.

import Konva from 'konva';

// Create select element for crop position
const select = document.createElement('select');
select.style.position = 'absolute';
select.style.top = '4px';
select.style.left = '4px';

const positions = [
  'left-top', 'center-top', 'right-top', '--',
  'left-middle', 'center-middle', 'right-middle', '--',
  'left-bottom', 'center-bottom', 'right-bottom'
];

positions.forEach(pos => {
  const option = document.createElement('option');
  option.value = pos;
  option.text = pos;
  if (pos === 'center-middle') option.selected = true;
  select.appendChild(option);
});

document.body.appendChild(select);

const stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth,
  height: window.innerHeight,
});

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

// function to calculate crop values from source image, its visible size and a crop strategy
function getCrop(image, size, clipPosition = 'center-middle') {
  const width = size.width;
  const height = size.height;
  const aspectRatio = width / height;

  let newWidth;
  let newHeight;

  const imageRatio = image.width / image.height;

  if (aspectRatio >= imageRatio) {
    newWidth = image.width;
    newHeight = image.width / aspectRatio;
  } else {
    newWidth = image.height * aspectRatio;
    newHeight = image.height;
  }

  let x = 0;
  let y = 0;
  if (clipPosition === 'left-top') {
    x = 0;
    y = 0;
  } else if (clipPosition === 'left-middle') {
    x = 0;
    y = (image.height - newHeight) / 2;
  } else if (clipPosition === 'left-bottom') {
    x = 0;
    y = image.height - newHeight;
  } else if (clipPosition === 'center-top') {
    x = (image.width - newWidth) / 2;
    y = 0;
  } else if (clipPosition === 'center-middle') {
    x = (image.width - newWidth) / 2;
    y = (image.height - newHeight) / 2;
  } else if (clipPosition === 'center-bottom') {
    x = (image.width - newWidth) / 2;
    y = image.height - newHeight;
  } else if (clipPosition === 'right-top') {
    x = image.width - newWidth;
    y = 0;
  } else if (clipPosition === 'right-middle') {
    x = image.width - newWidth;
    y = (image.height - newHeight) / 2;
  } else if (clipPosition === 'right-bottom') {
    x = image.width - newWidth;
    y = image.height - newHeight;
  }

  return {
    cropX: x,
    cropY: y,
    cropWidth: newWidth,
    cropHeight: newHeight,
  };
}

// function to apply crop
function applyCrop(img, pos) {
  img.setAttr('lastCropUsed', pos);
  const crop = getCrop(
    img.image(),
    { width: img.width(), height: img.height() },
    pos
  );
  img.setAttrs(crop);
}

Konva.Image.fromURL('https://new.konvajs.org/assets/darth-vader.jpg', (img) => {
  img.setAttrs({
    width: 300,
    height: 100,
    x: 80,
    y: 100,
    name: 'image',
    draggable: true,
  });
  layer.add(img);
  // apply default center-middle crop
  applyCrop(img, 'center-middle');

  const tr = new Konva.Transformer({
    nodes: [img],
    keepRatio: false,
    flipEnabled: false,
    boundBoxFunc: (oldBox, newBox) => {
      if (Math.abs(newBox.width) < 10 || Math.abs(newBox.height) < 10) {
        return oldBox;
      }
      return newBox;
    },
  });

  layer.add(tr);

  img.on('transform', () => {
    // reset scale on transform
    img.setAttrs({
      scaleX: 1,
      scaleY: 1,
      width: img.width() * img.scaleX(),
      height: img.height() * img.scaleY(),
    });
    applyCrop(img, img.getAttr('lastCropUsed'));
  });
});

select.addEventListener('change', (e) => {
  const img = layer.findOne('.image');
  applyCrop(img, e.target.value);
});

Instructions: Try to resize the image or change the crop strategy using the dropdown menu at the top. The image will maintain its aspect ratio while fitting into the specified dimensions.