crystal-draw
v1.0.2
Published
A multi-board drawing and whiteboard component for React with pdf import and export functionality.
Downloads
393
Maintainers
Readme
crystal-draw
A multi-board drawing and whiteboard React component with PDF import and export functionality.
Features
- 🖊️ Drawing tools — Pen, Highlighter, Eraser, and Text
- 📐 Shape tools — Lines, Arrows, Rectangles, Circles, Triangles, Polygons, 2D/3D Graphs, Cones, Cylinders, Cuboids
- 🗂️ Multi-board — Add, remove, and navigate between multiple boards
- ↩️ Undo / Redo — Full history with keyboard shortcuts (
Ctrl+Z/Ctrl+Y/Ctrl+Shift+Z) - 📄 PDF import — Upload a PDF and each page becomes a board background
- 📤 PDF export — Export all boards as a single PDF file
- 📱 Touch support — Works on touch devices
- 🎨 Customisable — Accepts
classNameandstyleprops for easy layout integration
Installation
npm install crystal-drawPeer dependencies — React 16 or higher is required and must be installed in your project.
Quick Start
import CrystalDraw from 'crystal-draw';
import 'crystal-draw/style.css';
export default function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<CrystalDraw />
</div>
);
}Important: Always import
crystal-draw/style.cssso that cursor and tool styles are applied correctly.
Component Props
| Prop | Type | Default | Description |
|-------------|-------------------------|-------------|--------------------------------------------------|
| className | string | undefined | CSS class(es) applied to the root container div |
| style | React.CSSProperties | undefined | Inline styles applied to the root container div |
The component fills its parent container. Give the parent a defined width and height to control the canvas size.
<CrystalDraw
className="my-whiteboard"
style={{ border: '1px solid #ccc', borderRadius: '8px' }}
/>API — Imperative Controls
All state is managed by an internal Zustand store. You can control the whiteboard programmatically using the exported helper functions — ideal for building your own toolbar.
Tool Selection
import { setTool } from 'crystal-draw';
// Switch the active drawing tool
setTool('pen'); // freehand pen
setTool('highlighter'); // semi-transparent highlighter
setTool('eraser'); // erase drawn strokes
setTool('text'); // click on canvas to place text
setTool('laser'); // laser pointer (no persistent marks)
setTool('select'); // selection mode
// Shape tools (2D)
setTool('line');
setTool('arrowline');
setTool('rectangle');
setTool('circle');
setTool('triangle');
setTool('pentagon');
setTool('hexagon');
setTool('octagon');
setTool('parabola');
setTool('twoDgraph');
setTool('threeDgraph');
// Shape tools (3D)
setTool('cuboid');
setTool('cone');
setTool('cylinder');Color & Line Width
import { setColor, setHighlighterColor, setLineWidth } from 'crystal-draw';
// Set pen / shape color (any valid CSS color string)
setColor('#ff0000');
setColor('rgb(0, 128, 255)');
// Set highlighter color (use rgba for transparency)
setHighlighterColor('rgba(255, 255, 0, 0.5)');
// Set line width for a specific tool type
setLineWidth(2, 'PEN'); // thin pen stroke
setLineWidth(16, 'HIGHLIGHTER'); // default highlighter width
setLineWidth(60, 'ERASER'); // large eraserUndo & Redo
import { undo, redo } from 'crystal-draw';
undo(); // remove last drawn stroke
redo(); // re-apply last undone strokeKeyboard shortcuts are enabled automatically when the canvas is mounted:
- Undo:
Ctrl+Z(Windows/Linux) or⌘+Z(macOS) - Redo:
Ctrl+YorCtrl+Shift+Z(Windows/Linux) /⌘+Shift+Z(macOS)
Board Management
import { addBoard, removeBoard, clearBoard, setCurrentBoardIndex } from 'crystal-draw';
addBoard(); // append a new blank board
removeBoard(2); // remove board at index 2
clearBoard(0); // erase all strokes on board at index 0
setCurrentBoardIndex(1); // navigate to board at index 1PDF Import
import { uploadPDF } from 'crystal-draw';
async function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (file) {
await uploadPDF(file); // each page becomes a new board background
}
}
// Usage in JSX
<input type="file" accept="application/pdf" onChange={handleFileChange} />If the whiteboard currently has only one empty board, uploadPDF replaces it with the PDF pages. Otherwise the pages are appended as additional boards.
PDF Export
import { exportPDF } from 'crystal-draw';
import { useRef } from 'react';
function App() {
const containerRef = useRef<HTMLDivElement>(null);
async function handleExport() {
const boardsContainer = document.getElementById('crystal--draw-container') as HTMLDivElement;
if (!boardsContainer) return;
const file = await exportPDF(boardsContainer);
if (file) {
const url = URL.createObjectURL(file);
const a = document.createElement('a');
a.href = url;
a.download = 'whiteboard.pdf';
a.click();
URL.revokeObjectURL(url);
}
}
return (
<>
<button onClick={handleExport}>Export PDF</button>
<div style={{ width: '100vw', height: '100vh' }}>
<CrystalDraw />
</div>
</>
);
}The root element rendered by
<CrystalDraw />always has the idcrystal--draw-container.
Advanced — Zustand Store
For reactive access to the internal state (e.g. building a live toolbar that reflects the current tool), use the exported useBoundStore hook directly:
import { useBoundStore } from 'crystal-draw';
function Toolbar() {
const tool = useBoundStore((state) => state.tool);
const boards = useBoundStore((state) => state.boards);
return (
<div>
<p>Active tool: {tool}</p>
<p>Total boards: {boards.length}</p>
</div>
);
}Store State Shape
| Field | Type | Description |
|---------------------|----------------------------------------------|----------------------------------------------|
| tool | ToolTypes | Currently active tool |
| color | string | Current pen / shape color |
| highlighterColor | string | Current highlighter color |
| lineWidths | { PEN: number, HIGHLIGHTER: number, ERASER: number } | Line widths per tool type |
| boards | Array<{ id, shapes, page }> | All board data |
| currentBoardIndex | number | Index of the currently active board |
| history | historyItem[] | Undo/redo history stack |
| undoIndex | number | Current position in the history stack |
Full Example — Custom Toolbar
import CrystalDraw, {
setTool,
setColor,
setLineWidth,
undo,
redo,
addBoard,
clearBoard,
uploadPDF,
exportPDF,
useBoundStore,
} from 'crystal-draw';
import 'crystal-draw/style.css';
export default function Whiteboard() {
const tool = useBoundStore((state) => state.tool);
const boards = useBoundStore((state) => state.boards);
async function handleExport() {
const el = document.getElementById('crystal--draw-container') as HTMLDivElement;
const file = await exportPDF(el);
if (file) {
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = 'drawing.pdf';
a.click();
}
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
{/* Toolbar */}
<div style={{ display: 'flex', gap: 8, padding: 8, background: '#1e1e2e' }}>
<button onClick={() => setTool('pen')} style={{ fontWeight: tool === 'pen' ? 'bold' : 'normal' }}>Pen</button>
<button onClick={() => setTool('highlighter')} style={{ fontWeight: tool === 'highlighter' ? 'bold' : 'normal' }}>Highlight</button>
<button onClick={() => setTool('eraser')} style={{ fontWeight: tool === 'eraser' ? 'bold' : 'normal' }}>Eraser</button>
<button onClick={() => setTool('text')}>Text</button>
<button onClick={() => setColor('#ff0000')}>Red</button>
<button onClick={() => setColor('#0000ff')}>Blue</button>
<button onClick={() => setLineWidth(2, 'PEN')}>Thin</button>
<button onClick={() => setLineWidth(6, 'PEN')}>Thick</button>
<button onClick={undo}>Undo</button>
<button onClick={redo}>Redo</button>
<button onClick={addBoard}>+ Board</button>
<button onClick={() => clearBoard(boards.length - 1)}>Clear</button>
<label style={{ cursor: 'pointer', color: 'white' }}>
Import PDF
<input
type="file"
accept="application/pdf"
style={{ display: 'none' }}
onChange={(e) => e.target.files?.[0] && uploadPDF(e.target.files[0])}
/>
</label>
<button onClick={handleExport}>Export PDF</button>
</div>
{/* Canvas */}
<div style={{ flex: 1 }}>
<CrystalDraw />
</div>
</div>
);
}Built-in Color Palette
The package exports sensible defaults you can reference:
Pen colors: #f8f8f8 (default), #262626 (black), #ff0000 (red), #00ff00 (lime), #0000ff (blue), #ffff00 (yellow), and more standard web colors.
Highlighter colors: Yellow rgba(225,203,5,0.7), Blue rgba(39,152,177,0.7), Purple rgba(234,151,255,0.7), Green rgba(100,255,75,0.7), Orange rgba(255,172,75,0.7).
You can use any valid CSS color string with setColor / setHighlighterColor.
Browser / Environment Requirements
- React 16+
- Modern browser with Canvas 2D API support
- PDF.js is used internally for PDF import (lazy-loaded, no extra setup needed)
- jsPDF and html2canvas-pro are used internally for PDF export (lazy-loaded)
License
ISC — © Nakul Soral ([email protected])
