Alien world tailoring tools from scratch
preface
This article realizes a complete clipping tool from the perspective of a Canvas white. In the end, it is found that there is not much Canvas content involved, but more implementation ideas.
Clipping tools currently have a lot of mature libraries, in most cases also enough, but occasionally there will be some specific clipping style requirements at this time need to customize, master or necessary ~.
Start with a scratch-off
Before the implementation of cutting tools to see a scratchoff implementation, scratchoff implementation of the idea is very simple, the bottom of a picture above is a gray mask, the mouse (click) after the place will be part of the gray mask removal.
Removal with CSS is not difficult, rare is arbitrary coordinates under the coherent removal, this is not easy to achieve CSS. Here it naturally comes to mind to use Canvas:
Some pre-canvas theory knowledge:
Save and Restore are used to store the brush state and restore the brush state, because we are working with the same brush and not storing and restoring may cause bugs that are difficult to debug. FillStyle can set the color of the current brush and support transparency. FillRect can draw a rectangle, x,y,width,height. ClearRect can erase an area as a rectangle, x,y,width,height.Copy the code
- Base drawing:
<style>
#canvas {
background: url(./gouzi.jpeg);
}
</style>
<canvas id="canvas"></canvas>
<script>
let canvas = document.querySelector("#canvas");
let ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 500;
</script>
Copy the code
- Draw a mask:
const drawModal = () = > {
ctx.save();
ctx.fillStyle = "Rgba (0,0,0,0.5)";
ctx.fillRect(0.0.500.500);
ctx.restore();
};
Copy the code
- When the mouse moves, we erase the corresponding block:
const clearRect = (x, y, w, h) = > {
ctx.save();
ctx.clearRect(x, y, w, h);
ctx.restore();
};
canvas.addEventListener("mousemove".(e) = > {
const { offsetX, offsetY } = e;
clearRect(offsetX, offsetY, 30.30);
});
Copy the code
This allows for a super simple scratch-off.
Scratch-off advances
Mask, mouse erase we have implemented, but the fly in the ointment is that the erased block can only be displayed in a rectangular way, can not support arbitrary shapes.
Canvas itself does not provide other methods for erasing. An interesting (and useful) way to save the country is to use blending (mix-blending-mode in CSS also supports blending). Among all blending modes, destination-out can be found to meet our needs. In this blending mode, the existing image is retained only where there is no overlap, just an erasing effect.
It’s not difficult to write, just replace clearRect with fillRect in 3 above:
const clearRect = (x, y, w, h) = > {
ctx.save();
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "rgba(0,0,0)";
ctx.fillRect(x, y, w, h);
ctx.restore();
};
Copy the code
FillStyle is not important here, the new image will only leave shape, not content.
In this way, fill is used to achieve an erasing effect consistent with clear. Now, just replace fillRect with drawImage to achieve the erasing of any graph.
const clearRect = (x, y, w, h) = > {
ctx.save();
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(img, x, y, w, h);
ctx.restore();
};
Copy the code
You can see that mammoth erasers are drawn, don’t ask me why I chose mammoth, because mammoth is not banned.
Implement image cropping
Cutting tool implementation ideas
Organize a wave of requirements before implementing:
- Draw a black and white transparent bottom.
- Use a Canvas to draw the image to be clipped. Note that the image may exceed the size of the Canvas and need to be scaled.
- Using a different Canvas to paint a grey mask and dig a hole, here using a Canvas to load the base and paint the mask is also possible, using the mixture mentioned above, I still split it into two more intuitive.
- Use HTML to draw clipped box borders and handle interactive events.
Black and white transparent bottom
Transparent base is usually represented by white and white grid, you can directly put an image, but this does not highlight our force grid, CSS is not difficult to achieve, use the related properties of background.
We know that background can be gradient, and can be many, background-size and background-position can also specify the state of each background individually.
First specify two of the same gradients:
background: linear-gradient(
45deg.#ccc 25%,
transparent 25%,
transparent 75%.#ccc 25%
), linear-gradient(45deg.#ccc 25%, transparent 25%, transparent 75%.#ccc 25%);
Copy the code
The linear-gradient(45deg, # CCC 25%, Transparent 75%, # CCC 25%) gradient produces a gradient that is gray in the upper right and lower left, and transparent in the middle.
Set the background size to the desired block size, in this case I set it to 8px:
background-size: 8px 8px;
Copy the code
Because background does not repeat itself by default, you will see a grid full of triangles.
Finally, we move the x and y of the second background (or the first one, as long as it meets the expectations) by 4px. The previous triangle and the next triangle will be combined into a whole square, which is repeated to create a transparent grid.
background-position: 0 0.4px 4px;
Copy the code
Zoom and draw pictures
As mentioned above, when drawing an image, we need to scale the image to the appropriate size of the canvas. There is nothing like object-fit in canvas: Contain (include) : the width and height of the image must be smaller than the width and height of the drawing board. If any of the folders does not contain, continue to reduce until all of them do:
const shrinkImage = ({ imageWidth, imageHeight, width, height, base = 1 }) = > {
// Return an appropriate imageWidth and imageHeight based on height and width.
if (imageWidth / base < width && imageHeight / base < height) {
return {
imageWidth: imageWidth / base,
imageHeight: imageHeight / base,
};
}
return shrinkImage({
imageWidth,
imageHeight,
width,
height,
base: base + 0.1}); };Copy the code
A simple version of tail recursion, the divisor step can increase or decrease depending on the demand.
DrawImage takes a number of parameters, so we’ll use them all:
let baseWidth = 500;
let baseHeight = 500;
let img = new Image();
let canvasOne = document.querySelector("#canvas-one");
let basePaintParams = {
baseOffsetX: 0.baseOffsetY: 0.w: 0.h: 0}; img.src ="./gouzi.jpeg";
img.onload = () = > {
let { imageWidth, imageHeight } = shrinkImage({
imageWidth: img.width,
imageHeight: img.height,
width: baseWidth,
height: baseHeight,
});
ctxOne.drawImage(
img,
0.0,
img.width,
img.height,
(baseWidth - imageWidth) / 2,
(baseHeight - imageHeight) / 2,
imageWidth,
imageHeight
);
basePaintParams = {
baseOffsetX: (baseWidth - imageWidth) / 2.baseOffsetY: (baseHeight - imageHeight) / 2.w: imageWidth,
h: imageHeight,
};
};
Copy the code
In drawImage, the first parameter is the original image, two or three parameters identify the clipping xy of the original image, and four or five parameters represent the clipping width and height of the original image. Six or seven parameters represent xy drawn to the Canvas. In this case, the width and height of the Canvas are subtracted from the width and height of the image drawn and divided by 2 to get the middle xy. Eight and nine are the width and height drawn to the Canvas.
Here we declare a basePaintParams variable, which we will not use now, but will use later when we draw the clipping box, and initialize it to the same xy width and height as the drawn image.
MDN Reference: drawImage
Mask and hollow out
The concrete implementation of mask and hollowing can be used in scratch-off, which implements a rectangular clipping box, blending and clearRect.
The mask:
let canvasTwo = document.querySelector("#canvas-two");
const paintModal = () = > {
ctxTwo.clearRect(0.0, baseWidth, baseHeight);
ctxTwo.fillStyle = "Rgba (0,0,0,0.5)";
ctxTwo.fillRect(0.0, baseWidth, baseHeight);
};
paintModal();
Copy the code
Hollowing out:
const clipModal = () = > {
ctxTwo.save();
ctxTwo.clearRect(
basePaintParams.baseOffsetX,
basePaintParams.baseOffsetY,
basePaintParams.w,
basePaintParams.h
);
ctxTwo.restore();
};
// Wait until after img onload to draw, or provide an initialization argument.
clipModal();
Copy the code
This is not difficult to achieve with HTML+CSS, there is no need to use Canvas.
Draw clipping box
Specific clipping box we use HTML to achieve, binding events these or HTML to comfortable, regular shape clipping box HTML can be very easy to write, strange shapes can also use clip-path to achieve, for the moment not to go into the Canvas of the rough water.
<style>
.cropper-clip {
position: absolute;
cursor: all-scroll;
border: 1px solid rgb(30.158.251);
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #1e9efb;
position: absolute;
}
.topleft {
top: -5px;
left: -5px;
cursor: nwse-resize;
}
.topright {
top: -5px;
right: -5px;
cursor: nesw-resize;
}
.topcenter {
top: -5px;
left: 50%;
transform: translateX(-50%);
cursor: ns-resize;
}
.bottomcenter {
bottom: -5px;
left: 50%;
transform: translateX(-50%);
cursor: ns-resize;
}
.bottomleft {
bottom: -5px;
left: -5px;
cursor: nesw-resize;
}
.bottomright {
bottom: -5px;
right: -5px;
cursor: nwse-resize;
}
.leftcenter {
top: 50%;
transform: translateY(-50%);
left: -5px;
cursor: ew-resize;
}
.rightcenter {
top: 50%;
transform: translateY(-50%);
right: -5px;
cursor: ew-resize;
}
</style>
<div class="cropper-clip">
<div class="dot topleft"></div>
<div class="dot topright"></div>
<div class="dot topcenter"></div>
<div class="dot bottomleft"></div>
<div class="dot bottomcenter"></div>
<div class="dot bottomright"></div>
<div class="dot leftcenter"></div>
<div class="dot rightcenter"></div>
</div>
Copy the code
A set of easily writable boxes, eight points in total, distributed around the edges, the position and size of the box are given when setting the mask and hollowed out:
const drawClipDiv = () = > {
let cropperClip = document.querySelector(".cropper-clip");
cropperClip.style.width = `The ${Math.abs(basePaintParams.w)}px`;
cropperClip.style.height = `The ${Math.abs(basePaintParams.h)}px`;
cropperClip.style.left = `${basePaintParams.baseOffsetX}px`;
cropperClip.style.top = `${basePaintParams.baseOffsetY}px`;
};
Copy the code
Math.abs is used to accommodate changing the size of the clipping box over the line, if not added (this article does not implement this).
The clipped skeleton is now fully drawn, just change the coordinates and size parameters in basePaintParams, and redraw the mask gouge and clipping box.
Cutting the movement and expansion of the box
Above we have drawn the basic skeleton, and finally we just need to make the clipping box move with the mouse.
First, our current HTML structure looks like this:
<div class="cropper">
<div class="cropper-clip">
<div class="dot topleft"></div>
<div class="dot topright"></div>
<div class="dot topcenter"></div>
<div class="dot bottomleft"></div>
<div class="dot bottomcenter"></div>
<div class="dot bottomright"></div>
<div class="dot leftcenter"></div>
<div class="dot rightcenter"></div>
</div>
</div>
Copy the code
Let’s register a Mousemove event with Cropper so it’s easier to process.
Clipping the box body and saving a certain point by pressing requires additional variable identification.
let dotType = "";
let dotDown = false;
let clipDown = false;
const registerEvents = () = > {
const register = (_class) = > {
let node = document.querySelector(`.${_class}`);
node.addEventListener("mousedown".(e) = > {
dotDown = true;
dotType = _class;
});
node.addEventListener("mouseup".() = > {
dotDown = false;
});
};
register("topcenter");
register("bottomcenter");
register("leftcenter");
register("rightcenter");
register("bottomright");
register("bottomleft");
register("topright");
register("topleft");
let clip = document.querySelector(".cropper-clip");
clip.addEventListener("mousedown".() = > {
clipDown = true;
});
clip.addEventListener("mouseup".() = > {
clipDown = false;
});
};
Copy the code
The overall idea of the event of the core Mousemove starts from one side. As long as the movement strategy of up and down and left and right is determined, other points can be combined:
const cardMouseMove = (e) = > {
if(! dotDown && ! clipDown) {return;
}
const { offsetX, offsetY } = e;
const topYChange = () = > {
if (e.target === canvasTwo) {
if(offsetY < basePaintParams.baseOffsetY) { basePaintParams.h += basePaintParams.baseOffsetY - offsetY; basePaintParams.baseOffsetY = offsetY; }}else if(e.target === clip) { basePaintParams.h -= offsetY; basePaintParams.baseOffsetY += offsetY; }};const bottomYChange = () = > {
if (e.target === canvasTwo) {
if(offsetY > basePaintParams.baseOffsetY) { basePaintParams.h = offsetY - basePaintParams.baseOffsetY; }}else if(e.target === clip) { basePaintParams.h = offsetY; }};const leftXChange = () = > {
if (e.target === canvasTwo) {
if(offsetX < basePaintParams.baseOffsetX) { basePaintParams.w += basePaintParams.baseOffsetX - offsetX; basePaintParams.baseOffsetX = offsetX; }}else if(e.target === clip) { basePaintParams.w -= offsetX; basePaintParams.baseOffsetX += offsetX; }};const rightXChange = () = > {
if (e.target === canvasTwo) {
if(offsetX > basePaintParams.baseOffsetX) { basePaintParams.w = offsetX - basePaintParams.baseOffsetX; }}else if(e.target === clip) { basePaintParams.w = offsetX; }}; };Copy the code
It is important to note that the event we registered is on the parent node, so the target of the event could be the mask we drew (when zooming in) or the clipping box (when zooming out), with a different offset.
Then we just need to call different methods based on the desired change in xy of the 8 points:
const dotRunMap = {
topcenter: () = > {
topYChange();
},
bottomcenter: () = > {
bottomYChange();
},
leftcenter: () = > {
leftXChange();
},
rightcenter: () = > {
rightXChange();
},
bottomright: () = > {
rightXChange();
bottomYChange();
},
bottomleft: () = > {
leftXChange();
bottomYChange();
},
topright: () = > {
rightXChange();
topYChange();
},
topleft: () = >{ leftXChange(); topYChange(); }};if (dotDown) {
dotRunMap[dotType]();
}
// If it is not pressed, it is moving, there is no edge limit here, it will go out of the circle.
if(clipDown && ! dotDown) { basePaintParams.baseOffsetX = basePaintParams.baseOffsetX + e.movementX; basePaintParams.baseOffsetY = basePaintParams.baseOffsetY + e.movementY; } drawClipDiv(); paintModal(); clipModal();Copy the code
Don’t forget to redraw cropped boxes, masks and hollows.
Preview and Save
The preview needs to use another Canvas to connect the disk. Here, getImageData and putImageData are mainly used, and most of the parameters are the same. The pit to be noted is that if it is a cross-domain image, Canvas will consider it contaminated by default. In this case, you need to set the cross-domain Settings of the original image request. For more details, please refer to this article.
<canvas id="clipImage"/ >;const save = () = > {
let bgCtx = canvasOne;
let imageData = bgCtx.getImageData(
basePaintParams.w < 0
? basePaintParams.baseOffsetX + basePaintParams.w
: basePaintParams.baseOffsetX,
basePaintParams.h < 0
? basePaintParams.baseOffsetY + basePaintParams.h
: basePaintParams.baseOffsetY,
Math.abs(basePaintParams.w),
Math.abs(basePaintParams.h)
);
let targetCanvas = clipImage.getContext("2d");
clipImage.width = basePaintParams.w;
clipImage.height = basePaintParams.h;
targetCanvas.putImageData(
imageData,
0.0.0.0,
basePaintParams.w,
basePaintParams.h
);
};
Copy the code
It is a common practice to save the Canvas to the base64 form of dataURL and then to the Blob, which can be downloaded.
const dataURItoBlob = async (url) => await (await fetch(url)).blob();
const saveData = (function () {
const a = document.createElement("a");
document.body.appendChild(a);
return function (blob, fileName) {
let url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url); }; }) ();const exportImg = async() = > {let MIME_TYPE = "image/png";
let imgURL = clipImage.toDataURL(MIME_TYPE);
let res = await dataURItoBlob(imgURL);
saveData(res, "data.png");
};
Copy the code
conclusion
Such a clipping tool with complete basic functions is completed. The core clipping part of the code is not much, and the content of Canvas is not much, so it is very suitable to get familiar with a wave of Canvas. With the skeleton, other functions can be expanded along this vein.
A Vue3 version is packaged here and can be found on Github if necessary.