Scaling image to fit a fixed area on canvas

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

The demo demonstrate 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 resulted image can be drawn without any stretching.

Instructions: try to resize an image or change crop strategy.

Konva Crop Imageview raw
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]/konva.min.js"></script>
<meta charset="utf-8" />
<title>Konva Scale to Fit Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #f0f0f0;
}
select {
position: absolute;
top: 4px;
left: 4px;
}
</style>
</head>

<body>
<div id="container"></div>
<select id="clip">
<option value="left-top" selected>left-top</option>
<option value="center-top">center-top</option>
<option value="right-top">right-top</option>
<option value="--">--</option>
<option value="left-middle">left-middle</option>
<option value="center-middle">center-middle</option>
<option value="right-middle">right-middle</option>
<option value="--">--</option>
<option value="left-bottom">left-bottom</option>
<option value="center-bottom">center-bottom</option>
<option value="right-bottom">right-bottom</option>
</select>
<script>
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;
} else if (clipPosition === 'scale') {
x = 0;
y = 0;
newWidth = width;
newHeight = height;
} else {
console.error(
new Error('Unknown clip position property - ' + clipPosition)
);
}

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

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

Konva.Image.fromURL(
'https://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 left-top crop
applyCrop('center-middle');

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

layer.add(tr);
layer.draw();

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.getAttr('lastCropUsed'));
});
}
);

document.querySelector('#clip').onchange = (e) => {
applyCrop(e.target.value);
};
</script>
</body>
</html>