feat: 放映画笔工具添加形状和箭头标注

This commit is contained in:
zxc 2025-03-15 22:22:46 +08:00
parent 82dc18f1f1
commit 6c8ed6d5ef
6 changed files with 195 additions and 26 deletions

View File

@ -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

View File

@ -141,7 +141,7 @@ npm run dev
- 公式线条粗细设置
### 幻灯片放映
- 全部幻灯片预览
- 画笔、黑板工具
- 画笔工具(画笔/形状/箭头/荧光笔标注、橡皮擦除、黑板模式)
- 计时器工具
- 激光笔
- 自动放映

View File

@ -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

View File

@ -37,7 +37,7 @@
}"
v-if="model === 'pen'"
>
<IconWrite class="icon" :size="penSize * 6" v-if="model === 'pen'" />
<IconWrite class="icon" :size="penSize * 6" />
</div>
<div
class="pen"
@ -48,7 +48,18 @@
}"
v-if="model === 'mark'"
>
<IconHighLight class="icon" :size="markSize * 1.5" v-if="model === 'mark'" />
<IconHighLight class="icon" :size="markSize * 1.5" />
</div>
<div
class="pen"
:style="{
left: mouse.x - 20 + 'px',
top: mouse.y - 20 + 'px',
color: color,
}"
v-if="model === 'shape'"
>
<IconPlus class="icon" :size="40" />
</div>
</template>
</div>
@ -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()

View File

@ -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,

View File

@ -14,23 +14,24 @@
:penSize="penSize"
:markSize="markSize"
:rubberSize="rubberSize"
:shapeSize="shapeSize"
:shapeType="shapeType"
@end="hanldeWritingEnd()"
/>
</div>
<MoveablePanel
class="tools-panel"
:width="520"
:width="510"
:height="50"
:left="left"
:top="top"
:moveable="sizePopoverType === ''"
>
<div class="tools" @mousedown.stop>
<div class="tool-content">
<Popover trigger="manual" :value="sizePopoverType === 'pen'">
<Popover placement="top" trigger="manual" :value="sizePopoverType === 'pen'">
<template #content>
<div class="size">
<div class="setting">
<div class="label">墨迹粗细</div>
<Slider class="size-slider" :min="4" :max="10" :step="2" v-model:value="penSize" />
</div>
@ -39,9 +40,26 @@
<IconWrite class="icon" />
</div>
</Popover>
<Popover trigger="manual" :value="sizePopoverType === 'mark'">
<Popover placement="top" trigger="manual" :value="sizePopoverType === 'shape'">
<template #content>
<div class="size">
<div class="setting shape">
<div class="shapes">
<IconSquare class="icon" :class="{ 'active': shapeType === 'rect' }" @click="shapeType = 'rect'" />
<IconRound class="icon" :class="{ 'active': shapeType === 'circle' }" @click="shapeType = 'circle'" />
<IconArrowRight class="icon" :class="{ 'active': shapeType === 'arrow' }" @click="shapeType = 'arrow'" />
</div>
<Divider type="vertical" />
<div class="label">墨迹粗细</div>
<Slider class="size-slider" :min="2" :max="8" :step="2" v-model:value="shapeSize" />
</div>
</template>
<div class="btn" :class="{ 'active': writingBoardModel === 'shape' }" v-tooltip="'形状'" @click="changeModel('shape')">
<IconGraphicDesign class="icon" />
</div>
</Popover>
<Popover placement="top" trigger="manual" :value="sizePopoverType === 'mark'">
<template #content>
<div class="setting">
<div class="label">墨迹粗细</div>
<Slider class="size-slider" :min="16" :max="40" :step="4" v-model:value="markSize" />
</div>
@ -50,9 +68,9 @@
<IconHighLight class="icon" />
</div>
</Popover>
<Popover trigger="manual" :value="sizePopoverType === 'eraser'">
<Popover placement="top" trigger="manual" :value="sizePopoverType === 'eraser'">
<template #content>
<div class="size">
<div class="setting">
<div class="label">橡皮大小</div>
<Slider class="size-slider" :min="20" :max="200" :step="20" v-model:value="rubberSize" />
</div>
@ -70,7 +88,7 @@
<div class="colors">
<div
class="color"
:class="{ 'active': color === writingBoardColor }"
:class="{ 'active': color === writingBoardColor, 'white': color === '#ffffff' }"
v-for="color in writingBoardColors"
:key="color"
:style="{ backgroundColor: color }"
@ -78,7 +96,7 @@
></div>
</div>
</div>
<div class="btn" v-tooltip="'关闭画笔'" @click="closeWritingBoard()">
<div class="btn close" v-tooltip="'关闭画笔'" @click="closeWritingBoard()">
<IconClose class="icon" />
</div>
</div>
@ -96,10 +114,11 @@ import WritingBoard from '@/components/WritingBoard.vue'
import MoveablePanel from '@/components/MoveablePanel.vue'
import Slider from '@/components/Slider.vue'
import Popover from '@/components/Popover.vue'
import Divider from '@/components//Divider.vue'
const writingBoardColors = ['#000000', '#ffffff', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c', '#ffff3a']
type WritingBoardModel = 'pen' | 'mark' | 'eraser'
type WritingBoardModel = 'pen' | 'mark' | 'eraser' | 'shape'
withDefaults(defineProps<{
slideWidth: number
@ -122,10 +141,12 @@ const writingBoardColor = ref('#e2534d')
const writingBoardModel = ref<WritingBoardModel>('pen')
const blackboard = ref(false)
const sizePopoverType = ref<'' | WritingBoardModel>('')
const shapeType = ref<'rect' | 'circle' | 'arrow'>('rect')
const penSize = ref(6)
const markSize = ref(24)
const rubberSize = ref(80)
const shapeSize = ref(4)
const changeModel = (model: WritingBoardModel) => {
if (writingBoardModel.value === model) {
@ -199,7 +220,9 @@ const hanldeWritingEnd = () => {
align-items: center;
}
.btn {
padding: 5px 10px;
padding: 5px;
margin-right: 5px;
border-radius: $borderRadius;
cursor: pointer;
&:hover {
@ -209,13 +232,17 @@ const hanldeWritingEnd = () => {
background-color: rgba($color: $themeColor, $alpha: .5);
color: #fff;
}
&.close {
margin-right: 0;
margin-left: 5px;
}
}
.icon {
font-size: 20px;
}
.colors {
display: flex;
padding: 0 10px;
padding: 0 5px;
}
.color {
width: 16px;
@ -229,19 +256,44 @@ const hanldeWritingEnd = () => {
&.active {
transform: scale(1.3);
}
&.white {
border: 1px solid #f1f1f1;
}
& + .color {
margin-left: 8px;
}
}
}
.size {
.setting {
width: 200px;
display: flex;
align-items: center;
user-select: none;
font-size: 13px;
&.shape {
width: 280px;
}
.shapes {
display: flex;
align-items: center;
.icon {
font-size: 20px;
cursor: pointer;
& + .icon {
margin-left: 6px;
}
&.active {
color: $themeColor;
}
}
}
.label {
width: 70px;
}