mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 画笔工具支持触摸屏
This commit is contained in:
parent
1cb955e287
commit
d67862f282
@ -1,256 +1,295 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="writing-board" ref="writingBoardRef">
|
<div class="writing-board" ref="writingBoardRef">
|
||||||
<canvas class="canvas" ref="canvasRef"
|
<canvas class="canvas" ref="canvasRef"
|
||||||
@mousedown="$event => handleMousedown($event)"
|
@mousedown="$event => handleMousedown($event)"
|
||||||
@mousemove="$event => handleMousemove($event)"
|
@mousemove="$event => handleMousemove($event)"
|
||||||
@mouseup="handleMouseup()"
|
@mouseup="handleMouseup()"
|
||||||
@mouseleave="handleMouseup(); mouseInCanvas = false"
|
@touchstart.prevent="$event => { handleTouchdown($event);mouseInCanvas = true }"
|
||||||
@mouseenter="mouseInCanvas = true"
|
@touchmove.prevent="$event => handleTouchmove($event)"
|
||||||
></canvas>
|
@touchend="handleMouseup();mouseInCanvas = false"
|
||||||
|
@mouseleave="handleMouseup(); mouseInCanvas = false"
|
||||||
<div
|
@mouseenter="mouseInCanvas = true"
|
||||||
class="pen"
|
></canvas>
|
||||||
:style="{
|
|
||||||
left: mouse.x - penSize / 2 + 'px',
|
<div
|
||||||
top: mouse.y - 36 + penSize / 2 + 'px',
|
class="pen"
|
||||||
color: color,
|
:style="{
|
||||||
}"
|
left: mouse.x - penSize / 2 + 'px',
|
||||||
v-if="mouseInCanvas && model === 'pen'"
|
top: mouse.y - 36 + penSize / 2 + 'px',
|
||||||
><IconWrite class="icon" size="36" /></div>
|
color: color,
|
||||||
|
}"
|
||||||
<div
|
v-if="mouseInCanvas && model === 'pen'"
|
||||||
class="eraser"
|
><IconWrite class="icon" size="36" /></div>
|
||||||
:style="{
|
|
||||||
left: mouse.x - rubberSize / 2 + 'px',
|
<div
|
||||||
top: mouse.y - rubberSize / 2 + 'px',
|
class="eraser"
|
||||||
width: rubberSize + 'px',
|
:style="{
|
||||||
height: rubberSize + 'px',
|
left: mouse.x - rubberSize / 2 + 'px',
|
||||||
}"
|
top: mouse.y - rubberSize / 2 + 'px',
|
||||||
v-if="mouseInCanvas && model === 'eraser'"
|
width: rubberSize + 'px',
|
||||||
></div>
|
height: rubberSize + 'px',
|
||||||
</div>
|
}"
|
||||||
</template>
|
v-if="mouseInCanvas && model === 'eraser'"
|
||||||
|
></div>
|
||||||
<script lang="ts">
|
</div>
|
||||||
import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
|
</template>
|
||||||
|
|
||||||
const penSize = 6
|
<script lang="ts">
|
||||||
const rubberSize = 80
|
import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
|
||||||
|
|
||||||
export default defineComponent({
|
const penSize = 6
|
||||||
name: 'writing-board',
|
const rubberSize = 80
|
||||||
props: {
|
|
||||||
color: {
|
export default defineComponent({
|
||||||
type: String,
|
name: 'writing-board',
|
||||||
default: '#ffcc00',
|
props: {
|
||||||
},
|
color: {
|
||||||
model: {
|
type: String,
|
||||||
type: String as PropType<'pen' | 'eraser'>,
|
default: '#ffcc00',
|
||||||
default: 'pen',
|
},
|
||||||
},
|
model: {
|
||||||
},
|
type: String as PropType<'pen' | 'eraser'>,
|
||||||
setup(props) {
|
default: 'pen',
|
||||||
let ctx: CanvasRenderingContext2D | null = null
|
},
|
||||||
const writingBoardRef = ref<HTMLElement>()
|
},
|
||||||
const canvasRef = ref<HTMLCanvasElement>()
|
setup(props) {
|
||||||
|
let ctx: CanvasRenderingContext2D | null = null
|
||||||
let lastPos = {
|
const writingBoardRef = ref<HTMLElement>()
|
||||||
x: 0,
|
const canvasRef = ref<HTMLCanvasElement>()
|
||||||
y: 0,
|
|
||||||
}
|
let lastPos = {
|
||||||
let isMouseDown = false
|
x: 0,
|
||||||
let lastTime = 0
|
y: 0,
|
||||||
let lastLineWidth = -1
|
}
|
||||||
|
let isMouseDown = false
|
||||||
// 鼠标位置坐标:用于画笔或橡皮位置跟随
|
let lastTime = 0
|
||||||
const mouse = reactive({
|
let lastLineWidth = -1
|
||||||
x: 0,
|
|
||||||
y: 0,
|
// 鼠标位置坐标:用于画笔或橡皮位置跟随
|
||||||
})
|
const mouse = reactive({
|
||||||
// 更新鼠标位置坐标
|
x: 0,
|
||||||
const updateMousePosition = (e: MouseEvent) => {
|
y: 0,
|
||||||
mouse.x = e.pageX
|
})
|
||||||
mouse.y = e.pageY
|
// 更新鼠标位置坐标
|
||||||
}
|
const updateMousePosition = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (e instanceof MouseEvent) {
|
||||||
// 鼠标是否处在画布范围内:处在范围内才会显示画笔或橡皮
|
mouse.x = e.pageX
|
||||||
const mouseInCanvas = ref(false)
|
mouse.y = e.pageY
|
||||||
|
}
|
||||||
|
else if (e instanceof TouchEvent) {
|
||||||
// 初始化画布
|
mouse.x = e.targetTouches[0].clientX
|
||||||
const initCanvas = () => {
|
mouse.y = e.targetTouches[0].clientY
|
||||||
if (!canvasRef.value || !writingBoardRef.value) return
|
}
|
||||||
|
}
|
||||||
ctx = canvasRef.value.getContext('2d')
|
|
||||||
if (!ctx) return
|
// 鼠标是否处在画布范围内:处在范围内才会显示画笔或橡皮
|
||||||
|
const mouseInCanvas = ref(false)
|
||||||
canvasRef.value.width = writingBoardRef.value.clientWidth
|
|
||||||
canvasRef.value.height = writingBoardRef.value.clientHeight
|
|
||||||
|
// 初始化画布
|
||||||
canvasRef.value.style.width = writingBoardRef.value.clientWidth + 'px'
|
const initCanvas = () => {
|
||||||
canvasRef.value.style.height = writingBoardRef.value.clientHeight + 'px'
|
if (!canvasRef.value || !writingBoardRef.value) return
|
||||||
|
|
||||||
ctx.lineCap = 'round'
|
ctx = canvasRef.value.getContext('2d')
|
||||||
ctx.lineJoin = 'round'
|
if (!ctx) return
|
||||||
}
|
|
||||||
onMounted(initCanvas)
|
canvasRef.value.width = writingBoardRef.value.clientWidth
|
||||||
|
canvasRef.value.height = writingBoardRef.value.clientHeight
|
||||||
// 绘制画笔墨迹方法
|
|
||||||
const draw = (posX: number, posY: number, lineWidth: number) => {
|
canvasRef.value.style.width = writingBoardRef.value.clientWidth + 'px'
|
||||||
if (!ctx) return
|
canvasRef.value.style.height = writingBoardRef.value.clientHeight + 'px'
|
||||||
|
|
||||||
const lastPosX = lastPos.x
|
ctx.lineCap = 'round'
|
||||||
const lastPosY = lastPos.y
|
ctx.lineJoin = 'round'
|
||||||
|
}
|
||||||
ctx.lineWidth = lineWidth
|
onMounted(initCanvas)
|
||||||
ctx.strokeStyle = props.color
|
|
||||||
ctx.beginPath()
|
// 绘制画笔墨迹方法
|
||||||
ctx.moveTo(lastPosX, lastPosY)
|
const draw = (posX: number, posY: number, lineWidth: number) => {
|
||||||
ctx.lineTo(posX, posY)
|
if (!ctx) return
|
||||||
ctx.stroke()
|
|
||||||
ctx.closePath()
|
const lastPosX = lastPos.x
|
||||||
}
|
const lastPosY = lastPos.y
|
||||||
|
|
||||||
// 擦除墨迹方法
|
ctx.lineWidth = lineWidth
|
||||||
const erase = (posX: number, posY: number) => {
|
ctx.strokeStyle = props.color
|
||||||
if (!ctx || !canvasRef.value) return
|
ctx.beginPath()
|
||||||
const lastPosX = lastPos.x
|
ctx.moveTo(lastPosX, lastPosY)
|
||||||
const lastPosY = lastPos.y
|
ctx.lineTo(posX, posY)
|
||||||
|
ctx.stroke()
|
||||||
const radius = rubberSize / 2
|
ctx.closePath()
|
||||||
|
}
|
||||||
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 erase = (posX: number, posY: number) => {
|
||||||
const rectPoint2: [number, number] = [lastPosX - sinRadius, lastPosY + cosRadius]
|
if (!ctx || !canvasRef.value) return
|
||||||
const rectPoint3: [number, number] = [posX + sinRadius, posY - cosRadius]
|
const lastPosX = lastPos.x
|
||||||
const rectPoint4: [number, number] = [posX - sinRadius, posY + cosRadius]
|
const lastPosY = lastPos.y
|
||||||
|
|
||||||
ctx.save()
|
const radius = rubberSize / 2
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(posX, posY, radius, 0, Math.PI * 2)
|
const sinRadius = radius * Math.sin(Math.atan((posY - lastPosY) / (posX - lastPosX)))
|
||||||
ctx.clip()
|
const cosRadius = radius * Math.cos(Math.atan((posY - lastPosY) / (posX - lastPosX)))
|
||||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
const rectPoint1: [number, number] = [lastPosX + sinRadius, lastPosY - cosRadius]
|
||||||
ctx.restore()
|
const rectPoint2: [number, number] = [lastPosX - sinRadius, lastPosY + cosRadius]
|
||||||
|
const rectPoint3: [number, number] = [posX + sinRadius, posY - cosRadius]
|
||||||
ctx.save()
|
const rectPoint4: [number, number] = [posX - sinRadius, posY + cosRadius]
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(...rectPoint1)
|
ctx.save()
|
||||||
ctx.lineTo(...rectPoint3)
|
ctx.beginPath()
|
||||||
ctx.lineTo(...rectPoint4)
|
ctx.arc(posX, posY, radius, 0, Math.PI * 2)
|
||||||
ctx.lineTo(...rectPoint2)
|
ctx.clip()
|
||||||
ctx.closePath()
|
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
||||||
ctx.clip()
|
ctx.restore()
|
||||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
|
||||||
ctx.restore()
|
ctx.save()
|
||||||
}
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(...rectPoint1)
|
||||||
// 准备开始绘制/擦除墨迹(落笔)
|
ctx.lineTo(...rectPoint3)
|
||||||
const handleMousedown = (e: MouseEvent) => {
|
ctx.lineTo(...rectPoint4)
|
||||||
isMouseDown = true
|
ctx.lineTo(...rectPoint2)
|
||||||
lastPos = { x: e.offsetX, y: e.offsetY }
|
ctx.closePath()
|
||||||
lastTime = new Date().getTime()
|
ctx.clip()
|
||||||
}
|
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
||||||
|
ctx.restore()
|
||||||
// 计算鼠标两次移动之间的距离
|
}
|
||||||
const getDistance = (posX: number, posY: number) => {
|
|
||||||
const lastPosX = lastPos.x
|
// 计算鼠标两次移动之间的距离
|
||||||
const lastPosY = lastPos.y
|
const getDistance = (posX: number, posY: number) => {
|
||||||
return Math.sqrt((posX - lastPosX) * (posX - lastPosX) + (posY - lastPosY) * (posY - lastPosY))
|
const lastPosX = lastPos.x
|
||||||
}
|
const lastPosY = lastPos.y
|
||||||
|
return Math.sqrt((posX - lastPosX) * (posX - lastPosX) + (posY - lastPosY) * (posY - lastPosY))
|
||||||
// 根据鼠标两次移动之间的距离s和时间t计算绘制速度,速度越快,墨迹越细
|
}
|
||||||
const getLineWidth = (s: number, t: number) => {
|
|
||||||
const maxV = 10
|
// 根据鼠标两次移动之间的距离s和时间t计算绘制速度,速度越快,墨迹越细
|
||||||
const minV = 0.1
|
const getLineWidth = (s: number, t: number) => {
|
||||||
const maxWidth = penSize
|
const maxV = 10
|
||||||
const minWidth = 3
|
const minV = 0.1
|
||||||
const v = s / t
|
const maxWidth = penSize
|
||||||
let lineWidth
|
const minWidth = 3
|
||||||
|
const v = s / t
|
||||||
if (v <= minV) lineWidth = maxWidth
|
let lineWidth
|
||||||
else if (v >= maxV) lineWidth = minWidth
|
|
||||||
else lineWidth = maxWidth - v / maxV * maxWidth
|
if (v <= minV) {
|
||||||
|
lineWidth = maxWidth
|
||||||
if (lastLineWidth === -1) return lineWidth
|
}
|
||||||
return lineWidth * 1 / 3 + lastLineWidth * 2 / 3
|
else if (v >= maxV) {
|
||||||
}
|
lineWidth = minWidth
|
||||||
|
}
|
||||||
// 开始绘制/擦除墨迹(移动)
|
else {
|
||||||
const handleMousemove = (e: MouseEvent) => {
|
lineWidth = maxWidth - v / maxV * maxWidth
|
||||||
updateMousePosition(e)
|
}
|
||||||
|
|
||||||
if (!isMouseDown) return
|
if (lastLineWidth === -1) return lineWidth
|
||||||
|
return lineWidth * 1 / 3 + lastLineWidth * 2 / 3
|
||||||
const time = new Date().getTime()
|
}
|
||||||
|
|
||||||
if (props.model === 'pen') {
|
// 路径操作
|
||||||
const s = getDistance(e.offsetX, e.offsetY)
|
const handleMove = (x: number, y: number) => {
|
||||||
const t = time - lastTime
|
const time = new Date().getTime()
|
||||||
const lineWidth = getLineWidth(s, t)
|
|
||||||
|
if (props.model === 'pen') {
|
||||||
draw(e.offsetX, e.offsetY, lineWidth)
|
const s = getDistance(x, y)
|
||||||
lastLineWidth = lineWidth
|
const t = time - lastTime
|
||||||
}
|
const lineWidth = getLineWidth(s, t)
|
||||||
else erase(e.offsetX, e.offsetY)
|
|
||||||
|
draw(x, y, lineWidth)
|
||||||
lastPos = { x: e.offsetX, y: e.offsetY }
|
lastLineWidth = lineWidth
|
||||||
lastTime = new Date().getTime()
|
}
|
||||||
}
|
else {
|
||||||
|
erase(x, y)
|
||||||
// 结束绘制/擦除墨迹(停笔)
|
}
|
||||||
const handleMouseup = () => {
|
|
||||||
if (!isMouseDown) return
|
lastPos = {x, y}
|
||||||
isMouseDown = false
|
lastTime = new Date().getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空画布
|
// 处理触摸事件
|
||||||
const clearCanvas = () => {
|
const handleTouchdown = (e: TouchEvent) => {
|
||||||
if (!ctx || !canvasRef.value) return
|
// mouseInCanvas.value = true
|
||||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
isMouseDown = true
|
||||||
}
|
lastPos = {x: e.targetTouches[0].clientX, y: e.targetTouches[0].clientY}
|
||||||
|
lastTime = new Date().getTime()
|
||||||
return {
|
}
|
||||||
mouse,
|
|
||||||
mouseInCanvas,
|
const handleTouchmove = (e: TouchEvent) => {
|
||||||
penSize,
|
updateMousePosition(e)
|
||||||
rubberSize,
|
if (!isMouseDown) return
|
||||||
writingBoardRef,
|
handleMove(e.targetTouches[0].clientX, e.targetTouches[0].clientY)
|
||||||
canvasRef,
|
}
|
||||||
handleMousedown,
|
|
||||||
handleMousemove,
|
// 处理鼠标事件
|
||||||
handleMouseup,
|
|
||||||
clearCanvas,
|
// 准备开始绘制/擦除墨迹(落笔)
|
||||||
}
|
const handleMousedown = (e: MouseEvent) => {
|
||||||
},
|
isMouseDown = true
|
||||||
})
|
lastPos = {x: e.offsetX, y: e.offsetY}
|
||||||
</script>
|
lastTime = new Date().getTime()
|
||||||
|
}
|
||||||
<style lang="scss" scoped>
|
|
||||||
.writing-board {
|
// 开始绘制/擦除墨迹(移动)
|
||||||
position: fixed;
|
const handleMousemove = (e: MouseEvent) => {
|
||||||
top: 0;
|
updateMousePosition(e)
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
if (!isMouseDown) return
|
||||||
right: 0;
|
handleMove(e.offsetX, e.offsetY)
|
||||||
z-index: 8;
|
}
|
||||||
cursor: none;
|
|
||||||
}
|
// 结束绘制/擦除墨迹(停笔)
|
||||||
.eraser, .pen {
|
const handleMouseup = () => {
|
||||||
pointer-events: none;
|
if (!isMouseDown) return
|
||||||
position: fixed;
|
isMouseDown = false
|
||||||
z-index: 9;
|
}
|
||||||
|
|
||||||
.icon {
|
// 清空画布
|
||||||
filter: drop-shadow(2px 2px 2px #555);
|
const clearCanvas = () => {
|
||||||
}
|
if (!ctx || !canvasRef.value) return
|
||||||
}
|
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
||||||
.eraser {
|
}
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
return {
|
||||||
align-items: center;
|
mouse,
|
||||||
border-radius: 50%;
|
mouseInCanvas,
|
||||||
border: 4px solid rgba($color: #555, $alpha: .15);
|
penSize,
|
||||||
color: rgba($color: #555, $alpha: .75);
|
rubberSize,
|
||||||
}
|
writingBoardRef,
|
||||||
|
canvasRef,
|
||||||
|
handleMousedown,
|
||||||
|
handleMousemove,
|
||||||
|
handleMouseup,
|
||||||
|
clearCanvas,
|
||||||
|
handleTouchdown,
|
||||||
|
handleTouchmove
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</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>
|
</style>
|
Loading…
x
Reference in New Issue
Block a user