whiteboard/src/Elements.js

483 lines
12 KiB
JavaScript
Raw Normal View History

2024-06-27 16:48:09 +08:00
import Rectangle from './elements/Rectangle'
import Circle from './elements/Circle'
import Diamond from './elements/Diamond'
import Triangle from './elements/Triangle'
import Freedraw from './elements/Freedraw'
import Arrow from './elements/Arrow'
import Image from './elements/Image'
import Line from './elements/Line'
import Text from './elements/Text'
import {
getTowPointDistance,
throttle,
computedLineWidthBySpeed,
createImageObj
} from './utils'
import { DRAG_ELEMENT_PARTS } from './constants'
// 元素管理类
export default class Elements {
constructor(app) {
this.app = app
// 所有元素
this.elementList = []
// 当前激活元素
this.activeElement = null
// 当前正在创建新元素
this.isCreatingElement = false
// 当前正在调整元素
this.isResizing = false
// 当前正在调整的元素
this.resizingElement = null
// 稍微缓解一下卡顿
this.handleResize = throttle(this.handleResize, this, 16)
}
// 序列化当前画布上的所有元素
serialize(stringify = false) {
let data = this.elementList.map(element => {
return element.serialize()
})
return stringify ? JSON.stringify(data) : data
}
// 获取当前画布上的元素数量
getElementsNum() {
return this.elementList.length
}
// 当前画布上是否有元素
hasElements() {
return this.elementList.length > 0
}
// 添加元素
addElement(element) {
this.elementList.push(element)
return this
}
// 向前添加元素
unshiftElement(element) {
this.elementList.unshift(element)
return this
}
// 添加元素到指定位置
insertElement(element, index) {
this.elementList.splice(index, 0, element)
}
// 删除元素
deleteElement(element) {
let index = this.getElementIndex(element)
if (index !== -1) {
this.elementList.splice(index, 1)
if (element.isActive) {
this.cancelActiveElement(element)
}
}
return this
}
// 删除全部元素
deleteAllElements() {
this.activeElement = null
if(this.app.isMobile){
this.elementList = this.elementList.filter(item => item.style.elReadonly)
}
else{
this.elementList = []
}
this.isCreatingElement = false
this.isResizing = false
this.resizingElement = null
return this
}
// 获取元素在元素列表里的索引
getElementIndex(element) {
return this.elementList.findIndex(item => {
return item === element
})
}
// 根据元素数据创建元素
createElementsFromData(data) {
data.forEach(item => {
let element = this.pureCreateElement(item)
element.isActive = false
element.isCreating = false
this.addElement(element)
})
this.app.group.initIdToElementList(this.elementList)
return this
}
// 是否存在激活元素
hasActiveElement() {
return !!this.activeElement
}
// 设置激活元素
setActiveElement(element) {
this.cancelActiveElement()
this.activeElement = element
if (element) {
element.isActive = true
}
this.app.emit('activeElementChange', this.activeElement)
return this
}
// 取消当前激活元素
cancelActiveElement() {
if (!this.hasActiveElement()) {
return this
}
this.activeElement.isActive = false
this.activeElement = null
this.app.emit('activeElementChange', this.activeElement)
return this
}
// 检测是否点击选中元素
checkIsHitElement(e) {
// 判断是否选中元素
let x = e.unGridClientX
let y = e.unGridClientY
// 从后往前遍历元素,默认认为新创建的元素在上一层
for (let i = this.elementList.length - 1; i >= 0; i--) {
let element = this.elementList[i]
if (element.isHit(x, y)) {
return element
}
}
return null
}
// 纯创建元素
pureCreateElement(opts = {}) {
switch (opts.type) {
case 'rectangle':
return new Rectangle(opts, this.app)
case 'diamond':
return new Diamond(opts, this.app)
case 'triangle':
return new Triangle(opts, this.app)
case 'circle':
return new Circle(opts, this.app)
case 'freedraw':
return new Freedraw(opts, this.app)
case 'image':
return new Image(opts, this.app)
case 'arrow':
return new Arrow(opts, this.app)
case 'line':
return new Line(opts, this.app)
case 'text':
return new Text(opts, this.app)
default:
return null
}
}
// 创建元素
createElement(opts = {}, callback = () => {}, ctx = null, notActive) {
if (this.hasActiveElement() || this.isCreatingElement) {
return this
}
let element = this.pureCreateElement(opts)
if (!element) {
return this
}
this.addElement(element)
if (!notActive) {
this.setActiveElement(element)
}
this.isCreatingElement = true
callback.call(ctx, element)
return this
}
// 复制元素
copyElement(element, notActive = false, pos) {
return new Promise(async resolve => {
if (!element) {
return resolve()
}
let data = this.app.group.handleCopyElementData(element.serialize())
// 图片元素需要先加载图片
if (data.type === 'image') {
data.imageObj = await createImageObj(data.url)
}
this.createElement(
data,
element => {
this.app.group.handleCopyElement(element)
element.startResize(DRAG_ELEMENT_PARTS.BODY)
// 默认偏移原图形20像素
let ox = 20
let oy = 20
// 指定了具体坐标则使用具体坐标
if (pos) {
ox = pos.x - element.x - element.width / 2
oy = pos.y - element.y - element.height / 2
}
// 如果开启了网格,那么要坐标要吸附到网格
let gridAdsorbentPos = this.app.coordinate.gridAdsorbent(ox, oy)
element.resize(
null,
null,
null,
gridAdsorbentPos.x,
gridAdsorbentPos.y
)
element.isCreating = false
if (notActive) {
element.isActive = false
}
this.isCreatingElement = false
resolve(element)
},
this,
notActive
)
})
}
// 正在创建类矩形元素
creatingRectangleLikeElement(type, x, y, offsetX, offsetY) {
this.createElement({
type,
x: x,
y: y,
width: offsetX,
height: offsetY
})
this.activeElement.updateSize(offsetX, offsetY)
}
// 正在创建圆形元素
creatingCircle(x, y, e) {
this.createElement({
type: 'circle',
x: x,
y: y
})
let radius = getTowPointDistance(e.clientX, e.clientY, x, y)
this.activeElement.updateSize(radius, radius)
}
// 正在创建自由画笔元素
creatingFreedraw(e, event) {
this.createElement({
type: 'freedraw'
})
let element = this.activeElement
// 计算画笔粗细
let lineWidth = computedLineWidthBySpeed(
event.mouseSpeed,
element.lastLineWidth
)
element.lastLineWidth = lineWidth
element.addPoint(e.clientX, e.clientY, lineWidth)
// 绘制自由线不重绘,采用增量绘制,否则会卡顿
let { coordinate, ctx, state } = this.app
// 事件对象的坐标默认是加上了画布偏移量的,临时绘制的时候不需要,所以需要减去
let tfp = coordinate.transformToCanvasCoordinate(
coordinate.subScrollX(event.lastMousePos.x),
coordinate.subScrollY(event.lastMousePos.y)
)
let ttp = coordinate.transformToCanvasCoordinate(
coordinate.subScrollX(e.clientX),
coordinate.subScrollY(e.clientY)
)
ctx.save()
ctx.scale(state.scale, state.scale)
element.singleRender(tfp.x, tfp.y, ttp.x, ttp.y, lineWidth)
ctx.restore()
}
// 正在创建图片元素
creatingImage(e, { width, height, imageObj, url, ratio }) {
// 吸附到网格,如果网格开启的话
let gp = this.app.coordinate.gridAdsorbent(
e.unGridClientX - width / 2,
e.unGridClientY - height / 2
)
this.createElement({
type: 'image',
x: gp.x,
y: gp.y,
url: url,
imageObj: imageObj,
width: width,
height: height,
ratio: ratio
})
}
// 正在编辑文本元素
editingText(element) {
if (element.type !== 'text') {
return
}
element.noRender = true
this.setActiveElement(element)
}
// 完成文本元素的编辑
completeEditingText() {
let element = this.activeElement
if (!element || element.type !== 'text') {
return
}
if (!element.text.trim()) {
// 没有输入则删除该文字元素
this.deleteElement(element)
this.setActiveElement(null)
return
}
element.noRender = false
}
// 完成箭头元素的创建
completeCreateArrow(e) {
this.activeElement.addPoint(e.clientX, e.clientY)
}
// 正在创建箭头元素
creatingArrow(x, y, e) {
this.createElement(
{
type: 'arrow',
x,
y
},
element => {
element.addPoint(x, y)
}
)
this.activeElement.updateFictitiousPoint(e.clientX, e.clientY)
}
// 正在创建线段/折线元素
creatingLine(x, y, e, isSingle = false, notCreate = false) {
if (!notCreate) {
this.createElement(
{
type: 'line',
x,
y,
isSingle
},
element => {
element.addPoint(x, y)
}
)
}
let element = this.activeElement
if (element) {
element.updateFictitiousPoint(e.clientX, e.clientY)
}
}
// 完成线段/折线元素的创建
completeCreateLine(e, completeCallback = () => {}) {
let element = this.activeElement
let x = e.clientX
let y = e.clientY
if (element && element.isSingle) {
// 单根线段模式,鼠标松开则代表绘制完成
element.addPoint(x, y)
completeCallback()
} else {
// 绘制折线模式,鼠标松开代表固定一个端点
this.createElement({
type: 'line',
isSingle: false
})
element = this.activeElement
element.addPoint(x, y)
element.updateFictitiousPoint(x, y)
}
}
// 创建元素完成
completeCreateElement() {
this.isCreatingElement = false
let element = this.activeElement
if (!element) {
return this
}
// 由多个端点构成的元素需要根据端点计算外包围框
if (['freedraw', 'arrow', 'line'].includes(element.type)) {
element.updateMultiPointBoundingRect()
}
element.isCreating = false
this.app.emitChange()
return this
}
// 为激活元素设置样式
setActiveElementStyle(style = {}) {
if (!this.hasActiveElement()) {
return this
}
Object.keys(style).forEach(key => {
this.activeElement.style[key] = style[key]
if (key === 'fontSize' && this.activeElement.type === 'text') {
this.activeElement.updateTextSize()
}
})
return this
}
// 检测指定位置是否在元素调整手柄上
checkInResizeHand(x, y) {
// 按住了拖拽元素的某个部分
let element = this.activeElement
let hand = element.dragElement.checkPointInDragElementWhere(x, y)
if (hand) {
return {
element,
hand
}
}
return null
}
// 检查是否需要进行元素调整操作
checkIsResize(x, y, e) {
if (!this.hasActiveElement()) {
return false
}
let res = this.checkInResizeHand(x, y)
if (res) {
this.isResizing = true
this.resizingElement = res.element
this.resizingElement.startResize(res.hand, e)
this.app.cursor.setResize(res.hand)
return true
}
return false
}
// 进行元素调整操作
handleResize(...args) {
if (!this.isResizing) {
return
}
this.resizingElement.resize(...args)
this.app.render.render()
}
// 结束元素调整操作
endResize() {
this.isResizing = false
this.resizingElement.endResize()
this.resizingElement = null
}
}