jellies-draw 0.3.5 → 0.4.1

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 CHANGED
@@ -1,24 +1,238 @@
1
1
  # jellies-draw
2
2
 
3
- ## Project setup
3
+ A Vue 2 + Konva drawing toolkit that turns any DOM container into a whiteboard. Ships three components — a canvas, a tool palette, and a property picker — that share state via a singleton store, so a single toolbar can drive one or many canvases simultaneously.
4
+
5
+ Features at a glance:
6
+
7
+ - **Drawing tools**: pen, rectangle, ellipse, line, arrow, text, image, eraser, clear.
8
+ - **Selection & manipulation**: selector tool with multi-select, drag, transform, copy/cut/paste, undo/redo.
9
+ - **Laser pointer**: temporary red trail for presenting, with a hot-key toggle.
10
+ - **Multi-canvas**: mount any number of `<drawing-canvas>` instances; one toolbar controls whichever canvas was last focused.
11
+ - **Save / load**: serialize a canvas — drawing plus its full undo history — to a JSON string, and restore it later.
12
+ - **Keyboard shortcuts**: tool picking, color/size cycling, clipboard, history, alt-to-penetrate.
13
+
14
+ ## Install
15
+
4
16
  ```
5
- yarn install
17
+ yarn add jellies-draw
18
+ # or
19
+ npm install jellies-draw
6
20
  ```
7
21
 
