jellies-draw 0.3.4 → 0.4.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 +224 -10
- package/package.json +1 -1
- package/src/App.vue +261 -17
- package/src/assets/jellies-draw-tool.ttf +0 -0
- package/src/components/DrawingCanvas.vue +13 -3
- package/src/components/ToolButton.vue +1 -8
- package/src/components/ToolButtons.vue +16 -2
- package/src/components/functions/canvas.js +316 -240
- package/src/components/functions/histories.js +223 -38
- package/src/components/functions/laser.js +239 -206
- package/src/components/functions/properties.js +18 -10
- package/src/components/functions/tools/clear.js +3 -0
- package/src/components/functions/tools/helper/attachedText.js +24 -14
- package/src/components/functions/tools/helper/connection.js +9 -0
- package/src/components/functions/tools/image.js +25 -11
- package/src/components/functions/tools/text.js +14 -4
- package/src/components/functions/transformer.js +356 -316
package/README.md
CHANGED
|
@@ -1,24 +1,238 @@
|
|
|
1
1
|
# jellies-draw
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
17
|
+
yarn add jellies-draw
|
|
18
|
+
# or
|
|
19
|
+
npm install jellies-draw
|
|
6
20
|
```
|
|
7
21
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT.
|
package/package.json
CHANGED
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
|
|
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
|
-
<
|
|
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
|
-
|
|
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:
|
|
90
|
-
right:
|
|
91
|
-
z-index:
|
|
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: '
|
|
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
|
-
|
|
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
|
|
103
|
-
|
|
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: {
|