Zero, introduce
This article focuses on how to make a complete picture cropping component from scratch
This paper mainly includes:
- Upload and read pictures
- Drawing pictures on Canvas
- Parsing picture information
- The preview image
- Clipping related operations
- Canvas save() and restore()
- Basic clipping process
- Clipping box drawing
- Clipping box movement and expansion
- rotating
- Output cropped picture
- Use canvas.toblob () to output the image
- Canvas getImageData() and putImageData()
- Uploaded to the CDN
background
A picture cutting component of the application scene is actually more, the corresponding third-party plug-ins are also many, but sometimes need some specific functions, such as want to have a specific style of cutting box, want to batch cutting, even want to directly cut out the customized size and so on, then can only handwritten a cutting component.
A flowchart
First, upload and read pictures
When uploading an image, use the onChange event to obtain the file object, which contains the name, size, type, and modification time of the file (read only). You can use this information to restrict the format and type of the uploaded image before previewing the image.
handleChange = (e) => { const files = Array.from(e.target.files); if (! Files.length) {// Release upload system store current value, avoid the same file does not trigger onchange event this.imageupload. value = null; return; } // Upload rule verification (e.g. image format, image size limit, etc.... } render() {return (<div> <input type="file" onChange={this.handlechange Ref ={e => {this.imageUpload = e; }} /> </div> ) }Copy the code
⚠️ Note: When uploading a file using onChange, if you select the same file twice in a row, onChange will not be triggered the second time because the value is the same. Therefore, after the first upload, set the value input to null.
Second, Canvas draws pictures
2.1 Analyzing Picture Information
Using the file object we just obtained, we can parse out some key information about the image, such as the width, height and most importantly base64. Here we mainly use FileReader. ReadAsDataURL.
When the filereader.onload method is triggered, a Base64-encoded data-URI object is returned.
FilesInfo = (file) => {return new Promise((res, rej) => {let reader = new FileReader(); reader.readAsDataURL(file); Reader.onload = function(e) {let Image = new Image(); Image.onload = function() {width: image.width, // width: image.height, // width: image.height, // width: image.height, // width: image.height, // width: image.height, // width: image.height, //... }); }; image.src = e.target.result; // base64 image.crossOrigin = 'Anonymous'; // Resolve cross-domain problems}; }); },Copy the code
In fact, in addition to the above, there is a second way window. URL. CreateObjectURL, interested friends can check by oneself, in this paper, it is no longer here.
2.2 Previewing Images
The width and height of canvas are divided into two types:
-
Width and height in Canvas style: refers to the width and height of the entire canvas, which determines the size of the entire Canvas context.
-
Width and height of canvas element property: Represents the canvas size of the canvas.
Therefore, our adaptive image center strategy:
⚠️ about device pixel ratio can poke 👉 why canvas drawing is very fuzzy ❓
Canvas renders images mainly through Canvas.drawimage (). The code of 🔨 is as follows:
/ drawing/picture / / parameter here is the image object above drawImage = (image) = > {/ / obtain the canvas context enclosing showImg = this. CanvasRef. GetContext (" 2 d "); // Clear the canvas this.showimg.clearRect (0, 0, this.canvasref.width, this.canvasref.height); // Set the default canvas element size const canvasDefaultSize = 300; Let proportion = image.width/image.height, scale = proportion > 1? canvasDefaultSize / image.width : CanvasDefaultSize/image.height, canvasWidth = image.width * scale * pixel ratio, canvasHeight = image.height * scale * pixel ratio; this.canvasRef.width = canvasWidth; this.canvasRef.height = canvasHeight; Enclosing canvasRef. Style. The width = canvasWidth/pixel than + 'px'; Enclosing canvasRef. Style. Height = canvasHeight/pixel than + 'px'; / /... This. Image = image; // Save the Image object this.showimg. drawImage(Image, 0, 0, this.canvasref.width, this.canvasref.height); }; render() { const canvasDefaultSize = 300; Return (<div className="modal-trim" style={{width: `${canvasDefaultSize}px`, height: '${canvasDefaultSize}px'}} > <canvas ref={e => {this.canvasRef = e}} // give a default initial width={canvasDefaultSize} height={canvasDefaultSize} // ... ></canvas> </div> ) }Copy the code
/* partial CSS */. Modal-trim {overflow: hidden; position: relative; /* background-image: url(https://s10.mogucdn.com/mlcdn/c45406/190723_3afckd96l9h4fh6lcb56117cld176_503x503.jpg); background-size: cover; /* Canvas {cursor: default; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); }}Copy the code
Three, cutting related operations
3.1 Canvas save() and restore()
(For canvas save() and restore() you can skip this section.)
In layman’s terms, save and restore are stores that hold the state of the canvas
context.save()
Pushes the current state onto the stack.context.restore()
Pops up the state at the top of the stack, restoring the context to that state.
So what is the state of canvas? One point that can be misunderstood here is that the state does not refer to the contents of the canvas, but to the paint properties of the canvas, such as:
- Current matrix transformation: translation
translate()
, zoomscale()
And rotationrotate()
Etc. - Current clipping area:
clip()
- Other attribute values:
strokeStyle
.fillStyle
.lineWidth
.shadowColor
. Etc.
So what do we know about this property? Because there is only one context of canvas, a large number of state changes will be involved in the clipping operation, such as the redrawing of clipping selection box and the rotation of picture, etc. During these operations, the drawing properties need to be restored. For example, 🌰 :
function draw() {
let ctx = document.getElementById("canvas").getContext("2d");
ctx.save(); // Default Settings
ctx.fillStyle = "#09f";
ctx.fillRect(15.15.120.120); // Fill the current setting with the #09f color
ctx.restore();
ctx.fillRect(30.30.90.90); // Fill in the default black
}
Copy the code
When the code above draws the first square, we fill it with blue, while the second square has no color and is black by default. The effect is as follows:
However, if save and restore are commented out above, all the squares drawn will be blue. This is because fillStyle changes the drawing property of canvas. If restore is not used to restore the previous drawing property, all the squares drawn after that will be blue.
⚠️ Note: Save () and restore() are both in pairs; do not break them up
3.2 Basic cutting process
Returning to the topic, our general operation process for picture cropping is as follows:
So we can complete a clipping operation by using the mouse’s onMouseDown (click), onMouseMove (move) and onMouseUp (release) events. 🔨 part of the implementation code is as follows:
/ / initial configuration of the each image initialConfigs = () = > {this. ShowImg = this. CanvasRef. GetContext (" 2 d "); this.dragging = false; // This. StartX = null; this.startY = null; } // Click the event mouseDownEvent = (e) => {// Click to trigger the tailoring operation this.dragging = true; StartX = e.nativeEvent. OffsetX; this.startx = e.nativeEvent. this.startY = e.nativeEvent.offsetY; } // mouseMoveEvent = (e) => {if (! this.dragging) return; Let tempWidth = e.ativeEvent. OffsetX - this.startX, tempHeight = e.ativeEvent. OffsetY - this.startY; DrawTrim (this.startX, this.startY, tempWidth, tempHeight, This.showimg)} // Remove/release the event mouseRemoveEvent = (e) => {// Save the related clipped selectbox information if (this.dragging) {... } // Save it and set it to false to end the current process this.dragging = false; } render() { return ( // ... <canvas ref={e => {this.canvasRef = e}} onMouseDown={(e) => this.mouseDownEvent(e)} onMouseMove={(e) => this.mouseMoveEvent(e)} onMouseUp={(e) => this.mouseRemoveEvent(e)} ></canvas> // ... ) }Copy the code
3.3 Clipping frame drawing
On the cutting box drawing implementation, the more commonly used way in the industry is probably the introduction:
How can these layers of images be stacked correctly as required ❓
. Here you need to use canvas globalCompositeOperation this API, it sets or returns a new image, how to draw to an existing image to merge images to realize cutting box. The parameters it draws can be detailed at 👉 globalCompositeOperation or MDN
Using the mouse coordinate parameters we need to just pass, we draw the clipping box and 8 border pixels, remember to save information about each operation ~ 🔨 part of the implementation code is as follows:
// Initial configuration for each image
initialConfigs = (a)= > {
// ...
// Coordinate information to save
this.trimPosition = {
startX: null.startY: null.width: null.height: null
}; // Trim the box coordinate information
this.borderArr = []; // Trim the frame frame node coordinates
this.borderOption = null; // Clipping the box border node event
}
// Draw the clipping box method
drawTrim = (startX, startY, width, height, ctx) = > {
// The canvas needs to be cleared every frame
ctx.clearRect(0.0.this.canvasRef.width, this.canvasRef.height);
// Draw a mask
ctx.save();
ctx.fillStyle = 'rgba (0,0,0,0.6)'; // Mask color
ctx.fillRect(0.0.this.canvasRef.width, this.canvasRef.height);
// Cut away the mask
ctx.globalCompositeOperation = 'source-atop';
ctx.clearRect(startX, startY, width, height); // Crop the selection box
// Draw 8 border pixels and save coordinate information and event parameters
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = '#fc178f';
let size = 10; // Customize the pixel size
ctx.fillRect(startX - size / 2, startY - size / 2, size, size);
/ /... Similarly, use ctx.fillrect to draw another 8 pixels
ctx.restore();
// Draw the image below the mask again using drawImage
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.drawImage(this.image, 0.0.this.canvasRef.width, this.canvasRef.height);
// ...
ctx.restore();
}
Copy the code
3.4 Movement and expansion of clipping frames
Light to achieve the cutting selection box is not enough, our usual cutting steps also need to move and free stretching, how to achieve ❓
We have used drawTrim() to draw the initial clipping box above, but every time the clipping box moves or shrinks, we only modify the coordinate information after the clipping box moves or shrinks, that is to say, we can use the drawTrim() method to redraw.
So we need to modify the basic clipping process above:
🔥 tip: Here you can use canvas.ispointinPath () to determine if the mouse has moved into an 8-pixel area
We mainly rely on the coordinates of the clipping box saved above and the coordinate information of the 8 pixels to determine the current event to be executed.
Similarly, if we need customized functions such as directionally modifying the size of the clipping box and directly cutting out the optimal size required, the idea is the same. We obtain the final coordinate information of the clipping box through some calculations, and then use the drawTrim() method above to redraw a clipping box.
3.5 rotate
The worst point in the clipping component is the rotation coordinate 🔨. Let’s take a look at canvas. Rotate ()
As we all know, the origin of the initial coordinate axis of canvas is at the upper left corner, that is to say, (0,0) represents the point at the upper left corner. Based on the upper left corner to the right, X is positive, Y is positive and vice versa.
The rotate method in the canvas rotates around the top left corner of the canvas (0,0), and the coordinate axis is also rotated and affected by translate. In other words, if the rotate method is used to rotate 90 degrees clockwise, the relative position of the picture in the canvas will be changed. The coordinate axes will go from “right X is positive, lower Y is positive” to “lower X is positive, left Y is positive.”
So how do we rotate the image around itself ❓
It’s worth mentioning canvas.translate(), which, as the name implies, is the method used to translate the origin of the canvas’s coordinate axis. Is it ok to shift the axes back to the original position after each rotation?
To reframe our thinking, if we needed to rotate the image 45 degrees around itself:
- Move the origin of the canvas axis to the center of the picture
- Rotate the canvas 45 degrees
- To draw the image, move it half the way to the upper right corner
⚠️ Note: Remember that after each rotation, the above mentioned Save and rotate should be used to restore the previous drawing attribute state. Since the code involved is still the calculation and transformation of coordinates, this paper only introduces the general idea of rotating pictures. For details, you can click 👉 canvas rotation for detailed explanation
Four, output cutting pictures
4.1 Output images using canvas.toblob ()
When we upload the image, we convert the file file into base64, and then use Canvas.drawImage () to realize the image preview. So how can we convert the canvas back to the IMG image after clipping ❓
In fact, Canvas provides two methods for converting 2D images:
-
canvas.toDataURL()
-
canvas.toBlob()
Since our ultimate goal is to upload to the CDN, we choose canvas.toblob () :
// Get the cropped image file
getImgTrim = (type) = > {
this.canvasRef.toBlob((blob) = >{
// Add a timestamp cache
blob.lastModifiedDate = new Date(a);let fd = new FormData();
fd.append('image', blob);
// Upload images to CDN
// ...
}, type)
}
Copy the code
❓ : What if I need to convert to a File object
const file = new File([blob], 'pictures. JPG', { type: blob.type })
Copy the code
4.2 Canvas getImageData() and putImageData()
Let’s take a look at MDN:
CanvasRenderingContext2D
.getImageData()
Returns aImageData
Object, used to describe the pixel data implied in the Canvas area, which is represented by a rectangle, starting at *(sx, sy),Wide forSw,High forsh*CanvasRenderingContext2D
.putImageData()
It’s the Canvas 2D API that takes the data from the existingImageData
Object to draw the position map method. If a drawn rectangle is provided, only the pixels of that rectangle are drawn. This method is not affected by the canvas transformation matrix.
In layman’s terms, getImageData() takes the pixel data of the canvas area and returns an ImageData object, while putImageData() puts the pixel data of the ImageData object back into the Canvas.
So why do we need to know about these two apis? Canvas.toblob () can be used to print images ❓
Since canvas.toblob () prints the entire Canvas element, not the part we cropped, we need to build a new Canvas canvas to do this:
// Get the cropped image file
getImgTrim = (type) = > {
// Rebuild a canvas
this.saveImg = this.saveCanvasRef.getContext('2d');
this.saveImg.clearRect(0.0.this.saveCanvasRef.width, this.saveCanvasRef.height);
// Crop the box's pixel data
let { startX, startY, width, height } = this.trimPosition
const data = this.canvasRef.getImageData(startX, startY, width, height)
// Output on another canvas
this.saveImg.putImageData(data, 0.0)
this.saveCanvasRef.toBlob((blob) = >{
// ...
}, type)
}
Copy the code
❓ : Why does my output image become larger/smaller overall
Although we cut the part of the picture in the clipping box onto the second canvas, the width and height of our canvas itself are calculated (in the section of preview picture), which is inconsistent with the width and height of the picture itself, resulting in the overall larger/smaller output picture.
Solution: HERE I create a third canvas canvas as a continuation, the idea is as follows:
4.3 Uploading to the CDN
Since we want to make a universal component, it is best to unify it into the same outlet, so we choose to upload to CDN here, and the unified output is picture link
// Get the cropped image file
getImgTrim = (cdnUrl, type) = > {
// Rebuild a canvas and print it
// ...
this.saveCanvasRef.toBlob((blob) = >{
// Add a timestamp cache
blob.lastModifiedDate = new Date(a);let fd = new FormData();
fd.append('image', blob);
// Create an XMLHttpRequest submission object
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
// ...}}// Start uploading
xhr.withCredentials = true; // This is useful when sending cookies across domains
xhr.open("POST", cdnUrl, true);
xhr.setRequestHeader('Access-Control-Allow-Headers'.The '*');
xhr.send(fd);
}, type)
}
Copy the code
❓ : Why do I get cross-domain errors after I output images
Canvas will cross the domain due to canvas pollution when exporting pictures. You need to set crossOrigin to ‘Anonymous’ and setRequestHeader, etc. You can stamp 👉 to unlock N cross-domain postures of exported pictures of Canvas
conclusion
This paper mainly introduces a complete cutting process of the approximate implementation, as for some of the more customized functions (batch cutting, scaling cutting, directional size cutting, etc.), the principle is virtually the same, just how to operate the batch picture information, cutting information problem
The most important point to operate canvas is the calculation of coordinates, especially the rotation coordinates, which must be carefully and clearly defined. In fact, the whole process, as long as the thinking is clear, or very simple.
The demo address can be 👉 github
Refer to the link
- www.jianshu.com/p/4c4312dc7…
- www.jianshu.com/p/ee4db072c…
- Juejin. Cn/post / 684490…
- www.jianshu.com/p/d2f14489b…
- Juejin. Cn/post / 684490…
- www.cnblogs.com/suyuanli/p/…