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()"
/>