PPTist/src/components/WritingBoard.vue
2021-01-14 09:42:26 +08:00

255 lines
6.8 KiB
Vue

<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'"
><IconFont class="icon" :style="{ fontSize: '36px' }" type="icon-pen" /></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'"
><IconFont class="icon" :style="{ fontSize: rubberSize * 0.6 + 'px' }" type="icon-eraser" /></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 | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)
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>