WritingBoard.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <template>
  2. <div class="writing-board" ref="writingBoardRef">
  3. <canvas class="canvas" ref="canvasRef"
  4. @mousedown="$event => handleMousedown($event)"
  5. @mousemove="$event => handleMousemove($event)"
  6. @mouseup="handleMouseup()"
  7. @mouseleave="handleMouseup(); mouseInCanvas = false"
  8. @mouseenter="mouseInCanvas = true"
  9. ></canvas>
  10. <div
  11. class="pen"
  12. :style="{
  13. left: mouse.x - penSize / 2 + 'px',
  14. top: mouse.y - 36 + penSize / 2 + 'px',
  15. color: color,
  16. }"
  17. v-if="mouseInCanvas && model === 'pen'"
  18. ><IconWrite class="icon" size="36" /></div>
  19. <div
  20. class="eraser"
  21. :style="{
  22. left: mouse.x - rubberSize / 2 + 'px',
  23. top: mouse.y - rubberSize / 2 + 'px',
  24. width: rubberSize + 'px',
  25. height: rubberSize + 'px',
  26. }"
  27. v-if="mouseInCanvas && model === 'eraser'"
  28. ><IconClearFormat class="icon" :size="rubberSize * 0.6" /></div>
  29. </div>
  30. </template>
  31. <script lang="ts">
  32. import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
  33. const penSize = 6
  34. const rubberSize = 80
  35. export default defineComponent({
  36. name: 'writing-board',
  37. props: {
  38. color: {
  39. type: String,
  40. default: '#ffcc00',
  41. },
  42. model: {
  43. type: String as PropType<'pen' | 'eraser'>,
  44. default: 'pen',
  45. },
  46. },
  47. setup(props) {
  48. let ctx: CanvasRenderingContext2D | null = null
  49. const writingBoardRef = ref<HTMLElement>()
  50. const canvasRef = ref<HTMLCanvasElement>()
  51. let lastPos = {
  52. x: 0,
  53. y: 0,
  54. }
  55. let isMouseDown = false
  56. let lastTime = 0
  57. let lastLineWidth = -1
  58. const mouse = reactive({
  59. x: 0,
  60. y: 0,
  61. })
  62. const mouseInCanvas = ref(false)
  63. const initCanvas = () => {
  64. if(!canvasRef.value || !writingBoardRef.value) return
  65. ctx = canvasRef.value.getContext('2d')
  66. if(!ctx) return
  67. canvasRef.value.width = writingBoardRef.value.clientWidth
  68. canvasRef.value.height = writingBoardRef.value.clientHeight
  69. canvasRef.value.style.width = writingBoardRef.value.clientWidth + 'px'
  70. canvasRef.value.style.height = writingBoardRef.value.clientHeight + 'px'
  71. ctx.lineCap = 'round'
  72. ctx.lineJoin = 'round'
  73. }
  74. const getDistance = (posX: number, posY: number) => {
  75. const lastPosX = lastPos.x
  76. const lastPosY = lastPos.y
  77. return Math.sqrt((posX - lastPosX) * (posX - lastPosX) + (posY - lastPosY) * (posY - lastPosY))
  78. }
  79. const getLineWidth = (s: number, t: number) => {
  80. const maxV = 10
  81. const minV = 0.1
  82. const maxWidth = penSize
  83. const minWidth = 3
  84. const v = s / t
  85. let lineWidth
  86. if(v <= minV) lineWidth = maxWidth
  87. else if(v >= maxV) lineWidth = minWidth
  88. else lineWidth = maxWidth - v / maxV * maxWidth
  89. if(lastLineWidth === -1) return lineWidth
  90. return lineWidth * 1 / 3 + lastLineWidth * 2 / 3
  91. }
  92. // 画笔绘制方法
  93. const draw = (posX: number, posY: number, lineWidth: number) => {
  94. if(!ctx) return
  95. const lastPosX = lastPos.x
  96. const lastPosY = lastPos.y
  97. ctx.lineWidth = lineWidth
  98. ctx.strokeStyle = props.color
  99. ctx.beginPath()
  100. ctx.moveTo(lastPosX, lastPosY)
  101. ctx.lineTo(posX, posY)
  102. ctx.stroke()
  103. ctx.closePath()
  104. }
  105. // 橡皮擦除方法
  106. const erase = (posX: number, posY: number) => {
  107. if(!ctx || !canvasRef.value) return
  108. const lastPosX = lastPos.x
  109. const lastPosY = lastPos.y
  110. const radius = rubberSize / 2
  111. const sinRadius = radius * Math.sin(Math.atan((posY - lastPosY) / (posX - lastPosX)))
  112. const cosRadius = radius * Math.cos(Math.atan((posY - lastPosY) / (posX - lastPosX)))
  113. const rectPoint1: [number, number] = [lastPosX + sinRadius, lastPosY - cosRadius]
  114. const rectPoint2: [number, number] = [lastPosX - sinRadius, lastPosY + cosRadius]
  115. const rectPoint3: [number, number] = [posX + sinRadius, posY - cosRadius]
  116. const rectPoint4: [number, number] = [posX - sinRadius, posY + cosRadius]
  117. ctx.save()
  118. ctx.beginPath()
  119. ctx.arc(posX, posY, radius, 0, Math.PI * 2)
  120. ctx.clip()
  121. ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
  122. ctx.restore()
  123. ctx.save()
  124. ctx.beginPath()
  125. ctx.moveTo(...rectPoint1)
  126. ctx.lineTo(...rectPoint3)
  127. ctx.lineTo(...rectPoint4)
  128. ctx.lineTo(...rectPoint2)
  129. ctx.closePath()
  130. ctx.clip()
  131. ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
  132. ctx.restore()
  133. }
  134. const startDraw = (posX: number, posY: number) => {
  135. lastPos = { x: posX, y: posY }
  136. lastTime = new Date().getTime()
  137. }
  138. const startMove = (posX: number, posY: number) => {
  139. const time = new Date().getTime()
  140. // 画笔模式(这里通过绘制速度调节画笔的粗细)
  141. if(props.model === 'pen') {
  142. const s = getDistance(posX, posY)
  143. const t = time - lastTime
  144. const lineWidth = getLineWidth(s, t)
  145. draw(posX, posY, lineWidth)
  146. lastLineWidth = lineWidth
  147. }
  148. // 橡皮模式
  149. else erase(posX, posY)
  150. lastPos = { x: posX, y: posY }
  151. lastTime = new Date().getTime()
  152. }
  153. const handleMousedown = (e: MouseEvent) => {
  154. isMouseDown = true
  155. startDraw(e.offsetX, e.offsetY)
  156. }
  157. const updateMousePosition = (e: MouseEvent) => {
  158. mouse.x = e.pageX
  159. mouse.y = e.pageY
  160. }
  161. const handleMousemove = (e: MouseEvent) => {
  162. updateMousePosition(e)
  163. if(!isMouseDown) return
  164. startMove(e.offsetX, e.offsetY)
  165. }
  166. const handleMouseup = () => {
  167. if(!isMouseDown) return
  168. isMouseDown = false
  169. }
  170. // 清空画布
  171. const clearCanvas = () => {
  172. if(!ctx || !canvasRef.value) return
  173. ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
  174. }
  175. onMounted(initCanvas)
  176. return {
  177. mouse,
  178. mouseInCanvas,
  179. penSize,
  180. rubberSize,
  181. writingBoardRef,
  182. canvasRef,
  183. handleMousedown,
  184. handleMousemove,
  185. handleMouseup,
  186. clearCanvas,
  187. }
  188. },
  189. })
  190. </script>
  191. <style lang="scss" scoped>
  192. .writing-board {
  193. position: fixed;
  194. top: 0;
  195. bottom: 0;
  196. left: 0;
  197. right: 0;
  198. z-index: 8;
  199. cursor: none;
  200. }
  201. .eraser, .pen {
  202. pointer-events: none;
  203. position: fixed;
  204. z-index: 9;
  205. .icon {
  206. filter: drop-shadow(2px 2px 2px #555);
  207. }
  208. }
  209. .eraser {
  210. display: flex;
  211. justify-content: center;
  212. align-items: center;
  213. border-radius: 50%;
  214. border: 4px solid rgba($color: #555, $alpha: .15);
  215. color: rgba($color: #555, $alpha: .75);
  216. }
  217. </style>