Picture cropping tool
introduce
Image cropping is a common feature in front end interaction. This article implements a common image cropping tool for React, Vue, Angular, and frameless front end pages.
Dynamic graph demonstration
use
const imageCut = new ImageCut({
scale: 2.listener: {
onOk: (info) = >{ setImgInfo(info); ,}}});const img = document.querySelector('#img');
imageCut.showRect(img);
Copy the code
Implementation approach
- With the help of
canvas
Ability to draw and generate pictures - Draw the image onto the canvas first
- Then operate the mouse to select the rectangular area to intercept
- Generates a Base64 image for the selected region
- You can also choose to copy or download.
The source code
import type { IRect, Point } from './interface';
import './index.less';
/** * generates canvas *@param param0
* @returns* /
export function getCanvas({ width = 0, height = 0, scale = 1, attrs = {} as Record<string.any>}) {
const canvas: any = document.createElement('canvas');
Object.keys(attrs).forEach((key) = > {
const value = attrs[key];
canvas.setAttribute(key, value);
});
canvas.setAttribute('width'.`${width * scale}`);
canvas.setAttribute('height'.`${height * scale}`);
canvas.style = `${attrs.style || ' '}; width:${width}px; height:${height}px; `;
const ctx = canvas.getContext('2d'); ctx? .scale(scale, scale);return { canvas, ctx };
}
/** * Download base64 as a file *@param Data base64 Data *@param Filename indicates the filename */
export function downloadBase64File(dataUrl: string, filename: string) {
const data = base64Img2Blob(dataUrl);
window.URL = window.URL || window.webkitURL;
const urlBlob = window.URL.createObjectURL(data);
const link = document.createElement('a');
link.style.display = 'none';
link.href = urlBlob;
const downloadFileName = filename;
link.setAttribute('download', downloadFileName);
document.body.appendChild(link);
link.click();
function base64Img2Blob(code: string) {
const parts = code.split('; base64,');
const contentType = parts[0].split(':') [1];
const raw = window.atob(parts[1]);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType }); }}/** * Copies the string to the clipboard *@param Value The character string */
export function copyStr(value: string) {
console.log('copyStr', value);
const transfer = document.createElement('input');
document.body.appendChild(transfer);
transfer.value = value;
transfer.setSelectionRange(0.999999999);
transfer.focus();
transfer.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
}
transfer.blur();
document.body.removeChild(transfer);
}
/** * Load base64 image *@param base64
* @returns* /
export const loadImage = (base64: string) = > {
return new Promise((resolve) = > {
const img = new Image();
img.crossOrigin = ' ';
img.onload = () = > resolve(img);
img.onerror = () = > resolve(null);
img.src = base64;
});
};
const resetRect = () = > {
return { x: 0.y: 0.width: 0.height: 0 } as IRect;
};
interface IImageInfo {
base64: string;
width: number;
height: number;
}
interfaceIListener { onOk? :(params: IImageInfo) = > void;
}
interfaceIProps { scale? :number; listener? : IListener; }class ImageCut {
scale = 1;
canvas: HTMLCanvasElement;
ctx: any;
pos = ' ';
initPoint: Point = { x: 0.y: 0 };
rect: IRect = resetRect();
initRect: IRect = resetRect();
originRect = resetRect();
img: any = null;
listener: IListener = {};
constructor(props: IProps = {}) {
const { scale = 1 } = props;
const { canvas, ctx } = getCanvas({ width: 0.height: 0 });
this.canvas = canvas;
this.ctx = ctx;
this.scale = scale;
this.listener = props.listener || {};
}
showRect(img: HTMLImageElement) {
this.reset();
const rect = img.getBoundingClientRect();
const rectContainer = this.renderRect(rect);
document.body.appendChild(rectContainer);
this.img = img;
}
getCutImageInfo() {
this.drawImage(this.img);
const { originRect, rect } = this;
const info = {
x: rect.x - originRect.x,
y: rect.y - originRect.y,
width: rect.width,
height: rect.height,
};
const base64 = this.cutImage(info);
return { base64, width: rect.width, height: rect.height };
}
startCut() {
const { listener } = this;
const res = this.getCutImageInfo();
this.reset(); listener.onOk? .(res); }reset() {
this.pos = ' ';
this.initPoint = { x: 0.y: 0 };
this.rect = resetRect();
this.initRect = resetRect();
this.originRect = resetRect();
this.removeRect();
}
drawImage(img: HTMLImageElement) {
const { ctx, scale } = this;
const { width, height } = img;
this.setCanvasSize(width, height);
ctx.drawImage(img, 0.0, width * scale, height * scale);
}
cutImage(rect: IRect) {
console.log(rect);
const { scale, ctx, canvas } = this;
const { x, y, width, height } = rect;
const imgData = ctx.getImageData(x * scale, y * scale, width * scale, height * scale);
ctx.clearRect(0.0, canvas.width, canvas.height);
this.setCanvasSize(width, height);
ctx.putImageData(imgData, 0.0);
const base64 = canvas.toDataURL('image/png');
return base64;
}
setCanvasSize(width: number, height: number) {
const { canvas, scale } = this;
canvas.setAttribute('width'.`${width * scale}`);
canvas.setAttribute('height'.`${height * scale}`);
canvas.setAttribute('style'.`width: ${width}px; height:${height}px; `);
}
renderRect(rect: IRect) {
const { x, y, width, height } = rect;
this.rect = { x, y, width, height };
this.originRect = { x, y, width, height };
const container: any = document.createElement('div');
container.setAttribute('id'.`rect__container`);
container.style = `
position: fixed;
border: 2px solid red;
left: ${rect.x}px;
top: ${rect.y}px;
width: ${rect.width}px;
height: ${rect.height}px;
`;
const rectList = [
{ x: 50.y: 0.pos: 'top'.mousePos: 'ns' },
{ x: 50.y: 100.pos: 'bottom'.mousePos: 'ns' },
{ x: 0.y: 50.pos: 'left'.mousePos: 'ew' },
{ x: 100.y: 50.pos: 'right'.mousePos: 'ew' },
{ x: 100.y: 0.pos: 'right-top'.mousePos: 'nesw' },
{ x: 100.y: 100.pos: 'right-bottom'.mousePos: 'nwse' },
{ x: 0.y: 100.pos: 'left-bottom'.mousePos: 'nesw' },
{ x: 0.y: 0.pos: 'left-top'.mousePos: 'nwse'},]; rectList.forEach((e: any, i: number) = > {
const rectDom: any = document.createElement('div');
rectDom.setAttribute('data-pos', e.pos);
rectDom.setAttribute('data-index', i);
rectDom.setAttribute('id'.`rect__${e.pos}`);
rectDom.style = `
position: absolute;
left: ${e.x}%;
top: ${e.y}%;
width: 7px;
height: 7px;
border: 1px solid #ddd;
background-color: #fff;
transform: translate(-50%, -50%);
cursor: ${e.mousePos}-resize;
`;
rectDom.addEventListener('mousedown'.this.handleMouseDown);
container.appendChild(rectDom);
});
const confirmIcon = ` < SVG viewBox = "0 0 1024 1024" version = "1.1" XMLNS = "http://www.w3.org/2000/svg" width = "18" height = "18" > < path D ="M159.405 462.713l218.634 218.634L859.54 196.055l94.784 94.151-580.076 583.236L69.676 568.87l89.729-106.157z" fill="currentColor" > `;
const cancelIcon = ` < SVG viewBox = "0 0 1024 1024" version = "1.1" XMLNS = "http://www.w3.org/2000/svg" width = "16" height = "16" > < path d = "M896 812.9 594.2 511.1 L301.2-301.2-82.3 -82.3L511.9 428.8 L-301-301L128.6 210.1 L301 301L128 812.7l82.3 82.3 301.6-301.6 301.8 301.8l896 812.9zm896 812.9" Fill ="currentColor" >
;
const downloadIcon = ` < SVG viewBox = "0 0 1024 1024" version = "1.1" XMLNS = "http://www.w3.org/2000/svg" width = "16" height = "16" > < path d = "M896 864.384 v96H128V-96H768zm564.096 64V506.176l201.376-201.44 67.84 67.84-316.768 316.8-316.8-316.8 67.904-67.84 200.448 200.416v64H96z "fill="currentColor" >
';
const copyIcon = ` < SVG viewBox = "0 0 1024 1024" version = "1.1" XMLNS = "http://www.w3.org/2000/svg" width = "15" height = "15" > < path d = "M921.6 819.2h-102.4v102.4c0 56.32-46.08 102.4-102.4 102.4H102.4C-56.320-102.4-46.08-102.4-102.4v307.2c0-56.3246.08-102.4 102.4-102.4h102.4v102.4c0-56.32 46.08-102.4 102.4-102.4h614.4c56.32 0 102.4 46.08 102.4 102.4v614.4c0 56.32-46.08 102.4-102.4 102.4zM153.6 307.2c-30.72 0-51.2 20.48-51.2 51.2v512c0 30.72 20.48 51.2 51.2 51.2 H512c30.72 0 51.2-20.48 51.2 51.2 V358.4 c0-30.72-20.48-51.2-51.2-51.2 H153.6 z m768 c0-153.6-30.72-20.48-51.2-51.2-51.2 H358.4 c 0-30.72-51.2 20.48-51.2 51.2v51.2h409.6 C56.32 0 102.4 46.08 102.4 102.4v409.6 H51.2 c30.72 0 51.2-20.48 51.2-51.2 v153.6z" fill="currentColor" > `;
const operationDom: any = document.createElement('div');
operationDom.style = ` position: absolute; right: 0; bottom: -30px; width: 120px; height: 26px; border: 1px solid #ddd; border-radius: 2px; background-color: #fff; display: flex; justify-content: center; padding: 0 4px; `;
const downloadDom: any = document.createElement('div');
downloadDom.setAttribute('title'.'download');
downloadDom.setAttribute('class'.'image-cut-operation-icon');
downloadDom.innerHTML = downloadIcon;
downloadDom.addEventListener('click'.() = > {
const { base64 } = this.getCutImageInfo();
downloadBase64File(base64, `The ${Date.now()}.png`);
});
operationDom.appendChild(downloadDom);
const copyDom: any = document.createElement('div');
copyDom.setAttribute('title'.'Copy to clipboard');
copyDom.setAttribute('class'.'image-cut-operation-icon');
copyDom.innerHTML = copyIcon;
copyDom.addEventListener('click'.() = > {
const { base64 } = this.getCutImageInfo();
copyStr(base64);
});
operationDom.appendChild(copyDom);
const cancelDom: any = document.createElement('div');
cancelDom.setAttribute('title'.'cancel');
cancelDom.setAttribute('class'.'image-cut-operation-icon');
cancelDom.innerHTML = cancelIcon;
cancelDom.addEventListener('click'.() = > this.reset());
operationDom.appendChild(cancelDom);
const confirmDom: any = document.createElement('div');
confirmDom.setAttribute('title'.'confirm');
confirmDom.setAttribute('class'.'image-cut-operation-icon');
confirmDom.innerHTML = confirmIcon;
confirmDom.addEventListener('click'.() = > this.startCut());
operationDom.appendChild(confirmDom);
container.appendChild(operationDom);
return container;
}
updateRect() {
const { rect } = this;
const container = document.getElementById('rect__container');
if(! container) {return;
}
container.style.left = `${rect.x}px`;
container.style.top = `${rect.y}px`;
container.style.width = `${rect.width}px`;
container.style.height = `${rect.height}px`;
}
removeRect() {
const container = document.getElementById('rect__container'); container? .parentNode? .removeChild(container); } handleMouseDown =(e: any) = > {
const { target, pageX: x, pageY: y } = e;
const pos = target.getAttribute('data-pos');
this.pos = pos;
this.initPoint = { x, y };
this.initRect = { ... this.rect };document.addEventListener('mousemove'.this.handleMouseMove);
document.addEventListener('mouseup'.this.handleMouseUp);
};
handleMouseMove = (e: any) = > {
if (!this.pos) {
return;
}
const { initRect } = this;
const { x, y } = this.initPoint;
const { pageX, pageY } = e;
let dx = 0;
let dy = 0;
let dw = 0;
let dh = 0;
if (this.pos.includes('top')) {
dy = pageY - y;
dh = -dy;
}
if (this.pos.includes('bottom')) {
dh = pageY - y;
}
if (this.pos.includes('left')) {
dx = pageX - x;
dw = -dx;
}
if (this.pos.includes('right')) {
dw = pageX - x;
}
dx = Math.min(initRect.width, Math.max(0, dx));
dy = Math.min(initRect.height, Math.max(0, dy));
dw = Math.min(0.Math.max(-initRect.width, dw));
dh = Math.min(0.Math.max(-initRect.height, dh));
this.rect = {
x: initRect.x + dx,
y: initRect.y + dy,
width: initRect.width + dw,
height: initRect.height + dh,
};
this.updateRect();
};
handleMouseUp = () = > {
if (!this.pos) {
return;
}
this.pos = ' ';
this.initPoint = { x: 0.y: 0 };
document.removeEventListener('mousemove'.this.handleMouseMove);
document.removeEventListener('mouseup'.this.handleMouseUp);
};
}
export default ImageCut;
Copy the code
- style
.image-cut-operation-icon {
color: # 555;
cursor: pointer;
margin-left: 8px;
display: flex;
align-items: center;
&:hover {
color: #1890ff; }}Copy the code