feat: add line and arrow annotations
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user