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
82dc18f1f1
commit
6c8ed6d5ef
@ -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
|
||||
|
@ -141,7 +141,7 @@ npm run dev
|
||||
- 公式线条粗细设置
|
||||
### 幻灯片放映
|
||||
- 全部幻灯片预览
|
||||
- 画笔、黑板工具
|
||||
- 画笔工具(画笔/形状/箭头/荧光笔标注、橡皮擦除、黑板模式)
|
||||
- 计时器工具
|
||||
- 激光笔
|
||||
- 自动放映
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user