| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- <template>
- <div class="writing-board" ref="writingBoardRef">
- <canvas class="canvas" ref="canvasRef"
- @mousedown="$event => handleMousedown($event)"
- @mousemove="$event => handleMousemove($event)"
- @mouseup="handleMouseup()"
- @mouseleave="handleMouseup(); mouseInCanvas = false"
- @mouseenter="mouseInCanvas = true"
- ></canvas>
- <div
- class="pen"
- :style="{
- left: mouse.x - penSize / 2 + 'px',
- top: mouse.y - 36 + penSize / 2 + 'px',
- color: color,
- }"
- v-if="mouseInCanvas && model === 'pen'"
- ><IconWrite class="icon" size="36" /></div>
-
- <div
- class="eraser"
- :style="{
- left: mouse.x - rubberSize / 2 + 'px',
- top: mouse.y - rubberSize / 2 + 'px',
- width: rubberSize + 'px',
- height: rubberSize + 'px',
- }"
- v-if="mouseInCanvas && model === 'eraser'"
- ><IconClearFormat class="icon" :size="rubberSize * 0.6" /></div>
- </div>
- </template>
- <script lang="ts">
- import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
- const penSize = 6
- const rubberSize = 80
- export default defineComponent({
- name: 'writing-board',
- props: {
- color: {
- type: String,
- default: '#ffcc00',
- },
- model: {
- type: String as PropType<'pen' | 'eraser'>,
- default: 'pen',
- },
- },
- setup(props) {
- let ctx: CanvasRenderingContext2D | null = null
- const writingBoardRef = ref<HTMLElement>()
- const canvasRef = ref<HTMLCanvasElement>()
- let lastPos = {
- x: 0,
- y: 0,
- }
- let isMouseDown = false
- let lastTime = 0
- let lastLineWidth = -1
- const mouse = reactive({
- x: 0,
- y: 0,
- })
- const mouseInCanvas = ref(false)
- const initCanvas = () => {
- if(!canvasRef.value || !writingBoardRef.value) return
- ctx = canvasRef.value.getContext('2d')
- if(!ctx) return
- canvasRef.value.width = writingBoardRef.value.clientWidth
- canvasRef.value.height = writingBoardRef.value.clientHeight
- canvasRef.value.style.width = writingBoardRef.value.clientWidth + 'px'
- canvasRef.value.style.height = writingBoardRef.value.clientHeight + 'px'
- ctx.lineCap = 'round'
- ctx.lineJoin = 'round'
- }
- const getDistance = (posX: number, posY: number) => {
- const lastPosX = lastPos.x
- const lastPosY = lastPos.y
- return Math.sqrt((posX - lastPosX) * (posX - lastPosX) + (posY - lastPosY) * (posY - lastPosY))
- }
- const getLineWidth = (s: number, t: number) => {
- const maxV = 10
- const minV = 0.1
- const maxWidth = penSize
- const minWidth = 3
- const v = s / t
- let lineWidth
- if(v <= minV) lineWidth = maxWidth
- else if(v >= maxV) lineWidth = minWidth
- else lineWidth = maxWidth - v / maxV * maxWidth
- if(lastLineWidth === -1) return lineWidth
- return lineWidth * 1 / 3 + lastLineWidth * 2 / 3
- }
- // 画笔绘制方法
- const draw = (posX: number, posY: number, lineWidth: number) => {
- if(!ctx) return
- const lastPosX = lastPos.x
- const lastPosY = lastPos.y
- ctx.lineWidth = lineWidth
- ctx.strokeStyle = props.color
- ctx.beginPath()
- ctx.moveTo(lastPosX, lastPosY)
- ctx.lineTo(posX, posY)
- ctx.stroke()
- ctx.closePath()
- }
- // 橡皮擦除方法
- const erase = (posX: number, posY: number) => {
- if(!ctx || !canvasRef.value) return
- const lastPosX = lastPos.x
- const lastPosY = lastPos.y
- const radius = rubberSize / 2
- const sinRadius = radius * Math.sin(Math.atan((posY - lastPosY) / (posX - lastPosX)))
- const cosRadius = radius * Math.cos(Math.atan((posY - lastPosY) / (posX - lastPosX)))
- const rectPoint1: [number, number] = [lastPosX + sinRadius, lastPosY - cosRadius]
- const rectPoint2: [number, number] = [lastPosX - sinRadius, lastPosY + cosRadius]
- const rectPoint3: [number, number] = [posX + sinRadius, posY - cosRadius]
- const rectPoint4: [number, number] = [posX - sinRadius, posY + cosRadius]
- ctx.save()
- ctx.beginPath()
- ctx.arc(posX, posY, radius, 0, Math.PI * 2)
- ctx.clip()
- ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
- ctx.restore()
- ctx.save()
- ctx.beginPath()
- ctx.moveTo(...rectPoint1)
- ctx.lineTo(...rectPoint3)
- ctx.lineTo(...rectPoint4)
- ctx.lineTo(...rectPoint2)
- ctx.closePath()
- ctx.clip()
- ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
- ctx.restore()
- }
- const startDraw = (posX: number, posY: number) => {
- lastPos = { x: posX, y: posY }
- lastTime = new Date().getTime()
- }
- const startMove = (posX: number, posY: number) => {
- const time = new Date().getTime()
- // 画笔模式(这里通过绘制速度调节画笔的粗细)
- if(props.model === 'pen') {
- const s = getDistance(posX, posY)
- const t = time - lastTime
- const lineWidth = getLineWidth(s, t)
-
- draw(posX, posY, lineWidth)
- lastLineWidth = lineWidth
- }
- // 橡皮模式
- else erase(posX, posY)
- lastPos = { x: posX, y: posY }
- lastTime = new Date().getTime()
- }
- const handleMousedown = (e: MouseEvent) => {
- isMouseDown = true
- startDraw(e.offsetX, e.offsetY)
- }
- const updateMousePosition = (e: MouseEvent) => {
- mouse.x = e.pageX
- mouse.y = e.pageY
- }
- const handleMousemove = (e: MouseEvent) => {
- updateMousePosition(e)
- if(!isMouseDown) return
- startMove(e.offsetX, e.offsetY)
- }
- const handleMouseup = () => {
- if(!isMouseDown) return
- isMouseDown = false
- }
- // 清空画布
- const clearCanvas = () => {
- if(!ctx || !canvasRef.value) return
- ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
- }
- onMounted(initCanvas)
- return {
- mouse,
- mouseInCanvas,
- penSize,
- rubberSize,
- writingBoardRef,
- canvasRef,
- handleMousedown,
- handleMousemove,
- handleMouseup,
- clearCanvas,
- }
- },
- })
- </script>
- <style lang="scss" scoped>
- .writing-board {
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 8;
- cursor: none;
- }
- .eraser, .pen {
- pointer-events: none;
- position: fixed;
- z-index: 9;
- .icon {
- filter: drop-shadow(2px 2px 2px #555);
- }
- }
- .eraser {
- display: flex;
- justify-content: center;
- align-items: center;
- border-radius: 50%;
- border: 4px solid rgba($color: #555, $alpha: .15);
- color: rgba($color: #555, $alpha: .75);
- }
- </style>
|