8
- ### Compiles and hot-reloads for development
9
- ```
10
- yarn serve
22
+ Peer requirements: `vue@^2.7` and `konva@^9`.
23
+
24
+ ## Quick start
25
+
26
+ Mount all three components inside a parent — the canvas can live anywhere, but `<tool-buttons>` must be mounted to drive the shared tool state, and to enable keyboard shortcuts when `has-short-cuts` is `true`.
27
+
28
+ ```vue
29
+ <template>
30
+ <div id="app">
31
+ <div class="toolbar">
32
+ <tool-buttons :has-short-cuts="true" />
33
+ <property-pickers />
34
+ </div>
35
+ <div class="canvas-area">
36
+ <drawing-canvas background-color="#ffffff" />
37
+ </div>
38
+ </div>
39
+ </template>
40
+
41
+ <script>
42
+ import { DrawingCanvas, ToolButtons, PropertyPickers } from 'jellies-draw'
43
+
44
+ export default {
45
+ components: { DrawingCanvas, ToolButtons, PropertyPickers }
46
+ }
47
+ </script>
48
+
49
+ <style>
50
+ .toolbar { display: flex; align-items: center; height: 40px; background: #EBEEF3; }
51
+ .canvas-area { position: relative; height: calc(100vh - 40px); }
52
+ </style>
11
53
  ```
12
54
 
13
- ### Compiles and minifies for production
55
+ The canvas fills its parent (`width: 100%; height: 100%`), so give the wrapper an explicit size. Each canvas observes its container with a `ResizeObserver`, so the Konva stage resizes whenever the wrapper's box changes (window resize, layout reflow, parent resize, etc.).
56
+
57
+ ## Components
58
+
59
+ ### `<drawing-canvas>`
60
+
61
+ Renders a Konva stage inside its container.
62
+
63
+ | Prop | Type | Default | Description |
64
+ | ----------------- | -------- | --------------- | ---------------------------------------------------------------- |
65
+ | `background-color`| `String` | `'transparent'` | CSS color for the whiteboard surface. |
66
+
67
+ Emits:
68
+
69
+ - `penetrable-updated(isPenetrable: Boolean)` — fires whenever the canvas becomes click-through (laser tool, Alt held, or selector locked).
70
+
71
+ Methods — call these on a component `ref`:
72
+
73
+ - `save()` → `String` — serialize the canvas to a JSON string. The JSON holds the **entire undo history** (every snapshot plus the current index), not just the visible drawing.
74
+ - `load(json)` — restore the canvas from a string produced by `save()`, bringing back both the drawing and its undo/redo stack. Pass `null` or `''` to clear the canvas to a blank state.
75
+
76
+ ### `<tool-buttons>`
77
+
78
+ Renders the tool palette. The palette mutates a shared singleton store, so one instance already drives every mounted canvas — **mount exactly one `<tool-buttons>` per page**. A second instance's `has-short-cuts` / `are-canvases-linked` watchers clobber the first's, so whichever mounted last silently wins.
79
+
80
+ | Prop | Type | Default | Description |
81
+ | ---------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
82
+ | `has-short-cuts` | `Boolean` | `false` | Enables keyboard shortcuts (tool picking, color/size, clipboard, history). |
83
+ | `are-canvases-linked` | `Boolean` | `false` | Treat every mounted canvas as one logical document: `clear` wipes them all, and undo/redo replays a shared history stack across all of them in lockstep. |
84
+
85
+ Tools in order: `laser`, `selector`, `rectangle`, `ellipse`, `arrow`, `line`, `pen`, `text`, `image`, `eraser`, `clear`. Numeric shortcuts `1`–`9, 0` map to positions 1–10 (laser is intentionally unbound; use `` ` `` to toggle it).
86
+
87
+ Tool behavior:
88
+
89
+ - **Lockable** (`selector`, `rectangle`, `ellipse`, `arrow`, `line`): click an active tool again to lock it on — it stays selected after each use instead of falling back to the previous tool.
90
+ - **Continuous** (`pen`, `text`, `eraser`, `laser`): stay selected until the user picks a different tool.
91
+ - **Instant** (`image`, `clear`): perform a one-shot action and revert to the previous tool after ~300 ms. `image` opens a file picker; `clear` wipes the active canvas (or every canvas, when `are-canvases-linked` is `true`).
92
+
93
+ ### `<property-pickers>`
94
+
95
+ Color and size controls. Two color swatches (fill and stroke) and a size dropdown that switches between stroke widths and font sizes depending on whether a text tool/node is active.
96
+
97
+ - Right-click the fill swatch to set fill to `transparent`.
98
+ - Stroke widths: `2` (H), `3` (HB), `4` (B), `6` (2B).
99
+ - Font sizes: `15` (S), `20` (M), `35` (L), `50` (XL).
100
+
101
+ ## Multi-canvas usage
102
+
103
+ Multiple `<drawing-canvas>` instances can coexist on the same page. The most recently interacted-with canvas becomes the active target for the toolbar — pointer events on an inactive canvas first promote it to active, then begin drawing.
104
+
105
+ ```vue
106
+ <template>
107
+ <div>
108
+ <div class="toolbar">
109
+ <tool-buttons :has-short-cuts="true" />
110
+ <property-pickers />
111
+ </div>
112
+ <div class="split">
113
+ <drawing-canvas background-color="#fffdfa" />
114
+ <drawing-canvas background-color="#fafdff" />
115
+ </div>
116
+ </div>
117
+ </template>
118
+
119
+ <style>
120
+ .split { display: flex; height: calc(100vh - 40px); }
121
+ .split > * { flex: 1; position: relative; }
122
+ </style>
14
123
  ```
