xiaodao-painter 0.1.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leeqwind
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # xiaodao-painter
2
+
3
+ A self-contained, embeddable SVG whiteboard component for Vue 3. Built with TypeScript, Pinia, and Vite.
4
+
5
+ Supports drawing tools, selection, resize, pan/zoom, undo/redo, clipboard, layer ordering, color palettes, i18n, and light/dark themes — all with two-way data binding via `v-model`.
6
+
7
+ ![Preview](./img/preview.png)
8
+
9
+ ## Features
10
+
11
+ - **9 tools** — Select, Pan, Zoom, Pencil, Line, Circle, Rectangle, Triangle, Star
12
+ - **Selection & manipulation** — click to select, box-select, drag-move, 8-handle resize (Shift = symmetric), layer reorder (bring to front / send to back)
13
+ - **Pan & zoom** — grab-drag panning, discrete zoom steps (10% – 1000%) anchored at cursor position
14
+ - **Undo / redo** — full stroke-level undo/redo stacks
15
+ - **Internal clipboard** — cut, copy, paste (within the whiteboard, offset +20 px)
16
+ - **Color system** — stroke (foreground) and fill (background) color slots, per-stroke color application to selections, canvas background color with transparent grid support
17
+ - **Themes** — light and dark, with auto-switching foreground color on theme change
18
+ - **Internationalization** — Chinese (zh-CN) and English (en-US) built in
19
+ - **Keyboard shortcuts** — Delete, Escape, Ctrl+Z / Ctrl+Y / Ctrl+X / Ctrl+C / Ctrl+V
20
+ - **Modifier keys** — Ctrl = constrain 1:1 ratio, Shift = center-out expansion / 45° line snap
21
+ - **Two-way binding** — parent reads/writes full drawing data via `v-model`
22
+
23
+ ## Installation
24
+
25
+ Install the package from npm:
26
+
27
+ ```bash
28
+ npm install xiaodao-painter
29
+ ```
30
+
31
+ Or with pnpm / yarn:
32
+
33
+ ```bash
34
+ pnpm add xiaodao-painter
35
+ yarn add xiaodao-painter
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ First, make sure [Pinia](https://pinia.vuejs.org/) is registered in your Vue app (xiaodao-painter depends on Pinia internally):
41
+
42
+ ```ts
43
+ // main.ts
44
+ import { createApp } from 'vue'
45
+ import { createPinia } from 'pinia'
46
+ import App from './App.vue'
47
+
48
+ const app = createApp(App)
49
+ app.use(createPinia())
50
+ app.mount('#app')
51
+ ```
52
+
53
+ Then import and use the `Painter` component:
54
+
55
+ ```vue
56
+ <script setup lang="ts">
57
+ import { ref, watch } from 'vue'
58
+ import { Painter } from 'xiaodao-painter'
59
+ import type { PainterData } from 'xiaodao-painter'
60
+
61
+ const data = ref<PainterData>({
62
+ strokes: [],
63
+ canvasWidth: 800,
64
+ canvasHeight: 600,
65
+ })
66
+
67
+ watch(data, (val) => {
68
+ console.log('Drawing data changed:', val)
69
+ }, { deep: true })
70
+ </script>
71
+
72
+ <template>
73
+ <Painter
74
+ v-model="data"
75
+ theme="light"
76
+ locale="zh-CN"
77
+ />
78
+ </template>
79
+ ```
80
+
81
+ With explicit dimensions and dark theme:
82
+
83
+ ```html
84
+ <Painter
85
+ v-model="data"
86
+ theme="dark"
87
+ locale="en-US"
88
+ width="1200"
89
+ height="800"
90
+ />
91
+ ```
92
+
93
+ With a custom canvas background color:
94
+
95
+ ```html
96
+ <Painter
97
+ v-model="data"
98
+ :modelValue="{
99
+ strokes: [],
100
+ canvasWidth: 800,
101
+ canvasHeight: 600,
102
+ canvasBackgroundColor: '#f5f5f5',
103
+ }"
104
+ />
105
+ ```
106
+
107
+ ## Props
108
+
109
+ | Prop | Type | Default | Description |
110
+ | --- | --- | --- | --- |
111
+ | `modelValue` | `PainterData` | `{ strokes: [], canvasWidth: 800, canvasHeight: 600 }` | Two-way bound drawing data (supports `canvasBackgroundColor`) |
112
+ | `theme` | `'light' \| 'dark'` | `'light'` | UI theme |
113
+ | `locale` | `string` | `'zh-CN'` | UI language (`'zh-CN'` or `'en-US'`) |
114
+ | `width` | `string \| number` | `'100%'` | Component width (CSS value or px number) |
115
+ | `height` | `string \| number` | `'100%'` | Component height (CSS value or px number) |
116
+
117
+ ## Data Model
118
+
119
+ ```ts
120
+ interface PainterData {
121
+ strokes: Stroke[]
122
+ canvasWidth: number
123
+ canvasHeight: number
124
+ canvasBackgroundColor?: string
125
+ }
126
+
127
+ type ToolType = 'select' | 'pencil' | 'line' | 'circle' | 'rect' | 'triangle' | 'star' | 'text' | 'pan' | 'zoom'
128
+
129
+ interface Point {
130
+ x: number
131
+ y: number
132
+ }
133
+
134
+ interface Stroke {
135
+ id: string
136
+ type: ToolType
137
+ x: number
138
+ y: number
139
+ width: number
140
+ height: number
141
+ points: Point[]
142
+ strokeColor: string
143
+ fillColor: string
144
+ strokeWidth: number
145
+ text?: string
146
+ fontSize?: number
147
+ textAlign?: 'left' | 'center' | 'right'
148
+ textColor?: string
149
+ textAutoWidth?: boolean
150
+ }
151
+ ```
152
+
153
+ ## Keyboard Shortcuts
154
+
155
+ | Key | Action |
156
+ | --- | --- |
157
+ | `Delete` / `Backspace` | Delete selected strokes |
158
+ | `Escape` | Clear selection |
159
+ | `Ctrl + Z` | Undo |
160
+ | `Ctrl + Y` / `Ctrl + Shift + Z` | Redo |
161
+ | `Ctrl + X` | Cut |
162
+ | `Ctrl + C` | Copy |
163
+ | `Ctrl + V` | Paste (offset +20 px) |
164
+
165
+ ## Project Structure
166
+
167
+ ```
168
+ src/
169
+ components/painter/
170
+ Painter.vue # Public entry (props, v-model, theme sync)
171
+ Whiteboard.vue # SVG canvas, keyboard shortcuts, pan/zoom centering
172
+ Toolbar.vue # Left toolbar (tools, canvas settings, color indicators)
173
+ ColorPalette.vue # Color picker popover
174
+ SelectionOverlay.vue # Selection bounding box + resize handles
175
+ StrokeRenderer.vue # Renders a single stroke as SVG
176
+ composables/
177
+ useDrawing.ts # Mouse interaction engine (draw, select, move, resize)
178
+ useI18n.ts # Locale and theme provide/inject
179
+ stores/
180
+ canvas.ts # Main store (strokes, colors, pan/zoom, undo, clipboard)
181
+ tools.ts # Active tool state
182
+ utils/
183
+ geometry.ts # Hit testing, star/triangle vertices, bounding box
184
+ svg.ts # Stroke-to-SVG element conversion
185
+ i18n.ts # zh-CN / en-US dictionaries
186
+ types/
187
+ index.ts # TypeScript types and default constants
188
+ style.css # CSS custom properties (themes)
189
+ ```
190
+
191
+ ## Development
192
+
193
+ ```bash
194
+ npm run dev # Start dev server
195
+ npm run build # Type-check and build for production
196
+ npm run preview # Preview production build
197
+ ```
198
+
199
+ ## License
200
+
201
+ MIT
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,22 @@
1
+ import type { PainterData } from './types/index.ts';
2
+ import './style.css';
3
+ type __VLS_Props = {
4
+ theme?: 'light' | 'dark';
5
+ locale?: string;
6
+ modelValue?: PainterData;
7
+ width?: string | number;
8
+ height?: string | number;
9
+ };
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:modelValue": (data: PainterData) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ "onUpdate:modelValue"?: ((data: PainterData) => any) | undefined;
14
+ }>, {
15
+ height: string | number;
16
+ width: string | number;
17
+ theme: "light" | "dark";
18
+ locale: string;
19
+ modelValue: PainterData;
20
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ declare const _default: typeof __VLS_export;
22
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import type { Stroke } from './types';
2
+ type __VLS_Props = {
3
+ stroke: Stroke;
4
+ startResize: (strokeId: string, handle: string, cursor: string, event: MouseEvent | TouchEvent) => void;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import type { Stroke } from './types';
2
+ type __VLS_Props = {
3
+ stroke: Stroke;
4
+ isSelected: boolean;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,23 @@
1
+ import type { Ref } from 'vue';
2
+ import type { Point, Stroke } from '../types';
3
+ export declare function useDrawing(whiteboardRef: Ref<HTMLElement | null>): {
4
+ isDrawing: Ref<boolean, boolean>;
5
+ previewStroke: import("vue").ComputedRef<Stroke | null>;
6
+ onMouseDown: (event: MouseEvent) => void;
7
+ onMouseMove: (event: MouseEvent) => void;
8
+ onMouseUp: (event: MouseEvent) => void;
9
+ onTouchStart: (event: TouchEvent) => void;
10
+ onTouchMove: (event: TouchEvent) => void;
11
+ onTouchEnd: (event: TouchEvent) => void;
12
+ getSVGPoint: (event: MouseEvent) => Point;
13
+ updateModifiers: (e: KeyboardEvent) => void;
14
+ clearModifiers: () => void;
15
+ isResizing: Ref<boolean, boolean>;
16
+ resizeCursor: Ref<string, string>;
17
+ startResize: (strokeId: string, handle: string, cursor: string, event: MouseEvent | TouchEvent) => void;
18
+ commitTextEdit: (strokeId: string, text: string) => void;
19
+ cancelTextEdit: (strokeId: string) => void;
20
+ startEditText: (strokeId: string) => void;
21
+ saveCurrentTextEdit: () => void;
22
+ setTextContentGetter: (getter: () => string | null) => void;
23
+ };
@@ -0,0 +1,13 @@
1
+ import type { Locale } from '../utils/i18n';
2
+ export declare const I18N_KEY: string;
3
+ export declare const THEME_KEY: string;
4
+ export declare function provideLocale(locale: Locale): void;
5
+ export declare function useI18n(): {
6
+ locale: Locale;
7
+ t: (key: string) => string;
8
+ };
9
+ export declare function provideTheme(theme: 'light' | 'dark'): void;
10
+ export declare function useTheme(): 'light' | 'dark';
11
+ export declare const ROOT_EL_KEY: unique symbol;
12
+ export declare function provideRootEl(el: Readonly<import('vue').Ref<HTMLElement | null>>): void;
13
+ export declare function useRootEl(): import('vue').Ref<HTMLElement | null> | null;
@@ -0,0 +1,238 @@
1
+ import type { Stroke } from '../types';
2
+ export declare const useCanvasStore: import("pinia").SetupStoreDefinition<"canvas", {
3
+ strokes: import("vue").Ref<{
4
+ id: string;
5
+ type: import("../types").ToolType;
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ points: {
11
+ x: number;
12
+ y: number;
13
+ }[];
14
+ strokeColor: string;
15
+ fillColor: string;
16
+ strokeWidth: number;
17
+ text?: string | undefined;
18
+ fontSize?: number | undefined;
19
+ textAlign?: "left" | "center" | "right" | undefined;
20
+ textColor?: string | undefined;
21
+ textAutoWidth?: boolean | undefined;
22
+ }[], Stroke[] | {
23
+ id: string;
24
+ type: import("../types").ToolType;
25
+ x: number;
26
+ y: number;
27
+ width: number;
28
+ height: number;
29
+ points: {
30
+ x: number;
31
+ y: number;
32
+ }[];
33
+ strokeColor: string;
34
+ fillColor: string;
35
+ strokeWidth: number;
36
+ text?: string | undefined;
37
+ fontSize?: number | undefined;
38
+ textAlign?: "left" | "center" | "right" | undefined;
39
+ textColor?: string | undefined;
40
+ textAutoWidth?: boolean | undefined;
41
+ }[]>;
42
+ selectedStrokeIds: import("vue").Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>;
43
+ selectedStrokeId: import("vue").ComputedRef<string | null>;
44
+ selectedStroke: import("vue").ComputedRef<{
45
+ id: string;
46
+ type: import("../types").ToolType;
47
+ x: number;
48
+ y: number;
49
+ width: number;
50
+ height: number;
51
+ points: {
52
+ x: number;
53
+ y: number;
54
+ }[];
55
+ strokeColor: string;
56
+ fillColor: string;
57
+ strokeWidth: number;
58
+ text?: string | undefined;
59
+ fontSize?: number | undefined;
60
+ textAlign?: "left" | "center" | "right" | undefined;
61
+ textColor?: string | undefined;
62
+ textAutoWidth?: boolean | undefined;
63
+ } | null>;
64
+ selectedStrokes: import("vue").ComputedRef<{
65
+ id: string;
66
+ type: import("../types").ToolType;
67
+ x: number;
68
+ y: number;
69
+ width: number;
70
+ height: number;
71
+ points: {
72
+ x: number;
73
+ y: number;
74
+ }[];
75
+ strokeColor: string;
76
+ fillColor: string;
77
+ strokeWidth: number;
78
+ text?: string | undefined;
79
+ fontSize?: number | undefined;
80
+ textAlign?: "left" | "center" | "right" | undefined;
81
+ textColor?: string | undefined;
82
+ textAutoWidth?: boolean | undefined;
83
+ }[]>;
84
+ selectedStrokeColor: import("vue").ComputedRef<string | null>;
85
+ selectedFillColor: import("vue").ComputedRef<string | null>;
86
+ selectedTextColor: import("vue").ComputedRef<string | null>;
87
+ foregroundColor: import("vue").Ref<string, string>;
88
+ backgroundColor: import("vue").Ref<string, string>;
89
+ activeColorSlot: import("vue").Ref<"background" | "textColor" | "foreground", "background" | "textColor" | "foreground">;
90
+ showColorPalette: import("vue").Ref<boolean, boolean>;
91
+ isSelecting: import("vue").Ref<boolean, boolean>;
92
+ selectionBox: import("vue").Ref<{
93
+ x: number;
94
+ y: number;
95
+ width: number;
96
+ height: number;
97
+ } | null, {
98
+ x: number;
99
+ y: number;
100
+ width: number;
101
+ height: number;
102
+ } | {
103
+ x: number;
104
+ y: number;
105
+ width: number;
106
+ height: number;
107
+ } | null>;
108
+ toggleColorPalette: () => void;
109
+ hideColorPalette: () => void;
110
+ addStroke: (stroke: Stroke) => void;
111
+ updateStroke: (id: string, changes: Partial<Stroke>) => void;
112
+ selectStroke: (id: string | null) => void;
113
+ selectStrokes: (ids: string[]) => void;
114
+ clearSelection: () => void;
115
+ isSelected: (id: string) => boolean;
116
+ deleteSelectedStrokes: () => void;
117
+ setColorSlot: (slot: "foreground" | "background" | "textColor") => void;
118
+ setColor: (color: string) => void;
119
+ buildStroke: (type: Stroke["type"], x: number, y: number, width: number, height: number, points: Stroke["points"]) => Stroke;
120
+ strokeWidth: import("vue").Ref<number, number>;
121
+ setStrokeWidth: (w: number) => void;
122
+ canvasBackgroundColor: import("vue").Ref<string, string>;
123
+ setCanvasBackgroundColor: (color: string) => void;
124
+ canUndo: import("vue").ComputedRef<boolean>;
125
+ canRedo: import("vue").ComputedRef<boolean>;
126
+ undoStack: import("vue").Ref<{
127
+ id: string;
128
+ type: import("../types").ToolType;
129
+ x: number;
130
+ y: number;
131
+ width: number;
132
+ height: number;
133
+ points: {
134
+ x: number;
135
+ y: number;
136
+ }[];
137
+ strokeColor: string;
138
+ fillColor: string;
139
+ strokeWidth: number;
140
+ text?: string | undefined;
141
+ fontSize?: number | undefined;
142
+ textAlign?: "left" | "center" | "right" | undefined;
143
+ textColor?: string | undefined;
144
+ textAutoWidth?: boolean | undefined;
145
+ }[][], Stroke[][] | {
146
+ id: string;
147
+ type: import("../types").ToolType;
148
+ x: number;
149
+ y: number;
150
+ width: number;
151
+ height: number;
152
+ points: {
153
+ x: number;
154
+ y: number;
155
+ }[];
156
+ strokeColor: string;
157
+ fillColor: string;
158
+ strokeWidth: number;
159
+ text?: string | undefined;
160
+ fontSize?: number | undefined;
161
+ textAlign?: "left" | "center" | "right" | undefined;
162
+ textColor?: string | undefined;
163
+ textAutoWidth?: boolean | undefined;
164
+ }[][]>;
165
+ pushUndo: () => void;
166
+ undo: () => void;
167
+ redo: () => void;
168
+ moveSelectedUp: () => void;
169
+ moveSelectedDown: () => void;
170
+ syncFromParent: (data: Stroke[]) => void;
171
+ getStrokeDisplayBounds: (stroke: Stroke) => {
172
+ x: number;
173
+ y: number;
174
+ width: number;
175
+ height: number;
176
+ };
177
+ clipboard: import("vue").Ref<{
178
+ id: string;
179
+ type: import("../types").ToolType;
180
+ x: number;
181
+ y: number;
182
+ width: number;
183
+ height: number;
184
+ points: {
185
+ x: number;
186
+ y: number;
187
+ }[];
188
+ strokeColor: string;
189
+ fillColor: string;
190
+ strokeWidth: number;
191
+ text?: string | undefined;
192
+ fontSize?: number | undefined;
193
+ textAlign?: "left" | "center" | "right" | undefined;
194
+ textColor?: string | undefined;
195
+ textAutoWidth?: boolean | undefined;
196
+ }[], Stroke[] | {
197
+ id: string;
198
+ type: import("../types").ToolType;
199
+ x: number;
200
+ y: number;
201
+ width: number;
202
+ height: number;
203
+ points: {
204
+ x: number;
205
+ y: number;
206
+ }[];
207
+ strokeColor: string;
208
+ fillColor: string;
209
+ strokeWidth: number;
210
+ text?: string | undefined;
211
+ fontSize?: number | undefined;
212
+ textAlign?: "left" | "center" | "right" | undefined;
213
+ textColor?: string | undefined;
214
+ textAutoWidth?: boolean | undefined;
215
+ }[]>;
216
+ cutSelectedStrokes: () => void;
217
+ copySelectedStrokes: () => void;
218
+ pasteStrokes: () => void;
219
+ canvasWidth: import("vue").Ref<number, number>;
220
+ canvasHeight: import("vue").Ref<number, number>;
221
+ panX: import("vue").Ref<number, number>;
222
+ panY: import("vue").Ref<number, number>;
223
+ zoomLevel: import("vue").Ref<number, number>;
224
+ showZoomIndicator: import("vue").Ref<boolean, boolean>;
225
+ viewBox: import("vue").ComputedRef<string>;
226
+ canvasTransform: import("vue").ComputedRef<string>;
227
+ setPan: (x: number, y: number) => void;
228
+ setCanvasSize: (w: number, h: number) => void;
229
+ zoomAt: (screenX: number, screenY: number, factor: number) => void;
230
+ continuousZoomAt: (screenX: number, screenY: number, newZoom: number) => void;
231
+ getZoomPercent: () => number;
232
+ dataVersion: import("vue").Ref<number, number>;
233
+ theme: import("vue").Ref<"dark" | "light", "dark" | "light">;
234
+ textColor: import("vue").Ref<string, string>;
235
+ editingTextId: import("vue").Ref<string | null, string | null>;
236
+ hoveredTextId: import("vue").Ref<string | null, string | null>;
237
+ reset: () => void;
238
+ }>;
@@ -0,0 +1,6 @@
1
+ import type { ToolType } from '../types';
2
+ export declare const useToolsStore: import("pinia").SetupStoreDefinition<"tools", {
3
+ activeTool: import("vue").Ref<ToolType, ToolType>;
4
+ setTool: (tool: ToolType) => void;
5
+ reset: () => void;
6
+ }>;
@@ -0,0 +1,37 @@
1
+ export type ToolType = 'select' | 'pencil' | 'line' | 'circle' | 'rect' | 'triangle' | 'star' | 'text' | 'pan' | 'zoom';
2
+ export interface Point {
3
+ x: number;
4
+ y: number;
5
+ }
6
+ export interface Stroke {
7
+ id: string;
8
+ type: ToolType;
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ points: Point[];
14
+ strokeColor: string;
15
+ fillColor: string;
16
+ strokeWidth: number;
17
+ text?: string;
18
+ fontSize?: number;
19
+ textAlign?: 'left' | 'center' | 'right';
20
+ textColor?: string;
21
+ textAutoWidth?: boolean;
22
+ }
23
+ export interface CanvasConfig {
24
+ width: number;
25
+ height: number;
26
+ }
27
+ export interface PainterData {
28
+ strokes: Stroke[];
29
+ canvasWidth: number;
30
+ canvasHeight: number;
31
+ canvasBackgroundColor?: string;
32
+ }
33
+ export declare const DEFAULT_CANVAS_WIDTH = 800;
34
+ export declare const DEFAULT_CANVAS_HEIGHT = 600;
35
+ export declare const STROKE_WIDTH = 3;
36
+ export declare const DEFAULT_FONT_SIZE = 16;
37
+ export declare const HIT_THRESHOLD = 8;
@@ -0,0 +1,13 @@
1
+ import type { Point, Stroke } from '../types';
2
+ export declare function starVertices(cx: number, cy: number, rx: number, ry: number, flipY?: boolean, innerRatio?: number): Point[];
3
+ export declare function triangleVertices(x: number, y: number, w: number, h: number): Point[];
4
+ export declare function pointToSegmentDistance(p: Point, a: Point, b: Point): number;
5
+ export declare function pointInPolygon(p: Point, polygon: Point[]): boolean;
6
+ export declare function pointInEllipse(p: Point, cx: number, cy: number, rx: number, ry: number): boolean;
7
+ export declare function computeBoundingBox(points: Point[]): {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+ export declare function hitTestStroke(p: Point, stroke: Stroke): boolean;
@@ -0,0 +1,2 @@
1
+ export type Locale = 'zh-CN' | 'en-US';
2
+ export declare function t(locale: Locale, key: string): string;
@@ -0,0 +1,7 @@
1
+ import type { Stroke, Point } from '../types';
2
+ export declare function pencilToPathD(points: Point[]): string;
3
+ export interface SvgAttrs {
4
+ tag: string;
5
+ attrs: Record<string, string | number>;
6
+ }
7
+ export declare function strokeToSvg(stroke: Stroke): SvgAttrs;
@@ -0,0 +1,6 @@
1
+ import Painter from './components/painter/Painter.vue';
2
+ import type { PainterData, Stroke, Point, ToolType, CanvasConfig } from './components/painter/types';
3
+ import { DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT, STROKE_WIDTH, DEFAULT_FONT_SIZE, HIT_THRESHOLD } from './components/painter/types';
4
+ import './components/painter/style.css';
5
+ export { Painter, Painter as default, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT, STROKE_WIDTH, DEFAULT_FONT_SIZE, HIT_THRESHOLD, };
6
+ export type { PainterData, Stroke, Point, ToolType, CanvasConfig, };
@@ -0,0 +1,2 @@
1
+ .toolbar[data-v-9532ade7]{background:var(--wb-surface-toolbar);border-right:1px solid var(--wb-border);flex-direction:column;flex-shrink:0;align-items:center;gap:2px;width:52px;padding:8px 6px;display:flex}.tool-group[data-v-9532ade7]{flex-direction:column;gap:2px;display:flex}.tools-scroll[data-v-9532ade7]{scrollbar-width:none;-ms-overflow-style:none;flex-direction:column;flex-shrink:1;gap:2px;min-height:0;display:flex;overflow:hidden auto}.tools-scroll[data-v-9532ade7]::-webkit-scrollbar{display:none}.scroll-btn[data-v-9532ade7]{border-radius:var(--wb-radius-sm);width:40px;height:22px;color:var(--wb-text-secondary);cursor:pointer;transition:all var(--wb-transition);background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;padding:0;display:flex}.scroll-btn[data-v-9532ade7]:hover:not(:disabled){background:var(--wb-surface-hover);color:var(--wb-text)}.scroll-btn[data-v-9532ade7]:disabled{opacity:.25;cursor:default}.scroll-icon[data-v-9532ade7]{pointer-events:none;width:14px;height:14px}.tool-btn[data-v-9532ade7]{border-radius:var(--wb-radius-sm);width:40px;height:40px;color:var(--wb-text-secondary);cursor:pointer;transition:all var(--wb-transition);background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:flex}.tool-btn[data-v-9532ade7]:hover{background:var(--wb-surface-hover);color:var(--wb-text)}.tool-btn.active[data-v-9532ade7]{background:var(--wb-accent);color:#fff}.tool-icon[data-v-9532ade7]{pointer-events:none;width:22px;height:22px}.spacer[data-v-9532ade7]{flex:1}.canvas-size-wrapper[data-v-9532ade7]{margin-bottom:6px;position:relative}.canvas-size-backdrop[data-v-9532ade7]{z-index:9998;position:fixed;inset:0}.canvas-size-popover[data-v-9532ade7]{background:var(--wb-surface-toolbar);border:1px solid var(--wb-border);z-index:9999;border-radius:8px;min-width:180px;padding:12px 14px;box-shadow:0 2px 8px #0000001a}.popover-title[data-v-9532ade7]{color:var(--wb-text);margin-bottom:10px;font-size:13px;font-weight:600}.popover-field[data-v-9532ade7]{align-items:center;gap:6px;margin-bottom:8px;display:flex}.popover-label[data-v-9532ade7]{color:var(--wb-text-secondary);flex-shrink:0;width:32px;font-size:12px}.popover-input[data-v-9532ade7]{border:1px solid var(--wb-border);background:var(--wb-surface);width:70px;height:28px;color:var(--wb-text);text-align:right;border-radius:4px;outline:none;flex:1;padding:0 6px;font-size:13px}.popover-input[data-v-9532ade7]:focus{border-color:var(--wb-accent)}.popover-unit[data-v-9532ade7]{color:var(--wb-text-secondary);flex-shrink:0;width:20px;font-size:11px}.bg-color-row[data-v-9532ade7]{align-items:center;gap:4px;display:flex}.bg-radio-btn[data-v-9532ade7]{border:1px solid var(--wb-border);background:var(--wb-surface);width:28px;height:28px;color:var(--wb-text-secondary);cursor:pointer;transition:all var(--wb-transition);border-radius:4px;justify-content:center;align-items:center;padding:0;display:flex;position:relative}.bg-radio-btn[data-v-9532ade7]:hover{border-color:var(--wb-text-secondary)}.bg-radio-btn.active[data-v-9532ade7]{border-color:var(--wb-accent);box-shadow:0 0 0 1px var(--wb-accent)}.bg-radio-btn--transparent[data-v-9532ade7]{background:repeating-conic-gradient(#ccc 0% 25%,#fff 0% 50%) 50%/8px 8px}.bg-color-fill[data-v-9532ade7]{border:1px solid var(--wb-border);border-radius:2px;width:16px;height:16px;display:block}.hidden-color-input[data-v-9532ade7]{opacity:0;pointer-events:none;width:0;height:0;position:absolute}.popover-actions[data-v-9532ade7]{justify-content:flex-end;gap:6px;margin-top:10px;display:flex}.popover-btn[data-v-9532ade7]{border:1px solid var(--wb-border);background:var(--wb-surface);color:var(--wb-text);cursor:pointer;transition:all var(--wb-transition);border-radius:4px;padding:4px 12px;font-size:12px}.popover-btn[data-v-9532ade7]:hover{background:var(--wb-surface-hover)}.popover-btn.confirm[data-v-9532ade7]{background:var(--wb-accent);color:#fff;border-color:var(--wb-accent)}.popover-btn.confirm[data-v-9532ade7]:hover{opacity:.9}.color-section[data-v-9532ade7]{flex-direction:column;align-items:center;gap:6px;padding:0;display:flex}.color-indicator[data-v-9532ade7]{cursor:pointer;width:32px;height:32px;transition:all var(--wb-transition);border:2px solid #0000;border-radius:8px;padding:2px;box-shadow:0 2px 8px #0000001a}.color-indicator[data-v-9532ade7]:hover{background:var(--wb-surface-hover);border-color:var(--wb-border)}.color-indicator.active[data-v-9532ade7]{border-color:var(--wb-accent);background:var(--wb-accent-light)}.color-indicator.disabled[data-v-9532ade7]{opacity:.35;cursor:default;pointer-events:none}.color-swatch[data-v-9532ade7]{border:1px solid var(--wb-border);border-radius:6px;justify-content:center;align-items:center;width:100%;height:100%;display:flex;position:relative}.bg[data-v-9532ade7]{border-style:dashed;border-radius:6px}.mixed[data-v-9532ade7],.transparent[data-v-9532ade7]{background:repeating-conic-gradient(#ccc 0% 25%,#fff 0% 50%) 50%/8px 8px!important}.mixed-icon[data-v-9532ade7]{width:16px;height:16px;color:var(--wb-text-secondary);pointer-events:none}.text-color-swatch[data-v-9532ade7]{justify-content:center;align-items:center;display:flex;position:relative}.text-color-icon[data-v-9532ade7]{pointer-events:none;width:18px;height:18px;transition:color .15s}.selected[data-v-d3706fa4]{filter:drop-shadow(0 0 3px #0078d799)}.whiteboard[data-v-975e0b8b]{background:var(--wb-canvas-surround);flex:1;position:relative;overflow:hidden}.canvas-wrapper[data-v-975e0b8b]{will-change:transform;margin:auto;position:absolute}.canvas[data-v-975e0b8b]{display:block}.canvas-toolbar[data-v-975e0b8b]{z-index:50;flex-direction:column;gap:2px;display:flex;position:absolute;top:8px;right:8px}.canvas-tb-btn[data-v-975e0b8b]{border:1px solid var(--wb-border);background:var(--wb-surface);width:28px;height:28px;color:var(--wb-text);cursor:pointer;transition:all var(--wb-transition);opacity:.85;border-radius:4px;justify-content:center;align-items:center;padding:0;display:flex}.canvas-tb-btn[data-v-975e0b8b]:hover:not(:disabled){background:var(--wb-surface-hover);opacity:1}.canvas-tb-btn[data-v-975e0b8b]:disabled{opacity:.35;cursor:default}.canvas-tb-btn--danger[data-v-975e0b8b]:hover:not(:disabled){color:#ef4444;border-color:#ef4444}.tb-separator[data-v-975e0b8b]{background:var(--wb-border);height:1px;margin:2px 4px}.zoom-control[data-v-975e0b8b]{color:#fff;z-index:100;background:#000000b3;border-radius:6px;align-items:center;gap:4px;padding:4px 6px;font-family:system-ui,sans-serif;font-size:13px;display:flex;position:absolute;bottom:16px;right:16px}.zoom-btn[data-v-975e0b8b]{color:#fff;cursor:pointer;background:#ffffff26;border:none;border-radius:4px;flex-shrink:0;justify-content:center;align-items:center;width:24px;height:24px;padding:0;transition:background .15s;display:flex}.zoom-btn[data-v-975e0b8b]:hover{background:#ffffff4d}.zoom-percent[data-v-975e0b8b]{text-align:center;-webkit-user-select:none;user-select:none;min-width:40px}.text-format-control[data-v-975e0b8b]{color:#fff;z-index:100;background:#000000b3;border-radius:6px;align-items:center;gap:2px;padding:4px 6px;font-family:system-ui,sans-serif;font-size:13px;display:flex;position:absolute;bottom:16px;right:16px}.font-size-btn[data-v-975e0b8b]{color:#fff;cursor:pointer;background:#ffffff26;border:none;border-radius:4px;flex-shrink:0;justify-content:center;align-items:center;width:24px;height:24px;padding:0;transition:background .15s;display:flex}.font-size-btn[data-v-975e0b8b]:hover{background:#ffffff4d}.font-size-value[data-v-975e0b8b]{text-align:center;-webkit-user-select:none;user-select:none;min-width:28px;font-size:12px}.text-align-separator[data-v-975e0b8b]{background:#ffffff4d;width:1px;height:16px;margin:0 4px}.text-align-btn[data-v-975e0b8b]{color:#fff;cursor:pointer;background:#ffffff26;border:none;border-radius:4px;flex-shrink:0;justify-content:center;align-items:center;width:28px;height:24px;padding:0;transition:background .15s;display:flex}.text-align-btn[data-v-975e0b8b]:hover{background:#ffffff4d}.text-align-btn.active[data-v-975e0b8b]{background:#6366f199}.overlay[data-v-a8b8bc9c]{z-index:100;position:fixed;inset:0}.color-palette[data-v-a8b8bc9c]{background:var(--wb-surface);border:1px solid var(--wb-border);z-index:101;border-radius:8px;flex-direction:column;gap:14px;min-width:300px;padding:16px;display:flex;box-shadow:0 1px 6px #0000001a}.palette-header[data-v-a8b8bc9c]{border-bottom:1px solid var(--wb-border);justify-content:space-between;align-items:center;padding-bottom:12px;display:flex}.palette-title[data-v-a8b8bc9c]{color:var(--wb-text);font-size:14px;font-weight:600}.current-color[data-v-a8b8bc9c]{align-items:center;display:flex}.current-swatch[data-v-a8b8bc9c]{border:2px solid var(--wb-border);border-radius:50%;width:28px;height:28px}.current-swatch.fill[data-v-a8b8bc9c]{border-radius:6px}.current-transparent[data-v-a8b8bc9c]{justify-content:center;align-items:center;display:flex;background:repeating-conic-gradient(#ccc 0% 25%,#fff 0% 50%) 50%/8px 8px!important}.preset-grid[data-v-a8b8bc9c]{grid-template-columns:repeat(8,1fr);gap:6px;display:grid}.color-swatch[data-v-a8b8bc9c]{cursor:pointer;width:30px;height:30px;transition:all var(--wb-transition);border:2px solid #0000;border-radius:50%;outline:none;padding:0}.color-swatch[data-v-a8b8bc9c]:hover{border-color:var(--wb-text);box-shadow:var(--wb-shadow-md);z-index:1;transform:scale(1.15)}.color-swatch[data-v-a8b8bc9c]:active{transform:scale(.95)}.transparent-row[data-v-a8b8bc9c]{justify-content:center;display:flex}.transparent-swatch[data-v-a8b8bc9c]{background:repeating-conic-gradient(#ccc 0% 25%,#fff 0% 50%) 50%/8px 8px;justify-content:center;align-items:center;display:flex}.palette-footer[data-v-a8b8bc9c]{border-top:1px solid var(--wb-border);padding-top:10px}.custom-color[data-v-a8b8bc9c]{align-items:center;gap:10px;display:flex}.color-picker[data-v-a8b8bc9c]{border:2px solid var(--wb-border);border-radius:var(--wb-radius-sm);cursor:pointer;background:0 0;width:36px;height:36px;padding:2px}.color-picker[data-v-a8b8bc9c]::-webkit-color-swatch-wrapper{padding:0}.color-picker[data-v-a8b8bc9c]::-webkit-color-swatch{border:none;border-radius:4px}.custom-label[data-v-a8b8bc9c]{color:var(--wb-text-secondary);font-size:13px;font-weight:500}:root,.wb-theme-light{--wb-bg:#f0f2f5;--wb-surface:#fff;--wb-surface-hover:#f5f6f8;--wb-surface-toolbar:#f8f9fb;--wb-text:#1a1a2e;--wb-text-secondary:#6b7280;--wb-border:#e5e7eb;--wb-accent:#6366f1;--wb-accent-light:#eef2ff;--wb-canvas-bg:#fff;--wb-canvas-surround:#e0e0e0;--wb-grid-a:#fff;--wb-grid-b:#f0f0f0;--wb-shadow-sm:0 1px 2px #0000000d;--wb-shadow-md:0 4px 12px #00000014;--wb-shadow-lg:0 8px 24px #0000001f;--wb-radius-sm:8px;--wb-radius-md:12px;--wb-radius-lg:16px;--wb-transition:.2s cubic-bezier(.4, 0, .2, 1)}.wb-theme-dark{--wb-bg:#1a1a2e;--wb-surface:#252540;--wb-surface-hover:#2e2e4a;--wb-surface-toolbar:#202038;--wb-text:#e2e2f0;--wb-text-secondary:#a0a0b8;--wb-border:#3a3a55;--wb-accent:#818cf8;--wb-accent-light:#818cf826;--wb-canvas-bg:#1e1e30;--wb-canvas-surround:#12121f;--wb-grid-a:#2a2a3e;--wb-grid-b:#1e1e30;--wb-shadow-sm:0 1px 2px #0000004d;--wb-shadow-md:0 4px 12px #0006;--wb-shadow-lg:0 8px 24px #00000080}.wb-root[data-v-40d42bb5]{width:100%;height:100%;display:flex;position:relative;overflow:hidden}
2
+ /*$vite$:1*/