Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions src/components/image-editor/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,27 @@ export const Canvas = forwardRef<CanvasHandle, Props>(function Canvas(
return
}

// Note: click → prompt → commit a sticky-note marker. Empty text = no-op.
if (tool === 'note') {
const text = window.prompt(t('pages.imageEditor.notePrompt'), '') ?? ''
if (text.trim()) {
onCommitLayer({
id: crypto.randomUUID(),
name: 'Note',
visible: true,
opacity: 100,
blend: 'normal',
kind: 'annotation',
shape: { kind: 'note', x: p.x, y: p.y, text, color: '#fde047' },
} as AnnotationLayer)
}
return
}

// Path Selection (arrowPath) is the vector counterpart of Move — until Pen
// exists end-to-end, treat it identically to the no-tool selection arrow.
// Drawing tools take priority over selection.
if (tool !== 'none') {
if (tool !== 'none' && tool !== 'arrowPath') {
startDrawing(p)
return
}
Expand Down Expand Up @@ -643,6 +662,15 @@ export const Canvas = forwardRef<CanvasHandle, Props>(function Canvas(
shape: { kind: 'blur', x: p.x, y: p.y, w: 0, h: 0, radius: 8 },
} as AnnotationLayer,
})
} else if (tool === 'frame') {
setInteraction({
kind: 'drawing',
layer: {
...baseLayer('Frame'),
kind: 'annotation',
shape: { kind: 'frame', x: p.x, y: p.y, w: 0, h: 0 },
} as AnnotationLayer,
})
} else if (tool === 'brush' || tool === 'eraser') {
setInteraction({
kind: 'drawing',
Expand Down Expand Up @@ -708,7 +736,13 @@ export const Canvas = forwardRef<CanvasHandle, Props>(function Canvas(
const drawing = interaction.layer
if (drawing.kind === 'annotation') {
const s = drawing.shape
if (s.kind === 'rect' || s.kind === 'mosaic' || s.kind === 'ellipse' || s.kind === 'blur') {
if (
s.kind === 'rect' ||
s.kind === 'mosaic' ||
s.kind === 'ellipse' ||
s.kind === 'blur' ||
s.kind === 'frame'
) {
setInteraction({
kind: 'drawing',
layer: { ...drawing, shape: { ...s, w: p.x - s.x, h: p.y - s.y } } as AnnotationLayer,
Expand Down Expand Up @@ -765,12 +799,16 @@ export const Canvas = forwardRef<CanvasHandle, Props>(function Canvas(
tool === 'marquee' ||
tool === 'lasso' ||
tool === 'polyLasso' ||
tool === 'wand'
tool === 'wand' ||
tool === 'note' ||
tool === 'frame'
) {
setHoverCursor('crosshair')
return
}
if (tool !== 'none') {
// Path Selection (arrowPath) shares the move/select cursor logic with
// 'none' — same hit-testing and resize-handle hover beneath.
if (tool !== 'none' && tool !== 'arrowPath') {
setHoverCursor('crosshair')
return
}
Expand Down Expand Up @@ -843,7 +881,13 @@ function shouldDiscardDrawing(layer: Layer): boolean {
}
if (layer.kind === 'annotation') {
const s = layer.shape
if (s.kind === 'rect' || s.kind === 'mosaic' || s.kind === 'ellipse' || s.kind === 'blur') {
if (
s.kind === 'rect' ||
s.kind === 'mosaic' ||
s.kind === 'ellipse' ||
s.kind === 'blur' ||
s.kind === 'frame'
) {
return Math.abs(s.w) < 4 && Math.abs(s.h) < 4
}
if (s.kind === 'arrow' || s.kind === 'line') {
Expand Down
4 changes: 4 additions & 0 deletions src/components/image-editor/LayersPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,5 +186,9 @@ function layerLabelKey(layer: Layer): string {
return 'pages.imageEditor.annoLabel.line'
case 'blur':
return 'pages.imageEditor.annoLabel.blur'
case 'note':
return 'pages.imageEditor.annoLabel.note'
case 'frame':
return 'pages.imageEditor.annoLabel.frame'
}
}
48 changes: 48 additions & 0 deletions src/components/image-editor/OptionsBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,54 @@ export function OptionsBar({
)
}

if (tool === 'hand') {
return (
<div className="pf-options">
<div className="pf-opt-group" style={{ borderRight: 0 }}>
<span className="pf-opt-label" style={{ marginRight: 0 }}>
{t('pages.imageEditor.handHint')}
</span>
</div>
</div>
)
}

if (tool === 'note') {
return (
<div className="pf-options">
<div className="pf-opt-group" style={{ borderRight: 0 }}>
<span className="pf-opt-label" style={{ marginRight: 0 }}>
{t('pages.imageEditor.noteHint')}
</span>
</div>
</div>
)
}

if (tool === 'frame') {
return (
<div className="pf-options">
<div className="pf-opt-group" style={{ borderRight: 0 }}>
<span className="pf-opt-label" style={{ marginRight: 0 }}>
{t('pages.imageEditor.frameHint')}
</span>
</div>
</div>
)
}

if (tool === 'arrowPath') {
return (
<div className="pf-options">
<div className="pf-opt-group" style={{ borderRight: 0 }}>
<span className="pf-opt-label" style={{ marginRight: 0 }}>
{t('pages.imageEditor.arrowPathHint')}
</span>
</div>
</div>
)
}

if (tool === 'mask' || tool === 'mosaic' || tool === 'blur') {
return (
<div className="pf-options">
Expand Down
20 changes: 19 additions & 1 deletion src/components/image-editor/ToolsPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
Frame,
Hand,
Lasso,
LayoutTemplate,
Moon,
MousePointer,
MousePointer2,
PaintBucket,
PenTool,
Expand All @@ -25,6 +27,7 @@ import {
Square,
SquareDashed,
Squircle,
StickyNote,
Sun,
Type,
Wand2,
Expand Down Expand Up @@ -140,6 +143,12 @@ const GROUPS: ToolDef[][] = [
shortcut: 'P',
stub: true,
},
{
id: 'arrowPath',
icon: <MousePointer className="h-4 w-4" />,
labelKey: 'pages.imageEditor.tool.arrowPath',
shortcut: 'A',
},
{ id: 'text', icon: <Type className="h-4 w-4" />, labelKey: 'pages.imageEditor.tool.text', shortcut: 'T' },
{ id: 'rect', icon: <Square className="h-4 w-4" />, labelKey: 'pages.imageEditor.tool.rect', shortcut: 'U' },
{ id: 'ellipse', icon: <Circle className="h-4 w-4" />, labelKey: 'pages.imageEditor.tool.ellipse', shortcut: 'U' },
Expand All @@ -158,6 +167,16 @@ const GROUPS: ToolDef[][] = [
// 4. Annotation / mask
[
{ id: 'mask', icon: <Frame className="h-4 w-4" />, labelKey: 'pages.imageEditor.tool.mask' },
{
id: 'frame',
icon: <LayoutTemplate className="h-4 w-4" />,
labelKey: 'pages.imageEditor.tool.frame',
},
{
id: 'note',
icon: <StickyNote className="h-4 w-4" />,
labelKey: 'pages.imageEditor.tool.note',
},
{ id: 'mosaic', icon: <Squircle className="h-4 w-4" />, labelKey: 'pages.imageEditor.tool.mosaic' },
{ id: 'blur', icon: <Aperture className="h-4 w-4" />, labelKey: 'pages.imageEditor.tool.blur' },
],
Expand All @@ -168,7 +187,6 @@ const GROUPS: ToolDef[][] = [
icon: <Hand className="h-4 w-4" />,
labelKey: 'pages.imageEditor.tool.hand',
shortcut: 'H',
stub: true,
},
{
id: 'rotateView',
Expand Down
4 changes: 1 addition & 3 deletions src/components/image-editor/tool-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,5 @@ export const STUB_TOOLS: ReadonlySet<Tool> = new Set<Tool>([
'stamp',
'historyBrush',
'pen',
'arrowPath',
'frame',
'note',
// hand, arrowPath, frame, note are functional — see ImageEditor / Canvas.
])
13 changes: 10 additions & 3 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,13 @@
"toolHint": "Drawing: {{tool}} — drag on the canvas; click the same tool again to deselect",
"adjust": "Adjust",
"noLayerSelected": "No layer selected",
"focusEnterHint": "Enter focus mode (F) — hides the toolbox chrome and gives the canvas the full viewport",
"focusExitHint": "Exit focus mode (F or Esc)",
"focusEnterHint": "Fullscreen — hide the toolbox chrome and give the canvas the full viewport",
"focusExitHint": "Exit fullscreen (Esc)",
"handHint": "Hand (H): drag to pan the canvas. Same as holding Space with any other tool.",
"noteHint": "Note (N): click to drop a sticky-note marker. Notes show on screen but never bake into export.",
"notePrompt": "Note text",
"frameHint": "Frame: drag to draw a rectangular placeholder frame.",
"arrowPathHint": "Path Selection (A): click to select a vector layer, drag to move. Behaves like the Move tool — full vector-anchor selection arrives with the Pen tool.",
"currentTool": "Tool",
"zoomFit": "Fit (Cmd/Ctrl+0)",
"style": "Style",
Expand Down Expand Up @@ -599,7 +604,9 @@
"gradient": "Gradient",
"blur": "Blur region",
"dodge": "Dodge stroke",
"burn": "Burn stroke"
"burn": "Burn stroke",
"note": "Note",
"frame": "Frame"
},
"project": "Project",
"projectSave": "Save project (.json)",
Expand Down
13 changes: 10 additions & 3 deletions src/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,13 @@
"toolHint": "绘制:{{tool}} — 在画布上拖拽;再次点击同一工具可取消选择",
"adjust": "调整",
"noLayerSelected": "未选中图层",
"focusEnterHint": "进入焦点模式(F)——隐藏工具站界面,画布占满整个视口",
"focusExitHint": "退出焦点模式(F 或 Esc)",
"focusEnterHint": "全屏 —— 隐藏工具站界面,画布占满整个视口",
"focusExitHint": "退出全屏(Esc)",
"handHint": "抓手 (H):拖动以平移画布,等同于按住空格 + 任意工具。",
"noteHint": "便签 (N):单击在画布上放一个便签标记。便签只在屏幕上显示,不会写入导出图片。",
"notePrompt": "便签文字",
"frameHint": "画框:拖动绘制一个矩形占位框。",
"arrowPathHint": "路径选择 (A):单击选中矢量图层,拖动移动。当前与移动工具行为一致 —— 完整的锚点选择需待钢笔工具上线。",
"currentTool": "工具",
"zoomFit": "适配 (Cmd/Ctrl+0)",
"style": "样式",
Expand Down Expand Up @@ -599,7 +604,9 @@
"gradient": "渐变",
"blur": "模糊区域",
"dodge": "减淡笔触",
"burn": "加深笔触"
"burn": "加深笔触",
"note": "便签",
"frame": "画框"
},
"project": "项目",
"projectSave": "保存项目 (.json)",
Expand Down
83 changes: 83 additions & 0 deletions src/lib/image-editor/drawing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type {
BlurShape,
BrushShape,
EllipseShape,
FrameShape,
ImageShape,
LineShape,
MosaicShape,
NoteShape,
RectShape,
Shape,
TextShape,
Expand Down Expand Up @@ -55,9 +57,90 @@ export function drawShape(
case 'blur':
drawBlurRegion(ctx, shape, scale, underlying)
break
case 'note':
drawNote(ctx, shape, scale)
break
case 'frame':
drawFrame(ctx, shape, scale)
break
}
}

/**
* Sticky-note marker. Rendered as a small folded-corner rectangle in the
* given colour with a 1-line text label below it. The full text is in the
* Properties panel on hover; the label is just a preview.
*/
function drawNote(ctx: CanvasRenderingContext2D, s: NoteShape, scale: number) {
const x = s.x * scale
const y = s.y * scale
const size = 16 * scale
const fold = size / 3
ctx.save()
// Note body (folded-corner pentagon)
ctx.fillStyle = s.color
ctx.strokeStyle = '#1a1a1a'
ctx.lineWidth = Math.max(1, scale)
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x + size - fold, y)
ctx.lineTo(x + size, y + fold)
ctx.lineTo(x + size, y + size)
ctx.lineTo(x, y + size)
ctx.closePath()
ctx.fill()
ctx.stroke()
// Folded-corner triangle
ctx.fillStyle = 'rgba(0,0,0,0.18)'
ctx.beginPath()
ctx.moveTo(x + size - fold, y)
ctx.lineTo(x + size - fold, y + fold)
ctx.lineTo(x + size, y + fold)
ctx.closePath()
ctx.fill()
ctx.stroke()
// 1-line preview label below
const label = s.text.split('\n')[0].slice(0, 24)
if (label) {
ctx.fillStyle = '#1a1a1a'
ctx.font = `${Math.round(11 * scale)}px sans-serif`
ctx.textBaseline = 'top'
ctx.fillText(label, x, y + size + 2 * scale)
}
ctx.restore()
}

/**
* Frame placeholder. Dashed grey rect with a diagonal X across — same visual
* convention PS uses for an empty Frame Tool layer.
*/
function drawFrame(ctx: CanvasRenderingContext2D, s: FrameShape, scale: number) {
const x = (s.w >= 0 ? s.x : s.x + s.w) * scale
const y = (s.h >= 0 ? s.y : s.y + s.h) * scale
const w = Math.abs(s.w) * scale
const h = Math.abs(s.h) * scale
if (w < 1 || h < 1) return
ctx.save()
ctx.strokeStyle = '#888'
ctx.lineWidth = Math.max(1, scale)
ctx.setLineDash([6 * scale, 4 * scale])
ctx.strokeRect(x, y, w, h)
ctx.setLineDash([])
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x + w, y + h)
ctx.moveTo(x + w, y)
ctx.lineTo(x, y + h)
ctx.stroke()
if (s.name) {
ctx.fillStyle = '#888'
ctx.font = `${Math.round(12 * scale)}px sans-serif`
ctx.textBaseline = 'top'
ctx.fillText(s.name, x + 4 * scale, y + 4 * scale)
}
ctx.restore()
}

/**
* Region blur — sample the underlying canvas under the rect, draw it back at
* the same place with `ctx.filter = blur(Npx)` applied. Same pattern as
Expand Down
Loading
Loading