@plannotator/ui 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- package/utils/wideMode.ts +48 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Tool } from './types';
|
|
3
|
+
import { COLORS } from './types';
|
|
4
|
+
|
|
5
|
+
interface ToolbarProps {
|
|
6
|
+
tool: Tool;
|
|
7
|
+
color: string;
|
|
8
|
+
strokeSize: number;
|
|
9
|
+
canUndo: boolean;
|
|
10
|
+
onToolChange: (tool: Tool) => void;
|
|
11
|
+
onColorChange: (color: string) => void;
|
|
12
|
+
onStrokeSizeChange: (size: number) => void;
|
|
13
|
+
onUndo: () => void;
|
|
14
|
+
onClear: () => void;
|
|
15
|
+
onSave: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PenIcon = () => (
|
|
19
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
20
|
+
<path d="M12 19l7-7 3 3-7 7-3-3z" />
|
|
21
|
+
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" />
|
|
22
|
+
<path d="M2 2l7.586 7.586" />
|
|
23
|
+
<circle cx="11" cy="11" r="2" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const ArrowIcon = () => (
|
|
28
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
29
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
30
|
+
<polyline points="12 5 19 12 12 19" />
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const CircleIcon = () => (
|
|
35
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
36
|
+
<circle cx="12" cy="12" r="10" />
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const UndoIcon = () => (
|
|
41
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
42
|
+
<path d="M3 7v6h6" />
|
|
43
|
+
<path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13" />
|
|
44
|
+
</svg>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const ClearIcon = () => (
|
|
48
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
49
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const MinusIcon = () => (
|
|
54
|
+
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
55
|
+
<path d="M5 12h14" />
|
|
56
|
+
</svg>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const PlusIcon = () => (
|
|
60
|
+
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
61
|
+
<path d="M12 5v14M5 12h14" />
|
|
62
|
+
</svg>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const CheckIcon = () => (
|
|
66
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5}>
|
|
67
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
68
|
+
</svg>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const TOOLS: { id: Tool; icon: React.FC; label: string }[] = [
|
|
72
|
+
{ id: 'pen', icon: PenIcon, label: 'Pen (1)' },
|
|
73
|
+
{ id: 'arrow', icon: ArrowIcon, label: 'Arrow (2)' },
|
|
74
|
+
{ id: 'circle', icon: CircleIcon, label: 'Circle (3)' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const STROKE_SIZES = [3, 6, 10, 16, 24];
|
|
78
|
+
|
|
79
|
+
export const Toolbar: React.FC<ToolbarProps> = ({
|
|
80
|
+
tool,
|
|
81
|
+
color,
|
|
82
|
+
strokeSize,
|
|
83
|
+
canUndo,
|
|
84
|
+
onToolChange,
|
|
85
|
+
onColorChange,
|
|
86
|
+
onStrokeSizeChange,
|
|
87
|
+
onUndo,
|
|
88
|
+
onClear,
|
|
89
|
+
onSave,
|
|
90
|
+
}) => {
|
|
91
|
+
const currentSizeIndex = STROKE_SIZES.indexOf(strokeSize);
|
|
92
|
+
const canDecrease = currentSizeIndex > 0;
|
|
93
|
+
const canIncrease = currentSizeIndex < STROKE_SIZES.length - 1;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="flex items-center gap-2 px-3 py-2 bg-popover border border-border rounded-lg shadow-xl">
|
|
97
|
+
{/* Tools */}
|
|
98
|
+
<div className="flex items-center gap-1">
|
|
99
|
+
{TOOLS.map(({ id, icon: Icon, label }) => (
|
|
100
|
+
<button
|
|
101
|
+
key={id}
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={() => onToolChange(id)}
|
|
104
|
+
title={label}
|
|
105
|
+
className={`p-1.5 rounded transition-colors ${
|
|
106
|
+
tool === id
|
|
107
|
+
? 'bg-primary text-primary-foreground'
|
|
108
|
+
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
109
|
+
}`}
|
|
110
|
+
>
|
|
111
|
+
<Icon />
|
|
112
|
+
</button>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div className="w-px h-5 bg-border" />
|
|
117
|
+
|
|
118
|
+
{/* Stroke size */}
|
|
119
|
+
<div className="flex items-center gap-0.5">
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={() => canDecrease && onStrokeSizeChange(STROKE_SIZES[currentSizeIndex - 1])}
|
|
123
|
+
disabled={!canDecrease}
|
|
124
|
+
title="Smaller stroke"
|
|
125
|
+
className={`p-1 rounded transition-colors ${
|
|
126
|
+
canDecrease
|
|
127
|
+
? 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
128
|
+
: 'text-muted-foreground/30 cursor-not-allowed'
|
|
129
|
+
}`}
|
|
130
|
+
>
|
|
131
|
+
<MinusIcon />
|
|
132
|
+
</button>
|
|
133
|
+
<div
|
|
134
|
+
className="w-5 h-5 flex items-center justify-center"
|
|
135
|
+
title={`Stroke size: ${strokeSize}`}
|
|
136
|
+
>
|
|
137
|
+
<div
|
|
138
|
+
className="rounded-full"
|
|
139
|
+
style={{
|
|
140
|
+
width: Math.min(strokeSize + 2, 16),
|
|
141
|
+
height: Math.min(strokeSize + 2, 16),
|
|
142
|
+
backgroundColor: color,
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={() => canIncrease && onStrokeSizeChange(STROKE_SIZES[currentSizeIndex + 1])}
|
|
149
|
+
disabled={!canIncrease}
|
|
150
|
+
title="Larger stroke"
|
|
151
|
+
className={`p-1 rounded transition-colors ${
|
|
152
|
+
canIncrease
|
|
153
|
+
? 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
154
|
+
: 'text-muted-foreground/30 cursor-not-allowed'
|
|
155
|
+
}`}
|
|
156
|
+
>
|
|
157
|
+
<PlusIcon />
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div className="w-px h-5 bg-border" />
|
|
162
|
+
|
|
163
|
+
{/* Colors */}
|
|
164
|
+
<div className="flex items-center gap-1">
|
|
165
|
+
{COLORS.map((c) => (
|
|
166
|
+
<button
|
|
167
|
+
key={c}
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={() => onColorChange(c)}
|
|
170
|
+
className={`w-5 h-5 rounded-full border-2 transition-transform ${
|
|
171
|
+
color === c ? 'scale-110 border-foreground' : 'border-transparent hover:scale-105'
|
|
172
|
+
}`}
|
|
173
|
+
style={{ backgroundColor: c }}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className="w-px h-5 bg-border" />
|
|
179
|
+
|
|
180
|
+
{/* Undo */}
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={onUndo}
|
|
184
|
+
disabled={!canUndo}
|
|
185
|
+
title="Undo (Cmd+Z)"
|
|
186
|
+
className={`p-1.5 rounded transition-colors ${
|
|
187
|
+
canUndo
|
|
188
|
+
? 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
189
|
+
: 'text-muted-foreground/30 cursor-not-allowed'
|
|
190
|
+
}`}
|
|
191
|
+
>
|
|
192
|
+
<UndoIcon />
|
|
193
|
+
</button>
|
|
194
|
+
|
|
195
|
+
{/* Clear all */}
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
onClick={onClear}
|
|
199
|
+
title="Clear all"
|
|
200
|
+
className="p-1.5 rounded transition-colors hover:bg-muted text-muted-foreground hover:text-destructive"
|
|
201
|
+
>
|
|
202
|
+
<ClearIcon />
|
|
203
|
+
</button>
|
|
204
|
+
|
|
205
|
+
<div className="w-px h-5 bg-border" />
|
|
206
|
+
|
|
207
|
+
{/* Save */}
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
onClick={onSave}
|
|
211
|
+
title="Save (Esc)"
|
|
212
|
+
className="p-1.5 rounded transition-colors bg-success text-success-foreground hover:opacity-90"
|
|
213
|
+
>
|
|
214
|
+
<CheckIcon />
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { Canvas } from './Canvas';
|
|
3
|
+
import { Toolbar } from './Toolbar';
|
|
4
|
+
import { renderStroke } from './utils';
|
|
5
|
+
import type { Point, Stroke, Tool, AnnotatorState } from './types';
|
|
6
|
+
import { DEFAULT_STATE } from './types';
|
|
7
|
+
|
|
8
|
+
interface ImageAnnotatorProps {
|
|
9
|
+
imageSrc: string;
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
onAccept: (blob: Blob, hasDrawings: boolean, name: string) => Promise<void>;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
/** Pre-populated image name (derived from filename) */
|
|
14
|
+
initialName?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ImageAnnotator: React.FC<ImageAnnotatorProps> = ({
|
|
18
|
+
imageSrc,
|
|
19
|
+
isOpen,
|
|
20
|
+
onAccept,
|
|
21
|
+
onClose,
|
|
22
|
+
initialName = '',
|
|
23
|
+
}) => {
|
|
24
|
+
const [state, setState] = useState<AnnotatorState>(DEFAULT_STATE);
|
|
25
|
+
const [saving, setSaving] = useState(false);
|
|
26
|
+
const [name, setName] = useState(initialName);
|
|
27
|
+
const imageRef = useRef<HTMLImageElement | null>(null);
|
|
28
|
+
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
29
|
+
|
|
30
|
+
// Reset state when dialog opens
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (isOpen) {
|
|
33
|
+
setState(DEFAULT_STATE);
|
|
34
|
+
setName(initialName);
|
|
35
|
+
}
|
|
36
|
+
}, [isOpen, initialName]);
|
|
37
|
+
|
|
38
|
+
// Keyboard shortcuts
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!isOpen) return;
|
|
41
|
+
|
|
42
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
43
|
+
// Don't intercept when typing in the name input
|
|
44
|
+
const target = e.target as HTMLElement;
|
|
45
|
+
if (target.tagName === 'INPUT') {
|
|
46
|
+
if (e.key === 'Escape') {
|
|
47
|
+
// Blur and let the next Escape close
|
|
48
|
+
target.blur();
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Escape or Enter to accept
|
|
55
|
+
if (e.key === 'Escape' || e.key === 'Enter') {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
handleAccept();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Cmd+Z to undo
|
|
62
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'z') {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
handleUndo();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 1/2/3 to switch tools
|
|
69
|
+
if (e.key === '1') setState(s => ({ ...s, tool: 'pen' }));
|
|
70
|
+
if (e.key === '2') setState(s => ({ ...s, tool: 'arrow' }));
|
|
71
|
+
if (e.key === '3') setState(s => ({ ...s, tool: 'circle' }));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
75
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
76
|
+
}, [isOpen, state.strokes]);
|
|
77
|
+
|
|
78
|
+
const handleStrokeStart = useCallback((point: Point) => {
|
|
79
|
+
const id = crypto.randomUUID();
|
|
80
|
+
setState(s => ({
|
|
81
|
+
...s,
|
|
82
|
+
currentStroke: {
|
|
83
|
+
id,
|
|
84
|
+
tool: s.tool,
|
|
85
|
+
points: [point],
|
|
86
|
+
color: s.color,
|
|
87
|
+
size: s.strokeSize,
|
|
88
|
+
},
|
|
89
|
+
}));
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const handleStrokeMove = useCallback((point: Point) => {
|
|
93
|
+
setState(s => {
|
|
94
|
+
if (!s.currentStroke) return s;
|
|
95
|
+
return {
|
|
96
|
+
...s,
|
|
97
|
+
currentStroke: {
|
|
98
|
+
...s.currentStroke,
|
|
99
|
+
points: [...s.currentStroke.points, point],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const handleStrokeEnd = useCallback(() => {
|
|
106
|
+
setState(s => {
|
|
107
|
+
if (!s.currentStroke || s.currentStroke.points.length < 2) {
|
|
108
|
+
return { ...s, currentStroke: null };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
...s,
|
|
112
|
+
strokes: [...s.strokes, s.currentStroke],
|
|
113
|
+
currentStroke: null,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const handleUndo = useCallback(() => {
|
|
119
|
+
setState(s => ({
|
|
120
|
+
...s,
|
|
121
|
+
strokes: s.strokes.slice(0, -1),
|
|
122
|
+
}));
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
const handleClear = useCallback(() => {
|
|
126
|
+
setState(s => ({
|
|
127
|
+
...s,
|
|
128
|
+
strokes: [],
|
|
129
|
+
currentStroke: null,
|
|
130
|
+
}));
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
const handleImageLoad = useCallback((img: HTMLImageElement) => {
|
|
134
|
+
imageRef.current = img;
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const handleAccept = async () => {
|
|
138
|
+
if (saving) return;
|
|
139
|
+
setSaving(true);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const img = imageRef.current;
|
|
143
|
+
if (!img) {
|
|
144
|
+
onClose();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const hasDrawings = state.strokes.length > 0;
|
|
149
|
+
const finalName = name.trim() || initialName || 'image';
|
|
150
|
+
|
|
151
|
+
// If no drawings, just pass through original image
|
|
152
|
+
if (!hasDrawings) {
|
|
153
|
+
const response = await fetch(imageSrc);
|
|
154
|
+
const blob = await response.blob();
|
|
155
|
+
await onAccept(blob, false, finalName);
|
|
156
|
+
onClose();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Composite image + drawings
|
|
161
|
+
const canvas = document.createElement('canvas');
|
|
162
|
+
const ctx = canvas.getContext('2d')!;
|
|
163
|
+
|
|
164
|
+
canvas.width = img.naturalWidth;
|
|
165
|
+
canvas.height = img.naturalHeight;
|
|
166
|
+
|
|
167
|
+
// Draw original image
|
|
168
|
+
ctx.drawImage(img, 0, 0);
|
|
169
|
+
|
|
170
|
+
// Scale factor from display size to natural size
|
|
171
|
+
const scale = img.naturalWidth / img.clientWidth;
|
|
172
|
+
|
|
173
|
+
// Draw all strokes at full resolution
|
|
174
|
+
state.strokes.forEach(stroke => {
|
|
175
|
+
renderStroke(ctx, stroke, scale);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Convert to blob
|
|
179
|
+
canvas.toBlob(async (blob) => {
|
|
180
|
+
if (blob) {
|
|
181
|
+
await onAccept(blob, true, finalName);
|
|
182
|
+
}
|
|
183
|
+
onClose();
|
|
184
|
+
}, 'image/png');
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error('Failed to save annotated image:', err);
|
|
187
|
+
onClose();
|
|
188
|
+
} finally {
|
|
189
|
+
setSaving(false);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
194
|
+
if (e.target === e.currentTarget) {
|
|
195
|
+
handleAccept();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (!isOpen) return null;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div
|
|
203
|
+
className="fixed inset-0 z-[200] flex items-center justify-center bg-background/90 backdrop-blur-sm"
|
|
204
|
+
data-popover-layer
|
|
205
|
+
onClick={handleBackdropClick}
|
|
206
|
+
>
|
|
207
|
+
{/* Canvas with image and toolbar */}
|
|
208
|
+
<div className="relative flex flex-col items-center gap-3" onClick={(e) => e.stopPropagation()}>
|
|
209
|
+
{/* Toolbar - above image */}
|
|
210
|
+
<Toolbar
|
|
211
|
+
tool={state.tool}
|
|
212
|
+
color={state.color}
|
|
213
|
+
strokeSize={state.strokeSize}
|
|
214
|
+
canUndo={state.strokes.length > 0}
|
|
215
|
+
onToolChange={(tool) => setState(s => ({ ...s, tool }))}
|
|
216
|
+
onColorChange={(color) => setState(s => ({ ...s, color }))}
|
|
217
|
+
onStrokeSizeChange={(strokeSize) => setState(s => ({ ...s, strokeSize }))}
|
|
218
|
+
onUndo={handleUndo}
|
|
219
|
+
onClear={handleClear}
|
|
220
|
+
onSave={handleAccept}
|
|
221
|
+
/>
|
|
222
|
+
|
|
223
|
+
<Canvas
|
|
224
|
+
imageSrc={imageSrc}
|
|
225
|
+
strokes={state.strokes}
|
|
226
|
+
currentStroke={state.currentStroke}
|
|
227
|
+
tool={state.tool}
|
|
228
|
+
color={state.color}
|
|
229
|
+
onStrokeStart={handleStrokeStart}
|
|
230
|
+
onStrokeMove={handleStrokeMove}
|
|
231
|
+
onStrokeEnd={handleStrokeEnd}
|
|
232
|
+
onImageLoad={handleImageLoad}
|
|
233
|
+
/>
|
|
234
|
+
|
|
235
|
+
{/* Image name input */}
|
|
236
|
+
{initialName && (
|
|
237
|
+
<div className="flex items-center gap-2 w-full max-w-xs">
|
|
238
|
+
<label className="text-xs text-muted-foreground whitespace-nowrap">Name</label>
|
|
239
|
+
<input
|
|
240
|
+
ref={nameInputRef}
|
|
241
|
+
type="text"
|
|
242
|
+
value={name}
|
|
243
|
+
onChange={(e) => setName(e.target.value)}
|
|
244
|
+
onKeyDown={(e) => {
|
|
245
|
+
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
handleAccept();
|
|
248
|
+
}
|
|
249
|
+
}}
|
|
250
|
+
className="flex-1 px-2 py-1 text-xs bg-muted/50 border border-border rounded-md text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
251
|
+
placeholder="Image name..."
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{/* Accept hint */}
|
|
257
|
+
<div className="text-xs text-muted-foreground">
|
|
258
|
+
Press <kbd className="px-1.5 py-0.5 bg-muted rounded text-foreground">Esc</kbd> or <kbd className="px-1.5 py-0.5 bg-muted rounded text-foreground">Enter</kbd> or click outside to accept
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{/* Loading overlay */}
|
|
263
|
+
{saving && (
|
|
264
|
+
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
|
|
265
|
+
<div className="text-sm text-muted-foreground">Saving...</div>
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export default ImageAnnotator;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type Tool = 'pen' | 'arrow' | 'circle';
|
|
2
|
+
|
|
3
|
+
export interface Point {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
pressure?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Stroke {
|
|
10
|
+
id: string;
|
|
11
|
+
tool: Tool;
|
|
12
|
+
points: Point[];
|
|
13
|
+
color: string;
|
|
14
|
+
size: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AnnotatorState {
|
|
18
|
+
tool: Tool;
|
|
19
|
+
color: string;
|
|
20
|
+
strokeSize: number;
|
|
21
|
+
strokes: Stroke[];
|
|
22
|
+
currentStroke: Stroke | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const COLORS = [
|
|
26
|
+
'#ef4444', // red
|
|
27
|
+
'#eab308', // yellow
|
|
28
|
+
'#22c55e', // green
|
|
29
|
+
'#3b82f6', // blue
|
|
30
|
+
'#ffffff', // white
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_STATE: AnnotatorState = {
|
|
34
|
+
tool: 'pen',
|
|
35
|
+
color: COLORS[0],
|
|
36
|
+
strokeSize: 6,
|
|
37
|
+
strokes: [],
|
|
38
|
+
currentStroke: null,
|
|
39
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import getStroke from 'perfect-freehand';
|
|
2
|
+
import type { Point, Stroke } from './types';
|
|
3
|
+
|
|
4
|
+
const STROKE_OPTIONS = {
|
|
5
|
+
size: 4,
|
|
6
|
+
thinning: 0.5,
|
|
7
|
+
smoothing: 0.5,
|
|
8
|
+
streamline: 0.5,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert perfect-freehand output to SVG path data
|
|
13
|
+
*/
|
|
14
|
+
function getSvgPathFromStroke(stroke: number[][]): string {
|
|
15
|
+
if (!stroke.length) return '';
|
|
16
|
+
|
|
17
|
+
const d = stroke.reduce(
|
|
18
|
+
(acc, [x0, y0], i, arr) => {
|
|
19
|
+
const [x1, y1] = arr[(i + 1) % arr.length];
|
|
20
|
+
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
|
|
21
|
+
return acc;
|
|
22
|
+
},
|
|
23
|
+
['M', ...stroke[0], 'Q']
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
d.push('Z');
|
|
27
|
+
return d.join(' ');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render a freehand pen stroke using perfect-freehand
|
|
32
|
+
*/
|
|
33
|
+
export function renderPenStroke(
|
|
34
|
+
ctx: CanvasRenderingContext2D,
|
|
35
|
+
points: Point[],
|
|
36
|
+
color: string,
|
|
37
|
+
size: number,
|
|
38
|
+
scale = 1
|
|
39
|
+
) {
|
|
40
|
+
if (points.length < 2) return;
|
|
41
|
+
|
|
42
|
+
const scaledPoints = points.map(p => [p.x * scale, p.y * scale, p.pressure ?? 0.5]);
|
|
43
|
+
const stroke = getStroke(scaledPoints, { ...STROKE_OPTIONS, size: size * scale });
|
|
44
|
+
|
|
45
|
+
const path = new Path2D(getSvgPathFromStroke(stroke));
|
|
46
|
+
ctx.fillStyle = color;
|
|
47
|
+
ctx.fill(path);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Render an arrow from start to end point
|
|
52
|
+
*/
|
|
53
|
+
export function renderArrow(
|
|
54
|
+
ctx: CanvasRenderingContext2D,
|
|
55
|
+
start: Point,
|
|
56
|
+
end: Point,
|
|
57
|
+
color: string,
|
|
58
|
+
size: number,
|
|
59
|
+
scale = 1
|
|
60
|
+
) {
|
|
61
|
+
const x1 = start.x * scale;
|
|
62
|
+
const y1 = start.y * scale;
|
|
63
|
+
const x2 = end.x * scale;
|
|
64
|
+
const y2 = end.y * scale;
|
|
65
|
+
|
|
66
|
+
const lineWidth = size * scale * 0.75;
|
|
67
|
+
const headLength = size * scale * 3;
|
|
68
|
+
|
|
69
|
+
// Draw line
|
|
70
|
+
ctx.strokeStyle = color;
|
|
71
|
+
ctx.lineWidth = lineWidth;
|
|
72
|
+
ctx.lineCap = 'round';
|
|
73
|
+
ctx.beginPath();
|
|
74
|
+
ctx.moveTo(x1, y1);
|
|
75
|
+
ctx.lineTo(x2, y2);
|
|
76
|
+
ctx.stroke();
|
|
77
|
+
|
|
78
|
+
// Draw arrowhead
|
|
79
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
80
|
+
ctx.fillStyle = color;
|
|
81
|
+
ctx.beginPath();
|
|
82
|
+
ctx.moveTo(x2, y2);
|
|
83
|
+
ctx.lineTo(
|
|
84
|
+
x2 - headLength * Math.cos(angle - Math.PI / 6),
|
|
85
|
+
y2 - headLength * Math.sin(angle - Math.PI / 6)
|
|
86
|
+
);
|
|
87
|
+
ctx.lineTo(
|
|
88
|
+
x2 - headLength * Math.cos(angle + Math.PI / 6),
|
|
89
|
+
y2 - headLength * Math.sin(angle + Math.PI / 6)
|
|
90
|
+
);
|
|
91
|
+
ctx.closePath();
|
|
92
|
+
ctx.fill();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Render a circle from edge to edge (first click is one edge, drag to opposite edge)
|
|
97
|
+
*/
|
|
98
|
+
export function renderCircle(
|
|
99
|
+
ctx: CanvasRenderingContext2D,
|
|
100
|
+
start: Point,
|
|
101
|
+
end: Point,
|
|
102
|
+
color: string,
|
|
103
|
+
size: number,
|
|
104
|
+
scale = 1
|
|
105
|
+
) {
|
|
106
|
+
const x1 = start.x * scale;
|
|
107
|
+
const y1 = start.y * scale;
|
|
108
|
+
const x2 = end.x * scale;
|
|
109
|
+
const y2 = end.y * scale;
|
|
110
|
+
|
|
111
|
+
// Center is midpoint between start and end
|
|
112
|
+
const cx = (x1 + x2) / 2;
|
|
113
|
+
const cy = (y1 + y2) / 2;
|
|
114
|
+
|
|
115
|
+
// Radius is half the distance
|
|
116
|
+
const radius = Math.hypot(x2 - x1, y2 - y1) / 2;
|
|
117
|
+
const lineWidth = size * scale * 0.75;
|
|
118
|
+
|
|
119
|
+
ctx.strokeStyle = color;
|
|
120
|
+
ctx.lineWidth = lineWidth;
|
|
121
|
+
ctx.beginPath();
|
|
122
|
+
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
123
|
+
ctx.stroke();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Render a stroke based on its tool type
|
|
128
|
+
*/
|
|
129
|
+
export function renderStroke(
|
|
130
|
+
ctx: CanvasRenderingContext2D,
|
|
131
|
+
stroke: Stroke,
|
|
132
|
+
scale = 1
|
|
133
|
+
) {
|
|
134
|
+
if (stroke.points.length < 2) return;
|
|
135
|
+
|
|
136
|
+
switch (stroke.tool) {
|
|
137
|
+
case 'pen':
|
|
138
|
+
renderPenStroke(ctx, stroke.points, stroke.color, stroke.size, scale);
|
|
139
|
+
break;
|
|
140
|
+
case 'arrow':
|
|
141
|
+
renderArrow(ctx, stroke.points[0], stroke.points[stroke.points.length - 1], stroke.color, stroke.size, scale);
|
|
142
|
+
break;
|
|
143
|
+
case 'circle':
|
|
144
|
+
renderCircle(ctx, stroke.points[0], stroke.points[stroke.points.length - 1], stroke.color, stroke.size, scale);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|