предисловие
Когда клиент сталкивается с проблемой и должен дать нам обратную связь во время использования нашего продукта, если мы описываем ее в виде простого текста, нам трудно понять, что клиент имеет в виду, если это может сопровождаться скриншотом проблемы , мы можем четко знать проблему клиента.
Затем нам нужно реализовать функцию пользовательского снимка экрана для нашего продукта.После того, как пользователь нажмет кнопку «Снимок экрана», он может выбрать любую область, а затем обвести, нарисовать стрелки, мозаику, прямые линии и ввести выделенную область.Подождите для операции.После завершения операции пользователь может сохранить содержимое области выбора кадра на локальном компьютере или отправить его непосредственно нам.
Умные разработчики могли догадаться, что это функция скриншота QQ/WeChat.Мой проект с открытым исходным кодом только что реализовал функцию скриншота.Прежде чем это сделать, я перерыл много информации, но не нашел такой вещи в сети , поэтому я просто решил реализовать его, обратившись к снимку экрана QQ, и сделать его подключаемым модулем для всех.
В этой статье мы поделимся с вами идеями реализации и процессом создания этой "функции пользовательского захвата экрана". Заинтересованные разработчики могут прочитать эту статью.
Видео результатов прогона:Внедрение пользовательских скриншотов в Интернете
написать впереди
Плагин в этой статье написан с использованием API композиции Vue3. Если вы не знаете об этом, перейдите к другой моей статье:Используйте Composition API Vue3 для оптимизации размера кода
Реализовать идеи
Давайте сначала посмотрим на скриншот процесса QQ, а затем проанализируем, как он реализован.
Анализ процесса захвата экрана
Давайте сначала проанализируем конкретный процесс при съемке скриншотов.
-
После нажатия кнопки скриншота мы обнаружим, что все динамические эффекты на странице остались прежними, как показано ниже.
-
Затем мы удерживаем левую кнопку мыши для перетаскивания, на экране появится черная маска, а в области перетаскивания мыши появится эффект пустоты, как показано ниже.
-
После перетаскивания под областью выбора рамки появится панель инструментов, которая включает в себя выбор рамки, выбор круга, стрелки, линии, кисти и другие инструменты, как показано на следующем рисунке.
-
Щелкните любой значок на панели инструментов, появится область выбора кисти, где вы можете выбрать размер и цвет кисти, как показано ниже.
-
Затем мы перетаскиваем выбранную область, чтобы нарисовать соответствующую графику, как показано ниже.
-
Наконец, щелкните значок загрузки на панели инструментов для снимков экрана, чтобы сохранить изображение локально, или щелкните изображение с галочкой, чтобы автоматически вставить его в поле ввода чата, как показано ниже.
Идеи реализации скриншотов
Благодаря приведенному выше процессу скриншота мы получаем следующие идеи реализации:
- Получить содержимое текущей видимой области и сохранить его
- нарисовать маску для всего холста cnavas
- Перетащите полученный контент, чтобы нарисовать пустое выделение.
- Выберите инструменты на панели инструментов Snipping, выберите размер кисти и другую информацию.
- Перетащите соответствующую графику в область выбора.
- Преобразование содержимого выделения в изображение
Процесс реализации
Мы проанализировали идеи реализации, а затем будем реализовывать вышеперечисленные идеи одну за другой.
Получить содержимое текущей области просмотра
После нажатия кнопки скриншота нам необходимо получить содержимое всей видимой области, все последующие операции выполняются над полученным содержимым, на веб-стороне мы можем использовать холст для достижения этих операций.
Затем нам нужно сначала преобразовать содержимое области тела в холст.Если мы хотим реализовать это преобразование с нуля, это немного сложно и требует много работы.
К счастью, в сообществе разработчиков интерфейсов есть библиотека с открытым исходным кодом, которая называетсяhtml2canvasМожно преобразовать указанный дом в холст, и мы используем эту библиотеку для реализации нашего преобразования.
Далее, давайте посмотрим на конкретный процесс реализации:
Создайте новый с именемscreen-short.vue
файл, в котором находится весь наш компонент скриншота.
- Сначала нам нужен контейнер холста для отображения преобразованного содержимого видимой области.
<template>
<teleport to="body">
<!--截图区域-->
<canvas
id="screenShotContainer"
:width="screenShortWidth"
:height="screenShortHeight"
ref="screenShortController"
></canvas>
</teleport>
</template>
Здесь показана только часть кода, перейдите к полному коду:screen-short.vue
- Когда компонент монтируется, вызывается метод, предоставленный html2canvas, для преобразования содержимого тела в холст и его сохранения.
import html2canvas from "html2canvas";
import InitData from "@/module/main-entrance/InitData";
export default class EventMonitoring {
// 当前实例的响应式data数据
private readonly data: InitData;
// 截图区域canvas容器
private screenShortController: Ref<HTMLCanvasElement | null>;
// 截图图片存放容器
private screenShortImageController: HTMLCanvasElement | undefined;
constructor(props: Record<string, any>, context: SetupContext<any>) {
// 实例化响应式data
this.data = new InitData();
// 获取截图区域canvas容器
this.screenShortController = this.data.getScreenShortController();
onMounted(() => {
// 设置截图区域canvas宽高
this.data.setScreenShortInfo(window.innerWidth, window.innerHeight);
html2canvas(document.body, {}).then(canvas => {
// 装载截图的dom为null则退出
if (this.screenShortController.value == null) return;
// 存放html2canvas截取的内容
this.screenShortImageController = canvas;
})
})
}
}
Здесь показана только часть кода, перейдите к полному коду:EventMonitoring.ts
нарисовать маску для холста
После того, как мы получим преобразованный дом, нам нужно нарисовать черную маску с прозрачностью 0,6, чтобы сообщить пользователю, что вы сейчас находитесь в состоянии выбора области скриншота.
Конкретный процесс реализации выглядит следующим образом:
- Создадим файл DrawMasking.ts В этом файле реализована логика отрисовки маски Код следующий.
/**
* 绘制蒙层
* @param context 需要进行绘制canvas
*/
export function drawMasking(context: CanvasRenderingContext2D) {
// 清除画布
context.clearRect(0, 0, window.innerWidth, window.innerHeight);
// 绘制蒙层
context.save();
context.fillStyle = "rgba(0, 0, 0, .6)";
context.fillRect(0, 0, window.innerWidth, window.innerHeight);
// 绘制结束
context.restore();
}
⚠️Комментарии написаны очень подробно.Для разработчиков, которые не понимают вышеуказанный API, пожалуйста, перейдите по адресу:clearRect,save,fillStyle,fillRect,restore
- существует
html2canvas
Вызов функции маски рисования в обратном вызове функции
html2canvas(document.body, {}).then(canvas => {
// 获取截图区域画canvas容器画布
const context = this.screenShortController.value?.getContext("2d");
if (context == null) return;
// 绘制蒙层
drawMasking(context);
})
Нарисуйте выделение
При перетаскивании черной маски нам нужно получить координаты начальной точки при нажатии мыши и координаты при движении мыши.По координатам начальной точки и координатам при движении мы можем получить Слой маски области вырезается, а полученное содержимое изображения холста рисуется под слоем маски, так что мы можем добиться эффекта полого выделения.
Располагая приведенные выше слова, идеи следующие:
- Прослушивание событий нажатия, перемещения и подъема мыши
- Получить координаты при нажатии и перемещении мыши
- Вырежьте слой маски по полученным координатам
- Нарисуйте полученное содержимое изображения холста под маской
- Реализовать перетаскивание и масштабирование полого выделения
Достигаемый эффект следующий:
Конкретный код выглядит следующим образом:
export default class EventMonitoring {
// 当前实例的响应式data数据
private readonly data: InitData;
// 截图区域canvas容器
private screenShortController: Ref<HTMLCanvasElement | null>;
// 截图图片存放容器
private screenShortImageController: HTMLCanvasElement | undefined;
// 截图区域画布
private screenShortCanvas: CanvasRenderingContext2D | undefined;
// 图形位置参数
private drawGraphPosition: positionInfoType = {
startX: 0,
startY: 0,
width: 0,
height: 0
};
// 临时图形位置参数
private tempGraphPosition: positionInfoType = {
startX: 0,
startY: 0,
width: 0,
height: 0
};
// 裁剪框边框节点坐标事件
private cutOutBoxBorderArr: Array<cutOutBoxBorder> = [];
// 裁剪框顶点边框直径大小
private borderSize = 10;
// 当前操作的边框节点
private borderOption: number | null = null;
// 点击裁剪框时的鼠标坐标
private movePosition: movePositionType = {
moveStartX: 0,
moveStartY: 0
};
// 裁剪框修剪状态
private draggingTrim = false;
// 裁剪框拖拽状态
private dragging = false;
// 鼠标点击状态
private clickFlag = false;
constructor(props: Record<string, any>, context: SetupContext<any>) {
// 实例化响应式data
this.data = new InitData();
// 获取截图区域canvas容器
this.screenShortController = this.data.getScreenShortController();
onMounted(() => {
// 设置截图区域canvas宽高
this.data.setScreenShortInfo(window.innerWidth, window.innerHeight);
html2canvas(document.body, {}).then(canvas => {
// 装载截图的dom为null则退出
if (this.screenShortController.value == null) return;
// 存放html2canvas截取的内容
this.screenShortImageController = canvas;
// 获取截图区域画canvas容器画布
const context = this.screenShortController.value?.getContext("2d");
if (context == null) return;
// 赋值截图区域canvas画布
this.screenShortCanvas = context;
// 绘制蒙层
drawMasking(context);
// 添加监听
this.screenShortController.value?.addEventListener(
"mousedown",
this.mouseDownEvent
);
this.screenShortController.value?.addEventListener(
"mousemove",
this.mouseMoveEvent
);
this.screenShortController.value?.addEventListener(
"mouseup",
this.mouseUpEvent
);
})
})
}
// 鼠标按下事件
private mouseDownEvent = (event: MouseEvent) => {
this.dragging = true;
this.clickFlag = true;
const mouseX = nonNegativeData(event.offsetX);
const mouseY = nonNegativeData(event.offsetY);
// 如果操作的是裁剪框
if (this.borderOption) {
// 设置为拖动状态
this.draggingTrim = true;
// 记录移动时的起始点坐标
this.movePosition.moveStartX = mouseX;
this.movePosition.moveStartY = mouseY;
} else {
// 绘制裁剪框,记录当前鼠标开始坐标
this.drawGraphPosition.startX = mouseX;
this.drawGraphPosition.startY = mouseY;
}
}
// 鼠标移动事件
private mouseMoveEvent = (event: MouseEvent) => {
this.clickFlag = false;
// 获取裁剪框位置信息
const { startX, startY, width, height } = this.drawGraphPosition;
// 获取当前鼠标坐标
const currentX = nonNegativeData(event.offsetX);
const currentY = nonNegativeData(event.offsetY);
// 裁剪框临时宽高
const tempWidth = currentX - startX;
const tempHeight = currentY - startY;
// 执行裁剪框操作函数
this.operatingCutOutBox(
currentX,
currentY,
startX,
startY,
width,
height,
this.screenShortCanvas
);
// 如果鼠标未点击或者当前操作的是裁剪框都return
if (!this.dragging || this.draggingTrim) return;
// 绘制裁剪框
this.tempGraphPosition = drawCutOutBox(
startX,
startY,
tempWidth,
tempHeight,
this.screenShortCanvas,
this.borderSize,
this.screenShortController.value as HTMLCanvasElement,
this.screenShortImageController as HTMLCanvasElement
) as drawCutOutBoxReturnType;
}
// 鼠标抬起事件
private mouseUpEvent = () => {
// 绘制结束
this.dragging = false;
this.draggingTrim = false;
// 保存绘制后的图形位置信息
this.drawGraphPosition = this.tempGraphPosition;
// 如果工具栏未点击则保存裁剪框位置
if (!this.data.getToolClickStatus().value) {
const { startX, startY, width, height } = this.drawGraphPosition;
this.data.setCutOutBoxPosition(startX, startY, width, height);
}
// 保存边框节点信息
this.cutOutBoxBorderArr = saveBorderArrInfo(
this.borderSize,
this.drawGraphPosition
);
}
}
⚠️ Существует много кода для рисования пустых выделений. Здесь показаны только соответствующие коды мониторинга трех событий мыши. Чтобы увидеть полный код, перейдите по ссылке:EventMonitoring.ts
- Код для рисования поля кадрирования выглядит следующим образом.
/**
* 绘制裁剪框
* @param mouseX 鼠标x轴坐标
* @param mouseY 鼠标y轴坐标
* @param width 裁剪框宽度
* @param height 裁剪框高度
* @param context 需要进行绘制的canvas画布
* @param borderSize 边框节点直径
* @param controller 需要进行操作的canvas容器
* @param imageController 图片canvas容器
* @private
*/
export function drawCutOutBox(
mouseX: number,
mouseY: number,
width: number,
height: number,
context: CanvasRenderingContext2D,
borderSize: number,
controller: HTMLCanvasElement,
imageController: HTMLCanvasElement
) {
// 获取画布宽高
const canvasWidth = controller?.width;
const canvasHeight = controller?.height;
// 画布、图片不存在则return
if (!canvasWidth || !canvasHeight || !imageController || !controller) return;
// 清除画布
context.clearRect(0, 0, canvasWidth, canvasHeight);
// 绘制蒙层
context.save();
context.fillStyle = "rgba(0, 0, 0, .6)";
context.fillRect(0, 0, canvasWidth, canvasHeight);
// 将蒙层凿开
context.globalCompositeOperation = "source-atop";
// 裁剪选择框
context.clearRect(mouseX, mouseY, width, height);
// 绘制8个边框像素点并保存坐标信息以及事件参数
context.globalCompositeOperation = "source-over";
context.fillStyle = "#2CABFF";
// 像素点大小
const size = borderSize;
// 绘制像素点
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
);
// 绘制结束
context.restore();
// 使用drawImage将图片绘制到蒙层下方
context.save();
context.globalCompositeOperation = "destination-over";
context.drawImage(
imageController,
0,
0,
controller?.width,
controller?.height
);
context.restore();
// 返回裁剪框临时位置信息
return {
startX: mouseX,
startY: mouseY,
width: width,
height: height
};
}
⚠️Аналогично комментарии очень подробные.Аппи холста,используемый в приведенном выше коде,в дополнение к предыдущим,использует следующие новые API:globalCompositeOperation,drawImage
Реализовать панель инструментов для скриншотов
После того, как мы реализуем связанные функции полой области выбора, следующее, что нужно сделать, это выполнить такие операции, как выбор круга, выбор прямоугольника и рисование линии в области выбора, На снимке экрана QQ эти операции расположены на снимке экрана. панель инструментов, поэтому нам нужно использовать инструмент скриншота.Панель предназначена для взаимодействия с холстом.
Что касается макета панели инструментов скриншота, сначала моя идея заключалась в том, чтобы рисовать эти инструменты прямо на холсте, с которым должно быть легче взаимодействовать, но после прочтения соответствующего API я обнаружил, что это немного хлопотно, что усложняет проблему. . . .
Поразмыслив некоторое время, я понял, что этот кусок все еще нужно выложить с помощью div.После того, как рамка обрезки нарисована, положение панели инструментов скриншота можно рассчитать в соответствии с информацией о положении рамки обрезки и ее положением. может быть изменено.
Взаимодействие между панелью инструментов и холстом, вы можете привязать событие клика кEventMonitoring.ts
, получить текущий элемент щелчка и указать соответствующую функцию рисования графики.
Достигаемый эффект следующий:
Конкретный процесс реализации выглядит следующим образом:
- существует
screen-short.vue
, создайте div панели инструментов скриншота и стилизуйте его
<template>
<teleport to="body">
<!--工具栏-->
<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>
<!--撤销部分单独处理-->
<div
v-if="undoStatus"
class="item-panel undo"
@click="toolClickEvent('undo', 9, $event)"
></div>
<div v-else class="item-panel undo-disabled"></div>
<!--关闭与确认进行单独处理-->
<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>
⚠️Вышеприведенный код показывает только часть кода компонента, перейдите к полному коду:screen-short.vue,screen-short.scss
Обработка стилей щелчка записи Snipping Tool
Каждая запись на панели инструментов скриншота имеет три состояния: нормальное состояние, перемещение мыши и щелчок.Мой подход здесь заключается в том, чтобы записать все состояния в css и отображать разные стили с помощью разных имен классов.
CSS некоторых состояний щелчка панели инструментов выглядит следующим образом:
.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");
}
Сначала я хотел определить переменную при рендеринге v-for, изменить состояние этой переменной при щелчке и отобразить стиль щелчка, соответствующий каждому элементу щелчка, но обнаружил проблему, когда делал это. Когда я щелкнул Имя класса является динамическим, и я не отправил его через эту форму, у меня не было другого выбора, кроме как выбрать форму операции dom для ее достижения, и оно было передано, когда я щелкнул.$event
К функции получите класс текущего элемента щелчка при нажатии, определите, имеет ли он выбранный класс, удалите его, если он есть, а затем добавьте класс к текущему элементу щелчка.
Код реализации выглядит следующим образом:
- дом структура
<div
v-for="item in toolbar"
:key="item.id"
:class="`item-panel ${item.title} `"
@click="toolClickEvent(item.title, item.id, $event)"
></div>
- Событие нажатия панели инструментов
/**
* 裁剪框工具栏点击事件
* @param toolName
* @param index
* @param mouseEvent
*/
public toolClickEvent = (
toolName: string,
index: number,
mouseEvent: MouseEvent
) => {
// 为当前点击项添加选中时的class名
setSelectedClassName(mouseEvent, index, false);
}
- Добавить выбранный класс к элементу, по которому в данный момент щелкнули, и удалить выбранный класс его родственного элемента.
import { getSelectedClassName } from "@/module/common-methords/GetSelectedCalssName";
import { getBrushSelectedName } from "@/module/common-methords/GetBrushSelectedName";
/**
* 为当前点击项添加选中时的class,移除其兄弟元素选中时的class
* @param mouseEvent 需要进行操作的元素
* @param index 当前点击项
* @param isOption 是否为画笔选项
*/
export function setSelectedClassName(
mouseEvent: any,
index: number,
isOption: boolean
) {
// 获取当前点击项选中时的class名
let className = getSelectedClassName(index);
if (isOption) {
// 获取画笔选项选中时的对应的class
className = getBrushSelectedName(index);
}
// 获取div下的所有子元素
const nodes = mouseEvent.path[1].children;
for (let i = 0; i < nodes.length; i++) {
const item = nodes[i];
// 如果工具栏中已经有选中的class则将其移除
if (item.className.includes("active")) {
item.classList.remove(item.classList[2]);
}
}
// 给当前点击项添加选中时的class
mouseEvent.target.className += " " + className;
}
- Получить имя класса при нажатии на панель инструментов скриншота
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;
}
- Получить имя класса, когда кисть выбрана и нажата
/**
* 获取画笔选项对应的选中时的class名
* @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;
}
Реализуйте каждую опцию на панели инструментов
Далее давайте рассмотрим конкретную реализацию каждой опции на панели инструментов.
Рисование каждой графики на панели инструментов должно быть завершено с помощью трех событий нажатия, перемещения и подъема мыши. Чтобы предотвратить повторное рисование графики при перемещении мыши, здесь мы используем «история запись», чтобы решить эту проблему. Давайте сначала посмотрим на сцену, когда она многократно рисуется, как показано ниже:
Далее давайте посмотрим, как использовать историю для решения этой проблемы.
- Во-первых, нам нужно определить переменную массива с именем
history
.
private history: Array<Record<string, any>> = [];
- Когда мышь поднята после рисования графики, сохраните текущее состояние холста в
history
.
/**
* 保存当前画布状态
* @private
*/
private addHistoy() {
if (
this.screenShortCanvas != null &&
this.screenShortController.value != null
) {
// 获取canvas画布与容器
const context = this.screenShortCanvas;
const controller = this.screenShortController.value;
if (this.history.length > this.maxUndoNum) {
// 删除最早的一条画布记录
this.history.unshift();
}
// 保存当前画布状态
this.history.push({
data: context.getImageData(0, 0, controller.width, controller.height)
});
// 启用撤销按钮
this.data.setUndoStatus(true);
}
}
- Когда мышка находится в подвижном состоянии, вынимаем
history
последняя запись в .
/**
* 显示最新的画布状态
* @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);
}
}
Вышеупомянутые функции могут быть выполнены в нужный момент, чтобы решить проблему повторного рисования графики.Далее давайте посмотрим на эффект рисования после решения, как показано ниже:
Реализовать рисование прямоугольника
В предыдущем анализе мы получили координаты начальной точки мыши и координаты движения мыши, мы можем использовать эти данные для расчета ширины и высоты области выделения кадра, как показано ниже.
// 获取鼠标起始点坐标
const { startX, startY } = this.drawGraphPosition;
// 获取当前鼠标坐标
const currentX = nonNegativeData(event.offsetX);
const currentY = nonNegativeData(event.offsetY);
// 裁剪框临时宽高
const tempWidth = currentX - startX;
const tempHeight = currentY - startY;
После того, как мы получим данные, мы можем нарисовать прямоугольник через rect API холста Код выглядит следующим образом:
/**
* 绘制矩形
* @param mouseX
* @param mouseY
* @param width
* @param height
* @param color 边框颜色
* @param borderWidth 边框大小
* @param context 需要进行绘制的canvas画布
* @param controller 需要进行操作的canvas容器
* @param imageController 图片canvas容器
*/
export function drawRectangle(
mouseX: number,
mouseY: number,
width: number,
height: number,
color: string,
borderWidth: number,
context: CanvasRenderingContext2D,
controller: HTMLCanvasElement,
imageController: HTMLCanvasElement
) {
context.save();
// 设置边框颜色
context.strokeStyle = color;
// 设置边框大小
context.lineWidth = borderWidth;
context.beginPath();
// 绘制矩形
context.rect(mouseX, mouseY, width, height);
context.stroke();
// 绘制结束
context.restore();
// 使用drawImage将图片绘制到蒙层下方
context.save();
context.globalCompositeOperation = "destination-over";
context.drawImage(
imageController,
0,
0,
controller?.width,
controller?.height
);
// 绘制结束
context.restore();
}
Реализовать рисование эллипса
При рисовании эллипса нам нужно вычислить радиус и координаты центра круга в соответствии с информацией о координатах, а затем вызвать функцию эллипса для рисования эллипса.Код выглядит следующим образом:
/**
* 绘制圆形
* @param context 需要进行绘制的画布
* @param mouseX 当前鼠标x轴坐标
* @param mouseY 当前鼠标y轴坐标
* @param mouseStartX 鼠标按下时的x轴坐标
* @param mouseStartY 鼠标按下时的y轴坐标
* @param borderWidth 边框宽度
* @param color 边框颜色
*/
export function drawCircle(
context: CanvasRenderingContext2D,
mouseX: number,
mouseY: number,
mouseStartX: number,
mouseStartY: number,
borderWidth: number,
color: string
) {
// 坐标边界处理,解决反向绘制椭圆时的报错问题
const startX = mouseX < mouseStartX ? mouseX : mouseStartX;
const startY = mouseY < mouseStartY ? mouseY : mouseStartY;
const endX = mouseX >= mouseStartX ? mouseX : mouseStartX;
const endY = mouseY >= mouseStartY ? mouseY : mouseStartY;
// 计算圆的半径
const radiusX = (endX - startX) * 0.5;
const radiusY = (endY - startY) * 0.5;
// 计算圆心的x、y坐标
const centerX = startX + radiusX;
const centerY = startY + radiusY;
// 开始绘制
context.save();
context.beginPath();
context.lineWidth = borderWidth;
context.strokeStyle = color;
if (typeof context.ellipse === "function") {
// 绘制圆,旋转角度与起始角度都为0,结束角度为2*PI
context.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
} else {
throw "你的浏览器不支持ellipse,无法绘制椭圆";
}
context.stroke();
context.closePath();
// 结束绘制
context.restore();
}
⚠️Комментарии написаны очень понятно.Здесь использованы следующие API:beginPath,lineWidth,ellipse,closePath, разработчиков, не знакомых с этими API, перейдите в указанное место для ознакомления.
Реализовать стрелку
По сравнению с другими инструментами рисование стрелок является наиболее сложным, потому что нам нужно вычислить координаты двух точек стрелки через тригонометрическую функцию, а угол стрелки через функцию арктангенса в тригонометрической функции.
Так как нам нужно использовать тригонометрические функции для реализации, давайте сначала посмотрим на наши известные условия:
/**
* 已知:
* 1. P1、P2的坐标
* 2. 箭头斜线P3到P2直线的长度,P4与P3是对称的,因此P4到P2的长度等于P3到P2的长度
* 3. 箭头斜线P3到P1、P2直线的夹角角度(θ),因为是对称的,所以P4与P1、P2直线的夹角角度是相等的
* 求:
* P3、P4的坐标
*/
Как показано на рисунке выше, P1 — это координаты, когда мышь нажата, P2 — это координаты, когда мышь движется, а угол прилежащего угла θ равен 30. Зная эту информацию, мы можем найти координаты P3. и P4.Найдя координаты, мы можем рисовать стрелки через moveTo и lineTo холста.
Код реализации выглядит следующим образом:
/**
* 绘制箭头
* @param context 需要进行绘制的画布
* @param mouseStartX 鼠标按下时的x轴坐标 P1
* @param mouseStartY 鼠标按下式的y轴坐标 P1
* @param mouseX 当前鼠标x轴坐标 P2
* @param mouseY 当前鼠标y轴坐标 P2
* @param theta 箭头斜线与直线的夹角角度 (θ) P3 ---> (P1、P2) || P4 ---> P1(P1、P2)
* @param headlen 箭头斜线的长度 P3 ---> P2 || P4 ---> P2
* @param borderWidth 边框宽度
* @param color 边框颜色
*/
export function drawLineArrow(
context: CanvasRenderingContext2D,
mouseStartX: number,
mouseStartY: number,
mouseX: number,
mouseY: number,
theta: number,
headlen: number,
borderWidth: number,
color: string
) {
/**
* 已知:
* 1. P1、P2的坐标
* 2. 箭头斜线(P3 || P4) ---> P2直线的长度
* 3. 箭头斜线(P3 || P4) ---> (P1、P2)直线的夹角角度(θ)
* 求:
* P3、P4的坐标
*/
const angle =
(Math.atan2(mouseStartY - mouseY, mouseStartX - mouseX) * 180) / Math.PI, // 通过atan2来获取箭头的角度
angle1 = ((angle + theta) * Math.PI) / 180, // P3点的角度
angle2 = ((angle - theta) * Math.PI) / 180, // P4点的角度
topX = headlen * Math.cos(angle1), // P3点的x轴坐标
topY = headlen * Math.sin(angle1), // P3点的y轴坐标
botX = headlen * Math.cos(angle2), // P4点的X轴坐标
botY = headlen * Math.sin(angle2); // P4点的Y轴坐标
// 开始绘制
context.save();
context.beginPath();
// P3的坐标位置
let arrowX = mouseStartX - topX,
arrowY = mouseStartY - topY;
// 移动笔触到P3坐标
context.moveTo(arrowX, arrowY);
// 移动笔触到P1
context.moveTo(mouseStartX, mouseStartY);
// 绘制P1到P2的直线
context.lineTo(mouseX, mouseY);
// 计算P3的位置
arrowX = mouseX + topX;
arrowY = mouseY + topY;
// 移动笔触到P3坐标
context.moveTo(arrowX, arrowY);
// 绘制P2到P3的斜线
context.lineTo(mouseX, mouseY);
// 计算P4的位置
arrowX = mouseX + botX;
arrowY = mouseY + botY;
// 绘制P2到P4的斜线
context.lineTo(arrowX, arrowY);
// 上色
context.strokeStyle = color;
context.lineWidth = borderWidth;
// 填充
context.stroke();
// 结束绘制
context.restore();
}
⚠️Здесь используются новые API:moveTo,lineTo, разработчиков, которые не знакомы с этими API, перейдите в указанное место для ознакомления.
Реализовать рисование кистью
Нам нужно использовать lineTo для рисования кисти, но нам нужно обратить внимание при рисовании: нам нужно очистить путь через beginPath при нажатии мыши, и переместить мазок кисти в положение, когда мышь нажата, иначе начальный позиция мыши всегда 0, баг следующий:
Итак, чтобы решить эту ошибку, вам нужно инициализировать положение штриха при нажатии мыши.Код выглядит следующим образом:
/**
* 画笔初始化
*/
export function initPencli(
context: CanvasRenderingContext2D,
mouseX: number,
mouseY: number
) {
// 开始||清空一条路径
context.beginPath();
// 移动画笔位置
context.moveTo(mouseX, mouseY);
}
Затем вы можете рисовать линии в соответствии с информацией о координатах в позиции мыши.Код выглядит следующим образом:
/**
* 画笔绘制
* @param context
* @param mouseX
* @param mouseY
* @param size
* @param color
*/
export function drawPencli(
context: CanvasRenderingContext2D,
mouseX: number,
mouseY: number,
size: number,
color: string
) {
// 开始绘制
context.save();
// 设置边框大小
context.lineWidth = size;
// 设置边框颜色
context.strokeStyle = color;
context.lineTo(mouseX, mouseY);
context.stroke();
// 绘制结束
context.restore();
}
Реализовать мозаичный рисунок
Все мы знаем, что изображения состоят из пикселей, и когда мы устанавливаем пиксели определенной области одного цвета, информация в этой области будет уничтожена, а область, которую мы разрушаем, называется мозаикой.
Зная принцип мозаики, мы можем проанализировать идею реализации:
- Получить информацию об изображении мыши над областью пути
- Нарисуйте пиксели в области с похожими цветами вокруг них
Конкретный код реализации выглядит следующим образом:
/**
* 获取图像指定坐标位置的颜色
* @param imgData 需要进行操作的图片
* @param x x点坐标
* @param y y点坐标
*/
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;
};
/**
* 设置图像指定坐标位置的颜色
* @param imgData 需要进行操作的图片
* @param x x点坐标
* @param y y点坐标
* @param color 颜色数组
*/
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];
};
/**
* 绘制马赛克
* 实现思路:
* 1. 获取鼠标划过路径区域的图像信息
* 2. 将区域内的像素点绘制成周围相近的颜色
* @param mouseX 当前鼠标X轴坐标
* @param mouseY 当前鼠标Y轴坐标
* @param size 马赛克画笔大小
* @param degreeOfBlur 马赛克模糊度
* @param context 需要进行绘制的画布
*/
export function drawMosaic(
mouseX: number,
mouseY: number,
size: number,
degreeOfBlur: number,
context: CanvasRenderingContext2D
) {
// 获取鼠标经过区域的图片像素信息
const imgData = context.getImageData(mouseX, mouseY, size, size);
// 获取图像宽高
const w = imgData.width;
const h = imgData.height;
// 等分图像宽高
const stepW = w / degreeOfBlur;
const stepH = h / degreeOfBlur;
// 循环画布像素点
for (let i = 0; i < stepH; i++) {
for (let j = 0; j < stepW; j++) {
// 随机获取一个小方格的随机颜色
const color = getAxisColor(
imgData,
j * degreeOfBlur + Math.floor(Math.random() * degreeOfBlur),
i * degreeOfBlur + Math.floor(Math.random() * degreeOfBlur)
);
// 循环小方格的像素点
for (let k = 0; k < degreeOfBlur; k++) {
for (let l = 0; l < degreeOfBlur; l++) {
// 设置小方格的颜色
setAxisColor(
imgData,
j * degreeOfBlur + l,
i * degreeOfBlur + k,
color
);
}
}
}
}
// 渲染打上马赛克后的图像信息
context.putImageData(imgData, mouseX, mouseY);
}
Реализовать рисование текста
Canvas не предоставляет API для ввода текста напрямую, но предоставляет API для заполнения текста, поэтому нам нужен элемент div, чтобы позволить пользователю вводить текст.После завершения пользовательского ввода введенный текст может быть заполнен в указанная область.
Достигаемый эффект следующий:
- Создайте div в компоненте, включите редактируемые свойства div и разместите стиль
<template>
<teleport to="body">
<!--文本输入区域-->
<div
id="textInputPanel"
ref="textInputController"
v-show="textStatus"
contenteditable="true"
spellcheck="false"
></div>
</teleport>
</template>
- При нажатии мыши вычислить положение области ввода текста
// 计算文本框显示位置
const textMouseX = mouseX - 15;
const textMouseY = mouseY - 15;
// 修改文本区域位置
this.textInputController.value.style.left = textMouseX + "px";
this.textInputController.value.style.top = textMouseY + "px";
- Когда положение поля ввода изменяется, это означает, что пользовательский ввод завершен, и содержимое, введенное пользователем, отображается на холсте.Код для рисования текста выглядит следующим образом
/**
* 绘制文本
* @param text 需要进行绘制的文字
* @param mouseX 绘制位置的X轴坐标
* @param mouseY 绘制位置的Y轴坐标
* @param color 字体颜色
* @param fontSize 字体大小
* @param context 需要你行绘制的画布
*/
export function drawText(
text: string,
mouseX: number,
mouseY: number,
color: string,
fontSize: number,
context: CanvasRenderingContext2D
) {
// 开始绘制
context.save();
context.lineWidth = 1;
// 设置字体颜色
context.fillStyle = color;
context.textBaseline = "middle";
context.font = `bold ${fontSize}px 微软雅黑`;
context.fillText(text, mouseX, mouseY);
// 结束绘制
context.restore();
}
Реализовать функцию загрузки
Функция загрузки относительно проста. Нам нужно только поместить содержимое области кадрирования в новый холст, а затем вызвать метод toDataURL, чтобы получить адрес изображения base64. Мы создаем тег, добавляем атрибут загрузки, и нажмите на тег a. События могут быть загружены.
Код реализации выглядит следующим образом:
export function saveCanvasToImage(
context: CanvasRenderingContext2D,
startX: number,
startY: number,
width: number,
height: number
) {
// 获取裁剪框区域图片信息
const img = context.getImageData(startX, startY, width, height);
// 创建canvas标签,用于存放裁剪区域的图片
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
// 获取裁剪框区域画布
const imgContext = canvas.getContext("2d");
if (imgContext) {
// 将图片放进裁剪框内
imgContext.putImageData(img, 0, 0);
const a = document.createElement("a");
// 获取图片
a.href = canvas.toDataURL("png");
// 下载图片
a.download = `${new Date().getTime()}.png`;
a.click();
}
}
Реализовать функцию отмены
Поскольку мы используем режим записи истории для рисования графики, состояние холста будет сохраняться каждый раз, когда графика рисуется.Нам нужно только всплывать последняя запись из истории, когда мы нажимаем кнопку отмены.
Код реализации выглядит следующим образом:
/**
* 取出一条历史记录
*/
private takeOutHistory() {
const lastImageData = this.history.pop();
if (this.screenShortCanvas != null && lastImageData) {
const context = this.screenShortCanvas;
if (this.undoClickNum == 0 && this.history.length > 0) {
// 首次取出需要取两条历史记录
const firstPopImageData = this.history.pop() as Record<string, any>;
context.putImageData(firstPopImageData["data"], 0, 0);
} else {
context.putImageData(lastImageData["data"], 0, 0);
}
}
this.undoClickNum++;
// 历史记录已取完,禁用撤回按钮点击
if (this.history.length <= 0) {
this.undoClickNum = 0;
this.data.setUndoStatus(false);
}
}
Реализовать функцию отключения
Функция закрытия относится к сбросу компонента скриншота, поэтому нам нужно передать уничтоженное сообщение родительскому компоненту через emit.
Код реализации выглядит следующим образом:
/**
* 重置组件
*/
private resetComponent = () => {
if (this.emit) {
// 隐藏截图工具栏
this.data.setToolStatus(false);
// 初始化响应式变量
this.data.setInitStatus(true);
// 销毁组件
this.emit("destroy-component", false);
return;
}
throw "组件重置失败";
};
Реализовать функцию подтверждения
Когда пользователь нажимает «Подтвердить», нам нужно преобразовать содержимое в поле кадрирования в base64, затем отправить платежный компонент через эммит и, наконец, сбросить компонент.
Код реализации выглядит следующим образом:
const base64 = this.getCanvasImgData(false);
this.emit("get-image-data", base64);
Адрес плагина
На данный момент процесс реализации плагина был разделен.
-
Адрес подключаемого модуля в Интернете:chat-system
-
Адрес репозитория плагина на GitHub:screen-shot
-
Адрес открытого проекта:chat-system-github
напиши в конце
- Если в статье есть ошибки, исправьте их в комментариях, если статья вам поможет, ставьте лайк и подписывайтесь 😊
- Эта статья была впервые опубликована на Наггетс, перепечатка без разрешения запрещена 💌