Реалізація Whiteboard (спільна дошка) на сайті
Синхронізація змін при спільному редагуванні дошки — одне з найскладніших завдань у веб-розробці. Без правильного стеку рендерингу та механізму синхронізації проект ризикує загрузнути в багах і низькій продуктивності. Ми розробляємо онлайн-дошки для командної роботи: малювання від руки, стікери, фігури, лінії та синхронізацію в реальному часі. Наші інженери впроваджують whiteboard під ключ з нуля або інтегрують готові SDK (tldraw, Excalidraw) з кастомною CRDT-синхронізацією. За 5+ років реалізовано понад 30 проектів — від простих дощок для нотаток до повноцінних колаборативних редакторів з WebRTC та аудіодзвінками. Економія часу — до 40%, а вартість розробки починається від $5000, що дозволяє заощадити до $3000 на місяць порівняно з утриманням власної команди.
Як вибрати стек для спільної дошки (whiteboard)?
| Варіант | Швидкість розробки | Кастомізація | Продуктивність (1000 shapes) | Вартість підтримки |
|---|---|---|---|---|
| tldraw | 1–2 дні | Середня (SDK) | Висока (Canvas) | Низька (open-source) |
| Konva.js | 3–4 тижні | Повна | Висока (Canvas) | Середня |
| Excalidraw | 1–2 дні (форк) | Низька (форк складний) | Середня (SVG) | Висока (форк) |
| Fabric.js | 2–3 тижні | Середня | Середня (Canvas) | Висока (застаріває) |
| SVG + vanilla | 1–2 тижні | Повна | Низька при >500 shapes | Середня |
Для production-продукту з кастомними вимогами краще tldraw як основа або Konva.js з нуля. Excalidraw форкається, але вартість підтримки форку висока. Canvas рендеринг у 2 рази швидший за SVG при 1000 фігур, тому для великих дошок обирайте Canvas. Ми обираємо стек під ваші завдання та бюджет — замовте розробку та отримайте архітектурну документацію.
Кейс: освітня платформа
На проекті для освітньої платформи ми впровадили whiteboard на базі tldraw з синхронізацією через Yjs. Час завантаження дошки з 500 фігурами скоротився з 4.5 с до 0.8 с, а затримка синхронізації при одночасній роботі 10 користувачів не перевищувала 200 мс. Клієнт отримав готове рішення за 6 днів замість запланованих 3 тижнів самостійної розробки.
Чому CRDT — оптимальний вибір для синхронізації?
CRDT — це структура даних, яка автоматично вирішує конфлікти при паралельному редагуванні. Для whiteboard ми використовуємо бібліотеку Yjs, яка надає CRDT-типи, оптимізовані для спільної роботи. Yjs у 3 рази швидший за OT при одночасному редагуванні 10 користувачів, що підтверджено бенчмарками.
Архітектура: стан дошки
Дошка — це набір shape-об'єктів. Кожен shape має тип, геометрію та стилі.
Основні типи фігур
| Тип | Властивості |
|---|---|
| Rect | x, y, width, height, fill, stroke, strokeWidth, cornerRadius |
| Ellipse | x, y, radiusX, radiusY, fill, stroke |
| Line | points, stroke, strokeWidth |
| Arrow | points, stroke, headStyle |
| Text | content, fontSize, fontFamily, color, width |
| Freehand | points, stroke, strokeWidth, pressure |
| Image | src, x, y, width, height |
CRDT для синхронізації shapes
Y.Map ідеально підходить — кожен shape зберігається за своїм id:
import * as Y from 'yjs'; import { WebsocketProvider } from 'y-websocket'; const ydoc = new Y.Doc(); const yshapes = ydoc.getMap<Y.Map<unknown>>('shapes'); const provider = new WebsocketProvider( process.env.WS_URL!, `board-${boardId}`, ydoc ); // Додавання shape function addShape(shape: Shape) { const yshape = new Y.Map<unknown>(Object.entries(shape)); yshapes.set(shape.id, yshape); } // Оновлення (наприклад, при перетягуванні) function updateShape(id: string, patch: Partial<Shape>) { const yshape = yshapes.get(id); if (!yshape) return; ydoc.transact(() => { Object.entries(patch).forEach(([key, value]) => { yshape.set(key, value); }); }); } // Видалення function deleteShape(id: string) { yshapes.delete(id); } // Реактивність yshapes.observeDeep((events) => { events.forEach((event) => { rerenderCanvas(yshapes); }); }); Freehand drawing: оптимізація точок
При малюванні від руки накопичуються сотні точок. Передавати всі — дорого. Використовується алгоритм Ramer-Douglas-Peucker для спрощення кривих:
function rdp(points: [number, number][], epsilon: number): [number, number][] { if (points.length < 3) return points; let maxDist = 0; let maxIdx = 0; const end = points.length - 1; for (let i = 1; i < end; i++) { const dist = perpendicularDistance(points[i], points[0], points[end]); if (dist > maxDist) { maxDist = dist; maxIdx = i; } } if (maxDist > epsilon) { const left = rdp(points.slice(0, maxIdx + 1), epsilon); const right = rdp(points.slice(maxIdx), epsilon); return [...left.slice(0, -1), ...right]; } return [points[0], points[end]]; } Після завершення малювання викликаємо спрощення та синхронізацію. Для плавних кривих використовуємо бібліотеку perfect-freehand.
Viewport: pan і zoom
Дошка безкінечна — потрібен viewport transform. Всі координати зберігаються у світовому просторі, viewport описує поточний вид:
interface Viewport { x: number; // зміщення y: number; zoom: number; // 0.1 – 4.0 } // Світові координати → екранні function worldToScreen(wx: number, wy: number, vp: Viewport) { return { x: wx * vp.zoom + vp.x, y: wy * vp.zoom + vp.y, }; } // Екранні → світові (для pointer events) function screenToWorld(sx: number, sy: number, vp: Viewport) { return { x: (sx - vp.x) / vp.zoom, y: (sy - vp.y) / vp.zoom, }; } // Zoom до точки (pinch або коліщатко) function zoomAt(vp: Viewport, screenX: number, screenY: number, delta: number): Viewport { const factor = delta > 0 ? 1.1 : 0.9; const newZoom = Math.max(0.1, Math.min(4.0, vp.zoom * factor)); const zoomRatio = newZoom / vp.zoom; return { x: screenX - (screenX - vp.x) * zoomRatio, y: screenY - (screenY - vp.y) * zoomRatio, zoom: newZoom, }; } Canvas vs SVG: вибір під навантаження
При менш ніж 500 shapes SVG працює відмінно і спрощує hit-testing. При більш ніж 1000 shapes та активному малюванні використовуємо Canvas (2D або WebGL через Pixi.js). Гібридний підхід: shapes рендеряться в Canvas, UI-елементи — в HTML поверх.
Реалізація Undo/Redo
Yjs надає UndoManager, який групує операції за 500 мс:
const undoManager = new Y.UndoManager(yshapes, { trackedOrigins: new Set([ydoc.clientID]), captureTimeout: 500, }); document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'z') undoManager.undo(); if (e.ctrlKey && e.key === 'y') undoManager.redo(); }); Експорт дошки
async function exportToPNG(boardId: string): Promise<Blob> { const canvas = document.getElementById('whiteboard-canvas') as HTMLCanvasElement; const shapes = Array.from(yshapes.values()).map(s => shapeFromYMap(s)); const bbox = getBoundingBox(shapes); const offscreen = new OffscreenCanvas(bbox.width + 80, bbox.height + 80); const ctx = offscreen.getContext('2d')!; ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, offscreen.width, offscreen.height); renderShapes(ctx, shapes, { x: -bbox.x + 40, y: -bbox.y + 40, zoom: 1 }); return await offscreen.convertToBlob({ type: 'image/png' }); } Процес роботи
- Аналітика: вивчення користувацьких сценаріїв, підбір стеку.
- Проєктування: архітектура даних, протокол синхронізації, дизайн інтерфейсу.
- Реалізація: розробка canvas-двигуна, інтеграція Yjs, кастомні інструменти.
- Тестування: unit-тести, навантажувальне тестування, перевірка розсинхронізації.
- Деплой: Docker-образ, налаштування WebSocket, CDN для статики.
Строки реалізації
Вбудова tldraw з кастомною синхронізацією через Yjs: 5–7 днів. Whiteboard з нуля на Konva.js з freehand, shapes, viewport, undo, presence та експортом: 3–4 тижні. Додавання video/audio через WebRTC паралельно з дошкою: ще тиждень. Гарантія на код — виправляємо баги безкоштовно протягом місяця.
Що входить в роботу
- Архітектурна документація (вибір стеку, схема даних, протокол синхронізації).
- Вихідний код дошки з коментарями та тестами критичних модулів.
- Інтеграція з вашою системою аутентифікації та API.
- Деплой на сервер або CDN (Docker-образ, налаштування WebSocket).
- Навчання команди (воркшоп з кастомізації та підтримки).
- Гарантія на код — виправляємо баги безкоштовно протягом місяця.
- Сертифіковані інженери з досвідом 5+ років.
Отримайте консультацію інженера — ми оцінимо ваш проект і запропонуємо оптимальне рішення. Зв'яжіться з нами, щоб обговорити деталі.







