feat: add line and arrow annotations

This commit is contained in:
2026-07-12 21:09:23 +08:00
parent b82108db38
commit 50551de105
4 changed files with 220 additions and 23 deletions
+84 -8
View File
@@ -24,7 +24,7 @@ interface Rect {
h: number
}
type Tool = 'pen' | 'rect' | 'ellipse' | 'text' | 'mosaic-brush' | 'mosaic-rect'
type Tool = 'pen' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'text' | 'mosaic-brush' | 'mosaic-rect'
type MosaicMode = 'brush' | 'rect'
interface Point {
x: number
@@ -150,18 +150,18 @@ const sizeLabel = computed(() => {
return `${Math.round(rect.value.w)} × ${Math.round(rect.value.h)}`
})
const MARK_TOOLBAR_W = 212
const MARK_TOOLBAR_W = 280
const ACTION_TOOLBAR_W = 288
const TOOLBAR_GROUP_GAP = 8
const toolOrder: Tool[] = ['pen', 'rect', 'ellipse', 'text', 'mosaic-brush', 'mosaic-rect']
const toolOrder: Tool[] = ['pen', 'line', 'arrow', 'rect', 'ellipse', 'text', 'mosaic-brush', 'mosaic-rect']
const toolSettingsPos = computed(() => {
if (!leftToolbarPos.value || !rect.value || !activeTool.value) return null
const isMosaic = activeTool.value.startsWith('mosaic-')
const menuW = isMosaic ? (mosaicMode.value === 'brush' ? 328 : 112) : activeTool.value === 'text' ? 462 : 398
const menuH = 58
const buttonIndex = isMosaic ? 4 : toolOrder.indexOf(activeTool.value)
const buttonIndex = isMosaic ? 6 : toolOrder.indexOf(activeTool.value)
const buttonCenter = 4 + buttonIndex * 34 + 14
let x = leftToolbarPos.value.x
let y = leftToolbarPos.value.y + 44
@@ -315,6 +315,28 @@ function textRenderBox(annotation: Annotation) {
}
}
function arrowHeadPoints(annotation: Annotation) {
if (annotation.points.length < 2) return ''
const start = annotation.points[0]
const end = annotation.points[annotation.points.length - 1]
const dx = end.x - start.x
const dy = end.y - start.y
const length = Math.hypot(dx, dy)
if (length < 1) return ''
const headLength = Math.min(length * 0.45, Math.max(10, (annotation.strokeWidth || 3) * 4))
const angle = Math.atan2(dy, dx)
const spread = Math.PI / 6
const left = {
x: end.x - headLength * Math.cos(angle - spread),
y: end.y - headLength * Math.sin(angle - spread),
}
const right = {
x: end.x - headLength * Math.cos(angle + spread),
y: end.y - headLength * Math.sin(angle + spread),
}
return `${left.x},${left.y} ${end.x},${end.y} ${right.x},${right.y}`
}
function textAnnotationIndexAtPoint(p: Point) {
if (!rect.value) return null
for (let i = annotations.value.length - 1; i >= 0; i--) {
@@ -795,6 +817,35 @@ onUnmounted(() => {
stroke-linejoin="round"
fill="none"
/>
<line
v-else-if="annotation.tool === 'line' && annotation.points.length >= 2"
:x1="annotation.points[0].x"
:y1="annotation.points[0].y"
:x2="annotation.points[annotation.points.length - 1].x"
:y2="annotation.points[annotation.points.length - 1].y"
:stroke="annotation.color"
:stroke-width="annotation.strokeWidth || 3"
stroke-linecap="round"
/>
<g v-else-if="annotation.tool === 'arrow' && annotation.points.length >= 2">
<line
:x1="annotation.points[0].x"
:y1="annotation.points[0].y"
:x2="annotation.points[annotation.points.length - 1].x"
:y2="annotation.points[annotation.points.length - 1].y"
:stroke="annotation.color"
:stroke-width="annotation.strokeWidth || 3"
stroke-linecap="round"
/>
<polyline
:points="arrowHeadPoints(annotation)"
:stroke="annotation.color"
:stroke-width="annotation.strokeWidth || 3"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
</g>
<rect
v-else-if="annotation.tool === 'rect' && annotation.points.length >= 2"
:x="Math.min(annotation.points[0].x, annotation.points[1].x)"
@@ -926,12 +977,37 @@ onUnmounted(() => {
<button
class="icon-btn"
:class="{ active: activeTool === 'pen' }"
title="划线标记"
title="画笔标记"
aria-label="画笔标记"
@click="selectTool('pen')"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 20c4-1 6-3 8-7l5-9 3 2-5 9c-2 4-5 6-9 7z" />
<path d="M14 5l5 3" />
<path d="m14.5 5.5 4-4 4 4-4 4" />
<path d="m17.5 8.5-7.8 7.8" />
<path d="M11 14.8c.8 2.8-.7 5.5-3.7 6.3-1.8.5-3.7.1-5.3-.9 2.1-.4 2.8-1.5 3.1-3.3.4-2.5 3.3-3.8 5.9-2.1Z" />
</svg>
</button>
<button
class="icon-btn"
:class="{ active: activeTool === 'line' }"
title="直线标记"
aria-label="直线标记"
@click="selectTool('line')"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 19 19 5" />
</svg>
</button>
<button
class="icon-btn"
:class="{ active: activeTool === 'arrow' }"
title="箭头标记"
aria-label="箭头标记"
@click="selectTool('arrow')"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 19 19 5" />
<path d="M11 5h8v8" />
</svg>
</button>
<button
@@ -1278,7 +1354,7 @@ onUnmounted(() => {
}
.mark-toolbar {
width: 212px;
width: 280px;
}
.action-toolbar {
box-sizing: border-box;