缩放图像以适应固定的画布区域
如何在不拉伸图像的情况下缩放图像以适应可用区域?
该演示展示了如何使用 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('中中'); const [size, setSize] = React.useState({ 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); setSize({ 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, size, position); }, [image, size, 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={80} y={100} width={size.width} height={size.height} {...crop} draggable onTransform={handleTransform} /> )} <Transformer ref={trRef} 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>