缩放图像以适应固定的画布区域
如何在不拉伸图像的情况下缩放图像以适应可用区域?
该演示展示了如何使用 Konva.Image 的 crop 属性来模拟 CSS 的 object-fit: cover。
crop 属性允许您仅使用源图像的指定区域进行画布绘制。如果做出正确的计算,则可以在不拉伸的情况下绘制得到的图像。
说明:尝试调整图像大小或使用顶部下拉菜单更改裁剪策略。图像将在符合指定尺寸的同时保持其纵横比。
- Vanilla
- React
- Vue
import Konva from 'konva';
// 创建选择元素以选择裁剪位置
const select = document.createElement('select');
select.style.position = 'absolute';
select.style.top = '4px';
select.style.left = '4px';
const positions = [
'左上', '中上', '右上', '--',
'左中', '中中', '右中', '--',
'左下', '中下', '右下'
];
positions.forEach(pos => {
const option = document.createElement('option');
option.value = pos;
option.text = pos;
if (pos === '中中') 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 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 === '左上') {
x = 0;
y = 0;
} else if (clipPosition === '左中') {
x = 0;
y = (image.height - newHeight) / 2;
} else if (clipPosition === '左下') {
x = 0;
y = image.height - newHeight;
} else if (clipPosition === '中上') {
x = (image.width - newWidth) / 2;
y = 0;
} else if (clipPosition === '中中') {
x = (image.width - newWidth) / 2;
y = (image.height - newHeight) / 2;
} else if (clipPosition === '中下') {
x = (image.width - newWidth) / 2;
y = image.height - newHeight;
} else if (clipPosition === '右上') {
x = image.width - newWidth;
y = 0;
} else if (clipPosition === '右中') {
x = image.width - newWidth;
y = (image.height - newHeight) / 2;
} else if (clipPosition === '右下') {
x = image.width - newWidth;
y = image.height - newHeight;
}
return {
cropX: x,
cropY: y,
cropWidth: newWidth,
cropHeight: newHeight,
};
}
// 应用裁剪的函数
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://konvajs.org/assets/darth-vader.jpg', (img) => {
img.setAttrs({
width: 300,
height: 100,
x: 80,
y: 100,
name: 'image',
draggable: true,
});
layer.add(img);
// 应用默认的中中裁 剪
applyCrop(img, '中中');
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', () => {
// 在变换时重置缩放
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);
});
import React from 'react';
import { Stage, Layer, Image, Transformer } from 'react-konva';
import { useImage } from 'react-konva-utils';
const positions = [
'左上', '中上', '右上', '--',
'左中', '中中', '右中', '--',
'左下', '中下', '右下'
];
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 === '左上') {
x = 0;
y = 0;
} else if (clipPosition === '左中') {
x = 0;
y = (image.height - newHeight) / 2;
} else if (clipPosition === '左下') {
x = 0;
y = image.height - newHeight;
} else if (clipPosition === '中上') {
x = (image.width - newWidth) / 2;
y = 0;
} else if (clipPosition === '中中') {
x = (image.width - newWidth) / 2;
y = (image.height - newHeight) / 2;
} else if (clipPosition === '中下') {
x = (image.width - newWidth) / 2;
y = image.height - newHeight;
} else if (clipPosition === '右上') {
x = image.width - newWidth;
y = 0;
} else if (clipPosition === '右中') {
x = image.width - newWidth;
y = (image.height - newHeight) / 2;
} else if (clipPosition === '右下') {
x = image.width - newWidth;
y = image.height - newHeight;
}
return {
cropX: x,
cropY: y,
cropWidth: newWidth,
cropHeight: newHeight,
};
}
const App = () => {
const [position, setPosition] = React.useState('center-middle');
const [imageAttrs, setImageAttrs] = React.useState({
x: 80,
y: 100,
width: 300,
height: 100,
});
const imageRef = React.useRef(null);
const trRef = React.useRef(null);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg');
const handleTransform = () => {
const node = imageRef.current;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
node.scaleX(1);
node.scaleY(1);
setImageAttrs({
x: node.x(),
y: node.y(),
width: Math.max(5, node.width() * scaleX),
height: Math.max(5, node.height() * scaleY),
});
};
const crop = React.useMemo(() => {
if (!image) return null;
return getCrop(
image,
{ width: imageAttrs.width, height: imageAttrs.height },
position
);
}, [image, imageAttrs.width, imageAttrs.height, position]);
React.useEffect(() => {
if (image && imageRef.current && trRef.current) {
trRef.current.nodes([imageRef.current]);
}
}, [image]);
return (
<>
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer>
{image && (
<Image
ref={imageRef}
image={image}
x={imageAttrs.x}
y={imageAttrs.y}
width={imageAttrs.width}
height={imageAttrs.height}
{...crop}
draggable
onDragEnd={(e) => {
const { x, y } = e.target.position();
setImageAttrs((current) => ({ ...current, x, y }));
}}
onTransform={handleTransform}
/>
)}
<Transformer
ref={trRef}
flipEnabled={false}
boundBoxFunc={(oldBox, newBox) => {
if (Math.abs(newBox.width) < 10 || Math.abs(newBox.height) < 10) {
return oldBox;
}
return newBox;
}}
/>
</Layer>
</Stage>
<select
style={{ position: 'absolute', top: '4px', left: '4px' }}
value={position}
onChange={(e) => setPosition(e.target.value)}
>
{positions.map((pos) => (
<option key={pos} value={pos}>
{pos}
</option>
))}
</select>
</>
);
};
export default App;
<template>
<div>
<v-stage :config="stageConfig">
<v-layer>
<v-image
ref="imageRef"
:config="{
image: image,
x: 80,
y: 100,
width: size.width,
height: size.height,
draggable: true,
...crop,
}"
@transform="handleTransform"
/>
<v-transformer
ref="transformerRef"
:config="{
boundBoxFunc: boundBoxFunc
}"
/>
</v-layer>
</v-stage>
<select
style="position: absolute; top: 4px; left: 4px"
v-model="position"
>
<option
v-for="pos in positions"
:key="pos"
:value="pos"
>
{{ pos }}
</option>
</select>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { useImage } from 'vue-konva';
const positions = [
'左上', '中上', '右上', '--',
'左中', '中中', '右中', '--',
'左下', '中下', '右下'
];
const position = ref('中中');
const size = ref({ width: 300, height: 100 });
const imageRef = ref(null);
const transformerRef = ref(null);
const stageConfig = {
width: window.innerWidth,
height: window.innerHeight
};
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 === '左上') {
x = 0;
y = 0;
} else if (clipPosition === '左中') {
x = 0;
y = (image.height - newHeight) / 2;
} else if (clipPosition === '左下') {
x = 0;
y = image.height - newHeight;
} else if (clipPosition === '中上') {
x = (image.width - newWidth) / 2;
y = 0;
} else if (clipPosition === '中中') {
x = (image.width - newWidth) / 2;
y = (image.height - newHeight) / 2;
} else if (clipPosition === '中下') {
x = (image.width - newWidth) / 2;
y = image.height - newHeight;
} else if (clipPosition === '右上') {
x = image.width - newWidth;
y = 0;
} else if (clipPosition === '右中') {
x = image.width - newWidth;
y = (image.height - newHeight) / 2;
} else if (clipPosition === '右下') {
x = image.width - newWidth;
y = image.height - newHeight;
}
return {
cropX: x,
cropY: y,
cropWidth: newWidth,
cropHeight: newHeight,
};
}
const [ image ] = useImage('https://konvajs.org/assets/darth-vader.jpg');
const crop = computed(() => {
if (!image.value) return {};
return getCrop(image.value, size.value, position.value);
});
const boundBoxFunc = (oldBox, newBox) => {
if (Math.abs(newBox.width) < 10 || Math.abs(newBox.height) < 10) {
return oldBox;
}
return newBox;
};
const handleTransform = () => {
const node = imageRef.value.getNode();
const scaleX = node.scaleX();
const scaleY = node.scaleY();
node.scaleX(1);
node.scaleY(1);
size.value = {
width: Math.max(5, node.width() * scaleX),
height: Math.max(5, node.height() * scaleY),
};
};
watch(image, () => {
if (image.value && imageRef.value && transformerRef.value) {
const transformer = transformerRef.value.getNode();
const imageNode = imageRef.value.getNode();
transformer.nodes([imageNode]);
}
});
</script>