jellies-draw
v0.4.1
Published
A drawer for jellies
Downloads
95
Readme
jellies-draw
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.
Features at a glance:
- Drawing tools: pen, rectangle, ellipse, line, arrow, text, image, eraser, clear.
- Selection & manipulation: selector tool with multi-select, drag, transform, copy/cut/paste, undo/redo.
- Laser pointer: temporary red trail for presenting, with a hot-key toggle.
- Multi-canvas: mount any number of
<drawing-canvas>instances; one toolbar controls whichever canvas was last focused. - Save / load: serialize a canvas — drawing plus its full undo history — to a JSON string, and restore it later.
- Keyboard shortcuts: tool picking, color/size cycling, clipboard, history, alt-to-penetrate.
Install
yarn add jellies-draw
# or
npm install jellies-drawPeer requirements: vue@^2.7 and konva@^9.
Quick start
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.
<template>
<div id="app">
<div class="toolbar">
<tool-buttons :has-short-cuts="true" />
<property-pickers />
</div>
<div class="canvas-area">
<drawing-canvas background-color="#ffffff" />
</div>
</div>
</template>
<script>
import { DrawingCanvas, ToolButtons, PropertyPickers } from 'jellies-draw'
export default {
components: { DrawingCanvas, ToolButtons, PropertyPickers }
}
</script>
<style>
.toolbar { display: flex; align-items: center; height: 40px; background: #EBEEF3; }
.canvas-area { position: relative; height: calc(100vh - 40px); }
</style>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.).
Components
<drawing-canvas>
Renders a Konva stage inside its container.
| Prop | Type | Default | Description |
| ----------------- | -------- | --------------- | ---------------------------------------------------------------- |
| background-color| String | 'transparent' | CSS color for the whiteboard surface. |
Emits:
penetrable-updated(isPenetrable: Boolean)— fires whenever the canvas becomes click-through (laser tool, Alt held, or selector locked).
Methods — call these on a component ref:
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.load(json)— restore the canvas from a string produced bysave(), bringing back both the drawing and its undo/redo stack. Passnullor''to clear the canvas to a blank state.
<tool-buttons>
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.
| Prop | Type | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| has-short-cuts | Boolean | false | Enables keyboard shortcuts (tool picking, color/size, clipboard, history). |
| 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. |
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).
Tool behavior:
- 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. - Continuous (
pen,text,eraser,laser): stay selected until the user picks a different tool. - Instant (
image,clear): perform a one-shot action and revert to the previous tool after ~300 ms.imageopens a file picker;clearwipes the active canvas (or every canvas, whenare-canvases-linkedistrue).
<property-pickers>
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.
- Right-click the fill swatch to set fill to
transparent. - Stroke widths:
2(H),3(HB),4(B),6(2B). - Font sizes:
15(S),20(M),35(L),50(XL).
Multi-canvas usage
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.
<template>
<div>
<div class="toolbar">
<tool-buttons :has-short-cuts="true" />
<property-pickers />
</div>
<div class="split">
<drawing-canvas background-color="#fffdfa" />
<drawing-canvas background-color="#fafdff" />
</div>
</div>
</template>
<style>
.split { display: flex; height: calc(100vh - 40px); }
.split > * { flex: 1; position: relative; }
</style>Use are-canvases-linked on <tool-buttons> to choose how the mounted canvases relate to each other:
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.)are-canvases-linked="true"(one logical document). All mounted canvases share a single history stack; undo/redo replays every canvas in lockstep, andclearwipes 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).
Always shared (regardless of mode):
- 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.
- The active canvas. Pointer events on an inactive canvas first promote it to active, then begin drawing.
Limitations:
- 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.
- 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.
- Pick a mode at mount time. The linked and per-canvas modes maintain separate history stacks under the hood; flipping
are-canvases-linkedmid-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 togglingv-if).
Paged documents
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.
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:
<template>
<div class="reader">
<div class="page-stage">
<div :key="page" class="page" v-html="pages[page - 1]" />
<drawing-canvas ref="board" class="overlay" background-color="transparent" />
</div>
<button :disabled="page === 1" @click="go(page - 1)">Prev</button>
<button :disabled="page === pages.length" @click="go(page + 1)">Next</button>
</div>
</template>
<script>
import { DrawingCanvas } from 'jellies-draw'
export default {
components: { DrawingCanvas },
data: () => ({
page: 1,
pages: ['<h2>Page 1</h2>', '<h2>Page 2</h2>'],
drawings: {}
}),
methods: {
go(target) {
if (target < 1 || target > this.pages.length) return
this.drawings[this.page] = this.$refs.board.save() // stash the page being left
this.page = target
this.$refs.board.load(this.drawings[target] || null) // restore the page being entered
}
}
}
</script>
<style>
.page-stage { position: relative; height: 70vh; }
.overlay { position: absolute; inset: 0; }
</style>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.
A couple of things to keep in mind:
- 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.
- While a drawing tool is active the overlay captures pointer events. Keep navigation controls outside the overlay, or rely on penetrable mode so clicks fall through when you are not drawing.
The demo's Paged layout is a working example of this pattern.
Keyboard shortcuts
Enabled when <tool-buttons :has-short-cuts="true"> is mounted and an input/text field is not focused.
| Key | Action |
| -------------------- | --------------------------------------------------------- |
| 1–9, 0 | Select tool 1–10 (see tool order above). |
| ` | Toggle laser pointer. |
| Esc | Deselect all nodes. |
| Backspace | Delete selected nodes. |
| Cmd/Ctrl + A | Select all nodes. |
| Cmd/Ctrl + C/X/V | Copy / cut / paste. |
| Cmd/Ctrl + Z | Undo. |
| Cmd/Ctrl + Shift+Z | Redo. |
| Shift + 1–9 | Pick stroke color (red, green, orange, blue, pink, yellow, purple, light gray, dark gray). |
| Shift + = / - | Increase / decrease size (font size or stroke width). |
| Hold Alt | Make the canvas click-through temporarily. |
Text editing suspends shortcuts so typing is uninterrupted.
Penetrable mode
The canvas becomes click-through when:
- The
lasertool is active. Altis held.- The
selectortool is locked.
While penetrable, pointer events fall through to whatever sits behind the canvas — useful for annotating over an underlying UI.
Demo / development
The repo includes a Vue CLI demo app showing single, split, paged, and scrollable layouts.
yarn install
yarn serve # dev server with hot reload
yarn build # build the UMD bundle to dist/jellies-draw.js
yarn lintLicense
MIT.
