preface
When customers encounter problems in the process of using our products and need to give us feedback, it is difficult for us to understand the meaning of customers if they describe the problems in the form of pure words. If we can add screenshots of the problems, we can clearly know the problems of customers.
So, we need to implement a custom for our product screenshot function, the user after the “capture” button box to choose any area, then in the box to choose the area of circle, draw the arrow, Mosaic, line, type, such as operation, after operation the user can choose save frame the content of the selected area to the local or sent directly to us.
Smart developers may have guessed that this is the screenshots function of QQ/ wechat, and my open source project just achieved the screenshots function. Before doing it, I found a lot of information, but did not find such a thing existed on the Web side, so I decided to refer to the screenshots of QQ and make it into a plug-in for everyone to use.
This article will share with you I do this “custom screen capture function” when the implementation of ideas and process, welcome you interested in the developers to read this article.
Run the result video: the realization of the Web side custom screen
Writing in the front
This plug-in is written using Vue3’s compositionAPI. For those who are not familiar with it, please refer to my other article: Using Vue3’s compositionAPI to optimize code volume
Implementation approach
Let’s first look at the QQ screenshot process, and then analyze how it is implemented.
Screen capture process analysis
So let’s start by analyzing how screenshots work.
-
After clicking the Screenshot button, we will notice that all the dynamic effects on the page are still, as shown below.
-
Then, we hold down the left mouse button and drag. A black mask appears on the screen, and the drag area of the mouse appears hollow out, as shown below.
-
After the dragging is complete, the toolbar will appear below the box selection area, which contains box selection, circle selection, arrow, line, brush and other tools, as shown in the picture below.
-
Click any icon in the toolbar, and the brush selection area will appear, where you can select the brush size and color as shown below.
-
We then drag and drop within the selected area of the box to draw the corresponding graph, as shown below.
-
Finally, click the download icon in the screenshot toolbar to save the picture to the local, or click the check mark and the picture will be automatically pasted into the chat input box, as shown below.
Screenshot implementation idea
Through the above screenshot process, we get the following implementation idea:
- Gets the contents of the currently visible area and stores it
- Draws a mask for the entire CNAVas canvas
- Drag and drop the obtained content to draw a hollowed-out selection
- Select the tool in the screenshot toolbar and select information such as brush size
- Drag and drop within the selection to draw the corresponding figure
- Converts the contents of the selection to an image
The implementation process
We have analyzed the implementation ideas, and then we will implement the above ideas one by one.
Gets the contents of the current viewable area
After clicking the screenshot button, we need to obtain the content of the entire visual area, and all subsequent operations are carried out on the obtained content. On the Web side, we can use Canvas to achieve these operations.
So, we need to convert the body area to canvas first, which is a bit complicated and a lot of work to do from scratch.
Fortunately, there is an open source library called HTML2Canvas that can convert the dom to canvas. We will use this library to implement our conversion.
Next, let’s look at the implementation process:
Create a new file named screen-short-vue to host our entire screenshot component.
- First we need a Canvas container to display the transformed viewable content
<template> <teleport to="body"> <! < Canvas ID ="screenShotContainer" :width="screenShortWidth" :height="screenShortHeight" ref="screenShortController" ></canvas> </teleport> </template>Copy the code
Only part of the code is shown here. For the full code, go to screen-short-.vue
- When the component is mounted, the html2Canvas method is called to convert the content in the body to canvas and store it.
import html2canvas from "html2canvas";
import InitData from "@/module/main-entrance/InitData";
export default class EventMonitoring {
// The response data of the current instance
private readonly data: InitData;
// Screenshot the area canvas container
private screenShortController: Ref<HTMLCanvasElement | null>;
// The container where the screenshot is stored
private screenShortImageController: HTMLCanvasElement | undefined;
constructor(props: Record<string.any>, context: SetupContext<any>) {
// Instantiate the response data
this.data = new InitData();
// Get the canvas container in the screenshot area
this.screenShortController = this.data.getScreenShortController();
onMounted(() = > {
// Set canvas width and height
this.data.setScreenShortInfo(window.innerWidth, window.innerHeight);
html2canvas(document.body, {}).then(canvas= > {
// If the dom of the screenshot is null, exit
if (this.screenShortController.value == null) return;
// Store the content captured by html2Canvas
this.screenShortImageController = canvas; }}})})Copy the code
Only part of the code is shown here; for the full code, go to EventMonitoring
Draws a mask for the canvas
Once we have the converted DOM, we need to draw a black mask with an opacity of 0.6 to inform the user that you are in the screenshot area selection.
The specific implementation process is as follows:
- Create the DrawMasking. Ts file where the drawing logic for the mask layer is implemented. The code is as follows.
/** * Draw mask *@param Context needs to be drawn to canvas */
export function drawMasking(context: CanvasRenderingContext2D) {
// Clear the canvas
context.clearRect(0.0.window.innerWidth, window.innerHeight);
// Draw the mask
context.save();
context.fillStyle = "rgba(0, 0, 0, .6)";
context.fillRect(0.0.window.innerWidth, window.innerHeight);
// The drawing is finished
context.restore();
}
Copy the code
The ⚠️ comments are detailed, so if you don’t understand the API, go to: clearRect, Save, fillStyle, fillRect, restore
- in
html2canvas
The draw mask function is called in the function callback
html2canvas(document.body, {}).then(canvas= > {
// Get the screenshot area to draw the canvas container canvas
const context = this.screenShortController.value? .getContext("2d");
if (context == null) return;
// Draw the mask
drawMasking(context);
})
Copy the code
Draw hollowed-out selection
We drag and drop in black layer, the need to get the mouse by pressing the starting point coordinates and the coordinates of the mouse movement, according to the coordinates of the starting point coordinates and mobile, we can get an area, at this time we will cover layer chiseling the area, to get to the canvas map to cover layer beneath the image content, so that we can achieve the effect of hollow out district.
Arrange the above discourse, the idea is as follows:
- Listen for mouse press, move, and lift events
- Gets the coordinates of mouse down and moving
- Chisel the mask according to the obtained coordinates
- Draws the obtained Canvas image content under the mask
- To achieve hollow selection drag and zoom
The effects are as follows:
The specific code is as follows:
export default class EventMonitoring {
// The response data of the current instance
private readonly data: InitData;
// Screenshot the area canvas container
private screenShortController: Ref<HTMLCanvasElement | null>;
// The container where the screenshot is stored
private screenShortImageController: HTMLCanvasElement | undefined;
// Screenshot the area canvas
private screenShortCanvas: CanvasRenderingContext2D | undefined;
// Figure position parameter
private drawGraphPosition: positionInfoType = {
startX: 0.startY: 0.width: 0.height: 0
};
// Temporary graphics position parameter
private tempGraphPosition: positionInfoType = {
startX: 0.startY: 0.width: 0.height: 0
};
// Crop the frame node coordinate event
private cutOutBoxBorderArr: Array<cutOutBoxBorder> = [];
// Trim the vertex border diameter of the box
private borderSize = 10;
// The border node of the current operation
private borderOption: number | null = null;
// Click the mouse coordinates of the cropping box
private movePosition: movePositionType = {
moveStartX: 0.moveStartY: 0
};
// Trim the box trim state
private draggingTrim = false;
// Crop the box drag state
private dragging = false;
// Mouse click state
private clickFlag = false;
constructor(props: Record<string.any>, context: SetupContext<any>) {
// Instantiate the response data
this.data = new InitData();
// Get the canvas container in the screenshot area
this.screenShortController = this.data.getScreenShortController();
onMounted(() = > {
// Set canvas width and height
this.data.setScreenShortInfo(window.innerWidth, window.innerHeight);
html2canvas(document.body, {}).then(canvas= > {
// If the dom of the screenshot is null, exit
if (this.screenShortController.value == null) return;
// Store the content captured by html2Canvas
this.screenShortImageController = canvas;
// Get the screenshot area to draw the canvas container canvas
const context = this.screenShortController.value? .getContext("2d");
if (context == null) return;
// Assign the screenshot area to canvas
this.screenShortCanvas = context;
// Draw the mask
drawMasking(context);
// Add listener
this.screenShortController.value? .addEventListener("mousedown".this.mouseDownEvent
);
this.screenShortController.value? .addEventListener("mousemove".this.mouseMoveEvent
);
this.screenShortController.value? .addEventListener("mouseup".this.mouseUpEvent ); })})}// Mouse down event
private mouseDownEvent = (event: MouseEvent) = > {
this.dragging = true;
this.clickFlag = true;
const mouseX = nonNegativeData(event.offsetX);
const mouseY = nonNegativeData(event.offsetY);
// If the operation is clipping box
if (this.borderOption) {
// Set the drag state
this.draggingTrim = true;
// Record the starting point of the move
this.movePosition.moveStartX = mouseX;
this.movePosition.moveStartY = mouseY;
} else {
// Draw a clipping box to record the current mouse start coordinates
this.drawGraphPosition.startX = mouseX;
this.drawGraphPosition.startY = mouseY; }}// Mouse movement event
private mouseMoveEvent = (event: MouseEvent) = > {
this.clickFlag = false;
// Get the clipping box location information
const { startX, startY, width, height } = this.drawGraphPosition;
// Get the current mouse coordinates
const currentX = nonNegativeData(event.offsetX);
const currentY = nonNegativeData(event.offsetY);
// Cut the temporary width and height of the frame
const tempWidth = currentX - startX;
const tempHeight = currentY - startY;
// Perform the clipping function
this.operatingCutOutBox(
currentX,
currentY,
startX,
startY,
width,
height,
this.screenShortCanvas
);
// Return if the mouse is not clicked or if the current operation is cropping box
if (!this.dragging || this.draggingTrim) return;
// Draw the clipping box
this.tempGraphPosition = drawCutOutBox(
startX,
startY,
tempWidth,
tempHeight,
this.screenShortCanvas,
this.borderSize,
this.screenShortController.value as HTMLCanvasElement,
this.screenShortImageController as HTMLCanvasElement
) as drawCutOutBoxReturnType;
}
// Mouse raise event
private mouseUpEvent = () = > {
// The drawing is finished
this.dragging = false;
this.draggingTrim = false;
// Save the position information of the drawn graph
this.drawGraphPosition = this.tempGraphPosition;
// Save the clipping position if the toolbar is not clicked
if (!this.data.getToolClickStatus().value) {
const { startX, startY, width, height } = this.drawGraphPosition;
this.data.setCutOutBoxPosition(startX, startY, width, height);
}
// Save the border node information
this.cutOutBoxBorderArr = saveBorderArrInfo(
this.borderSize,
this.drawGraphPosition ); }}Copy the code
⚠️ There are many codes to draw hole-out selection. Here only show the related codes of the three mouse event listeners. For the complete code, go to EventMonitoring
- The code to draw the clipping box is as follows
/** * Draw clipping box *@param MouseX x axis coordinates *@param MouseY y coordinate *@param Width Width of the clipping box *@param Height Height of clipping box *@param Context is the canvas that needs to be drawn@param BorderSize Border node diameter *@param Controller Canvas container * to operate on@param ImageController Image canvas container *@private* /
export function drawCutOutBox(
mouseX: number,
mouseY: number,
width: number,
height: number,
context: CanvasRenderingContext2D,
borderSize: number,
controller: HTMLCanvasElement,
imageController: HTMLCanvasElement
) {
// Get the canvas width and height
constcanvasWidth = controller? .width;constcanvasHeight = controller? .height;// Return if the canvas and image do not exist
if(! canvasWidth || ! canvasHeight || ! imageController || ! controller)return;
// Clear the canvas
context.clearRect(0.0, canvasWidth, canvasHeight);
// Draw the mask
context.save();
context.fillStyle = "rgba(0, 0, 0, .6)";
context.fillRect(0.0, canvasWidth, canvasHeight);
// Cut the cover
context.globalCompositeOperation = "source-atop";
// Crop the selection box
context.clearRect(mouseX, mouseY, width, height);
// Draw 8 border pixels and save the coordinate information and event parameters
context.globalCompositeOperation = "source-over";
context.fillStyle = "#2CABFF";
// Pixel size
const size = borderSize;
// Draw the pixels
context.fillRect(mouseX - size / 2, mouseY - size / 2, size, size);
context.fillRect(
mouseX - size / 2 + width / 2,
mouseY - size / 2,
size,
size
);
context.fillRect(mouseX - size / 2 + width, mouseY - size / 2, size, size);
context.fillRect(
mouseX - size / 2,
mouseY - size / 2 + height / 2,
size,
size
);
context.fillRect(
mouseX - size / 2 + width,
mouseY - size / 2 + height / 2,
size,
size
);
context.fillRect(mouseX - size / 2, mouseY - size / 2 + height, size, size);
context.fillRect(
mouseX - size / 2 + width / 2,
mouseY - size / 2 + height,
size,
size
);
context.fillRect(
mouseX - size / 2 + width,
mouseY - size / 2 + height,
size,
size
);
// The drawing is finished
context.restore();
// Use drawImage to draw the image below the mask
context.save();
context.globalCompositeOperation = "destination-over";
context.drawImage(
imageController,
0.0, controller? .width, controller? .height ); context.restore();// Return the temporary location information of the clipping box
return {
startX: mouseX,
startY: mouseY,
width: width,
height: height
};
}
Copy the code
⚠️ Again, the comments are very detailed. In addition to the canvas API described before, the above code uses the following new apis: globalCompositeOperation and drawImage
Implement the screenshot toolbar
After we realize the function of hollowing out the selection, the next thing to do is to select circles, boxes and lines in the selection. In the screenshot of QQ, these operations are located in the screenshot toolbar, so we need to make the screenshot toolbar to interact with canvas.
In terms of the layout of the toolbar in the screenshot, at first MY idea was to draw these tools directly on the canvas, which should be easier to interact with, but after looking at the RELATED API, I found that it was a bit cumbersome and complicated the problem.
After thinking about it for a while, I realized that this piece still needs to use div for layout. After drawing the clipping box, calculate the position of the toolbar in the screenshot according to the position information of the clipping box, and then change its position.
The toolbar interacts with the Canvas by binding a click event to EventMonitoring. Ts, obtaining the current click, and specifying the corresponding graph drawing function.
The effects are as follows:
The specific implementation process is as follows:
- in
screen-short.vue
Create a screenshot toolbar div and style it
<template>
<teleport to="body">
<! -- Toolbar -->
<div
id="toolPanel"
v-show="toolStatus"
:style="{ left: toolLeft + 'px', top: toolTop + 'px' }"
ref="toolController"
>
<div
v-for="item in toolbar"
:key="item.id"
:class="`item-panel ${item.title} `"
@click="toolClickEvent(item.title, item.id, $event)"
></div>
<! -- Undo part is handled separately -->
<div
v-if="undoStatus"
class="item-panel undo"
@click="toolClickEvent('undo', 9, $event)"
></div>
<div v-else class="item-panel undo-disabled"></div>
<! -- Closing and validation are handled separately -->
<div
class="item-panel close"
@click="toolClickEvent('close', 10, $event)"
></div>
<div
class="item-panel confirm"
@click="toolClickEvent('confirm', 11, $event)"
></div>
</div>
</teleport>
</template>
<script lang="ts">
import eventMonitoring from "@/module/main-entrance/EventMonitoring";
import toolbar from "@/module/config/Toolbar.ts";
export default {
name: "screen-short".setup(props: Record<string, any>, context: SetupContext<any>) {
const event = new eventMonitoring(props, context as SetupContext<any>);
const toolClickEvent = event.toolClickEvent;
return {
toolClickEvent,
toolbar
}
}
}
</script>
Copy the code
⚠️ The above code shows only part of the component code. For the complete code, go to screen-short-. vue and screen-short-
Screenshot tool entry click style processing
Each item in the screenshot toolbar has three states: Normal, mouse over, and click. What I did here was to write all the states in THE CSS, using different class names to display different styles.
Part of the toolbar click status CSS is as follows:
.square-active {
background-image: url("~@/assets/img/square-click.png");
}
.round-active {
background-image: url("~@/assets/img/round-click.png");
}
.right-top-active {
background-image: url("~@/assets/img/right-top-click.png");
}
Copy the code
At first when I want to v – for rendering, define a variable, click change the state of the variable, displaying each click entries corresponding click style, but I found a problem, when do I click the class name is dynamic, and didn’t send this form to get, but I had to choose the form of dom manipulation, $event = $event = $event = $event; $event = $event; $event = $event;
The implementation code is as follows:
- Dom structure
<div
v-for="item in toolbar"
:key="item.id"
:class="`item-panel ${item.title} `"
@click="toolClickEvent(item.title, item.id, $event)"
></div>
Copy the code
- Toolbar click events
/** * Clipping toolbar click event *@param toolName
* @param index
* @param mouseEvent* /
public toolClickEvent = (
toolName: string,
index: number,
mouseEvent: MouseEvent
) = > {
// Add the selected class name for the currently clicked item
setSelectedClassName(mouseEvent, index, false);
}
Copy the code
- Adds the selected class for the currently clicked item and removes the selected class for its sibling elements
import { getSelectedClassName } from "@/module/common-methords/GetSelectedCalssName";
import { getBrushSelectedName } from "@/module/common-methords/GetBrushSelectedName";
/** * Adds the selected class to the current clicked item and removes the selected class * from its sibling@param MouseEvent The element * to operate on@param Index Indicates the current click *@param IsOption is a brush option */
export function setSelectedClassName(
mouseEvent: any,
index: number,
isOption: boolean
) {
// Get the class name when the currently clicked item is selected
let className = getSelectedClassName(index);
if (isOption) {
// Get the corresponding class when the brush option is selected
className = getBrushSelectedName(index);
}
// Get all the child elements under div
const nodes = mouseEvent.path[1].children;
for (let i = 0; i < nodes.length; i++) {
const item = nodes[i];
// Remove the selected class from the toolbar if it already exists
if (item.className.includes("active")) {
item.classList.remove(item.classList[2]); }}// Adds the selected class to the currently clicked item
mouseEvent.target.className += "" + className;
}
Copy the code
- Gets the class name when the screenshot toolbar is clicked
export function getSelectedClassName(index: number) {
let className = "";
switch (index) {
case 1:
className = "square-active";
break;
case 2:
className = "round-active";
break;
case 3:
className = "right-top-active";
break;
case 4:
className = "brush-active";
break;
case 5:
className = "mosaicPen-active";
break;
case 6:
className = "text-active";
}
return className;
}
Copy the code
- Gets the class name when the brush selection is clicked
/** * get the class name of the selected brush option *@param itemName* /
export function getBrushSelectedName(itemName: number) {
let className = "";
switch (itemName) {
case 1:
className = "brush-small-active";
break;
case 2:
className = "brush-medium-active";
break;
case 3:
className = "brush-big-active";
break;
}
return className;
}
Copy the code
Implement each option in the toolbar
Next, let’s look at the implementation of each of the options in the toolbar.
The drawing of each graph in the toolbar requires the cooperation of the three events of mouse pressing, moving and lifting. In order to prevent repeated drawing of the graph when the mouse is moving, we adopt the “history” mode to solve this problem. Let’s first look at the scene of repeated drawing, as shown below:
Next, let’s look at how to use history to solve this problem.
- First, we need to define an array variable called
history
.
private history: Array<Record<string.any> > = [];Copy the code
- When the mouse is raised at the end of drawing, save the current canvas state to
history
.
/** * Save the current canvas state *@private* /
private addHistoy() {
if (
this.screenShortCanvas ! =null &&
this.screenShortController.value ! =null
) {
// Get canvas and container
const context = this.screenShortCanvas;
const controller = this.screenShortController.value;
if (this.history.length > this.maxUndoNum) {
// Delete the earliest canvas record
this.history.unshift();
}
// Save the current canvas state
this.history.push({
data: context.getImageData(0.0, controller.width, controller.height)
});
// Enable the undo button
this.data.setUndoStatus(true); }}Copy the code
- When the mouse is moving, we take out
history
Is the last record in.
/** * Displays the latest canvas state *@private* /
private showLastHistory() {
if (this.screenShortCanvas ! =null) {
const context = this.screenShortCanvas;
if (this.history.length <= 0) {
this.addHistoy();
}
context.putImageData(this.history[this.history.length - 1] ["data"].0.0); }}Copy the code
The above functions can be executed at an appropriate time to solve the problem of repeated drawing of graphs. Next, let’s look at the drawing effect after solving, as shown below:
Realize rectangle drawing
In the previous analysis, we got the coordinates of the starting point of the mouse and the coordinates when the mouse moved. We can calculate the width and height of the selected area of the box through these data, as shown below.
// Get the mouse starting point coordinates
const { startX, startY } = this.drawGraphPosition;
// Get the current mouse coordinates
const currentX = nonNegativeData(event.offsetX);
const currentY = nonNegativeData(event.offsetY);
// Cut the temporary width and height of the frame
const tempWidth = currentX - startX;
const tempHeight = currentY - startY;
Copy the code
After we get this data, we can draw a rectangle using the Canvas RECt API. The code is as follows:
/** * Draw rectangle *@param mouseX
* @param mouseY
* @param width
* @param height
* @param Color Border color *@param BorderWidth Border size *@param Context is the canvas that needs to be drawn@param Controller Canvas container * to operate on@param ImageController Image canvas container */
export function drawRectangle(
mouseX: number,
mouseY: number,
width: number,
height: number,
color: string,
borderWidth: number,
context: CanvasRenderingContext2D,
controller: HTMLCanvasElement,
imageController: HTMLCanvasElement
) {
context.save();
// Set the border color
context.strokeStyle = color;
// Set the border size
context.lineWidth = borderWidth;
context.beginPath();
// Draw a rectangle
context.rect(mouseX, mouseY, width, height);
context.stroke();
// The drawing is finished
context.restore();
// Use drawImage to draw the image below the mask
context.save();
context.globalCompositeOperation = "destination-over";
context.drawImage(
imageController,
0.0, controller? .width, controller? .height );// The drawing is finished
context.restore();
}
Copy the code
Ellipse rendering
When drawing an ellipse, we need to calculate the radius of the circle and the coordinates of the center of the circle according to the coordinate information, and then call the ellipse function to draw an ellipse. The code is as follows:
/** * Draw circle *@param Context the canvas that needs to be drawn *@param MouseX Current mouse X-axis coordinates *@param MouseY Indicates the y axis of the current mouse@param MouseStartX X coordinate * when the mouse is held down@param MouseStartY Y coordinate * when the mouse is held down@param BorderWidth borderWidth *@param Color Border color */
export function drawCircle(
context: CanvasRenderingContext2D,
mouseX: number,
mouseY: number,
mouseStartX: number,
mouseStartY: number,
borderWidth: number,
color: string
) {
// The coordinate boundary processing solves the error problem when the reverse ellipse drawing
const startX = mouseX < mouseStartX ? mouseX : mouseStartX;
const startY = mouseY < mouseStartY ? mouseY : mouseStartY;
const endX = mouseX >= mouseStartX ? mouseX : mouseStartX;
const endY = mouseY >= mouseStartY ? mouseY : mouseStartY;
// Calculate the radius of the circle
const radiusX = (endX - startX) * 0.5;
const radiusY = (endY - startY) * 0.5;
// Calculate the x and y coordinates of the center of the circle
const centerX = startX + radiusX;
const centerY = startY + radiusY;
// Start drawing
context.save();
context.beginPath();
context.lineWidth = borderWidth;
context.strokeStyle = color;
if (typeof context.ellipse === "function") {
// Draw a circle with a rotation Angle of 0 and an end Angle of 2*PI
context.ellipse(centerX, centerY, radiusX, radiusY, 0.0.2 * Math.PI);
} else {
throw "Your browser does not support ellipse, so you cannot draw ellipses.";
}
context.stroke();
context.closePath();
// Finish drawing
context.restore();
}
Copy the code
⚠️ annotation has been written very clearly, the API used here are: beginPath, lineWidth, ellipse, closePath, developers who are not familiar with these apis, please step to the specified position to consult.
Implementation of arrow drawing
Arrow drawing is the most complicated tool compared to other tools, because we need to use trigonometric functions to calculate the coordinates of the two points of the arrow, and use the arc tangent function of trigonometric functions to calculate the Angle of the arrow
Since we need to use trigonometric functions to do this, let’s take a look at what we know:
The length of the line from P3 to P2. P4 is symmetric with P3, so the length from P4 to P2 is equal to the length from P3 to P2 * 3. The Angle between the arrow slant line P3 and the lines P1 and P2 (θ) is symmetric, so the Angle between P4 and the lines P1 and P2 is equal * Find: * coordinates of P3 and P4 */
Copy the code
As shown in the figure above, P1 is the coordinate when the mouse is held down, P2 is the coordinate when the mouse is moved, and the Angle θ is 30. After we know these information, we can work out the coordinates of P3 and P4, and then we can draw arrows by moveTo and lineTo on canvas.
The implementation code is as follows:
/** * Draw arrow *@param Context the canvas that needs to be drawn *@param MouseStartX x coordinate P1 * when the mouse is down@param MouseStartY click on the y axis P1 *@param MouseX current mouse X-axis coordinate P2 *@param MouseY current mouseY coordinate P2 *@param Theta arrow slash the Angle (theta) and linear P3 - > (P1, P2) | | P4 - > P1 (P1, P2) *@param Headlen arrow slash the length of the P3 - > P2 | | P4 - > P2 *@param BorderWidth borderWidth *@param Color Border color */
export function drawLineArrow(
context: CanvasRenderingContext2D,
mouseStartX: number,
mouseStartY: number,
mouseX: number,
mouseY: number,
theta: number,
headlen: number,
borderWidth: number,
color: string
) {
Known: / * * * * 1. * 2. The coordinates of P1 and P2 arrow slash (P3 | | P4) - > P2 the length of the straight line * 3. Arrow slash (P3 | | P4) - > (P1, P2) the Angle (theta) linear: * o * the coordinates of P3 or P4 * /
const angle =
(Math.atan2(mouseStartY - mouseY, mouseStartX - mouseX) * 180) / Math.PI, // Use atan2 to get the Angle of the arrow
angle1 = ((angle + theta) * Math.PI) / 180.// The Angle of P3
angle2 = ((angle - theta) * Math.PI) / 180.// P4 point Angle
topX = headlen * Math.cos(angle1), // the x coordinate of P3
topY = headlen * Math.sin(angle1), // the y-coordinate of P3
botX = headlen * Math.cos(angle2), // The X coordinate at P4
botY = headlen * Math.sin(angle2); // The y-coordinate of P4
// Start drawing
context.save();
context.beginPath();
// The coordinates of P3
let arrowX = mouseStartX - topX,
arrowY = mouseStartY - topY;
// Move the stroke to P3
context.moveTo(arrowX, arrowY);
// Move the stroke to P1
context.moveTo(mouseStartX, mouseStartY);
// Draw the line from P1 to P2
context.lineTo(mouseX, mouseY);
// Calculate the position of P3
arrowX = mouseX + topX;
arrowY = mouseY + topY;
// Move the stroke to P3
context.moveTo(arrowX, arrowY);
// Draw the slash from P2 to P3
context.lineTo(mouseX, mouseY);
// Calculate the position of P4
arrowX = mouseX + botX;
arrowY = mouseY + botY;
// Draw the slash from P2 to P4
context.lineTo(arrowX, arrowY);
/ / color
context.strokeStyle = color;
context.lineWidth = borderWidth;
/ / fill
context.stroke();
// Finish drawing
context.restore();
}
Copy the code
The new apis used here are: moveTo and lineTo. If you are not familiar with these apis, please go to the specified location to check them.
Achieve brush drawing
We need to use lineTo to draw the brush, but we need to pay attention to this when drawing: When the mouse is pressed, we need to use beginPath to clear a path, and move the brush to the position when the mouse is pressed, otherwise the starting position of the mouse is always 0, and the bug is as follows:
To fix this bug, initialize the stroke position when the mouse is down.
/** * Brush initialization */
export function initPencli(
context: CanvasRenderingContext2D,
mouseX: number,
mouseY: number
) {
/ / | | clear a path
context.beginPath();
// Move the brush position
context.moveTo(mouseX, mouseY);
}
Copy the code
Then, draw the line according to the coordinate information when the mouse is located. The code is as follows:
/** * Brush draw *@param context
* @param mouseX
* @param mouseY
* @param size
* @param color* /
export function drawPencli(
context: CanvasRenderingContext2D,
mouseX: number,
mouseY: number,
size: number,
color: string
) {
// Start drawing
context.save();
// Set the border size
context.lineWidth = size;
// Set the border color
context.strokeStyle = color;
context.lineTo(mouseX, mouseY);
context.stroke();
// The drawing is finished
context.restore();
}
Copy the code
Mosaic rendering
As we all know, a picture is made up of pixels. When we set the pixels in a certain area to the same color, the information in this area will be destroyed. The area destroyed by us is called Mosaic.
After knowing the principle of Mosaic, we can analyze the realization of the train of thought:
- Get the image information of the mouse over the path area
- Draws the pixels in the region in a similar color to their surroundings
The specific implementation code is as follows:
/** * Get the color of the image at the specified coordinates *@param ImgData Image * to operate@param X dot x dot x@param Y dot y dot */
const getAxisColor = (imgData: ImageData, x: number, y: number) = > {
const w = imgData.width;
const d = imgData.data;
const color = [];
color[0] = d[4 * (y * w + x)];
color[1] = d[4 * (y * w + x) + 1];
color[2] = d[4 * (y * w + x) + 2];
color[3] = d[4 * (y * w + x) + 3];
return color;
};
/** * Sets the color of the image at the specified coordinates *@param ImgData Image * to operate@param X dot x dot x@param Y, y point *@param Color Array of colors */
const setAxisColor = (
imgData: ImageData,
x: number,
y: number,
color: Array<number>
) = > {
const w = imgData.width;
const d = imgData.data;
d[4 * (y * w + x)] = color[0];
d[4 * (y * w + x) + 1] = color[1];
d[4 * (y * w + x) + 2] = color[2];
d[4 * (y * w + x) + 3] = color[3];
};
/** * draw a Mosaic ** 1. Obtain the image information * 2. Draws the pixels in the region with the same color as the surrounding pixels *@param MouseX Current mouse X-axis coordinates *@param MouseY Indicates the Y axis of the current mouse@param Size Mosaic brush size *@param DegreeOfBlur * degreeOfBlur in a Mosaic@param Context is the canvas to be drawn */
export function drawMosaic(
mouseX: number,
mouseY: number,
size: number,
degreeOfBlur: number,
context: CanvasRenderingContext2D
) {
// Get the image pixel information of the mouse over the area
const imgData = context.getImageData(mouseX, mouseY, size, size);
// Get the image width and height
const w = imgData.width;
const h = imgData.height;
// Divide the image width and height equally
const stepW = w / degreeOfBlur;
const stepH = h / degreeOfBlur;
// Loop the canvas pixels
for (let i = 0; i < stepH; i++) {
for (let j = 0; j < stepW; j++) {
// Get a random color for a small square
const color = getAxisColor(
imgData,
j * degreeOfBlur + Math.floor(Math.random() * degreeOfBlur),
i * degreeOfBlur + Math.floor(Math.random() * degreeOfBlur)
);
// Loop the pixels of the small box
for (let k = 0; k < degreeOfBlur; k++) {
for (let l = 0; l < degreeOfBlur; l++) {
// Set the color of the small squaressetAxisColor( imgData, j * degreeOfBlur + l, i * degreeOfBlur + k, color ); }}}}// Render the pixelated image information
context.putImageData(imgData, mouseX, mouseY);
}
Copy the code
Realization of text drawing
Canvas does not directly provide API for us to input text, but it does provide API for filling text. Therefore, we need a div for the user to input text. After the user completes the input, the input text can be filled into the specified area.
The effects are as follows:
- Create a div in the component, enable the editable property of the div, and lay out the style
<template>
<teleport to="body">
<! -- Text input area -->
<div
id="textInputPanel"
ref="textInputController"
v-show="textStatus"
contenteditable="true"
spellcheck="false"
></div>
</teleport>
</template>
Copy the code
- When the mouse is down, the text input area position is calculated
// Calculate the display position of the text box
const textMouseX = mouseX - 15;
const textMouseY = mouseY - 15;
// Modify the text area location
this.textInputController.value.style.left = textMouseX + "px";
this.textInputController.value.style.top = textMouseY + "px";
Copy the code
- When the position of the input box changes, it means that the user has finished the input. The content entered by the user is rendered to the canvas. The code for drawing the text is as follows
/** * Draw text *@param Text Specifies the text * to be drawn@param MouseX draws the X-axis coordinates * of the position@param MouseY draws the Y-axis coordinates of the position *@param Color Font color *@param FontSize fontSize *@param Context needs the canvas that you're drawing */
export function drawText(
text: string,
mouseX: number,
mouseY: number,
color: string,
fontSize: number,
context: CanvasRenderingContext2D
) {
// Start drawing
context.save();
context.lineWidth = 1;
// Set the font color
context.fillStyle = color;
context.textBaseline = "middle";
context.font = `bold ${fontSize}Px Microsoft Yahei ';
context.fillText(text, mouseX, mouseY);
// Finish drawing
context.restore();
}
Copy the code
Implement download function
The download function is relatively simple. We just need to put the content of the clipped area into a new canvas, and then call the toDataURL method to get the base64 address of the image. We create a tag, add the Download property, and start the click event of the A tag to download.
The implementation code is as follows:
export function saveCanvasToImage(
context: CanvasRenderingContext2D,
startX: number,
startY: number,
width: number,
height: number
) {
// Get the picture information of the cropped box area
const img = context.getImageData(startX, startY, width, height);
// Create a Canvas tag to hold the images in the cropped area
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
// Get the clipping area canvas
const imgContext = canvas.getContext("2d");
if (imgContext) {
// Put the picture in the cropping box
imgContext.putImageData(img, 0.0);
const a = document.createElement("a");
// Get the image
a.href = canvas.toDataURL("png");
// Download the image
a.download = `The ${new Date().getTime()}.png`; a.click(); }}Copy the code
Implement undo function
Since we draw the graph in history mode, each time we draw the graph, we will store the canvas state once. We only need to pop up the last record from history when we click the undo button.
The implementation code is as follows:
/** * retrieve a history */
private takeOutHistory() {
const lastImageData = this.history.pop();
if (this.screenShortCanvas ! =null && lastImageData) {
const context = this.screenShortCanvas;
if (this.undoClickNum == 0 && this.history.length > 0) {
// For the first time, two historical records are required
const firstPopImageData = this.history.pop() as Record<string.any>;
context.putImageData(firstPopImageData["data"].0.0);
} else {
context.putImageData(lastImageData["data"].0.0); }}this.undoClickNum++;
// The history has been taken, disable the recall button click
if (this.history.length <= 0) {
this.undoClickNum = 0;
this.data.setUndoStatus(false); }}Copy the code
Realize the shutdown function
The shutdown function refers to resetting the screenshot component, so we need to push a destroy message to the parent component through the Emit.
The implementation code is as follows:
/** * reset the component */
private resetComponent = () = > {
if (this.emit) {
// Hide the screenshot toolbar
this.data.setToolStatus(false);
// Initialize the response variable
this.data.setInitStatus(true);
// Destroy the component
this.emit("destroy-component".false);
return;
}
throw "Component reset failed";
};
Copy the code
Implement validation function
When the user clicks ok, we need to convert the content in the clipping box to Base64, then push the payment component through emit, and finally reset the component.
The implementation code is as follows:
const base64 = this.getCanvasImgData(false);
this.emit("get-image-data", base64);
Copy the code
Plug-in address
At this point, the plug-in implementation process is shared.
-
Plug-in online experience address: Chat-system
-
Plugins GitHub repository address: Screen-shot
-
Open source project address: Chat-System-Github
Write in the last
- Feel free to correct any mistakes in the comments section, and if this post helped you, feel free to like and follow 😊
- This article was originally published in Nuggets and cannot be reproduced without permission at 💌