Реалізація 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+ років.
Отримайте консультацію інженера — ми оцінимо ваш проект і запропонуємо оптимальне рішення. Зв'яжіться з нами, щоб обговорити деталі.