15
- yarn build
124
+
125
+ Use `are-canvases-linked` on `<tool-buttons>` to choose how the mounted canvases relate to each other:
126
+
127
+ - **`are-canvases-linked="false"` (default — independent documents).** Each canvas keeps its own undo/redo stack and its own clear action. Pick this when several canvases are mounted at once as separate artboards. (For pages shown one at a time, keep a single canvas and swap content instead — see [Paged documents](#paged-documents).)
128
+ - **`are-canvases-linked="true"` (one logical document).** All mounted canvases share a single history stack; undo/redo replays every canvas in lockstep, and `clear` wipes them all. Pick this when the canvases together form one drawing surface (e.g. a split view, or canvases overlaid on a long scrollable page).
129
+
130
+ Always shared (regardless of mode):
131
+
132
+ - **Tool selection, colors, sizes, and the laser pointer.** Picking a tool or color in the toolbar applies to whichever canvas the user interacts with next.
133
+ - **The active canvas.** Pointer events on an inactive canvas first promote it to active, then begin drawing.
134
+
135
+ Limitations:
136
+
137
+ - **A drawing stays on the canvas it started on.** A stroke or shape begun on one canvas is bound to that canvas until the pointer is released. Dragging onto another canvas (or off-canvas) mid-draw is not captured there, and leaves a straight gap if the pointer returns. You cannot draw one continuous stroke spanning two canvases — treat each as a separate drawing surface.
138
+ - **Line / arrow attachments are scoped to a single canvas.** Endpoints that snap to a rectangle/ellipse only track shapes on the same canvas; cross-canvas attachments are not supported.
139
+ - **Pick a mode at mount time.** The linked and per-canvas modes maintain separate history stacks under the hood; flipping `are-canvases-linked` mid-session switches which stack drives undo/redo, but does not migrate entries between them. If you need to flip dynamically, unmount and remount the canvases (the demo's mode switcher does this by toggling `v-if`).
140
+
141
+ ## Paged documents
142
+
143
+ When pages are shown one at a time — a carousel, a paged reader, a slide deck — don't mount one canvas per page. As pages scroll out of view their DOM is usually unmounted, which destroys the canvas and its drawing along with it.
144
+
145
+ Instead, keep a **single** `<drawing-canvas>` overlaid on the page area and swap each page's content with `save()` / `load()`. The host stores one JSON string per page:
146
+
147
+ ```vue
148
+ <template>
149
+ <div class="reader">
150
+ <div class="page-stage">
151
+ <div :key="page" class="page" v-html="pages[page - 1]" />
152
+ <drawing-canvas ref="board" class="overlay" background-color="transparent" />
153
+ </div>
154
+ <button :disabled="page === 1" @click="go(page - 1)">Prev</button>
155
+ <button :disabled="page === pages.length" @click="go(page + 1)">Next</button>
156
+ </div>
157
+ </template>
158
+
159
+ <script>
160
+ import { DrawingCanvas } from 'jellies-draw'
161
+
162
+ export default {
163
+ components: { DrawingCanvas },
164
+ data: () => ({
165
+ page: 1,
166
+ pages: ['<h2>Page 1</h2>', '<h2>Page 2</h2>'],
167
+ drawings: {}
168
+ }),
169
+ methods: {
170
+ go(target) {
171
+ if (target < 1 || target > this.pages.length) return
172
+ this.drawings[this.page] = this.$refs.board.save() // stash the page being left
173
+ this.page = target
174
+ this.$refs.board.load(this.drawings[target] || null) // restore the page being entered
175
+ }
176
+ }
177
+ }
178
+ </script>
179
+
180
+ <style>
181
+ .page-stage { position: relative; height: 70vh; }
182
+ .overlay { position: absolute; inset: 0; }
183
+ </style>
16
184
  ```
17
185
 
18
- ### Lints and fixes files
186
+ Because `save()` captures the full undo history, undo/redo keeps working per page after you navigate away and back. Persist the `drawings` map to a backend if annotations must outlive a reload.
187
+
188
+ A couple of things to keep in mind:
189
+
190
+ - The single canvas is one fixed overlay — it does not scroll with page content. If a page scrolls internally, mount the canvas inside the scrolling element (sized to the full content) rather than over the viewport.
191
+ - While a drawing tool is active the overlay captures pointer events. Keep navigation controls outside the overlay, or rely on [penetrable mode](#penetrable-mode) so clicks fall through when you are not drawing.
192
+
193
+ The demo's **Paged** layout is a working example of this pattern.
194
+
195
+ ## Keyboard shortcuts
196
+
197
+ Enabled when `<tool-buttons :has-short-cuts="true">` is mounted and an input/text field is not focused.
198
+
199
+ | Key | Action |
200
+ | -------------------- | --------------------------------------------------------- |
201
+ | `1`–`9`, `0` | Select tool 1–10 (see tool order above). |
202
+ | `` ` `` | Toggle laser pointer. |
203
+ | `Esc` | Deselect all nodes. |
204
+ | `Backspace` | Delete selected nodes. |
205
+ | `Cmd/Ctrl + A` | Select all nodes. |
206
+ | `Cmd/Ctrl + C/X/V` | Copy / cut / paste. |
207
+ | `Cmd/Ctrl + Z` | Undo. |
208
+ | `Cmd/Ctrl + Shift+Z` | Redo. |
209
+ | `Shift + 1`–`9` | Pick stroke color (red, green, orange, blue, pink, yellow, purple, light gray, dark gray). |
210
+ | `Shift + =` / `-` | Increase / decrease size (font size or stroke width). |
211
+ | Hold `Alt` | Make the canvas click-through temporarily. |
212
+
213
+ Text editing suspends shortcuts so typing is uninterrupted.
214
+
215
+ ## Penetrable mode
216
+
217
+ The canvas becomes click-through when:
218
+
219
+ - The `laser` tool is active.
220
+ - `Alt` is held.
221
+ - The `selector` tool is locked.
222
+
223
+ While penetrable, pointer events fall through to whatever sits behind the canvas — useful for annotating over an underlying UI.
224
+
225
+ ## Demo / development
226
+
227
+ The repo includes a Vue CLI demo app showing single, split, paged, and scrollable layouts.
228
+
19
229
  ```
230
+ yarn install
231
+ yarn serve # dev server with hot reload
232
+ yarn build # build the UMD bundle to dist/jellies-draw.js
20
233
  yarn lint
21
234
  ```
22
235
 
23
- ### Customize configuration
24
- See [Configuration Reference](https://cli.vuejs.org/config/).
236
+ ## License
237
+
238
+ MIT.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jellies-draw",
3
- "version": "0.3.5",
3
+ "version": "0.4.1",
4
4
  "description": "A drawer for jellies",
5
5
  "private": false,
6
6
  "main": "./src/index.js",
package/src/App.vue CHANGED
@@ -1,18 +1,82 @@
1
1
  <template>
2
2
  <div id="app">
3
3
  <div class="toolbar" v-show="isCanvasEnabled">
4
- <tool-buttons :has-short-cuts="isCanvasEnabled" />
4
+ <tool-buttons
5
+ :has-short-cuts="isCanvasEnabled"
6
+ :are-canvases-linked="mode !== 'paged'"
7
+ />
5
8
  <div class="toolbar-divider"/>
6
9
  <property-pickers />
7
10
  </div>
8
11
  <div class="demo-entry">
9
- <input type="checkbox" v-model="isCanvasEnabled"/> Enable Canvas
12
+ <label>
13
+ <input type="checkbox" v-model="isCanvasEnabled"/> Enable Canvas
14
+ </label>
15
+ <span v-if="isCanvasEnabled" class="mode-switcher">
16
+ <button
17
+ v-for="opt in modes"
18
+ :key="opt.value"
19
+ :class="['mode-btn', { active: mode === opt.value }]"
20
+ @click="mode = opt.value"
21
+ >
22
+ {{ opt.label }}
23
+ </button>
24
+ </span>
25
+ </div>
26
+ <div v-if="isCanvasCreated" v-show="isCanvasVisible" class="canvas-area">
27
+ <template v-if="mode === 'split'">
28
+ <div key="split-layout" class="split-layout">
29
+ <div class="split-pane">
30
+ <div class="pane-label">Left</div>
31
+ <drawing-canvas background-color="#fffdfa" />
32
+ </div>
33
+ <div class="split-divider"/>
34
+ <div class="split-pane">
35
+ <div class="pane-label">Right</div>
36
+ <drawing-canvas background-color="#fafdff" />
37
+ </div>
38
+ </div>
39
+ </template>
40
+ <template v-else-if="mode === 'paged'">
41
+ <div key="lesson-layout" class="lesson-layout">
42
+ <div class="lesson-stage">
43
+ <div :key="currentPage" class="lesson-page" v-html="currentPageHtml" />
44
+ <div class="lesson-board">
45
+ <drawing-canvas ref="board" background-color="transparent" />
46
+ </div>
47
+ </div>
48
+ <div class="lesson-nav">
49
+ <button
50
+ class="nav-arrow"
51
+ :disabled="currentPage === 1"
52
+ @click="goToPage(currentPage - 1)"
53
+ >‹</button>
54
+ <button
55
+ v-for="page in pages.length"
56
+ :key="page"
57
+ :class="['page-dot', { active: currentPage === page }]"
58
+ @click="goToPage(page)"
59
+ >
60
+ {{ page }}
61
+ </button>
62
+ <button
63
+ class="nav-arrow"
64
+ :disabled="currentPage === pages.length"
65
+ @click="goToPage(currentPage + 1)"
66
+ >›</button>
67
+ </div>
68
+ </div>
69
+ </template>
70
+ <template v-else-if="mode === 'scroll'">
71
+ <div key="scroll-layout" class="scroll-layout">
72
+ <div class="scroll-viewport">
73
+ <div class="scroll-content">
74
+ <drawing-canvas background-color="#fafafa" />
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </template>
10
79
  </div>
11
- <drawing-canvas
12
- ref="drawingCanvas"
13
- v-if="isCanvasCreated"
14
- v-show="isCanvasVisible"
15
- />
16
80
  </div>
17
81
  </template>
18
82
 
@@ -29,7 +93,21 @@ export default {
29
93
  },
30
94
  data: () => ({
31
95
  isCanvasCreated: false,
32
- isCanvasVisible: false
96
+ isCanvasVisible: false,
97
+ mode: 'split',
98
+ currentPage: 1,
99
+ pages: [
100
+ '<h2>Lesson 1 · Variables</h2><p>A <strong>variable</strong> is a named place to keep a value. Put a value in, read it back later.</p><p>Try it: circle the word <em>variable</em> with the pen, then turn the page.</p>',
101
+ '<h2>Lesson 2 · Loops</h2><p>A <strong>loop</strong> repeats a block of code, so you do not write the same line ten times.</p><p>Your drawing from Lesson 1 stays on its own page — flip back and check.</p>',
102
+ '<h2>Lesson 3 · Functions</h2><p>A <strong>function</strong> wraps logic under a name so you can reuse it. Call the name, get the result.</p><p>Each page keeps its own annotations.</p>',
103
+ '<h2>Lesson 4 · Recap</h2><p>Variables hold values, loops repeat work, functions package logic.</p><p>One whiteboard overlays every page; <code>save()</code> and <code>load()</code> swap the drawing as you navigate.</p>'
104
+ ],
105
+ drawings: {},
106
+ modes: [
107
+ { value: 'split', label: 'Split' },
108
+ { value: 'paged', label: 'Paged' },
109
+ { value: 'scroll', label: 'Scroll' }
110
+ ]
33
111
  }),
34
112
  computed: {
35
113
  isCanvasEnabled: {
@@ -38,16 +116,30 @@ export default {
38
116
  },
39
117
  set(isEnabled) {
40
118
  if (isEnabled) {
41
- if (this.isCanvasCreated) {
42
- this.isCanvasVisible = true
43
- } else {
44
- this.isCanvasCreated = true
45
- this.isCanvasVisible = true
46
- }
119
+ if (!this.isCanvasCreated) this.isCanvasCreated = true
120
+ this.isCanvasVisible = true
47
121
  } else {
48
122
  this.isCanvasVisible = false
49
123
  }
50
124
  }
125
+ },
126
+ currentPageHtml() {
127
+ return this.pages[this.currentPage - 1] || ''
128
+ }
129
+ },
130
+ watch: {
131
+ mode() {
132
+ this.currentPage = 1
133
+ this.drawings = {}
134
+ }
135
+ },
136
+ methods: {
137
+ goToPage(target) {
138
+ if (target < 1 || target > this.pages.length || target === this.currentPage) return
139
+ const board = this.$refs.board
140
+ if (board) this.drawings[this.currentPage] = board.save()
141
+ this.currentPage = target
142
+ if (board) board.load(this.drawings[target] || null)
51
143
  }
52
144
  }
53
145
  }
@@ -69,6 +161,8 @@ body {
69
161
  #app {
70
162
  height: 100%;
71
163
  width: 100%;
164
+ display: flex;
165
+ flex-direction: column;
72
166
  }
73
167
  .toolbar {
74
168
  height: 40px;
@@ -77,6 +171,7 @@ body {
77
171
  justify-content: center;
78
172
  align-items: center;
79
173
  background: #EBEEF3;
174
+ flex-shrink: 0;
80
175
  }
81
176
  .toolbar-divider {
82
177
  height: 30px;
@@ -86,8 +181,157 @@ body {
86
181
  }
87
182
  .demo-entry {
88
183
  position: absolute;
89
- top: 0;
90
- right: 0;
91
- z-index: 2;
184
+ top: 6px;
185
+ right: 12px;
186
+ z-index: 10;
187
+ display: flex;
188
+ gap: 12px;
189
+ align-items: center;
190
+ font-size: 12px;
191
+ }
192
+ .mode-switcher {
193
+ display: inline-flex;
194
+ border: 1px solid rgba(0, 0, 0, 0.12);
195
+ border-radius: 4px;
196
+ overflow: hidden;
197
+ background: #fff;
198
+ }
199
+ .mode-btn {
200
+ border: none;
201
+ background: transparent;
202
+ padding: 3px 10px;
203
+ cursor: pointer;
204
+ font-size: 12px;
205
+ color: #555;
206
+ }
207
+ .mode-btn + .mode-btn {
208
+ border-left: 1px solid rgba(0, 0, 0, 0.08);
209
+ }
210
+ .mode-btn.active {
211
+ background: #399af4;
212
+ color: #fff;
213
+ }
214
+ .canvas-area {
215
+ flex: 1;
216
+ min-height: 0;
217
+ width: 100%;
218
+ position: relative;
219
+ }
220
+ .split-layout {
221
+ height: 100%;
222
+ width: 100%;
223
+ display: flex;
224
+ }
225
+ .split-pane {
226
+ flex: 1;
227
+ position: relative;
228
+ min-width: 0;
229
+ }
230
+ .split-divider {
231
+ width: 1px;
232
+ background: rgba(0, 0, 0, 0.08);
233
+ }
234
+ .pane-label {
235
+ position: absolute;
236
+ top: 6px;
237
+ left: 8px;
238
+ z-index: 10000;
239
+ font-size: 11px;
240
+ color: rgba(0, 0, 0, 0.45);
241
+ pointer-events: none;
242
+ }
243
+ .lesson-layout {
244
+ height: 100%;
245
+ width: 100%;
246
+ display: flex;
247
+ flex-direction: column;
248
+ }
249
+ .lesson-stage {
250
+ flex: 1;
251
+ min-height: 0;
252
+ position: relative;
253
+ background: #ffffff;
254
+ }
255
+ .lesson-page {
256
+ position: absolute;
257
+ inset: 0;
258
+ overflow: auto;
259
+ box-sizing: border-box;
260
+ padding: 24px 32px;
261
+ text-align: left;
262
+ font-size: 14px;
263
+ color: #333;
264
+ }
265
+ .lesson-page h2 {
266
+ margin: 0 0 12px;
267
+ font-size: 17px;
268
+ color: #1f2d3d;
269
+ }
270
+ .lesson-page p {
271
+ margin: 0 0 10px;
272
+ line-height: 1.6;
273
+ }
274
+ .lesson-page code {
275
+ background: #f1f3f5;
276
+ border-radius: 3px;
277
+ padding: 1px 5px;
278
+ font-size: 13px;
279
+ }
280
+ .lesson-board {
281
+ position: absolute;
282
+ inset: 0;
283
+ }
284
+ .lesson-nav {
285
+ flex-shrink: 0;
286
+ display: flex;
287
+ justify-content: center;
288
+ align-items: center;
289
+ gap: 6px;
290
+ padding: 6px;
291
+ background: #f5f6f8;
292
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
293
+ }
294
+ .nav-arrow,
295
+ .page-dot {
296
+ border: 1px solid rgba(0, 0, 0, 0.12);
297
+ background: #fff;
298
+ border-radius: 4px;
299
+ cursor: pointer;
300
+ font-size: 12px;
301
+ color: #555;
302
+ }
303
+ .nav-arrow {
304
+ width: 26px;
305
+ height: 26px;
306
+ line-height: 1;
307
+ }
308
+ .nav-arrow:disabled {
309
+ opacity: 0.4;
310
+ cursor: default;
311
+ }
312
+ .page-dot {
313
+ min-width: 26px;
314
+ height: 26px;
315
+ padding: 0 6px;
316
+ }
317
+ .page-dot.active {
318
+ background: #399af4;
319
+ color: #fff;
320
+ border-color: #399af4;
321
+ }
322
+ .scroll-layout {
323
+ height: 100%;
324
+ width: 100%;
325
+ overflow: hidden;
326
+ }
327
+ .scroll-viewport {
328
+ height: 100%;
329
+ width: 100%;
330
+ overflow: auto;
331
+ }
332
+ .scroll-content {
333
+ width: 100%;
334
+ height: 1800px;
335
+ position: relative;
92
336
  }
93
337
  </style>
Binary file
@@ -31,7 +31,7 @@ export default {
31
31
  props: {
32
32
  backgroundColor: {
33
33
  type: String,
34
- default: 'penetrable'
34
+ default: 'transparent'
35
35
  }
36
36
  },
37
37
  computed: {
@@ -64,7 +64,13 @@ export default {
64
64
  Canvas.initialize(this.canvas, this.container)
65
65
  },
66
66
  destroyStage() {
67
- Canvas.destroy()
67
+ Canvas.destroy(this.canvas)
68
+ },
69
+ save() {
70
+ return Canvas.exportJSON(this.canvas)
71
+ },
72
+ load(json) {
73
+ Canvas.loadJSON(this.canvas, json)
68
74
  }
69
75
  }
70
76
  }
@@ -78,6 +84,8 @@ export default {
78
84
  .container {
79
85
  width: 100%;
80
86
  height: 100%;
87
+ position: relative;
88
+ box-sizing: border-box;
81
89
  }
82
90
  .container-penetrable,
83
91
  .container-penetrable * {
@@ -88,6 +96,8 @@ export default {
88
96
  }
89
97
  .whiteboard {
90
98
  position: relative;
91
- z-index: 9999; /* 确保画板在最上层 */
99
+ width: 100%;
100
+ height: 100%;
101
+ z-index: 9999;
92
102
  }
93
103
  </style>
@@ -142,13 +142,6 @@ button {
142
142
  content: "\e7a9";
143
143
  }
144
144
  .tool-laser:before {
145
- content: "";
146
- display: inline-block;
147
- width: 8px;
148
- height: 8px;
149
- border-radius: 50%;
150
- background: #f33b29;
151
- box-shadow: 0 0 4px #f33b29, 0 0 8px rgba(243, 59, 41, 0.6);
152
- vertical-align: middle;
145
+ content: "\e900";
153
146
  }
154
147
  </style>
@@ -22,6 +22,7 @@ import ToolButton from './ToolButton.vue'
22
22
  import Properties from './functions/properties'
23
23
  import Tools from './functions/tools'
24
24
  import Canvas from './functions/canvas'
25
+
25
26
  export default {
26
27
  name: 'ToolButtons',
27
28
  components: {
@@ -31,6 +32,10 @@ export default {
31
32
  hasShortCuts: {
32
33
  type: Boolean,
33
34
  default: false
35
+ },
36
+ areCanvasesLinked: {
37
+ type: Boolean,
38
+ default: false
34
39
  }
35
40
  },
36
41
  mounted() {
@@ -99,8 +104,17 @@ export default {
99
104
  }
100
105
  },
101
106
  watch: {
102
- hasShortCuts(hasShortCuts) {
103
- Canvas.hasShortCuts = hasShortCuts
107
+ hasShortCuts: {
108
+ immediate: true,
109
+ handler(hasShortCuts) {
110
+ Canvas.hasShortCuts = hasShortCuts
111
+ }
112
+ },
113
+ areCanvasesLinked: {
114
+ immediate: true,
115
+ handler(areCanvasesLinked) {
116
+ Properties.areCanvasesLinked = areCanvasesLinked
117
+ }
104
118
  }
105
119
  },
106
120
  methods: {