Эта статья правильная«Принципиальный анализ некоторых технических моментов библиотеки компонентов визуального перетаскивания»добавка. В предыдущей статье в основном объяснялись следующие функции:
- редактор
- пользовательский компонент
- тянуть
- Удалить компоненты, настроить уровни слоев
- приблизить
- отменить повторить
- Настройки свойств компонента
- адсорбция
- Предварительный просмотр и сохранение кода
- привязать событие
- Связать анимацию
- Импорт PSD
- режим телефона
Теперь эта статья добавит на этом основании 4 функциональных пункта, а именно:
- перетащите, чтобы повернуть
- копировать вставить вырезать
- Взаимодействие данных
- выпускать
Как и в предыдущем посте, я обновил код новой функции на github:
Дружеское напоминание: Рекомендуется читать вместе с исходным кодом, эффект лучше (это DEMO использует стек технологий Vue).
14. Перетащите, чтобы повернуть
На момент написания последней статьи оригинальный DEMO уже может поддерживать функцию поворота. Но эта функция вращения все еще имеет много недостатков:
- Перетаскивание для поворота не поддерживается.
- Неправильное увеличение и уменьшение масштаба после поворота.
- Неправильная автоматическая привязка после поворота.
- Неправильный курсор для восьми выдвижных точек после поворота.
В этом разделе мы рассмотрим эти четыре вопроса один за другим.
перетащите, чтобы повернуть
Перетащите и поверните нужно использоватьMath.atan2()функция.
Math.atan2() возвращает плоский угол (в радианах) между отрезком линии от начала координат (0,0) до точки (x,y) и положительным направлением оси x, то есть Math.atan2( у, х). И y, и x в Math.atan2(y,x) — это расстояния от точки (0,0).
Проще говоря, он принимает центральную точку компонента в качестве начала координат.(centerX,centerY)
, координаты, когда пользователь нажимает кнопку мыши, устанавливаются в(startX,startY)
, координаты движения мыши устанавливаются равными(curX,curY)
. Угол поворота может быть(startX,startY)
а также(curX,curY)
Рассчитано.
Итак, как мы получаем от точки(startX,startY)
к точке(curX,curY)
Как насчет угла поворота между?
первый шаг, координаты щелчка мыши устанавливаются равными(startX,startY)
:
const startY = e.clientY
const startX = e.clientX
второй шаг, вычислить центральную точку компонента:
// 获取组件中心点位置
const rect = this.$el.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
третий шаг, координаты при нажатии и перемещении мыши устанавливаются равными(curX,curY)
:
const curX = moveEvent.clientX
const curY = moveEvent.clientY
четвертый шаг, рассчитывается отдельно(startX,startY)
а также(curX,curY)
соответствующих углов, а затем вычесть их, чтобы получить угол поворота. Кроме того, следует отметить, чтоMath.atan2()
Возвращаемое значение метода — радианы, поэтому вам также необходимо преобразовать радианы в углы. Итак, полный код:
// 旋转前的角度
const rotateDegreeBefore = Math.atan2(startY - centerY, startX - centerX) / (Math.PI / 180)
// 旋转后的角度
const rotateDegreeAfter = Math.atan2(curY - centerY, curX - centerX) / (Math.PI / 180)
// 获取旋转的角度值, startRotate 为初始角度值
pos.rotate = startRotate + rotateDegreeAfter - rotateDegreeBefore
приблизить
Будет ошибка увеличения и уменьшения масштаба после поворота компонента.
Как видно из изображения выше, смещение происходит при увеличении и уменьшении масштаба. Кроме того, направление растяжения и направление, в котором мы перетаскиваем, неверны. Причина этой ошибки в том, что функция увеличения и уменьшения масштаба изначально была разработана без учета вращающейся сцены. Таким образом, независимо от того, на сколько углов повернуто изображение, увеличение и уменьшение масштаба все равно рассчитываются так, как если бы они не вращались.
Давайте рассмотрим конкретный пример:
Как видно из рисунка выше, когда нет вращения, зажмите вершину и перетащите ее вверх, просто используйтеy2 - y1
вы можете получить расстояние перетаскиванияs
. В это время добавьте исходную высоту компонента кs
Новая высота может быть получена, и компонентtop
,left
Обновление недвижимости.
Теперь поверните на 180 градусов, если мы перетащим вершину вниз в этой точке, мы ожидаем, что результатом будет увеличение высоты компонента. Но в это время метод расчета тот же, что и когда он не вращался, поэтому результат противоположен тому, что мы ожидали, и высота компонента станет меньше (если вы не понимаете этого явления, то можете себе представить изображение без поворота, нажмите и удерживайте перетаскивание вершины вниз).
Как решить эту проблему? Я получил это из проекта на githubsnapping-demoНашел решение: связать увеличение и уменьшение с углом поворота.
решение
Ниже прямоугольник, который был повернут на угол, допустим, теперь он растянут путем перетаскивания его левой верхней точки.
Теперь мы шаг за шагом разберем, как получить правильный размер и смещение растянутого компонента.
первый шаг, передать координаты компонента при нажатии мыши (независимо от того, на сколько градусов она повернута, компонентtop
left
свойства остаются неизменными) и размер для вычисления центральной точки компонента:
const center = {
x: style.left + style.width / 2,
y: style.top + style.height / 2,
}
второй шаг,использоватьтекущие координаты кликаРассчитывается от центральной точки компонентатекущие координаты кликаКоординаты точки симметрии:
// 获取画布位移信息
const editorRectInfo = document.querySelector('#editor').getBoundingClientRect()
// 当前点击坐标
const curPoint = {
x: e.clientX - editorRectInfo.left,
y: e.clientY - editorRectInfo.top,
}
// 获取对称点的坐标
const symmetricPoint = {
x: center.x - (curPoint.x - center.x),
y: center.y - (curPoint.y - center.y),
}
третий шаг, при нажатии на левый верхний угол компонента для растяжения центральная точка нового компонента вычисляется по текущим координатам мыши в реальном времени и точке симметрии:
const curPositon = {
x: moveEvent.clientX - editorRectInfo.left,
y: moveEvent.clientY - editorRectInfo.top,
}
const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
// 求两点之间的中点坐标
function getCenterPoint(p1, p2) {
return {
x: p1.x + ((p2.x - p1.x) / 2),
y: p1.y + ((p2.y - p1.y) / 2),
}
}
Поскольку компонент находится в повернутом состоянии, даже если вы знаетеxy
расстояние, и его нельзя рассчитать непосредственно на компоненте. В противном случае будут баги, а направление сдвига или приближения и отдаления будет неправильным. Поэтому нам нужно вычислить его без вращения компонента.
четвертый шаг, который можно рассчитать по известному углу поворота, новой центральной точке компонента и текущим координатам мыши в реальном времени.Текущие координаты мыши в реальном времени currentPosition
координаты без поворотаnewTopLeftPoint
. В то же время его также можно рассчитать на основе известного угла поворота, новой центральной точки компонента и точки симметрии.Симметрия компонентов sPoint
координаты без поворотаnewBottomRightPoint
.
Соответствующая формула расчета выглядит следующим образом:
/**
* 计算根据圆心旋转后的点的坐标
* @param {Object} point 旋转前的点坐标
* @param {Object} center 旋转中心
* @param {Number} rotate 旋转的角度
* @return {Object} 旋转后的坐标
* https://www.zhihu.com/question/67425734/answer/252724399 旋转矩阵公式
*/
export function calculateRotatedPointCoordinate(point, center, rotate) {
/**
* 旋转公式:
* 点a(x, y)
* 旋转中心c(x, y)
* 旋转后点n(x, y)
* 旋转角度θ tan ??
* nx = cosθ * (ax - cx) - sinθ * (ay - cy) + cx
* ny = sinθ * (ax - cx) + cosθ * (ay - cy) + cy
*/
return {
x: (point.x - center.x) * Math.cos(angleToRadian(rotate)) - (point.y - center.y) * Math.sin(angleToRadian(rotate)) + center.x,
y: (point.x - center.x) * Math.sin(angleToRadian(rotate)) + (point.y - center.y) * Math.cos(angleToRadian(rotate)) + center.y,
}
}
Приведенная выше формула предполагает знание матриц вращения в линейной алгебре, что слишком сложно для человека, не окончившего колледж. К счастью, я получил один от Zhihu.отвечатьПроцесс рассуждения этой формулы был найден в , и ниже приводится исходный текст ответа:
С помощью приведенных выше расчетных значений можно получить новое значение смещения компонента.top
left
и новый размер компонента. Соответствующий полный код выглядит следующим образом:
function calculateLeftTop(style, curPositon, pointInfo) {
const { symmetricPoint } = pointInfo
const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
const newTopLeftPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)
const newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
const newWidth = newBottomRightPoint.x - newTopLeftPoint.x
const newHeight = newBottomRightPoint.y - newTopLeftPoint.y
if (newWidth > 0 && newHeight > 0) {
style.width = Math.round(newWidth)
style.height = Math.round(newHeight)
style.left = Math.round(newTopLeftPoint.x)
style.top = Math.round(newTopLeftPoint.y)
}
}
Теперь давайте посмотрим на увеличение и уменьшение масштаба после поворота:
Автоматическая адсорбция
Автоматическая адсорбция основана на четырех свойствах компонентаtop
left
width
height
Рассчитано, после поворота компонента значения этих свойств не изменятся. Таким образом, независимо от того, на сколько градусов повернут компонент, адсорбция все равно рассчитывается, как если бы он не вращался. Тогда есть проблема, хотя на самом деле компонентtop
left
width
height
Свойства не изменились. Но внешний вид изменился. Вот два одинаковых компонента: один без поворота и один повернутый на 45 градусов.
Видно, что кнопка после поворотаheight
Атрибут отличается от высоты, которую мы видим по внешнему виду, поэтому в данном случае присутствует ошибка неправильной адсорбции.
решение
Как решить эту проблему? Нам нужно сравнить размер и смещение компонентов после вращения для сравнения адсорбции. То есть не сравнивайте фактические свойства компонентов, а сравнивайте размер и смещение, которые мы видим.
Как видно из приведенного выше рисунка, проекция длины повернутого компонента на ось X представляет собой сумму длин двух красных линий. Длину этих двух красных линий можно вычислить с помощью синуса и косинуса, красная линия слева вычисляется как синус, а красная линия справа рассчитывается как косинус:
const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)
Аналогично, высота одинакова:
const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)
Доступны новая ширина и высота, а затем в соответствии с исходным компонентомtop
left
свойство, вы можете получить новый компонент после поворота компонентаtop
left
Атрибуты. Полный код прикреплен ниже:
translateComponentStyle(style) {
style = { ...style }
if (style.rotate != 0) {
const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)
const diffX = (style.width - newWidth) / 2
style.left += diffX
style.right = style.left + newWidth
const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)
const diffY = (newHeight - style.height) / 2
style.top -= diffY
style.bottom = style.top + newHeight
style.width = newWidth
style.height = newHeight
} else {
style.bottom = style.top + style.height
style.right = style.left + style.width
}
return style
}
После ремонта адсорбция также может отображаться нормально.
курсор
Направление курсора и перетаскивания неправильное, потому что восьмиточечный курсор фиксирован и не меняется в зависимости от угла.
решение
из-за360 / 8 = 45
, поэтому каждому направлению может быть назначен диапазон в 45 градусов, по одному курсору на диапазон. При этом для каждого направления задается начальный угол, то есть угол, соответствующий каждому направлению компонента, когда он не вращается.
pointList: ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l'], // 八个方向
initialAngle: { // 每个点对应的初始角度
lt: 0,
t: 45,
rt: 90,
r: 135,
rb: 180,
b: 225,
lb: 270,
l: 315,
},
angleToCursor: [ // 每个范围的角度对应的光标
{ start: 338, end: 23, cursor: 'nw' },
{ start: 23, end: 68, cursor: 'n' },
{ start: 68, end: 113, cursor: 'ne' },
{ start: 113, end: 158, cursor: 'e' },
{ start: 158, end: 203, cursor: 'se' },
{ start: 203, end: 248, cursor: 's' },
{ start: 248, end: 293, cursor: 'sw' },
{ start: 293, end: 338, cursor: 'w' },
],
cursors: {},
Метод расчета также очень прост:
- Предположим теперь, что компонент был повернут на угол a.
- Пройдите восемь направлений и используйте начальный угол + a для каждого направления, чтобы получить текущий угол b.
- траверс
angleToCursor
массив, посмотрите, в каком диапазоне находится b, и верните соответствующий курсор.
После вышеуказанных трех шагов можно рассчитать правильное направление курсора после поворота компонента. Конкретный код выглядит следующим образом:
getCursor() {
const { angleToCursor, initialAngle, pointList, curComponent } = this
const rotate = (curComponent.style.rotate + 360) % 360 // 防止角度有负数,所以 + 360
const result = {}
let lastMatchIndex = -1 // 从上一个命中的角度的索引开始匹配下一个,降低时间复杂度
pointList.forEach(point => {
const angle = (initialAngle[point] + rotate) % 360
const len = angleToCursor.length
while (true) {
lastMatchIndex = (lastMatchIndex + 1) % len
const angleLimit = angleToCursor[lastMatchIndex]
if (angle < 23 || angle >= 338) {
result[point] = 'nw-resize'
return
}
if (angleLimit.start <= angle && angle < angleLimit.end) {
result[point] = angleLimit.cursor + '-resize'
return
}
}
})
return result
},
Как видно из анимации выше, курсоры в восьми направлениях теперь могут отображаться правильно.
15. Скопируйте и вставьте вырезание
По сравнению с функцией перетаскивания, копирование и вставка относительно просты.
const ctrlKey = 17, vKey = 86, cKey = 67, xKey = 88
let isCtrlDown = false
window.onkeydown = (e) => {
if (e.keyCode == ctrlKey) {
isCtrlDown = true
} else if (isCtrlDown && e.keyCode == cKey) {
this.$store.commit('copy')
} else if (isCtrlDown && e.keyCode == vKey) {
this.$store.commit('paste')
} else if (isCtrlDown && e.keyCode == xKey) {
this.$store.commit('cut')
}
}
window.onkeyup = (e) => {
if (e.keyCode == ctrlKey) {
isCtrlDown = false
}
}
Отслеживайте действия клавиш пользователя и запускайте соответствующие операции при нажатии определенной клавиши.
Операция копирования
использовать в vuexcopyData
для представления реплицированных данных. когда пользователь нажимаетctrl + c
, глубоко скопируйте текущие данные компонента вcopyData
.
copy(state) {
state.copyData = {
data: deepCopy(state.curComponent),
index: state.curComponentIndex,
}
},
При этом необходимо записать в данные компонента индекс текущего компонента, который используется при резке.
операция вставки
paste(state, isMouse) {
if (!state.copyData) {
toast('请选择组件')
return
}
const data = state.copyData.data
if (isMouse) {
data.style.top = state.menuTop
data.style.left = state.menuLeft
} else {
data.style.top += 10
data.style.left += 10
}
data.id = generateID()
store.commit('addComponent', { component: data })
store.commit('recordSnapshot')
state.copyData = null
},
При вставке, если это ключевая операцияctrl+v
. тогда компонентtop
left
Свойство увеличивается на 10, чтобы избежать перекрытия с исходным компонентом. Если операция вставки выполняется с помощью правой кнопки мыши, скопированный компонент размещается в том месте, где был сделан щелчок мышью.
операция вырезания
cut(state) {
if (!state.curComponent) {
toast('请选择组件')
return
}
if (state.copyData) {
store.commit('addComponent', { component: state.copyData.data, index: state.copyData.index })
if (state.curComponentIndex >= state.copyData.index) {
// 如果当前组件索引大于等于插入索引,需要加一,因为当前组件往后移了一位
state.curComponentIndex++
}
}
store.commit('copy')
store.commit('deleteComponent')
},
Операция вырезания по сути является копированием, но после выполнения копирования текущий компонент необходимо удалить. Во избежание того, что после выполнения пользователем операции вырезания операция вставки не выполняется, а вырезание продолжается. В это время необходимо восстановить исходные данные резки. Поэтому индекс, записанный в скопированных данных, работает, и исходные данные можно восстановить в исходное положение через индекс.
Щелчок правой кнопкой мыши
Действие правой клавиши такое же, как и действие клавиши, одна функция и два метода срабатывания.
<li @click="copy" v-show="curComponent">复制</li>
<li @click="paste">粘贴</li>
<li @click="cut" v-show="curComponent">剪切</li>
cut() {
this.$store.commit('cut')
},
copy() {
this.$store.commit('copy')
},
paste() {
this.$store.commit('paste', true)
},
16. Взаимодействие с данными
метод первый
Заранее напишите серию API-интерфейсов запросов ajax, выберите API по мере необходимости, щелкнув компонент, а затем заполните параметры после выбора API. Например, следующий компонент показывает, как использовать ajax-запросы для взаимодействия с фоном:
<template>
<div>{{ propValue.data }}</div>
</template>
<script>
export default {
// propValue: {
// api: {
// request: a,
// params,
// },
// data: null
// }
props: {
propValue: {
type: Object,
default: () => {},
},
},
created() {
this.propValue.api.request(this.propValue.api.params).then(res => {
this.propValue.data = res.data
})
},
}
</script>
Способ 2
Режим 2 подходит для чистых компонентов отображения, например, есть компонент сигнализации, который может отображать соответствующий цвет в соответствии с данными, отправленными из фона. При редактировании страницы вы можете запросить данные веб-сокета, которые страница может использовать из фона через ajax:
const data = ['status', 'text'...]
Затем добавьте разные свойства к разным компонентам. Например, есть компонент, его связанное свойство равноstatus
.
// 组件能接收的数据
props: {
propValue: {
type: String,
},
element: {
type: Object,
},
wsKey: {
type: String,
default: '',
},
},
передать компонентwsKey
Получите свойства этой привязки. После публикации или предварительного просмотра страницы запросите глобальные данные из фона через weboscket и поместите их на vuex. компоненты могут бытьwsKey
доступ к данным.
<template>
<div>{{ wsData[wsKey] }}</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
props: {
propValue: {
type: String,
},
element: {
type: Object,
},
wsKey: {
type: String,
default: '',
},
},
computed: mapState([
'wsData',
]),
</script>
Есть много способов взаимодействия с фоном, не только два вышеперечисленных, здесь я привожу некоторые идеи только для справки.
17. Сообщение
Есть два способа опубликовать страницу: один — визуализировать данные компонента как отдельную HTML-страницу, другой — извлечь минимальное время выполнения из этого проекта как отдельный проект.
Давайте поговорим о втором способе.Минимальное время выполнения в этом проекте - это фактически страница предварительного просмотра плюс пользовательские компоненты. Извлеките эти коды и упакуйте их отдельно как проект. При публикации страницы данные компонента отправляются на сервер в формате JSON, и для каждой страницы генерируется уникальный идентификатор.
Предположим, что теперь есть три страницы, и идентификаторы, сгенерированные страницей публикации, равны a, b и c. При посещении страницы нужно привести только ID, чтобы данные компонента, соответствующие каждой странице, можно было получить по ID.
www.test.com/?id=a
www.test.com/?id=c
www.test.com/?id=b
нагрузка по требованию
Например, если пользовательский компонент слишком велик, их десятки или даже сотни. На этом этапе вы можете использовать пользовательские компоненты сimport
Способ импорта, чтобы добиться загрузки по требованию, сократить время рендеринга первого экрана:
import Vue from 'vue'
const components = [
'Picture',
'VText',
'VButton',
]
components.forEach(key => {
Vue.component(key, () => import(`@/custom-component/${key}`))
})
Выпуск по версии
Пользовательские компоненты могут быть обновлены. Например, оригинальный компонент использовался более полугода, а теперь есть функциональные изменения, чтобы не затрагивать исходную страницу. При публикации рекомендуется указывать номер версии компонента:
- v-text
- v1.vue
- v2.vue
Напримерv-text
Существует две версии компонента, вы можете указать номер версии при его использовании в области списка компонентов слева:
{
component: 'v-text',
version: 'v1'
...
}
Таким образом, при импорте компонента его можно импортировать по номеру версии компонента:
import Vue from 'vue'
import componentList from '@/custom-component/component-list`
componentList.forEach(component => {
Vue.component(component.name, () => import(`@/custom-component/${component.name}/${component.version}`))
})
Визуальное перетаскивание серии статей:
- Принципиальный анализ некоторых технических моментов библиотеки компонентов визуального перетаскивания
- Принципиальный анализ некоторых технических моментов библиотеки компонентов визуального перетаскивания (2)
- Принципиальный анализ некоторых технических моментов библиотеки компонентов визуального перетаскивания (3)