feat: add mosaic screenshot annotations
This commit is contained in:
@@ -24,7 +24,8 @@ interface Rect {
|
||||
h: number
|
||||
}
|
||||
|
||||
type Tool = 'pen' | 'rect' | 'ellipse' | 'text'
|
||||
type Tool = 'pen' | 'rect' | 'ellipse' | 'text' | 'mosaic-brush' | 'mosaic-rect'
|
||||
type MosaicMode = 'brush' | 'rect'
|
||||
interface Point {
|
||||
x: number
|
||||
y: number
|
||||
@@ -77,6 +78,8 @@ const activeTool = ref<Tool>('pen')
|
||||
const activeColor = ref('#ef4444')
|
||||
const activeStrokeWidth = ref(3)
|
||||
const activeFontSize = ref(20)
|
||||
const mosaicMode = ref<MosaicMode>('brush')
|
||||
const mosaicBrushWidth = ref(24)
|
||||
const textDraft = ref<{
|
||||
point: Point
|
||||
value: string
|
||||
@@ -104,6 +107,7 @@ const textDragOriginalPoint = ref<Point | null>(null)
|
||||
|
||||
const DEFAULT_TEXT_FONT_SIZE = 20
|
||||
const STROKE_WIDTHS = [2, 4, 6]
|
||||
const MOSAIC_WIDTHS = [16, 24, 36, 52]
|
||||
const FONT_SIZES = [16, 20, 28, 36]
|
||||
|
||||
const colors = [
|
||||
@@ -146,17 +150,18 @@ const sizeLabel = computed(() => {
|
||||
return `${Math.round(rect.value.w)} × ${Math.round(rect.value.h)}`
|
||||
})
|
||||
|
||||
const MARK_TOOLBAR_W = 178
|
||||
const MARK_TOOLBAR_W = 212
|
||||
const ACTION_TOOLBAR_W = 288
|
||||
const TOOLBAR_GROUP_GAP = 8
|
||||
|
||||
const toolOrder: Tool[] = ['pen', 'rect', 'ellipse', 'text']
|
||||
const toolOrder: Tool[] = ['pen', 'rect', 'ellipse', 'text', 'mosaic-brush', 'mosaic-rect']
|
||||
|
||||
const toolSettingsPos = computed(() => {
|
||||
if (!leftToolbarPos.value || !rect.value || !activeTool.value) return null
|
||||
const menuW = activeTool.value === 'text' ? 462 : 398
|
||||
const isMosaic = activeTool.value.startsWith('mosaic-')
|
||||
const menuW = isMosaic ? (mosaicMode.value === 'brush' ? 328 : 112) : activeTool.value === 'text' ? 462 : 398
|
||||
const menuH = 58
|
||||
const buttonIndex = toolOrder.indexOf(activeTool.value)
|
||||
const buttonIndex = isMosaic ? 4 : toolOrder.indexOf(activeTool.value)
|
||||
const buttonCenter = 4 + buttonIndex * 34 + 14
|
||||
let x = leftToolbarPos.value.x
|
||||
let y = leftToolbarPos.value.y + 44
|
||||
@@ -432,7 +437,7 @@ function onSelectionMouseDown(e: MouseEvent) {
|
||||
draftAnnotation.value = {
|
||||
tool: activeTool.value,
|
||||
color: activeColor.value,
|
||||
strokeWidth: activeStrokeWidth.value,
|
||||
strokeWidth: activeTool.value === 'mosaic-brush' ? mosaicBrushWidth.value : activeStrokeWidth.value,
|
||||
points: [local],
|
||||
}
|
||||
}
|
||||
@@ -466,7 +471,7 @@ function onMouseMove(e: MouseEvent) {
|
||||
annotation.points[0] = clampTextLocalPoint(next, annotation)
|
||||
}
|
||||
} else if (dragMode.value === 'annotating' && draftAnnotation.value) {
|
||||
if (activeTool.value === 'pen') {
|
||||
if (activeTool.value === 'pen' || activeTool.value === 'mosaic-brush') {
|
||||
draftAnnotation.value.points.push(localPoint(p))
|
||||
} else {
|
||||
draftAnnotation.value.points = [
|
||||
@@ -524,6 +529,19 @@ function selectTool(tool: Tool) {
|
||||
activeTool.value = tool
|
||||
}
|
||||
|
||||
function selectMosaic() {
|
||||
activeTool.value = mosaicMode.value === 'brush' ? 'mosaic-brush' : 'mosaic-rect'
|
||||
}
|
||||
|
||||
function chooseMosaicMode(mode: MosaicMode) {
|
||||
mosaicMode.value = mode
|
||||
selectMosaic()
|
||||
}
|
||||
|
||||
function chooseMosaicWidth(width: number) {
|
||||
mosaicBrushWidth.value = width
|
||||
}
|
||||
|
||||
function chooseColor(color: string) {
|
||||
activeColor.value = color
|
||||
if (textDraft.value) {
|
||||
@@ -748,6 +766,13 @@ onUnmounted(() => {
|
||||
:viewBox="`0 0 ${width} ${height}`"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<defs>
|
||||
<pattern id="mosaic-preview" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<rect width="10" height="10" fill="rgba(71,85,105,.82)" />
|
||||
<rect width="5" height="5" fill="rgba(203,213,225,.82)" />
|
||||
<rect x="5" y="5" width="5" height="5" fill="rgba(148,163,184,.82)" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<path :d="maskPath" fill="rgba(0,0,0,0.45)" fill-rule="evenodd" />
|
||||
<rect
|
||||
v-if="rect"
|
||||
@@ -811,6 +836,25 @@ onUnmounted(() => {
|
||||
{{ annotation.text }}
|
||||
</text>
|
||||
</template>
|
||||
<polyline
|
||||
v-else-if="annotation.tool === 'mosaic-brush'"
|
||||
:points="annotation.points.map((p) => `${p.x},${p.y}`).join(' ')"
|
||||
stroke="url(#mosaic-preview)"
|
||||
:stroke-width="annotation.strokeWidth || 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<rect
|
||||
v-else-if="annotation.tool === 'mosaic-rect' && annotation.points.length >= 2"
|
||||
:x="Math.min(annotation.points[0].x, annotation.points[1].x)"
|
||||
:y="Math.min(annotation.points[0].y, annotation.points[1].y)"
|
||||
:width="Math.abs(annotation.points[1].x - annotation.points[0].x)"
|
||||
:height="Math.abs(annotation.points[1].y - annotation.points[0].y)"
|
||||
fill="url(#mosaic-preview)"
|
||||
stroke="rgba(255,255,255,.8)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
@@ -922,6 +966,21 @@ onUnmounted(() => {
|
||||
<path d="M9 19h6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="icon-btn"
|
||||
:class="{ active: activeTool.startsWith('mosaic-') }"
|
||||
title="马赛克标记"
|
||||
aria-label="马赛克标记"
|
||||
:aria-expanded="activeTool.startsWith('mosaic-')"
|
||||
@click="selectMosaic"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="4" y="4" width="6" height="6" />
|
||||
<rect x="14" y="4" width="6" height="6" />
|
||||
<rect x="4" y="14" width="6" height="6" />
|
||||
<rect x="14" y="14" width="6" height="6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="icon-btn"
|
||||
:class="{ muted: annotations.length === 0 }"
|
||||
@@ -947,7 +1006,55 @@ onUnmounted(() => {
|
||||
}"
|
||||
@mousedown.stop
|
||||
>
|
||||
<div v-if="activeTool !== 'text'" class="setting-group stroke-group">
|
||||
<template v-if="activeTool.startsWith('mosaic-')">
|
||||
<div class="setting-group mosaic-mode-group" role="group" aria-label="马赛克标记方式">
|
||||
<button
|
||||
class="mode-choice"
|
||||
:class="{ active: mosaicMode === 'brush' }"
|
||||
title="涂抹"
|
||||
aria-label="涂抹马赛克"
|
||||
:aria-pressed="mosaicMode === 'brush'"
|
||||
@click="chooseMosaicMode('brush')"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M9 11V5.5a2 2 0 0 1 4 0V10" />
|
||||
<path d="M13 10V8a2 2 0 0 1 4 0v3" />
|
||||
<path d="M17 10a2 2 0 0 1 4 0v4.5c0 4-2.5 6.5-6.5 6.5h-1.2a6 6 0 0 1-4.7-2.3L4 13.2a1.9 1.9 0 0 1 2.8-2.5L9 12.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="mode-choice"
|
||||
:class="{ active: mosaicMode === 'rect' }"
|
||||
title="框选"
|
||||
aria-label="框选马赛克"
|
||||
:aria-pressed="mosaicMode === 'rect'"
|
||||
@click="chooseMosaicMode('rect')"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="2.5" y="2.5" width="19" height="19" rx="2" />
|
||||
<rect class="pixel-block" x="6" y="6" width="4" height="4" rx=".5" />
|
||||
<rect class="pixel-block" x="14" y="6" width="4" height="4" rx=".5" />
|
||||
<rect class="pixel-block" x="6" y="14" width="4" height="4" rx=".5" />
|
||||
<rect class="pixel-block" x="14" y="14" width="4" height="4" rx=".5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="mosaicMode === 'brush'">
|
||||
<div class="settings-divider" />
|
||||
<span class="setting-label">粗细</span>
|
||||
<div class="setting-group stroke-group">
|
||||
<button
|
||||
v-for="widthValue in MOSAIC_WIDTHS"
|
||||
:key="widthValue"
|
||||
class="stroke-choice"
|
||||
:class="{ active: mosaicBrushWidth === widthValue }"
|
||||
:title="`${widthValue}px`"
|
||||
@click="chooseMosaicWidth(widthValue)"
|
||||
><span :style="{ width: Math.min(22, widthValue / 2) + 'px', height: Math.min(22, widthValue / 2) + 'px' }" /></button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-else-if="activeTool !== 'text'" class="setting-group stroke-group">
|
||||
<button
|
||||
v-for="widthValue in STROKE_WIDTHS"
|
||||
:key="widthValue"
|
||||
@@ -971,8 +1078,8 @@ onUnmounted(() => {
|
||||
{{ size }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="settings-divider" />
|
||||
<div class="setting-group color-group">
|
||||
<div v-if="!activeTool.startsWith('mosaic-')" class="settings-divider" />
|
||||
<div v-if="!activeTool.startsWith('mosaic-')" class="setting-group color-group">
|
||||
<button
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
@@ -1171,7 +1278,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.mark-toolbar {
|
||||
width: 178px;
|
||||
width: 212px;
|
||||
}
|
||||
.action-toolbar {
|
||||
box-sizing: border-box;
|
||||
@@ -1183,7 +1290,8 @@ onUnmounted(() => {
|
||||
.action-btn,
|
||||
.swatch,
|
||||
.stroke-choice,
|
||||
.font-choice {
|
||||
.font-choice,
|
||||
.mode-choice {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
@@ -1341,6 +1449,53 @@ onUnmounted(() => {
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mosaic-mode-group {
|
||||
gap: 0;
|
||||
padding: 2px;
|
||||
border-radius: 7px;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
.mode-choice {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
color: #475569;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mode-choice svg {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.mode-choice svg .pixel-block {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
.mode-choice:hover,
|
||||
.mode-choice.active {
|
||||
color: #1d4ed8;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
.setting-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.stroke-choice:hover,
|
||||
.stroke-choice.active,
|
||||
.font-choice:hover,
|
||||
|
||||
Reference in New Issue
Block a user