index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. import EventEmitter from 'eventemitter3'
  2. import {
  3. createCanvas,
  4. getTowPointDistance,
  5. throttle,
  6. createImageObj
  7. } from './utils'
  8. import * as utils from './utils'
  9. import * as checkHit from './utils/checkHit'
  10. import * as draw from './utils/draw'
  11. import Coordinate from './Coordinate'
  12. import Event from './Event'
  13. import Elements from './Elements'
  14. import ImageEdit from './ImageEdit'
  15. import Cursor from './Cursor'
  16. import TextEdit from './TextEdit'
  17. import History from './History'
  18. import Export from './Export'
  19. import Background from './Background'
  20. import Selection from './Selection'
  21. import Grid from './Grid'
  22. import Mode from './Mode'
  23. import KeyCommand from './KeyCommand'
  24. import Render from './Render'
  25. import elements from './elements'
  26. import Group from './Group'
  27. // 主类
  28. class TinyWhiteboard extends EventEmitter {
  29. constructor(opts = {}) {
  30. super()
  31. // 参数
  32. this.opts = opts
  33. // 容器元素
  34. this.container = opts.container
  35. // 当前绘制类型
  36. this.drawType = opts.drawType || 'selection'
  37. // 对容器做一些必要检查
  38. if (!this.container) {
  39. throw new Error('缺少 container 参数!')
  40. }
  41. if (
  42. !['absolute', 'fixed', 'relative'].includes(
  43. window.getComputedStyle(this.container).position
  44. )
  45. ) {
  46. throw new Error('container元素需要设置定位!')
  47. }
  48. // 容器宽高位置信息
  49. this.width = 0
  50. this.height = 0
  51. this.left = 0
  52. this.top = 0
  53. // 主要的渲染canvas元素
  54. this.canvas = null
  55. // canvas绘制上下文
  56. this.ctx = null
  57. // 画布状态
  58. this.state = {
  59. scale: 1, // 缩放
  60. scrollX: 0, // 水平方向的滚动偏移量
  61. scrollY: 0, // 垂直方向的滚动偏移量
  62. scrollStep: 50, // 滚动步长
  63. backgroundColor: '', // 背景颜色
  64. strokeStyle: '#000000', // 默认线条颜色
  65. fillStyle: 'transparent', // 默认填充颜色
  66. fontFamily: '微软雅黑, Microsoft YaHei', // 默认文字字体
  67. fontSize: 18, // 默认文字字号
  68. dragStrokeStyle: '#666', // 选中元素的拖拽元素的默认线条颜色
  69. showGrid: false, // 是否显示网格
  70. readonly: false, // 是否是只读模式
  71. gridConfig: {
  72. size: 20, // 网格大小
  73. strokeStyle: '#dfe0e1', // 网格线条颜色
  74. lineWidth: 1 // 网格线条宽度
  75. },
  76. ...(opts.state || {})
  77. }
  78. // 初始化画布
  79. this.initCanvas()
  80. // 坐标转换类
  81. this.coordinate = new Coordinate(this)
  82. // 事件类
  83. this.event = new Event(this)
  84. this.event.on('mousedown', this.onMousedown, this)
  85. this.event.on('mousemove', this.onMousemove, this)
  86. this.event.on('mouseup', this.onMouseup, this)
  87. this.event.on('dblclick', this.onDblclick, this)
  88. this.event.on('mousewheel', this.onMousewheel, this)
  89. this.event.on('contextmenu', this.onContextmenu, this)
  90. // 快捷键类
  91. this.keyCommand = new KeyCommand(this)
  92. // 图片选择类
  93. this.imageEdit = new ImageEdit(this)
  94. this.imageEdit.on('imageSelectChange', this.onImageSelectChange, this)
  95. // 文字编辑类
  96. this.textEdit = new TextEdit(this)
  97. this.textEdit.on('blur', this.onTextInputBlur, this)
  98. // 鼠标样式类
  99. this.cursor = new Cursor(this)
  100. // 历史记录管理类
  101. this.history = new History(this)
  102. // 导入导出类
  103. this.export = new Export(this)
  104. // 背景设置类
  105. this.background = new Background(this)
  106. // 多选类
  107. this.selection = new Selection(this)
  108. // 编组类
  109. this.group = new Group(this)
  110. // 网格类
  111. this.grid = new Grid(this)
  112. // 模式类
  113. this.mode = new Mode(this)
  114. // 元素管理类
  115. this.elements = new Elements(this)
  116. // 渲染类
  117. this.render = new Render(this)
  118. // 代理
  119. this.proxy()
  120. this.checkIsOnElement = throttle(this.checkIsOnElement, this)
  121. this.emitChange()
  122. this.helpUpdate()
  123. }
  124. // 代理各个类的方法到实例上
  125. proxy() {
  126. // history类
  127. ;['undo', 'redo'].forEach(method => {
  128. this[method] = this.history[method].bind(this.history)
  129. })
  130. // elements类
  131. ;[].forEach(method => {
  132. this[method] = this.elements[method].bind(this.elements)
  133. })
  134. // 渲染类
  135. ;[
  136. 'deleteElement',
  137. 'setActiveElementStyle',
  138. 'setCurrentElementsStyle',
  139. 'cancelActiveElement',
  140. 'deleteActiveElement',
  141. 'deleteCurrentElements',
  142. 'empty',
  143. 'zoomIn',
  144. 'zoomOut',
  145. 'setZoom',
  146. 'scrollTo',
  147. 'scrollToCenter',
  148. 'copyPasteCurrentElements',
  149. 'setBackgroundColor',
  150. 'copyElement',
  151. 'copyCurrentElement',
  152. 'cutCurrentElement',
  153. 'pasteCurrentElement',
  154. 'updateActiveElementRotate',
  155. 'updateActiveElementSize',
  156. 'updateActiveElementPosition',
  157. 'moveBottomCurrentElement',
  158. 'moveTopCurrentElement',
  159. 'moveUpCurrentElement',
  160. 'moveDownCurrentElement',
  161. 'selectAll',
  162. 'fit'
  163. ].forEach(method => {
  164. this[method] = this.render[method].bind(this.render)
  165. })
  166. // 导入导出类
  167. ;['exportImage', 'exportJson'].forEach(method => {
  168. this[method] = this.export[method].bind(this.export)
  169. })
  170. // 多选类
  171. ;['setSelectedElementStyle'].forEach(method => {
  172. this[method] = this.selection[method].bind(this.selection)
  173. })
  174. // 编组类
  175. ;['dogroup', 'ungroup'].forEach(method => {
  176. this[method] = this.group[method].bind(this.group)
  177. })
  178. // 网格类
  179. ;['showGrid', 'hideGrid', 'updateGrid'].forEach(method => {
  180. this[method] = this.grid[method].bind(this.grid)
  181. })
  182. // 模式类
  183. ;['setEditMode', 'setReadonlyMode'].forEach(method => {
  184. this[method] = this.mode[method].bind(this.mode)
  185. })
  186. }
  187. // 获取容器尺寸位置信息
  188. getContainerRectInfo() {
  189. let { width, height, left, top } = this.container.getBoundingClientRect()
  190. this.width = width
  191. this.height = height
  192. this.left = left
  193. this.top = top
  194. }
  195. // 必要的重新渲染
  196. helpUpdate() {
  197. // 设置背景
  198. this.background.set()
  199. // 设置网格
  200. if (this.state.showGrid) {
  201. this.grid.showGrid()
  202. }
  203. // 设置模式
  204. if (this.state.readonly) {
  205. this.setReadonlyMode()
  206. }
  207. }
  208. // 设置数据,包括状态数据及元素数据
  209. async setData({ state = {}, elements = [] }, noEmitChange) {
  210. this.state = state
  211. // 图片需要预加载
  212. for (let i = 0; i < elements.length; i++) {
  213. if (elements[i].type === 'image') {
  214. elements[i].imageObj = await createImageObj(elements[i].url)
  215. }
  216. }
  217. this.helpUpdate()
  218. this.elements.deleteAllElements().createElementsFromData(elements)
  219. this.render.render()
  220. if (!noEmitChange) {
  221. this.emitChange()
  222. }
  223. }
  224. // 初始化画布
  225. initCanvas() {
  226. this.getContainerRectInfo()
  227. // 删除旧的canvas元素
  228. if (this.canvas) {
  229. this.container.removeChild(this.canvas)
  230. }
  231. // 创建canvas元素
  232. let { canvas, ctx } = createCanvas(this.width, this.height, {
  233. className: 'main'
  234. })
  235. this.canvas = canvas
  236. this.ctx = ctx
  237. this.container.appendChild(this.canvas)
  238. }
  239. // 容器尺寸调整
  240. resize() {
  241. // 初始化canvas元素
  242. this.initCanvas()
  243. // 在新的画布上绘制元素
  244. this.render.render()
  245. // 多选画布重新初始化
  246. this.selection.init()
  247. // 网格画布重新初始化
  248. this.grid.init()
  249. // 重新判断是否渲染网格
  250. this.grid.renderGrid()
  251. }
  252. // 更新状态数据,只是更新状态数据,不会触发重新渲染,如有需要重新渲染或其他操作需要自行调用相关方法
  253. updateState(data = {}) {
  254. this.state = {
  255. ...this.state,
  256. ...data
  257. }
  258. this.emitChange()
  259. }
  260. // 更新当前绘制类型
  261. updateCurrentType(drawType) {
  262. this.drawType = drawType
  263. // 图形绘制类型
  264. if (this.drawType === 'image') {
  265. this.imageEdit.selectImage()
  266. }
  267. // 设置鼠标指针样式
  268. // 开启橡皮擦模式
  269. if (this.drawType === 'eraser') {
  270. this.cursor.setEraser()
  271. this.cancelActiveElement()
  272. } else if (this.drawType !== 'selection') {
  273. this.cursor.setCrosshair()
  274. } else {
  275. this.cursor.reset()
  276. }
  277. this.emit('currentTypeChange', this.drawType)
  278. }
  279. // 获取数据,包括状态数据及元素数据
  280. getData() {
  281. return {
  282. state: {
  283. ...this.state
  284. },
  285. elements: this.elements.serialize()
  286. }
  287. }
  288. // 图片选择事件
  289. onImageSelectChange() {
  290. this.cursor.hide()
  291. }
  292. // 鼠标按下事件
  293. onMousedown(e, event) {
  294. if (this.state.readonly || this.mode.isDragMode) {
  295. // 只读模式下即将进行整体拖动
  296. this.mode.onStart()
  297. return
  298. }
  299. if (!this.elements.isCreatingElement && !this.textEdit.isEditing) {
  300. // 是否击中了某个元素
  301. let hitElement = this.elements.checkIsHitElement(e)
  302. if (this.drawType === 'selection') {
  303. // 当前是选择模式
  304. // 当前存在激活元素
  305. if (this.elements.hasActiveElement()) {
  306. // 判断按下的位置是否是拖拽部位
  307. let isResizing = this.elements.checkIsResize(
  308. event.mousedownPos.unGridClientX,
  309. event.mousedownPos.unGridClientY,
  310. e
  311. )
  312. // 不在拖拽部位则将当前的激活元素替换成hitElement
  313. if (!isResizing) {
  314. this.elements.setActiveElement(hitElement)
  315. this.render.render()
  316. }
  317. } else {
  318. // 当前没有激活元素
  319. if (this.selection.hasSelection) {
  320. // 当前存在多选元素,则判断按下的位置是否是多选元素的拖拽部位
  321. let isResizing = this.selection.checkIsResize(
  322. event.mousedownPos.unGridClientX,
  323. event.mousedownPos.unGridClientY,
  324. e
  325. )
  326. // 不在拖拽部位则复位多选,并将当前的激活元素替换成hitElement
  327. if (!isResizing) {
  328. this.selection.reset()
  329. this.elements.setActiveElement(hitElement)
  330. this.render.render()
  331. }
  332. } else if (hitElement) {
  333. // 激活击中的元素
  334. if (hitElement.hasGroup()) {
  335. this.group.setSelection(hitElement)
  336. this.onMousedown(e, event)
  337. } else {
  338. this.elements.setActiveElement(hitElement)
  339. this.render.render()
  340. this.onMousedown(e, event)
  341. }
  342. } else {
  343. // 上述条件都不符合则进行多选创建选区操作
  344. this.selection.onMousedown(e, event)
  345. }
  346. }
  347. } else if (this.drawType === 'eraser') {
  348. // 当前有击中元素
  349. // 橡皮擦模式则删除该元素
  350. this.deleteElement(hitElement)
  351. }
  352. }
  353. }
  354. // 鼠标移动事件
  355. onMousemove(e, event) {
  356. if (this.state.readonly || this.mode.isDragMode) {
  357. if (event.isMousedown) {
  358. // 只读模式下进行整体拖动
  359. this.mode.onMove(e, event)
  360. }
  361. return
  362. }
  363. // 鼠标按下状态
  364. if (event.isMousedown) {
  365. let mx = event.mousedownPos.x
  366. let my = event.mousedownPos.y
  367. let offsetX = Math.max(event.mouseOffset.x, 0)
  368. let offsetY = Math.max(event.mouseOffset.y, 0)
  369. // 选中模式
  370. if (this.drawType === 'selection') {
  371. if (this.selection.isResizing) {
  372. // 多选调整元素中
  373. this.selection.handleResize(
  374. e,
  375. mx,
  376. my,
  377. event.mouseOffset.x,
  378. event.mouseOffset.y
  379. )
  380. } else if (this.selection.creatingSelection) {
  381. // 多选创建选区中
  382. this.selection.onMousemove(e, event)
  383. } else {
  384. // 检测是否是正常的激活元素的调整操作
  385. this.elements.handleResize(
  386. e,
  387. mx,
  388. my,
  389. event.mouseOffset.x,
  390. event.mouseOffset.y
  391. )
  392. }
  393. } else if (['rectangle', 'diamond', 'triangle'].includes(this.drawType)) {
  394. // 类矩形元素绘制模式
  395. this.elements.creatingRectangleLikeElement(
  396. this.drawType,
  397. mx,
  398. my,
  399. offsetX,
  400. offsetY
  401. )
  402. this.render.render()
  403. } else if (this.drawType === 'circle') {
  404. // 绘制圆形模式
  405. this.elements.creatingCircle(mx, my, e)
  406. this.render.render()
  407. } else if (this.drawType === 'freedraw') {
  408. // 自由画笔模式
  409. this.elements.creatingFreedraw(e, event)
  410. } else if (this.drawType === 'arrow') {
  411. this.elements.creatingArrow(mx, my, e)
  412. this.render.render()
  413. } else if (this.drawType === 'line') {
  414. if (getTowPointDistance(mx, my, e.clientX, e.clientY) > 3) {
  415. this.elements.creatingLine(mx, my, e, true)
  416. this.render.render()
  417. }
  418. }
  419. } else {
  420. // 鼠标没有按下状态
  421. // 图片放置中
  422. if (this.imageEdit.isReady) {
  423. this.cursor.hide()
  424. this.imageEdit.updatePreviewElPos(
  425. e.originEvent.clientX,
  426. e.originEvent.clientY
  427. )
  428. } else if (this.drawType === 'selection') {
  429. if (this.elements.hasActiveElement()) {
  430. // 当前存在激活元素
  431. // 检测是否划过激活元素的各个收缩手柄
  432. let handData = ''
  433. if (
  434. (handData = this.elements.checkInResizeHand(
  435. e.unGridClientX,
  436. e.unGridClientY
  437. ))
  438. ) {
  439. this.cursor.setResize(handData.hand)
  440. } else {
  441. this.checkIsOnElement(e)
  442. }
  443. } else if (this.selection.hasSelection) {
  444. // 多选中检测是否可进行调整元素
  445. let hand = this.selection.checkInResizeHand(
  446. e.unGridClientX,
  447. e.unGridClientY
  448. )
  449. if (hand) {
  450. this.cursor.setResize(hand)
  451. } else {
  452. this.checkIsOnElement(e)
  453. }
  454. } else {
  455. // 检测是否划过元素
  456. this.checkIsOnElement(e)
  457. }
  458. } else if (this.drawType === 'line') {
  459. // 线段绘制中
  460. this.elements.creatingLine(null, null, e, false, true)
  461. this.render.render()
  462. }
  463. }
  464. }
  465. // 检测是否滑过元素
  466. checkIsOnElement(e) {
  467. let hitElement = this.elements.checkIsHitElement(e)
  468. if (hitElement) {
  469. this.cursor.setMove()
  470. } else {
  471. this.cursor.reset()
  472. }
  473. }
  474. // 复位当前类型到选择模式
  475. resetCurrentType() {
  476. if (this.drawType !== 'selection') {
  477. this.drawType = 'selection'
  478. this.emit('currentTypeChange', 'selection')
  479. }
  480. }
  481. // 创建新元素完成
  482. completeCreateNewElement() {
  483. this.resetCurrentType()
  484. this.elements.completeCreateElement()
  485. this.render.render()
  486. }
  487. // 鼠标松开事件
  488. onMouseup(e) {
  489. if (this.state.readonly || this.mode.isDragMode) {
  490. return
  491. }
  492. if (this.drawType === 'text') {
  493. // 文字编辑模式
  494. if (!this.textEdit.isEditing) {
  495. this.createTextElement(e)
  496. this.resetCurrentType()
  497. }
  498. } else if (this.imageEdit.isReady) {
  499. // 图片放置模式
  500. this.elements.creatingImage(e, this.imageEdit.imageData)
  501. this.completeCreateNewElement()
  502. this.cursor.reset()
  503. this.imageEdit.reset()
  504. } else if (this.drawType === 'arrow') {
  505. // 箭头绘制模式
  506. this.elements.completeCreateArrow(e)
  507. this.completeCreateNewElement()
  508. } else if (this.drawType === 'line') {
  509. this.elements.completeCreateLine(e, () => {
  510. this.completeCreateNewElement()
  511. })
  512. this.render.render()
  513. } else if (this.elements.isCreatingElement) {
  514. // 正在创建元素中
  515. if (this.drawType === 'freedraw') {
  516. // 自由绘画模式可以连续绘制
  517. this.elements.completeCreateElement()
  518. this.elements.setActiveElement()
  519. } else {
  520. // 创建新元素完成
  521. this.completeCreateNewElement()
  522. }
  523. } else if (this.elements.isResizing) {
  524. // 调整元素操作结束
  525. this.elements.endResize()
  526. this.emitChange()
  527. } else if (this.selection.creatingSelection) {
  528. // 多选选区操作结束
  529. this.selection.onMouseup(e)
  530. } else if (this.selection.isResizing) {
  531. // 多选元素调整结束
  532. this.selection.endResize()
  533. this.emitChange()
  534. }
  535. }
  536. // 双击事件
  537. onDblclick(e) {
  538. if (this.drawType === 'line') {
  539. // 结束折线绘制
  540. this.completeCreateNewElement()
  541. } else {
  542. // 是否击中了某个元素
  543. let hitElement = this.elements.checkIsHitElement(e)
  544. if (hitElement) {
  545. // 编辑文字
  546. if (hitElement.type === 'text') {
  547. this.elements.editingText(hitElement)
  548. this.render.render()
  549. this.keyCommand.unBindEvent()
  550. this.textEdit.showTextEdit()
  551. }
  552. } else {
  553. // 双击空白处新增文字
  554. if (!this.textEdit.isEditing) {
  555. this.createTextElement(e)
  556. }
  557. }
  558. }
  559. }
  560. // 文本框失焦事件
  561. onTextInputBlur() {
  562. this.keyCommand.bindEvent()
  563. this.elements.completeEditingText()
  564. this.render.render()
  565. this.emitChange()
  566. }
  567. // 创建文本元素
  568. createTextElement(e) {
  569. this.elements.createElement({
  570. type: 'text',
  571. x: e.clientX,
  572. y: e.clientY
  573. })
  574. this.keyCommand.unBindEvent()
  575. this.textEdit.showTextEdit()
  576. }
  577. // 鼠标滚动事件
  578. onMousewheel(dir) {
  579. let stepNum = this.state.scrollStep / this.state.scale
  580. let step = dir === 'down' ? stepNum : -stepNum
  581. this.scrollTo(this.state.scrollX, this.state.scrollY + step)
  582. }
  583. // 右键菜单事件
  584. onContextmenu(e) {
  585. let elements = []
  586. if (this.elements.hasActiveElement()) {
  587. elements = [this.elements.activeElement]
  588. } else if (this.selection.hasSelectionElements()) {
  589. elements = this.selection.getSelectionElements()
  590. }
  591. this.emit('contextmenu', e.originEvent, elements)
  592. }
  593. // 触发更新事件
  594. emitChange() {
  595. let data = this.getData()
  596. this.history.add(data)
  597. this.emit('change', data)
  598. }
  599. }
  600. TinyWhiteboard.utils = utils
  601. TinyWhiteboard.checkHit = checkHit
  602. TinyWhiteboard.draw = draw
  603. TinyWhiteboard.elements = elements
  604. export default TinyWhiteboard