From 6c8ed6d5ef7d695add1414be1c143884241faead Mon Sep 17 00:00:00 2001 From: zxc <1171051090@qq.com> Date: Sat, 15 Mar 2025 22:22:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=BE=E6=98=A0=E7=94=BB=E7=AC=94?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=B7=BB=E5=8A=A0=E5=BD=A2=E7=8A=B6=E5=92=8C?= =?UTF-8?q?=E7=AE=AD=E5=A4=B4=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README_zh.md | 2 +- src/components/MoveablePanel.vue | 2 +- src/components/WritingBoard.vue | 133 ++++++++++++++++++++++++-- src/plugins/icon.ts | 2 + src/views/Screen/WritingBoardTool.vue | 80 +++++++++++++--- 6 files changed, 195 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index d398d848..d32d6092 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ Browser access: http://127.0.0.1:5173/ - Formula line thickness settings ### Slide Show - Preview all slides -- Pen and blackboard tools +- Brush tools (pen/shape/arrow/highlighter annotation, eraser, blackboard mode) - Timer tool - Laser pointer - Auto play diff --git a/README_zh.md b/README_zh.md index eb00bbdc..6a8ea832 100644 --- a/README_zh.md +++ b/README_zh.md @@ -141,7 +141,7 @@ npm run dev - 公式线条粗细设置 ### 幻灯片放映 - 全部幻灯片预览 -- 画笔、黑板工具 +- 画笔工具(画笔/形状/箭头/荧光笔标注、橡皮擦除、黑板模式) - 计时器工具 - 激光笔 - 自动放映 diff --git a/src/components/MoveablePanel.vue b/src/components/MoveablePanel.vue index 8a383c80..8efd8981 100644 --- a/src/components/MoveablePanel.vue +++ b/src/components/MoveablePanel.vue @@ -76,7 +76,7 @@ onMounted(() => { else x.value = document.body.clientWidth + props.left - props.width if (props.top >= 0) y.value = props.top - else y.value = document.body.clientHeight + props.top - realHeight.value + else y.value = document.body.clientHeight + props.top - (props.height || realHeight.value) w.value = props.width h.value = props.height diff --git a/src/components/WritingBoard.vue b/src/components/WritingBoard.vue index 8e737c78..af54e6cd 100644 --- a/src/components/WritingBoard.vue +++ b/src/components/WritingBoard.vue @@ -37,7 +37,7 @@ }" v-if="model === 'pen'" > - +
- + +
+
+
@@ -59,18 +70,22 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue' const props = withDefaults(defineProps<{ color?: string - model?: 'pen' | 'eraser' | 'mark' + model?: 'pen' | 'eraser' | 'mark' | 'shape' + shapeType?: 'rect' | 'circle' | 'arrow' blackboard?: boolean penSize?: number markSize?: number rubberSize?: number + shapeSize?: number }>(), { color: '#ffcc00', model: 'pen', + shapeType: 'rect', blackboard: false, penSize: 6, markSize: 24, rubberSize: 80, + shapeSize: 4, }) const emit = defineEmits<{ @@ -89,6 +104,8 @@ let isMouseDown = false let lastTime = 0 let lastLineWidth = -1 +let initialImageData: ImageData | null = null + // 鼠标位置坐标:用于画笔或橡皮位置跟随 const mouse = ref({ x: 0, @@ -140,7 +157,7 @@ const updateCtx = () => { ctx.globalCompositeOperation = 'xor' ctx.globalAlpha = 0.5 } - else if (props.model === 'pen') { + else if (props.model === 'pen' || props.model === 'shape') { ctx.globalCompositeOperation = 'source-over' ctx.globalAlpha = 1 } @@ -221,6 +238,92 @@ const getLineWidth = (s: number, t: number) => { return lineWidth * 1 / 3 + lastLineWidth * 2 / 3 } +// 形状绘制 +const drawShape = (currentX: number, currentY: number) => { + if (!ctx || !initialImageData) return + + ctx.putImageData(initialImageData, 0, 0) + + const startX = lastPos.x + const startY = lastPos.y + + ctx.save() + ctx.lineCap = 'butt' + ctx.lineJoin = 'miter' + + ctx.beginPath() + if (props.shapeType === 'rect') { + const width = currentX - startX + const height = currentY - startY + ctx.rect(startX, startY, width, height) + } + else if (props.shapeType === 'circle') { + const width = currentX - startX + const height = currentY - startY + const centerX = startX + width / 2 + const centerY = startY + height / 2 + const radiusX = Math.abs(width) / 2 + const radiusY = Math.abs(height) / 2 + + ctx.ellipse( + centerX, + centerY, + Math.abs(radiusX), + Math.abs(radiusY), + 0, + 0, + Math.PI * 2, + ) + } + else if (props.shapeType === 'arrow') { + const dx = currentX - startX + const dy = currentY - startY + const angle = Math.atan2(dy, dx) + const arrowLength = Math.max(props.shapeSize, 4) * 2 + + const endX = currentX - (Math.cos(angle) * arrowLength) + const endY = currentY - (Math.sin(angle) * arrowLength) + + ctx.moveTo(startX, startY) + ctx.lineTo(endX, endY) + } + + ctx.strokeStyle = props.color + ctx.lineWidth = props.shapeSize + ctx.stroke() + ctx.restore() + + if (props.shapeType === 'arrow') { + const dx = currentX - startX + const dy = currentY - startY + const angle = Math.atan2(dy, dx) + + const arrowLength = Math.max(props.shapeSize, 4) * 2.6 + const arrowWidth = Math.max(props.shapeSize, 4) * 1.6 + + const arrowBaseX = currentX - (Math.cos(angle) * arrowLength) + const arrowBaseY = currentY - (Math.sin(angle) * arrowLength) + + ctx.save() + ctx.beginPath() + + ctx.moveTo(currentX, currentY) + + const leftX = arrowBaseX + arrowWidth * Math.cos(angle + Math.PI / 2) + const leftY = arrowBaseY + arrowWidth * Math.sin(angle + Math.PI / 2) + const rightX = arrowBaseX + arrowWidth * Math.cos(angle - Math.PI / 2) + const rightY = arrowBaseY + arrowWidth * Math.sin(angle - Math.PI / 2) + + ctx.lineTo(leftX, leftY) + ctx.lineTo(rightX, rightY) + ctx.closePath() + + ctx.fillStyle = props.color + ctx.fill() + ctx.restore() + } +} + // 路径操作 const handleMove = (x: number, y: number) => { const time = new Date().getTime() @@ -232,12 +335,21 @@ const handleMove = (x: number, y: number) => { draw(x, y, lineWidth) lastLineWidth = lineWidth - } - else if (props.model === 'mark') draw(x, y, props.markSize) - else erase(x, y) - lastPos = { x, y } - lastTime = new Date().getTime() + lastPos = { x, y } + lastTime = new Date().getTime() + } + else if (props.model === 'mark') { + draw(x, y, props.markSize) + lastPos = { x, y } + } + else if (props.model ==='eraser') { + erase(x, y) + lastPos = { x, y } + } + else if (props.model === 'shape') { + drawShape(x, y) + } } // 获取鼠标在canvas中的相对位置 @@ -257,6 +369,9 @@ const handleMousedown = (e: MouseEvent | TouchEvent) => { const x = mouseX / widthScale.value const y = mouseY / heightScale.value + if (props.model === 'shape') { + initialImageData = ctx!.getImageData(0, 0, canvasRef.value!.width, canvasRef.value!.height) + } isMouseDown = true lastPos = { x, y } lastTime = new Date().getTime() diff --git a/src/plugins/icon.ts b/src/plugins/icon.ts index b74c16aa..b566950c 100644 --- a/src/plugins/icon.ts +++ b/src/plugins/icon.ts @@ -78,6 +78,7 @@ import { Click, Theme, ArrowCircleLeft, + ArrowRight, GraphicDesign, Logout, Erase, @@ -209,6 +210,7 @@ export const icons: Icons = { IconClick: Click, IconTheme: Theme, IconArrowCircleLeft: ArrowCircleLeft, + IconArrowRight: ArrowRight, IconGraphicDesign: GraphicDesign, IconLogout: Logout, IconErase: Erase, diff --git a/src/views/Screen/WritingBoardTool.vue b/src/views/Screen/WritingBoardTool.vue index 6b620df8..1b61d722 100644 --- a/src/views/Screen/WritingBoardTool.vue +++ b/src/views/Screen/WritingBoardTool.vue @@ -14,23 +14,24 @@ :penSize="penSize" :markSize="markSize" :rubberSize="rubberSize" + :shapeSize="shapeSize" + :shapeType="shapeType" @end="hanldeWritingEnd()" />
- +